Skip to content

Commit d87dc1e

Browse files
Rewrite everything
1 parent 3073f60 commit d87dc1e

File tree

2 files changed

+141
-92
lines changed

2 files changed

+141
-92
lines changed

packages/firestore/src/local/indexeddb_persistence.ts

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

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

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

@@ -375,8 +384,10 @@ export class IndexedDbPersistence implements Persistence {
375384
* primary lease.
376385
*/
377386
private updateClientMetadataAndTryBecomePrimary(): Promise<void> {
378-
return this.simpleDb
379-
.runTransaction('readwrite', ALL_STORES, txn => {
387+
return this.runTransaction(
388+
'updateClientMetadataAndTryBecomePrimary',
389+
'readwrite',
390+
txn => {
380391
const metadataStore = clientMetadataStore(txn);
381392
return metadataStore
382393
.put(
@@ -409,19 +420,19 @@ export class IndexedDbPersistence implements Persistence {
409420
return /* canActAsPrimary= */ false;
410421
}
411422
});
412-
})
423+
}
424+
)
413425
.catch(e => {
414426
if (!this.allowTabSynchronization) {
415427
if (e.name === 'IndexedDbTransactionError') {
416-
logDebug(LOG_TAG, "Failed to extend owner lease: ", e);
417-
// Proceed in primary mode since the client was not initialized
418-
// to support multi-tab. Any subsequent access to IndexedDB will
419-
// verify the lease.
420-
return true;
428+
logDebug(LOG_TAG, 'Failed to extend owner lease: ', e);
429+
// Proceed with the existing state. Any subsequent access to
430+
// IndexedDB will verify the lease.
431+
return this.isPrimary;
421432
} else {
422433
throw e;
423434
}
424-
}
435+
}
425436

426437
logDebug(
427438
LOG_TAG,
@@ -441,7 +452,7 @@ export class IndexedDbPersistence implements Persistence {
441452
}
442453

443454
private verifyPrimaryLease(
444-
txn: SimpleDbTransaction
455+
txn: PersistenceTransaction
445456
): PersistencePromise<boolean> {
446457
const store = primaryClientStore(txn);
447458
return store.get(DbPrimaryClient.key).next(primaryClient => {
@@ -450,7 +461,7 @@ export class IndexedDbPersistence implements Persistence {
450461
}
451462

452463
private removeClientMetadata(
453-
txn: SimpleDbTransaction
464+
txn: PersistenceTransaction
454465
): PersistencePromise<void> {
455466
const metadataStore = clientMetadataStore(txn);
456467
return metadataStore.delete(this.clientId);
@@ -544,7 +555,7 @@ export class IndexedDbPersistence implements Persistence {
544555
* (foreground) client should become leaseholder instead.
545556
*/
546557
private canActAsPrimary(
547-
txn: SimpleDbTransaction
558+
txn: PersistenceTransaction
548559
): PersistencePromise<boolean> {
549560
const store = primaryClientStore(txn);
550561
return store
@@ -569,33 +580,10 @@ export class IndexedDbPersistence implements Persistence {
569580
if (currentLeaseIsValid) {
570581
if (this.isLocalClient(currentPrimary) && this.networkEnabled) {
571582
return true;
572-
}
573-
574-
if (!this.isLocalClient(currentPrimary)) {
575-
if (!currentPrimary!.allowTabSynchronization) {
576-
// Fail the `canActAsPrimary` check if the current leaseholder has
577-
// not opted into multi-tab synchronization. If this happens at
578-
// client startup, we reject the Promise returned by
579-
// `enablePersistence()` and the user can continue to use Firestore
580-
// with in-memory persistence.
581-
// If this fails during a lease refresh, we will instead block the
582-
// AsyncQueue from executing further operations. Note that this is
583-
// acceptable since mixing & matching different `synchronizeTabs`
584-
// settings is not supported.
585-
//
586-
// TODO(b/114226234): Remove this check when `synchronizeTabs` can
587-
// no longer be turned off.
588-
throw new FirestoreError(
589-
Code.FAILED_PRECONDITION,
590-
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
591-
);
592-
}
593-
583+
} else if (!this.isLocalClient(currentPrimary)) {
594584
return false;
595585
}
596-
}
597-
598-
if (this.networkEnabled && this.inForeground) {
586+
} else if (this.networkEnabled && this.inForeground) {
599587
return true;
600588
}
601589

@@ -653,15 +641,13 @@ export class IndexedDbPersistence implements Persistence {
653641
}
654642
this.detachVisibilityHandler();
655643
this.detachWindowUnloadHook();
656-
await this.simpleDb.runTransaction(
657-
'readwrite',
658-
[DbPrimaryClient.store, DbClientMetadata.store],
659-
txn => {
660-
return this.releasePrimaryLeaseIfHeld(txn).next(() =>
661-
this.removeClientMetadata(txn)
662-
);
663-
}
664-
);
644+
await this.runTransaction('shutdown', 'readwrite', txn => {
645+
return this.releasePrimaryLeaseIfHeld(txn).next(() =>
646+
this.removeClientMetadata(txn)
647+
);
648+
}).catch(e => {
649+
logDebug(LOG_TAG, 'Proceeding with shutdown despite failure: ', e);
650+
});
665651
this.simpleDb.close();
666652

667653
// Remove the entry marking the client as zombied from LocalStorage since
@@ -692,19 +678,15 @@ export class IndexedDbPersistence implements Persistence {
692678
* PORTING NOTE: This is only used for Web multi-tab.
693679
*/
694680
getActiveClients(): Promise<ClientId[]> {
695-
return this.simpleDb.runTransaction(
696-
'readonly',
697-
[DbClientMetadata.store],
698-
txn => {
699-
return clientMetadataStore(txn)
700-
.loadAll()
701-
.next(clients =>
702-
this.filterActiveClients(clients, MAX_CLIENT_AGE_MS).map(
703-
clientMetadata => clientMetadata.clientId
704-
)
705-
);
706-
}
707-
);
681+
return this.runTransaction('getActiveClients', 'readonly', txn => {
682+
return clientMetadataStore(txn)
683+
.loadAll()
684+
.next(clients =>
685+
this.filterActiveClients(clients, MAX_CLIENT_AGE_MS).map(
686+
clientMetadata => clientMetadata.clientId
687+
)
688+
);
689+
});
708690
}
709691

710692
static async clearPersistence(persistenceKey: string): Promise<void> {
@@ -775,7 +757,9 @@ export class IndexedDbPersistence implements Persistence {
775757
.runTransaction(simpleDbMode, ALL_STORES, simpleDbTxn => {
776758
persistenceTransaction = new IndexedDbTransaction(
777759
simpleDbTxn,
778-
this.listenSequence.next()
760+
this.listenSequence
761+
? this.listenSequence.next()
762+
: ListenSequence.INVALID
779763
);
780764

781765
if (mode === 'readwrite-primary') {
@@ -784,12 +768,12 @@ export class IndexedDbPersistence implements Persistence {
784768
// executing transactionOperation(). This ensures that even if the
785769
// transactionOperation takes a long time, we'll use a recent
786770
// leaseTimestampMs in the extended (or newly acquired) lease.
787-
return this.verifyPrimaryLease(simpleDbTxn)
771+
return this.verifyPrimaryLease(persistenceTransaction)
788772
.next(holdsPrimaryLease => {
789773
if (holdsPrimaryLease) {
790774
return /* holdsPrimaryLease= */ true;
791775
}
792-
return this.canActAsPrimary(simpleDbTxn);
776+
return this.canActAsPrimary(persistenceTransaction);
793777
})
794778
.next(holdsPrimaryLease => {
795779
if (!holdsPrimaryLease) {
@@ -808,14 +792,14 @@ export class IndexedDbPersistence implements Persistence {
808792
return transactionOperation(persistenceTransaction);
809793
})
810794
.next(result => {
811-
return this.acquireOrExtendPrimaryLease(simpleDbTxn).next(
812-
() => result
813-
);
795+
return this.acquireOrExtendPrimaryLease(
796+
persistenceTransaction
797+
).next(() => result);
814798
});
815799
} else {
816-
return this.verifyAllowTabSynchronization(simpleDbTxn).next(() =>
817-
transactionOperation(persistenceTransaction)
818-
);
800+
return this.verifyAllowTabSynchronization(
801+
persistenceTransaction
802+
).next(() => transactionOperation(persistenceTransaction));
819803
}
820804
})
821805
.then(result => {
@@ -831,7 +815,7 @@ export class IndexedDbPersistence implements Persistence {
831815
// TODO(b/114226234): Remove this check when `synchronizeTabs` can no longer
832816
// be turned off.
833817
private verifyAllowTabSynchronization(
834-
txn: SimpleDbTransaction
818+
txn: PersistenceTransaction
835819
): PersistencePromise<void> {
836820
const store = primaryClientStore(txn);
837821
return store.get(DbPrimaryClient.key).next(currentPrimary => {
@@ -844,7 +828,10 @@ export class IndexedDbPersistence implements Persistence {
844828
!this.isClientZombied(currentPrimary.ownerId);
845829

846830
if (currentLeaseIsValid && !this.isLocalClient(currentPrimary)) {
847-
if (!currentPrimary!.allowTabSynchronization) {
831+
if (
832+
!this.allowTabSynchronization ||
833+
!currentPrimary!.allowTabSynchronization
834+
) {
848835
throw new FirestoreError(
849836
Code.FAILED_PRECONDITION,
850837
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
@@ -859,7 +846,7 @@ export class IndexedDbPersistence implements Persistence {
859846
* method does not verify that the client is eligible for this lease.
860847
*/
861848
private acquireOrExtendPrimaryLease(
862-
txn: SimpleDbTransaction
849+
txn: PersistenceTransaction
863850
): PersistencePromise<void> {
864851
const newPrimary = new DbPrimaryClient(
865852
this.clientId,
@@ -895,7 +882,7 @@ export class IndexedDbPersistence implements Persistence {
895882

896883
/** Checks the primary lease and removes it if we are the current primary. */
897884
private releasePrimaryLeaseIfHeld(
898-
txn: SimpleDbTransaction
885+
txn: PersistenceTransaction
899886
): PersistencePromise<void> {
900887
const store = primaryClientStore(txn);
901888
return store.get(DbPrimaryClient.key).next(primaryClient => {
@@ -1060,18 +1047,22 @@ export class IndexedDbPersistence implements Persistence {
10601047
* Helper to get a typed SimpleDbStore for the primary client object store.
10611048
*/
10621049
function primaryClientStore(
1063-
txn: SimpleDbTransaction
1050+
txn: PersistenceTransaction
10641051
): SimpleDbStore<DbPrimaryClientKey, DbPrimaryClient> {
1065-
return txn.store<DbPrimaryClientKey, DbPrimaryClient>(DbPrimaryClient.store);
1052+
return IndexedDbPersistence.getStore<DbPrimaryClientKey, DbPrimaryClient>(
1053+
txn,
1054+
DbPrimaryClient.store
1055+
);
10661056
}
10671057

10681058
/**
10691059
* Helper to get a typed SimpleDbStore for the client metadata object store.
10701060
*/
10711061
function clientMetadataStore(
1072-
txn: SimpleDbTransaction
1062+
txn: PersistenceTransaction
10731063
): SimpleDbStore<DbClientMetadataKey, DbClientMetadata> {
1074-
return txn.store<DbClientMetadataKey, DbClientMetadata>(
1064+
return IndexedDbPersistence.getStore<DbClientMetadataKey, DbClientMetadata>(
1065+
txn,
10751066
DbClientMetadata.store
10761067
);
10771068
}

0 commit comments

Comments
 (0)