@@ -104,7 +104,9 @@ const CLIENT_METADATA_REFRESH_INTERVAL_MS = 4000;
104
104
const PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG =
105
105
'Failed to obtain exclusive access to the persistence layer. ' +
106
106
'To allow shared access, make sure to invoke ' +
107
- '`enablePersistence()` with `synchronizeTabs:true` in all tabs.' ;
107
+ '`enablePersistence()` with `synchronizeTabs:true` in all tabs. ' +
108
+ 'If you are using `experimentalForceOwningTab:true`, make sure that only ' +
109
+ 'one tab has persistence enabled at any given time.' ;
108
110
const UNSUPPORTED_PLATFORM_ERROR_MSG =
109
111
'This platform is either missing' +
110
112
' IndexedDB or is known to have an incomplete implementation. Offline' +
@@ -190,7 +192,7 @@ export class IndexedDbPersistence implements Persistence {
190
192
static MAIN_DATABASE = 'main' ;
191
193
192
194
private readonly document : Document | null ;
193
- private readonly window : Window ;
195
+ private readonly window : Window | null ;
194
196
195
197
// Technically `simpleDb` should be `| undefined` because it is
196
198
// initialized asynchronously by start(), but that would be more misleading
@@ -225,18 +227,29 @@ export class IndexedDbPersistence implements Persistence {
225
227
private readonly targetCache : IndexedDbTargetCache ;
226
228
private readonly indexManager : IndexedDbIndexManager ;
227
229
private readonly remoteDocumentCache : IndexedDbRemoteDocumentCache ;
228
- private readonly webStorage : Storage ;
230
+ private readonly webStorage : Storage | null ;
229
231
readonly referenceDelegate : IndexedDbLruDelegate ;
230
232
231
233
constructor (
234
+ /**
235
+ * Whether to synchronize the in-memory state of multiple tabs and share
236
+ * access to local persistence.
237
+ */
232
238
private readonly allowTabSynchronization : boolean ,
239
+
233
240
private readonly persistenceKey : string ,
234
241
private readonly clientId : ClientId ,
235
242
platform : Platform ,
236
243
lruParams : LruParams ,
237
244
private readonly queue : AsyncQueue ,
238
245
serializer : JsonProtoSerializer ,
239
- private readonly sequenceNumberSyncer : SequenceNumberSyncer
246
+ private readonly sequenceNumberSyncer : SequenceNumberSyncer ,
247
+
248
+ /**
249
+ * If set to true, forcefully obtains database access. Existing tabs will
250
+ * no longer be able to access IndexedDB.
251
+ */
252
+ private readonly forceOwningTab : boolean
240
253
) {
241
254
if ( ! IndexedDbPersistence . isAvailable ( ) ) {
242
255
throw new FirestoreError (
@@ -258,14 +271,19 @@ export class IndexedDbPersistence implements Persistence {
258
271
this . serializer ,
259
272
this . indexManager
260
273
) ;
274
+ this . window = platform . window ;
261
275
if ( platform . window && platform . window . localStorage ) {
262
- this . window = platform . window ;
263
- this . webStorage = this . window . localStorage ;
276
+ this . webStorage = platform . window . localStorage ;
264
277
} else {
265
- throw new FirestoreError (
266
- Code . UNIMPLEMENTED ,
267
- 'IndexedDB persistence is only available on platforms that support LocalStorage.'
268
- ) ;
278
+ this . webStorage = null ;
279
+ if ( forceOwningTab === false ) {
280
+ logError (
281
+ LOG_TAG ,
282
+ 'LocalStorage is unavailable. As a result, persistence may not work ' +
283
+ 'reliably. In particular enablePersistence() could fail immediately ' +
284
+ 'after refreshing the page.'
285
+ ) ;
286
+ }
269
287
}
270
288
}
271
289
@@ -287,7 +305,9 @@ export class IndexedDbPersistence implements Persistence {
287
305
this . simpleDb = db ;
288
306
// NOTE: This is expected to fail sometimes (in the case of another tab already
289
307
// having the persistence lock), so it's the first thing we should do.
290
- return this . updateClientMetadataAndTryBecomePrimary ( ) ;
308
+ return this . updateClientMetadataAndTryBecomePrimary (
309
+ this . forceOwningTab
310
+ ) ;
291
311
} )
292
312
. then ( ( ) => {
293
313
if ( ! this . isPrimary && ! this . allowTabSynchronization ) {
@@ -384,7 +404,9 @@ export class IndexedDbPersistence implements Persistence {
384
404
* primary state listener if the client either newly obtained or released its
385
405
* primary lease.
386
406
*/
387
- private updateClientMetadataAndTryBecomePrimary ( ) : Promise < void > {
407
+ private updateClientMetadataAndTryBecomePrimary (
408
+ forceOwningTab = false
409
+ ) : Promise < void > {
388
410
return this . runTransaction (
389
411
'updateClientMetadataAndTryBecomePrimary' ,
390
412
'readwrite' ,
@@ -519,11 +541,13 @@ export class IndexedDbPersistence implements Persistence {
519
541
// Ideally we'd delete the IndexedDb and LocalStorage zombie entries for
520
542
// the client atomically, but we can't. So we opt to delete the IndexedDb
521
543
// entries first to avoid potentially reviving a zombied client.
522
- inactiveClients . forEach ( inactiveClient => {
523
- this . window . localStorage . removeItem (
524
- this . zombiedClientLocalStorageKey ( inactiveClient . clientId )
525
- ) ;
526
- } ) ;
544
+ if ( this . webStorage ) {
545
+ for ( const inactiveClient of inactiveClients ) {
546
+ this . webStorage . removeItem (
547
+ this . zombiedClientLocalStorageKey ( inactiveClient . clientId )
548
+ ) ;
549
+ }
550
+ }
527
551
}
528
552
}
529
553
@@ -558,6 +582,9 @@ export class IndexedDbPersistence implements Persistence {
558
582
private canActAsPrimary (
559
583
txn : PersistenceTransaction
560
584
) : PersistencePromise < boolean > {
585
+ if ( this . forceOwningTab ) {
586
+ return PersistencePromise . resolve < boolean > ( true ) ;
587
+ }
561
588
const store = primaryClientStore ( txn ) ;
562
589
return store
563
590
. get ( DbPrimaryClient . key )
@@ -578,6 +605,7 @@ export class IndexedDbPersistence implements Persistence {
578
605
// foreground.
579
606
// - every clients network is disabled and no other client's tab is in
580
607
// the foreground.
608
+ // - the `forceOwningTab` setting was passed in.
581
609
if ( currentLeaseIsValid ) {
582
610
if ( this . isLocalClient ( currentPrimary ) && this . networkEnabled ) {
583
611
return true ;
@@ -853,8 +881,9 @@ export class IndexedDbPersistence implements Persistence {
853
881
854
882
if ( currentLeaseIsValid && ! this . isLocalClient ( currentPrimary ) ) {
855
883
if (
856
- ! this . allowTabSynchronization ||
857
- ! currentPrimary ! . allowTabSynchronization
884
+ ! this . forceOwningTab &&
885
+ ( ! this . allowTabSynchronization ||
886
+ ! currentPrimary ! . allowTabSynchronization )
858
887
) {
859
888
throw new FirestoreError (
860
889
Code . FAILED_PRECONDITION ,
@@ -983,7 +1012,7 @@ export class IndexedDbPersistence implements Persistence {
983
1012
* handler.
984
1013
*/
985
1014
private attachWindowUnloadHook ( ) : void {
986
- if ( typeof this . window . addEventListener === 'function' ) {
1015
+ if ( typeof this . window ? .addEventListener === 'function' ) {
987
1016
this . windowUnloadHandler = ( ) => {
988
1017
// Note: In theory, this should be scheduled on the AsyncQueue since it
989
1018
// accesses internal state. We execute this code directly during shutdown
@@ -1003,10 +1032,10 @@ export class IndexedDbPersistence implements Persistence {
1003
1032
private detachWindowUnloadHook ( ) : void {
1004
1033
if ( this . windowUnloadHandler ) {
1005
1034
debugAssert (
1006
- typeof this . window . removeEventListener === 'function' ,
1035
+ typeof this . window ? .removeEventListener === 'function' ,
1007
1036
"Expected 'window.removeEventListener' to be a function"
1008
1037
) ;
1009
- this . window . removeEventListener ( 'unload' , this . windowUnloadHandler ) ;
1038
+ this . window ! . removeEventListener ( 'unload' , this . windowUnloadHandler ) ;
1010
1039
this . windowUnloadHandler = null ;
1011
1040
}
1012
1041
}
@@ -1019,8 +1048,9 @@ export class IndexedDbPersistence implements Persistence {
1019
1048
private isClientZombied ( clientId : ClientId ) : boolean {
1020
1049
try {
1021
1050
const isZombied =
1022
- this . webStorage . getItem ( this . zombiedClientLocalStorageKey ( clientId ) ) !==
1023
- null ;
1051
+ this . webStorage ?. getItem (
1052
+ this . zombiedClientLocalStorageKey ( clientId )
1053
+ ) !== null ;
1024
1054
logDebug (
1025
1055
LOG_TAG ,
1026
1056
`Client '${ clientId } ' ${
@@ -1040,6 +1070,9 @@ export class IndexedDbPersistence implements Persistence {
1040
1070
* clients are ignored during primary tab selection.
1041
1071
*/
1042
1072
private markClientZombied ( ) : void {
1073
+ if ( ! this . webStorage ) {
1074
+ return ;
1075
+ }
1043
1076
try {
1044
1077
this . webStorage . setItem (
1045
1078
this . zombiedClientLocalStorageKey ( this . clientId ) ,
@@ -1053,6 +1086,9 @@ export class IndexedDbPersistence implements Persistence {
1053
1086
1054
1087
/** Removes the zombied client entry if it exists. */
1055
1088
private removeClientZombiedEntry ( ) : void {
1089
+ if ( ! this . webStorage ) {
1090
+ return ;
1091
+ }
1056
1092
try {
1057
1093
this . webStorage . removeItem (
1058
1094
this . zombiedClientLocalStorageKey ( this . clientId )
0 commit comments