Skip to content

Commit ae2fa31

Browse files
Merge branch 'master' into mrschmidt/pendingwrites
2 parents 001cf9c + 2a28a80 commit ae2fa31

File tree

6 files changed

+445
-272
lines changed

6 files changed

+445
-272
lines changed

packages/firestore/src/core/sync_engine.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ export class MultiTabSyncEngine extends SyncEngine
919919
// The primary state is set to `true` or `false` immediately after Firestore
920920
// startup. In the interim, a client should only be considered primary if
921921
// `isPrimary` is true.
922-
private isPrimary: undefined | boolean = undefined;
922+
private _isPrimaryClient: undefined | boolean = undefined;
923923

924924
constructor(
925925
protected localStore: MultiTabLocalStore,
@@ -938,7 +938,7 @@ export class MultiTabSyncEngine extends SyncEngine
938938
}
939939

940940
get isPrimaryClient(): boolean {
941-
return this.isPrimary === true;
941+
return this._isPrimaryClient === true;
942942
}
943943

944944
enableNetwork(): Promise<void> {
@@ -965,7 +965,7 @@ export class MultiTabSyncEngine extends SyncEngine
965965
const viewSnapshot = queryView.view.synchronizeWithPersistedState(
966966
queryResult
967967
);
968-
if (this.isPrimary) {
968+
if (this._isPrimaryClient) {
969969
this.updateTrackedLimbos(queryView.targetId, viewSnapshot.limboChanges);
970970
}
971971
return viewSnapshot;
@@ -1032,7 +1032,7 @@ export class MultiTabSyncEngine extends SyncEngine
10321032
}
10331033

10341034
async applyPrimaryState(isPrimary: boolean): Promise<void> {
1035-
if (isPrimary === true && this.isPrimary !== true) {
1035+
if (isPrimary === true && this._isPrimaryClient !== true) {
10361036
// Secondary tabs only maintain Views for their local listeners and the
10371037
// Views internal state may not be 100% populated (in particular
10381038
// secondary tabs don't track syncedDocuments, the set of documents the
@@ -1044,12 +1044,12 @@ export class MultiTabSyncEngine extends SyncEngine
10441044
activeTargets.toArray(),
10451045
/*transitionToPrimary=*/ true
10461046
);
1047-
this.isPrimary = true;
1047+
this._isPrimaryClient = true;
10481048
await this.remoteStore.applyPrimaryState(true);
10491049
for (const targetData of activeQueries) {
10501050
this.remoteStore.listen(targetData);
10511051
}
1052-
} else if (isPrimary === false && this.isPrimary !== false) {
1052+
} else if (isPrimary === false && this._isPrimaryClient !== false) {
10531053
const activeTargets: TargetId[] = [];
10541054

10551055
let p = Promise.resolve();
@@ -1074,7 +1074,7 @@ export class MultiTabSyncEngine extends SyncEngine
10741074
/*transitionToPrimary=*/ false
10751075
);
10761076
this.resetLimboDocuments();
1077-
this.isPrimary = false;
1077+
this._isPrimaryClient = false;
10781078
await this.remoteStore.applyPrimaryState(false);
10791079
}
10801080
}
@@ -1189,7 +1189,7 @@ export class MultiTabSyncEngine extends SyncEngine
11891189
state: QueryTargetState,
11901190
error?: FirestoreError
11911191
): Promise<void> {
1192-
if (this.isPrimary) {
1192+
if (this._isPrimaryClient) {
11931193
// If we receive a target state notification via WebStorage, we are
11941194
// either already secondary or another tab has taken the primary lease.
11951195
logDebug(LOG_TAG, 'Ignoring unexpected query state notification.');
@@ -1229,7 +1229,7 @@ export class MultiTabSyncEngine extends SyncEngine
12291229
added: TargetId[],
12301230
removed: TargetId[]
12311231
): Promise<void> {
1232-
if (!this.isPrimary) {
1232+
if (!this._isPrimaryClient) {
12331233
return;
12341234
}
12351235

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const MAX_PRIMARY_ELIGIBLE_AGE_MS = 5000;
100100
const CLIENT_METADATA_REFRESH_INTERVAL_MS = 4000;
101101
/** User-facing error when the primary lease is required but not available. */
102102
const PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG =
103-
'Another tab has exclusive access to the persistence layer. ' +
103+
'Failed to obtain exclusive access to the persistence layer. ' +
104104
'To allow shared access, make sure to invoke ' +
105105
'`enablePersistence()` with `synchronizeTabs:true` in all tabs.';
106106
const UNSUPPORTED_PLATFORM_ERROR_MSG =
@@ -190,11 +190,12 @@ export class IndexedDbPersistence implements Persistence {
190190
private readonly document: Document | null;
191191
private readonly window: Window;
192192

193-
// Technically these types should be `| undefined` because they are
193+
// Technically `simpleDb` should be `| undefined` because it is
194194
// initialized asynchronously by start(), but that would be more misleading
195195
// than useful.
196196
private simpleDb!: SimpleDb;
197-
private listenSequence!: ListenSequence;
197+
198+
private listenSequence: ListenSequence | null = null;
198199

199200
private _started = false;
200201
private isPrimary = false;
@@ -286,7 +287,15 @@ export class IndexedDbPersistence implements Persistence {
286287
// having the persistence lock), so it's the first thing we should do.
287288
return this.updateClientMetadataAndTryBecomePrimary();
288289
})
289-
.then(() => {
290+
.then(e => {
291+
if (!this.isPrimary && !this.allowTabSynchronization) {
292+
// Fail `start()` if `synchronizeTabs` is disabled and we cannot
293+
// obtain the primary lease.
294+
throw new FirestoreError(
295+
Code.FAILED_PRECONDITION,
296+
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
297+
);
298+
}
290299
this.attachVisibilityHandler();
291300
this.attachWindowUnloadHook();
292301

@@ -374,8 +383,10 @@ export class IndexedDbPersistence implements Persistence {
374383
* primary lease.
375384
*/
376385
private updateClientMetadataAndTryBecomePrimary(): Promise<void> {
377-
return this.simpleDb
378-
.runTransaction('readwrite', ALL_STORES, txn => {
386+
return this.runTransaction(
387+
'updateClientMetadataAndTryBecomePrimary',
388+
'readwrite',
389+
txn => {
379390
const metadataStore = clientMetadataStore(txn);
380391
return metadataStore
381392
.put(
@@ -408,10 +419,18 @@ export class IndexedDbPersistence implements Persistence {
408419
return /* canActAsPrimary= */ false;
409420
}
410421
});
411-
})
422+
}
423+
)
412424
.catch(e => {
413425
if (!this.allowTabSynchronization) {
414-
throw e;
426+
if (e.name === 'IndexedDbTransactionError') {
427+
logDebug(LOG_TAG, 'Failed to extend owner lease: ', e);
428+
// Proceed with the existing state. Any subsequent access to
429+
// IndexedDB will verify the lease.
430+
return this.isPrimary;
431+
} else {
432+
throw e;
433+
}
415434
}
416435

417436
logDebug(
@@ -432,7 +451,7 @@ export class IndexedDbPersistence implements Persistence {
432451
}
433452

434453
private verifyPrimaryLease(
435-
txn: SimpleDbTransaction
454+
txn: PersistenceTransaction
436455
): PersistencePromise<boolean> {
437456
const store = primaryClientStore(txn);
438457
return store.get(DbPrimaryClient.key).next(primaryClient => {
@@ -441,7 +460,7 @@ export class IndexedDbPersistence implements Persistence {
441460
}
442461

443462
private removeClientMetadata(
444-
txn: SimpleDbTransaction
463+
txn: PersistenceTransaction
445464
): PersistencePromise<void> {
446465
const metadataStore = clientMetadataStore(txn);
447466
return metadataStore.delete(this.clientId);
@@ -535,7 +554,7 @@ export class IndexedDbPersistence implements Persistence {
535554
* (foreground) client should become leaseholder instead.
536555
*/
537556
private canActAsPrimary(
538-
txn: SimpleDbTransaction
557+
txn: PersistenceTransaction
539558
): PersistencePromise<boolean> {
540559
const store = primaryClientStore(txn);
541560
return store
@@ -644,15 +663,13 @@ export class IndexedDbPersistence implements Persistence {
644663
}
645664
this.detachVisibilityHandler();
646665
this.detachWindowUnloadHook();
647-
await this.simpleDb.runTransaction(
648-
'readwrite',
649-
[DbPrimaryClient.store, DbClientMetadata.store],
650-
txn => {
651-
return this.releasePrimaryLeaseIfHeld(txn).next(() =>
652-
this.removeClientMetadata(txn)
653-
);
654-
}
655-
);
666+
await this.runTransaction('shutdown', 'readwrite', txn => {
667+
return this.releasePrimaryLeaseIfHeld(txn).next(() =>
668+
this.removeClientMetadata(txn)
669+
);
670+
}).catch(e => {
671+
logDebug(LOG_TAG, 'Proceeding with shutdown despite failure: ', e);
672+
});
656673
this.simpleDb.close();
657674

658675
// Remove the entry marking the client as zombied from LocalStorage since
@@ -683,19 +700,15 @@ export class IndexedDbPersistence implements Persistence {
683700
* PORTING NOTE: This is only used for Web multi-tab.
684701
*/
685702
getActiveClients(): Promise<ClientId[]> {
686-
return this.simpleDb.runTransaction(
687-
'readonly',
688-
[DbClientMetadata.store],
689-
txn => {
690-
return clientMetadataStore(txn)
691-
.loadAll()
692-
.next(clients =>
693-
this.filterActiveClients(clients, MAX_CLIENT_AGE_MS).map(
694-
clientMetadata => clientMetadata.clientId
695-
)
696-
);
697-
}
698-
);
703+
return this.runTransaction('getActiveClients', 'readonly', txn => {
704+
return clientMetadataStore(txn)
705+
.loadAll()
706+
.next(clients =>
707+
this.filterActiveClients(clients, MAX_CLIENT_AGE_MS).map(
708+
clientMetadata => clientMetadata.clientId
709+
)
710+
);
711+
});
699712
}
700713

701714
static async clearPersistence(persistenceKey: string): Promise<void> {
@@ -766,7 +779,9 @@ export class IndexedDbPersistence implements Persistence {
766779
.runTransaction(simpleDbMode, ALL_STORES, simpleDbTxn => {
767780
persistenceTransaction = new IndexedDbTransaction(
768781
simpleDbTxn,
769-
this.listenSequence.next()
782+
this.listenSequence
783+
? this.listenSequence.next()
784+
: ListenSequence.INVALID
770785
);
771786

772787
if (mode === 'readwrite-primary') {
@@ -775,12 +790,12 @@ export class IndexedDbPersistence implements Persistence {
775790
// executing transactionOperation(). This ensures that even if the
776791
// transactionOperation takes a long time, we'll use a recent
777792
// leaseTimestampMs in the extended (or newly acquired) lease.
778-
return this.verifyPrimaryLease(simpleDbTxn)
793+
return this.verifyPrimaryLease(persistenceTransaction)
779794
.next(holdsPrimaryLease => {
780795
if (holdsPrimaryLease) {
781796
return /* holdsPrimaryLease= */ true;
782797
}
783-
return this.canActAsPrimary(simpleDbTxn);
798+
return this.canActAsPrimary(persistenceTransaction);
784799
})
785800
.next(holdsPrimaryLease => {
786801
if (!holdsPrimaryLease) {
@@ -799,14 +814,14 @@ export class IndexedDbPersistence implements Persistence {
799814
return transactionOperation(persistenceTransaction);
800815
})
801816
.next(result => {
802-
return this.acquireOrExtendPrimaryLease(simpleDbTxn).next(
803-
() => result
804-
);
817+
return this.acquireOrExtendPrimaryLease(
818+
persistenceTransaction
819+
).next(() => result);
805820
});
806821
} else {
807-
return this.verifyAllowTabSynchronization(simpleDbTxn).next(() =>
808-
transactionOperation(persistenceTransaction)
809-
);
822+
return this.verifyAllowTabSynchronization(
823+
persistenceTransaction
824+
).next(() => transactionOperation(persistenceTransaction));
810825
}
811826
})
812827
.then(result => {
@@ -822,7 +837,7 @@ export class IndexedDbPersistence implements Persistence {
822837
// TODO(b/114226234): Remove this check when `synchronizeTabs` can no longer
823838
// be turned off.
824839
private verifyAllowTabSynchronization(
825-
txn: SimpleDbTransaction
840+
txn: PersistenceTransaction
826841
): PersistencePromise<void> {
827842
const store = primaryClientStore(txn);
828843
return store.get(DbPrimaryClient.key).next(currentPrimary => {
@@ -835,7 +850,10 @@ export class IndexedDbPersistence implements Persistence {
835850
!this.isClientZombied(currentPrimary.ownerId);
836851

837852
if (currentLeaseIsValid && !this.isLocalClient(currentPrimary)) {
838-
if (!currentPrimary!.allowTabSynchronization) {
853+
if (
854+
!this.allowTabSynchronization ||
855+
!currentPrimary!.allowTabSynchronization
856+
) {
839857
throw new FirestoreError(
840858
Code.FAILED_PRECONDITION,
841859
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
@@ -850,7 +868,7 @@ export class IndexedDbPersistence implements Persistence {
850868
* method does not verify that the client is eligible for this lease.
851869
*/
852870
private acquireOrExtendPrimaryLease(
853-
txn: SimpleDbTransaction
871+
txn: PersistenceTransaction
854872
): PersistencePromise<void> {
855873
const newPrimary = new DbPrimaryClient(
856874
this.clientId,
@@ -886,7 +904,7 @@ export class IndexedDbPersistence implements Persistence {
886904

887905
/** Checks the primary lease and removes it if we are the current primary. */
888906
private releasePrimaryLeaseIfHeld(
889-
txn: SimpleDbTransaction
907+
txn: PersistenceTransaction
890908
): PersistencePromise<void> {
891909
const store = primaryClientStore(txn);
892910
return store.get(DbPrimaryClient.key).next(primaryClient => {
@@ -1051,18 +1069,22 @@ export class IndexedDbPersistence implements Persistence {
10511069
* Helper to get a typed SimpleDbStore for the primary client object store.
10521070
*/
10531071
function primaryClientStore(
1054-
txn: SimpleDbTransaction
1072+
txn: PersistenceTransaction
10551073
): SimpleDbStore<DbPrimaryClientKey, DbPrimaryClient> {
1056-
return txn.store<DbPrimaryClientKey, DbPrimaryClient>(DbPrimaryClient.store);
1074+
return IndexedDbPersistence.getStore<DbPrimaryClientKey, DbPrimaryClient>(
1075+
txn,
1076+
DbPrimaryClient.store
1077+
);
10571078
}
10581079

10591080
/**
10601081
* Helper to get a typed SimpleDbStore for the client metadata object store.
10611082
*/
10621083
function clientMetadataStore(
1063-
txn: SimpleDbTransaction
1084+
txn: PersistenceTransaction
10641085
): SimpleDbStore<DbClientMetadataKey, DbClientMetadata> {
1065-
return txn.store<DbClientMetadataKey, DbClientMetadata>(
1086+
return IndexedDbPersistence.getStore<DbClientMetadataKey, DbClientMetadata>(
1087+
txn,
10661088
DbClientMetadata.store
10671089
);
10681090
}

0 commit comments

Comments
 (0)