Skip to content

Commit 09ec0ee

Browse files
Rewrite everything
1 parent 3073f60 commit 09ec0ee

File tree

2 files changed

+126
-92
lines changed

2 files changed

+126
-92
lines changed

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 69 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,11 @@ 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+
});
665649
this.simpleDb.close();
666650

667651
// Remove the entry marking the client as zombied from LocalStorage since
@@ -692,19 +676,15 @@ export class IndexedDbPersistence implements Persistence {
692676
* PORTING NOTE: This is only used for Web multi-tab.
693677
*/
694678
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-
);
679+
return this.runTransaction('getActiveClients', 'readonly', txn => {
680+
return clientMetadataStore(txn)
681+
.loadAll()
682+
.next(clients =>
683+
this.filterActiveClients(clients, MAX_CLIENT_AGE_MS).map(
684+
clientMetadata => clientMetadata.clientId
685+
)
686+
);
687+
});
708688
}
709689

710690
static async clearPersistence(persistenceKey: string): Promise<void> {
@@ -775,7 +755,9 @@ export class IndexedDbPersistence implements Persistence {
775755
.runTransaction(simpleDbMode, ALL_STORES, simpleDbTxn => {
776756
persistenceTransaction = new IndexedDbTransaction(
777757
simpleDbTxn,
778-
this.listenSequence.next()
758+
this.listenSequence
759+
? this.listenSequence.next()
760+
: ListenSequence.INVALID
779761
);
780762

781763
if (mode === 'readwrite-primary') {
@@ -784,12 +766,12 @@ export class IndexedDbPersistence implements Persistence {
784766
// executing transactionOperation(). This ensures that even if the
785767
// transactionOperation takes a long time, we'll use a recent
786768
// leaseTimestampMs in the extended (or newly acquired) lease.
787-
return this.verifyPrimaryLease(simpleDbTxn)
769+
return this.verifyPrimaryLease(persistenceTransaction)
788770
.next(holdsPrimaryLease => {
789771
if (holdsPrimaryLease) {
790772
return /* holdsPrimaryLease= */ true;
791773
}
792-
return this.canActAsPrimary(simpleDbTxn);
774+
return this.canActAsPrimary(persistenceTransaction);
793775
})
794776
.next(holdsPrimaryLease => {
795777
if (!holdsPrimaryLease) {
@@ -808,14 +790,14 @@ export class IndexedDbPersistence implements Persistence {
808790
return transactionOperation(persistenceTransaction);
809791
})
810792
.next(result => {
811-
return this.acquireOrExtendPrimaryLease(simpleDbTxn).next(
812-
() => result
813-
);
793+
return this.acquireOrExtendPrimaryLease(
794+
persistenceTransaction
795+
).next(() => result);
814796
});
815797
} else {
816-
return this.verifyAllowTabSynchronization(simpleDbTxn).next(() =>
817-
transactionOperation(persistenceTransaction)
818-
);
798+
return this.verifyAllowTabSynchronization(
799+
persistenceTransaction
800+
).next(() => transactionOperation(persistenceTransaction));
819801
}
820802
})
821803
.then(result => {
@@ -831,7 +813,7 @@ export class IndexedDbPersistence implements Persistence {
831813
// TODO(b/114226234): Remove this check when `synchronizeTabs` can no longer
832814
// be turned off.
833815
private verifyAllowTabSynchronization(
834-
txn: SimpleDbTransaction
816+
txn: PersistenceTransaction
835817
): PersistencePromise<void> {
836818
const store = primaryClientStore(txn);
837819
return store.get(DbPrimaryClient.key).next(currentPrimary => {
@@ -844,7 +826,10 @@ export class IndexedDbPersistence implements Persistence {
844826
!this.isClientZombied(currentPrimary.ownerId);
845827

846828
if (currentLeaseIsValid && !this.isLocalClient(currentPrimary)) {
847-
if (!currentPrimary!.allowTabSynchronization) {
829+
if (
830+
!this.allowTabSynchronization ||
831+
!currentPrimary!.allowTabSynchronization
832+
) {
848833
throw new FirestoreError(
849834
Code.FAILED_PRECONDITION,
850835
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
@@ -859,7 +844,7 @@ export class IndexedDbPersistence implements Persistence {
859844
* method does not verify that the client is eligible for this lease.
860845
*/
861846
private acquireOrExtendPrimaryLease(
862-
txn: SimpleDbTransaction
847+
txn: PersistenceTransaction
863848
): PersistencePromise<void> {
864849
const newPrimary = new DbPrimaryClient(
865850
this.clientId,
@@ -895,7 +880,7 @@ export class IndexedDbPersistence implements Persistence {
895880

896881
/** Checks the primary lease and removes it if we are the current primary. */
897882
private releasePrimaryLeaseIfHeld(
898-
txn: SimpleDbTransaction
883+
txn: PersistenceTransaction
899884
): PersistencePromise<void> {
900885
const store = primaryClientStore(txn);
901886
return store.get(DbPrimaryClient.key).next(primaryClient => {
@@ -1060,18 +1045,22 @@ export class IndexedDbPersistence implements Persistence {
10601045
* Helper to get a typed SimpleDbStore for the primary client object store.
10611046
*/
10621047
function primaryClientStore(
1063-
txn: SimpleDbTransaction
1048+
txn: PersistenceTransaction
10641049
): SimpleDbStore<DbPrimaryClientKey, DbPrimaryClient> {
1065-
return txn.store<DbPrimaryClientKey, DbPrimaryClient>(DbPrimaryClient.store);
1050+
return IndexedDbPersistence.getStore<DbPrimaryClientKey, DbPrimaryClient>(
1051+
txn,
1052+
DbPrimaryClient.store
1053+
);
10661054
}
10671055

10681056
/**
10691057
* Helper to get a typed SimpleDbStore for the client metadata object store.
10701058
*/
10711059
function clientMetadataStore(
1072-
txn: SimpleDbTransaction
1060+
txn: PersistenceTransaction
10731061
): SimpleDbStore<DbClientMetadataKey, DbClientMetadata> {
1074-
return txn.store<DbClientMetadataKey, DbClientMetadata>(
1062+
return IndexedDbPersistence.getStore<DbClientMetadataKey, DbClientMetadata>(
1063+
txn,
10751064
DbClientMetadata.store
10761065
);
10771066
}

0 commit comments

Comments
 (0)