Skip to content

Commit 5547861

Browse files
author
Brian Chen
authored
Add experimentalForceOwningTab for Web Worker support (#2197)
1 parent 31f7afc commit 5547861

13 files changed

+155
-45
lines changed

packages/firebase/index.d.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -7760,7 +7760,7 @@ declare namespace firebase.firestore {
77607760
export interface PersistenceSettings {
77617761
/**
77627762
* Whether to synchronize the in-memory state of multiple tabs. Setting this
7763-
* to 'true' in all open tabs enables shared access to local persistence,
7763+
* to `true` in all open tabs enables shared access to local persistence,
77647764
* shared execution of queries and latency-compensated local document updates
77657765
* across all connected instances.
77667766
*
@@ -7772,14 +7772,27 @@ declare namespace firebase.firestore {
77727772

77737773
/**
77747774
* Whether to synchronize the in-memory state of multiple tabs. Setting this
7775-
* to 'true' in all open tabs enables shared access to local persistence,
7775+
* to `true` in all open tabs enables shared access to local persistence,
77767776
* shared execution of queries and latency-compensated local document updates
77777777
* across all connected instances.
77787778
*
7779-
* @deprecated This setting is deprecated. To enabled synchronization between
7779+
* @deprecated This setting is deprecated. To enable synchronization between
77807780
* multiple tabs, please use `synchronizeTabs: true` instead.
77817781
*/
77827782
experimentalTabSynchronization?: boolean;
7783+
7784+
/**
7785+
* Whether to force enable persistence for the client. This cannot be used
7786+
* with `synchronizeTabs:true` and is primarily intended for use with Web
7787+
* Workers. Setting this to `true` will enable persistence, but cause other
7788+
* tabs using persistence to fail.
7789+
*
7790+
* This setting may be removed in a future release. If you find yourself
7791+
* using it for a specific use case or run into any issues, please tell us
7792+
* about it in
7793+
* https://github.com/firebase/firebase-js-sdk/issues/983.
7794+
*/
7795+
experimentalForceOwningTab?: boolean;
77837796
}
77847797

77857798
export type LogLevel = 'debug' | 'error' | 'silent';

packages/firestore-types/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface Settings {
3535
export interface PersistenceSettings {
3636
synchronizeTabs?: boolean;
3737
experimentalTabSynchronization?: boolean;
38+
experimentalForceOwningTab?: boolean;
3839
}
3940

4041
export type LogLevel =

packages/firestore/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
in IndexedDB. Previously, these errors crashed the client.
1515
- [fixed] Fixed a source of IndexedDB-related crashes for tabs that receive
1616
multi-tab notifications while the file system is locked.
17+
- [feature] Added an `experimentalForceOwningTab` setting that can be used to
18+
enable persistence in environments without LocalStorage, which allows
19+
persistence to be used in Web Workers (#983).
1720

1821
# 1.10.2
1922
- [fixed] Temporarily reverted the use of window.crypto to generate document

packages/firestore/src/api/database.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
384384
}
385385

386386
let synchronizeTabs = false;
387+
let experimentalForceOwningTab = false;
387388

388389
if (settings) {
389390
if (settings.experimentalTabSynchronization !== undefined) {
@@ -395,12 +396,24 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
395396
settings.synchronizeTabs ??
396397
settings.experimentalTabSynchronization ??
397398
DEFAULT_SYNCHRONIZE_TABS;
399+
400+
experimentalForceOwningTab = settings.experimentalForceOwningTab
401+
? settings.experimentalForceOwningTab
402+
: false;
403+
404+
if (synchronizeTabs && experimentalForceOwningTab) {
405+
throw new FirestoreError(
406+
Code.INVALID_ARGUMENT,
407+
"The 'experimentalForceOwningTab' setting cannot be used with 'synchronizeTabs'."
408+
);
409+
}
398410
}
399411

400412
return this.configureClient(this._componentProvider, {
401413
durable: true,
402414
cacheSizeBytes: this._settings.cacheSizeBytes,
403-
synchronizeTabs
415+
synchronizeTabs,
416+
forceOwningTab: experimentalForceOwningTab
404417
});
405418
}
406419

packages/firestore/src/core/component_provider.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider {
259259
LruParams.withCacheSize(cfg.persistenceSettings.cacheSizeBytes),
260260
cfg.asyncQueue,
261261
serializer,
262-
this.sharedClientState
262+
this.sharedClientState,
263+
cfg.persistenceSettings.forceOwningTab
263264
);
264265
}
265266

packages/firestore/src/core/firestore_client.ts

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export type PersistenceSettings =
6565
readonly durable: true;
6666
readonly cacheSizeBytes: number;
6767
readonly synchronizeTabs: boolean;
68+
readonly forceOwningTab: boolean;
6869
};
6970

7071
/**

packages/firestore/src/local/indexeddb_persistence.ts

+60-24
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ const CLIENT_METADATA_REFRESH_INTERVAL_MS = 4000;
104104
const PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG =
105105
'Failed to obtain exclusive access to the persistence layer. ' +
106106
'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.';
108110
const UNSUPPORTED_PLATFORM_ERROR_MSG =
109111
'This platform is either missing' +
110112
' IndexedDB or is known to have an incomplete implementation. Offline' +
@@ -190,7 +192,7 @@ export class IndexedDbPersistence implements Persistence {
190192
static MAIN_DATABASE = 'main';
191193

192194
private readonly document: Document | null;
193-
private readonly window: Window;
195+
private readonly window: Window | null;
194196

195197
// Technically `simpleDb` should be `| undefined` because it is
196198
// initialized asynchronously by start(), but that would be more misleading
@@ -225,18 +227,29 @@ export class IndexedDbPersistence implements Persistence {
225227
private readonly targetCache: IndexedDbTargetCache;
226228
private readonly indexManager: IndexedDbIndexManager;
227229
private readonly remoteDocumentCache: IndexedDbRemoteDocumentCache;
228-
private readonly webStorage: Storage;
230+
private readonly webStorage: Storage | null;
229231
readonly referenceDelegate: IndexedDbLruDelegate;
230232

231233
constructor(
234+
/**
235+
* Whether to synchronize the in-memory state of multiple tabs and share
236+
* access to local persistence.
237+
*/
232238
private readonly allowTabSynchronization: boolean,
239+
233240
private readonly persistenceKey: string,
234241
private readonly clientId: ClientId,
235242
platform: Platform,
236243
lruParams: LruParams,
237244
private readonly queue: AsyncQueue,
238245
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
240253
) {
241254
if (!IndexedDbPersistence.isAvailable()) {
242255
throw new FirestoreError(
@@ -258,14 +271,19 @@ export class IndexedDbPersistence implements Persistence {
258271
this.serializer,
259272
this.indexManager
260273
);
274+
this.window = platform.window;
261275
if (platform.window && platform.window.localStorage) {
262-
this.window = platform.window;
263-
this.webStorage = this.window.localStorage;
276+
this.webStorage = platform.window.localStorage;
264277
} 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+
}
269287
}
270288
}
271289

@@ -287,7 +305,9 @@ export class IndexedDbPersistence implements Persistence {
287305
this.simpleDb = db;
288306
// NOTE: This is expected to fail sometimes (in the case of another tab already
289307
// 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+
);
291311
})
292312
.then(() => {
293313
if (!this.isPrimary && !this.allowTabSynchronization) {
@@ -384,7 +404,9 @@ export class IndexedDbPersistence implements Persistence {
384404
* primary state listener if the client either newly obtained or released its
385405
* primary lease.
386406
*/
387-
private updateClientMetadataAndTryBecomePrimary(): Promise<void> {
407+
private updateClientMetadataAndTryBecomePrimary(
408+
forceOwningTab = false
409+
): Promise<void> {
388410
return this.runTransaction(
389411
'updateClientMetadataAndTryBecomePrimary',
390412
'readwrite',
@@ -519,11 +541,13 @@ export class IndexedDbPersistence implements Persistence {
519541
// Ideally we'd delete the IndexedDb and LocalStorage zombie entries for
520542
// the client atomically, but we can't. So we opt to delete the IndexedDb
521543
// 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+
}
527551
}
528552
}
529553

@@ -558,6 +582,9 @@ export class IndexedDbPersistence implements Persistence {
558582
private canActAsPrimary(
559583
txn: PersistenceTransaction
560584
): PersistencePromise<boolean> {
585+
if (this.forceOwningTab) {
586+
return PersistencePromise.resolve<boolean>(true);
587+
}
561588
const store = primaryClientStore(txn);
562589
return store
563590
.get(DbPrimaryClient.key)
@@ -578,6 +605,7 @@ export class IndexedDbPersistence implements Persistence {
578605
// foreground.
579606
// - every clients network is disabled and no other client's tab is in
580607
// the foreground.
608+
// - the `forceOwningTab` setting was passed in.
581609
if (currentLeaseIsValid) {
582610
if (this.isLocalClient(currentPrimary) && this.networkEnabled) {
583611
return true;
@@ -853,8 +881,9 @@ export class IndexedDbPersistence implements Persistence {
853881

854882
if (currentLeaseIsValid && !this.isLocalClient(currentPrimary)) {
855883
if (
856-
!this.allowTabSynchronization ||
857-
!currentPrimary!.allowTabSynchronization
884+
!this.forceOwningTab &&
885+
(!this.allowTabSynchronization ||
886+
!currentPrimary!.allowTabSynchronization)
858887
) {
859888
throw new FirestoreError(
860889
Code.FAILED_PRECONDITION,
@@ -983,7 +1012,7 @@ export class IndexedDbPersistence implements Persistence {
9831012
* handler.
9841013
*/
9851014
private attachWindowUnloadHook(): void {
986-
if (typeof this.window.addEventListener === 'function') {
1015+
if (typeof this.window?.addEventListener === 'function') {
9871016
this.windowUnloadHandler = () => {
9881017
// Note: In theory, this should be scheduled on the AsyncQueue since it
9891018
// accesses internal state. We execute this code directly during shutdown
@@ -1003,10 +1032,10 @@ export class IndexedDbPersistence implements Persistence {
10031032
private detachWindowUnloadHook(): void {
10041033
if (this.windowUnloadHandler) {
10051034
debugAssert(
1006-
typeof this.window.removeEventListener === 'function',
1035+
typeof this.window?.removeEventListener === 'function',
10071036
"Expected 'window.removeEventListener' to be a function"
10081037
);
1009-
this.window.removeEventListener('unload', this.windowUnloadHandler);
1038+
this.window!.removeEventListener('unload', this.windowUnloadHandler);
10101039
this.windowUnloadHandler = null;
10111040
}
10121041
}
@@ -1019,8 +1048,9 @@ export class IndexedDbPersistence implements Persistence {
10191048
private isClientZombied(clientId: ClientId): boolean {
10201049
try {
10211050
const isZombied =
1022-
this.webStorage.getItem(this.zombiedClientLocalStorageKey(clientId)) !==
1023-
null;
1051+
this.webStorage?.getItem(
1052+
this.zombiedClientLocalStorageKey(clientId)
1053+
) !== null;
10241054
logDebug(
10251055
LOG_TAG,
10261056
`Client '${clientId}' ${
@@ -1040,6 +1070,9 @@ export class IndexedDbPersistence implements Persistence {
10401070
* clients are ignored during primary tab selection.
10411071
*/
10421072
private markClientZombied(): void {
1073+
if (!this.webStorage) {
1074+
return;
1075+
}
10431076
try {
10441077
this.webStorage.setItem(
10451078
this.zombiedClientLocalStorageKey(this.clientId),
@@ -1053,6 +1086,9 @@ export class IndexedDbPersistence implements Persistence {
10531086

10541087
/** Removes the zombied client entry if it exists. */
10551088
private removeClientZombiedEntry(): void {
1089+
if (!this.webStorage) {
1090+
return;
1091+
}
10561092
try {
10571093
this.webStorage.removeItem(
10581094
this.zombiedClientLocalStorageKey(this.clientId)

packages/firestore/src/local/simple_db.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class SimpleDb {
7878
// suggests IE9 and older WebKit browsers handle upgrade
7979
// differently. They expect setVersion, as described here:
8080
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
81-
const request = window.indexedDB.open(name, version);
81+
const request = indexedDB.open(name, version);
8282

8383
request.onsuccess = (event: Event) => {
8484
const db = (event.target as IDBOpenDBRequest).result;
@@ -145,21 +145,14 @@ export class SimpleDb {
145145

146146
/** Returns true if IndexedDB is available in the current environment. */
147147
static isAvailable(): boolean {
148-
if (typeof window === 'undefined' || window.indexedDB == null) {
148+
if (typeof indexedDB === 'undefined') {
149149
return false;
150150
}
151151

152152
if (SimpleDb.isMockPersistence()) {
153153
return true;
154154
}
155155

156-
// In some Node environments, `window` is defined, but `window.navigator` is
157-
// not. We don't support IndexedDB persistence in Node if the
158-
// isMockPersistence() check above returns false.
159-
if (window.navigator === undefined) {
160-
return false;
161-
}
162-
163156
// We extensively use indexed array values and compound keys,
164157
// which IE and Edge do not support. However, they still have indexedDB
165158
// defined on the window, so we need to check for them here and make sure

0 commit comments

Comments
 (0)