Skip to content

Commit 7ef2d47

Browse files
Merge
2 parents 4c3e474 + a36b51b commit 7ef2d47

File tree

10 files changed

+220
-90
lines changed

10 files changed

+220
-90
lines changed

packages/firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Unreleased
2+
- [fixed] Firestore now rejects write operations if they cannot be persisted
3+
in IndexedDB. Previously, these errors crashed the client.
24
- [fixed] Fixed a source of IndexedDB-related crashes for tabs that receive
35
multi-tab notifications while the file system is locked.
46

packages/firestore/src/core/sync_engine.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { User } from '../auth/user';
1919
import {
2020
ignoreIfPrimaryLeaseLoss,
2121
LocalStore,
22+
LocalWriteResult,
2223
MultiTabLocalStore
2324
} from '../local/local_store';
2425
import { LocalViewChanges } from '../local/local_view_changes';
@@ -38,7 +39,7 @@ import { RemoteStore } from '../remote/remote_store';
3839
import { RemoteSyncer } from '../remote/remote_syncer';
3940
import { debugAssert, fail, hardAssert } from '../util/assert';
4041
import { Code, FirestoreError } from '../util/error';
41-
import { logDebug } from '../util/log';
42+
import { logDebug, logError } from '../util/log';
4243
import { primitiveComparator } from '../util/misc';
4344
import { ObjectMap } from '../util/obj_map';
4445
import { Deferred } from '../util/promise';
@@ -352,7 +353,24 @@ export class SyncEngine implements RemoteSyncer {
352353
*/
353354
async write(batch: Mutation[], userCallback: Deferred<void>): Promise<void> {
354355
this.assertSubscribed('write()');
355-
const result = await this.localStore.localWrite(batch);
356+
357+
let result: LocalWriteResult;
358+
try {
359+
result = await this.localStore.localWrite(batch);
360+
} catch (e) {
361+
if (e.name === 'IndexedDbTransactionError') {
362+
// If we can't persist the mutation, we reject the user callback and
363+
// don't send the mutation. The user can then retry the write.
364+
logError(LOG_TAG, 'Dropping write that cannot be persisted: ' + e);
365+
userCallback.reject(
366+
new FirestoreError(Code.UNAVAILABLE, 'Failed to persist write: ' + e)
367+
);
368+
return;
369+
} else {
370+
throw e;
371+
}
372+
}
373+
356374
this.sharedClientState.addPendingMutation(result.batchId);
357375
this.addMutationCallback(result.batchId, userCallback);
358376
await this.emitNewSnapsAndNotifyLocalStore(result.changes);
@@ -1209,10 +1227,12 @@ export class MultiTabSyncEngine extends SyncEngine
12091227
}
12101228

12111229
for (const targetId of added) {
1212-
debugAssert(
1213-
!this.queriesByTarget.has(targetId),
1214-
'Trying to add an already active target'
1215-
);
1230+
if (this.queriesByTarget.has(targetId)) {
1231+
// A target might have been added in a previous attempt
1232+
logDebug(LOG_TAG, 'Adding an already active target ' + targetId);
1233+
continue;
1234+
}
1235+
12161236
const target = await this.localStore.getTarget(targetId);
12171237
debugAssert(
12181238
!!target,

packages/firestore/src/local/shared_client_state.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import { Platform } from '../platform/platform';
2929
import { hardAssert, debugAssert } from '../util/assert';
3030
import { AsyncQueue } from '../util/async_queue';
3131
import { Code, FirestoreError } from '../util/error';
32-
import { forEach } from '../util/obj';
3332
import { logError, logDebug } from '../util/log';
3433
import { SortedSet } from '../util/sorted_set';
34+
import { SortedMap } from '../util/sorted_map';
35+
import { primitiveComparator } from '../util/misc';
3536
import { isSafeInteger } from '../util/types';
3637
import {
3738
QueryTargetState,
@@ -475,12 +476,14 @@ export class WebStorageSharedClientState implements SharedClientState {
475476
private readonly storage: Storage;
476477
private readonly localClientStorageKey: string;
477478
private readonly sequenceNumberKey: string;
478-
private readonly activeClients: { [key: string]: ClientState } = {};
479479
private readonly storageListener = this.handleWebStorageEvent.bind(this);
480480
private readonly onlineStateKey: string;
481481
private readonly clientStateKeyRe: RegExp;
482482
private readonly mutationBatchKeyRe: RegExp;
483483
private readonly queryTargetKeyRe: RegExp;
484+
private activeClients = new SortedMap<string, ClientState>(
485+
primitiveComparator
486+
);
484487
private started = false;
485488
private currentUser: User;
486489

@@ -519,7 +522,10 @@ export class WebStorageSharedClientState implements SharedClientState {
519522
this.sequenceNumberKey = createWebStorageSequenceNumberKey(
520523
this.persistenceKey
521524
);
522-
this.activeClients[this.localClientId] = new LocalClientState();
525+
this.activeClients = this.activeClients.insert(
526+
this.localClientId,
527+
new LocalClientState()
528+
);
523529

524530
this.clientStateKeyRe = new RegExp(
525531
`^${CLIENT_STATE_KEY_PREFIX}_${escapedPersistenceKey}_([^_]*)$`
@@ -576,7 +582,10 @@ export class WebStorageSharedClientState implements SharedClientState {
576582
storageItem
577583
);
578584
if (clientState) {
579-
this.activeClients[clientState.clientId] = clientState;
585+
this.activeClients = this.activeClients.insert(
586+
clientState.clientId,
587+
clientState
588+
);
580589
}
581590
}
582591
}
@@ -611,24 +620,17 @@ export class WebStorageSharedClientState implements SharedClientState {
611620
}
612621

613622
getAllActiveQueryTargets(): TargetIdSet {
614-
let activeTargets = targetIdSet();
615-
forEach(this.activeClients, (key, value) => {
616-
activeTargets = activeTargets.unionWith(value.activeTargetIds);
617-
});
618-
return activeTargets;
623+
return this.extractActiveQueryTargets(this.activeClients);
619624
}
620625

621626
isActiveQueryTarget(targetId: TargetId): boolean {
622-
// This is not using `obj.forEach` since `forEach` doesn't support early
623-
// return.
624-
for (const clientId in this.activeClients) {
625-
if (this.activeClients.hasOwnProperty(clientId)) {
626-
if (this.activeClients[clientId].activeTargetIds.has(targetId)) {
627-
return true;
628-
}
627+
let found = false;
628+
this.activeClients.forEach((key, value) => {
629+
if (value.activeTargetIds.has(targetId)) {
630+
found = true;
629631
}
630-
}
631-
return false;
632+
});
633+
return found;
632634
}
633635

634636
addPendingMutation(batchId: BatchId): void {
@@ -823,7 +825,7 @@ export class WebStorageSharedClientState implements SharedClientState {
823825
}
824826

825827
private get localClientState(): LocalClientState {
826-
return this.activeClients[this.localClientId] as LocalClientState;
828+
return this.activeClients.get(this.localClientId) as LocalClientState;
827829
}
828830

829831
private persistClientState(): void {
@@ -979,26 +981,23 @@ export class WebStorageSharedClientState implements SharedClientState {
979981
clientId: ClientId,
980982
clientState: RemoteClientState | null
981983
): Promise<void> {
982-
const existingTargets = this.getAllActiveQueryTargets();
983-
984-
if (clientState) {
985-
this.activeClients[clientId] = clientState;
986-
} else {
987-
delete this.activeClients[clientId];
988-
}
984+
const updatedClients = clientState
985+
? this.activeClients.insert(clientId, clientState)
986+
: this.activeClients.remove(clientId);
989987

990-
const newTargets = this.getAllActiveQueryTargets();
988+
const existingTargets = this.extractActiveQueryTargets(this.activeClients);
989+
const newTargets = this.extractActiveQueryTargets(updatedClients);
991990

992991
const addedTargets: TargetId[] = [];
993992
const removedTargets: TargetId[] = [];
994993

995-
newTargets.forEach(async targetId => {
994+
newTargets.forEach(targetId => {
996995
if (!existingTargets.has(targetId)) {
997996
addedTargets.push(targetId);
998997
}
999998
});
1000999

1001-
existingTargets.forEach(async targetId => {
1000+
existingTargets.forEach(targetId => {
10021001
if (!newTargets.has(targetId)) {
10031002
removedTargets.push(targetId);
10041003
}
@@ -1007,7 +1006,9 @@ export class WebStorageSharedClientState implements SharedClientState {
10071006
return this.syncEngine!.applyActiveTargetsChange(
10081007
addedTargets,
10091008
removedTargets
1010-
);
1009+
).then(() => {
1010+
this.activeClients = updatedClients;
1011+
});
10111012
}
10121013

10131014
private handleOnlineStateEvent(onlineState: SharedOnlineState): void {
@@ -1016,10 +1017,20 @@ export class WebStorageSharedClientState implements SharedClientState {
10161017
// IndexedDb. If a client does not update their IndexedDb client state
10171018
// within 5 seconds, it is considered inactive and we don't emit an online
10181019
// state event.
1019-
if (this.activeClients[onlineState.clientId]) {
1020+
if (this.activeClients.get(onlineState.clientId)) {
10201021
this.onlineStateHandler!(onlineState.onlineState);
10211022
}
10221023
}
1024+
1025+
private extractActiveQueryTargets(
1026+
clients: SortedMap<string, ClientState>
1027+
): SortedSet<TargetId> {
1028+
let activeTargets = targetIdSet();
1029+
clients.forEach((kev, value) => {
1030+
activeTargets = activeTargets.unionWith(value.activeTargetIds);
1031+
});
1032+
return activeTargets;
1033+
}
10231034
}
10241035

10251036
function fromWebStorageSequenceNumber(

packages/firestore/test/integration/bootstrap.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2017 Google Inc.
3+
* Copyright 2017 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -26,7 +26,11 @@ import '../../index';
2626

2727
// 'context()' definition requires additional dependency on webpack-env package.
2828
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29-
const testsContext = (require as any).context('.', true, /^((?!node).)*\.test$/);
29+
const testsContext = (require as any).context(
30+
'.',
31+
true,
32+
/^((?!node).)*\.test$/
33+
);
3034
const browserTests = testsContext
3135
.keys()
3236
.filter((file: string) => !file.match(/([\/.])node([\/.])/));

packages/firestore/test/unit/bootstrap.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import '../../src/platform_browser/browser_init';
2626

2727
// 'context()' definition requires additional dependency on webpack-env package.
2828
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29-
const testsContext = (require as any).context('.', true, /^((?!node).)*\.test$/);
29+
const testsContext = (require as any).context(
30+
'.',
31+
true,
32+
/^((?!node).)*\.test$/
33+
);
3034
const browserTests = testsContext
3135
.keys()
3236
.filter((file: string) => !file.match(/([\/.])node([\/.])/));

packages/firestore/test/unit/remote/serializer.helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const protoJsReader = testUserDataReader(/* useProto3Json= */ false);
9696
*/
9797
export function serializerTest(
9898
protobufJsVerifier: (jsonValue: api.Value) => void = () => {}
99-
) : void {
99+
): void {
100100
describe('Serializer', () => {
101101
const partition = new DatabaseId('p', 'd');
102102
const s = new JsonProtoSerializer(partition, { useProto3Json: false });

0 commit comments

Comments
 (0)