Skip to content

Add experimentalForceOwningTab for Web Worker support #2197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Jun 1, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f0287d4
cleaned up
Sep 26, 2019
15cf32e
fix tests
Sep 27, 2019
66655d0
remove comments
Sep 27, 2019
9ac6e2d
fix michael comments
Sep 30, 2019
d813905
Merge branch 'master' into bc/ww-persist
Oct 21, 2019
347b722
Merge branch 'master' into bc/ww-persist
Nov 12, 2019
91404e3
[AUTOMATED]: Prettier Code Styling
Nov 12, 2019
00c6dc7
Merge branch 'release' into bc/ww-persist
Nov 14, 2019
6dc5e51
Merge branch 'master' into bc/ww-persist
Dec 10, 2019
fef8971
Merge branch 'master' into bc/ww-persist
Feb 13, 2020
8366646
Merge branch 'master' into bc/ww-persist
May 7, 2020
741e88e
update post API council
May 7, 2020
86ca4aa
[AUTOMATED]: Prettier Code Styling
May 7, 2020
16479d4
[AUTOMATED]: License Headers
May 7, 2020
0784827
Merge branch 'master' into bc/ww-persist
May 7, 2020
59d07ee
update error message for primary lease unavailability
May 8, 2020
5dae5b6
Merge branch 'master' into bc/ww-persist
May 13, 2020
71785f2
add changelog
May 13, 2020
ca02909
Merge branch 'master' into bc/ww-persist
May 13, 2020
c5f491a
Merge branch 'master' into bc/ww-persist
May 13, 2020
08f8fb9
resolve comments
May 13, 2020
398c330
Merge branch 'master' into bc/ww-persist
May 18, 2020
6b6db57
Merge branch 'master' into bc/ww-persist
May 18, 2020
b178a75
resolve comments and post-merge issues
May 18, 2020
7d8e41a
update isAvailable() logic + comments round3
May 20, 2020
6d20b20
add forceOwningTab logic back to canActAsPrimary, remove from updateC…
May 21, 2020
016aed8
update forceOwningTab comment
May 27, 2020
31a8772
move forceOwningTab logic up in canActAsPrimary
May 27, 2020
c7cbf9a
Merge branch 'master' into bc/ww-persist
May 28, 2020
be3a0d4
update new test from master
May 28, 2020
47cd1f6
Merge branch 'master' into bc/ww-persist
May 28, 2020
0f45287
s/'true'/
Jun 1, 2020
9cfe34d
remove project.json
Jun 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/firebase/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6226,6 +6226,15 @@ declare namespace firebase.firestore {
* multiple tabs, please use `synchronizeTabs: true` instead.
*/
experimentalTabSynchronization?: boolean;

/**
* Whether to force enabling persistence for the client even if another
* client is already running with persistence enabled. This cannot be used
* with `synchronizeTabs:true` and is primarily intended for use with Web
* Workers. Setting this to 'true' will enable persistence, but cause other
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the intention to use backticks around true ? That would work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

* tabs using persistence to fail.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: If/when we ship this for real we should add more detail about exactly how other tabs will fail (so that developers can potentially write code to detect it, etc.).

*/
experimentalForce?: boolean;
}

export type LogLevel = 'debug' | 'error' | 'silent';
Expand Down
8 changes: 8 additions & 0 deletions packages/firestore-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ export interface PersistenceSettings {
* multiple tabs, please use `synchronizeTabs: true` instead.
*/
experimentalTabSynchronization?: boolean;

/**
* Whether to force enable persistence for the client. This cannot be used
* with `synchronizeTabs:true` and is primarily intended for use with Web
* Workers. Setting this to 'true' will enable persistence, but cause other
* tabs using persistence to fail.
*/
experimentalForce?: boolean;
}

export type LogLevel = 'debug' | 'error' | 'silent';
Expand Down
7 changes: 6 additions & 1 deletion packages/firestore/src/api/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
}

let synchronizeTabs = false;
let experimentalForce = false;

if (settings) {
if (settings.experimentalTabSynchronization !== undefined) {
Expand All @@ -402,12 +403,16 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
: settings.experimentalTabSynchronization,
DEFAULT_SYNCHRONIZE_TABS
);
experimentalForce = settings.experimentalForce
? settings.experimentalForce
: false;
}

return this.configureClient(
new IndexedDbPersistenceSettings(
this._settings.cacheSizeBytes,
synchronizeTabs
synchronizeTabs,
experimentalForce
)
);
}
Expand Down
10 changes: 6 additions & 4 deletions packages/firestore/src/core/firestore_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ const DOM_EXCEPTION_QUOTA_EXCEEDED = 22;
export class IndexedDbPersistenceSettings {
constructor(
readonly cacheSizeBytes: number,
readonly synchronizeTabs: boolean
readonly synchronizeTabs: boolean,
readonly force: boolean
) {}

lruParams(): LruParams {
Expand Down Expand Up @@ -343,7 +344,8 @@ export class FirestoreClient {
) {
throw new FirestoreError(
Code.UNIMPLEMENTED,
'IndexedDB persistence is only available on platforms that support LocalStorage.'
'IndexedDB persistence with synchronizeTabs is only available on ' +
'platforms that support LocalStorage.'
);
}

Expand All @@ -358,7 +360,6 @@ export class FirestoreClient {
user
)
: new MemorySharedClientState();

const persistence = await IndexedDbPersistence.createIndexedDbPersistence(
{
allowTabSynchronization: settings.synchronizeTabs,
Expand All @@ -368,7 +369,8 @@ export class FirestoreClient {
queue: this.asyncQueue,
serializer,
lruParams,
sequenceNumberSyncer: this.sharedClientState
sequenceNumberSyncer: this.sharedClientState,
force: settings.force
}
);

Expand Down
67 changes: 48 additions & 19 deletions packages/firestore/src/local/indexeddb_persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export class IndexedDbPersistence implements Persistence {
queue: AsyncQueue;
serializer: JsonProtoSerializer;
sequenceNumberSyncer: SequenceNumberSyncer;
force: boolean;
}): Promise<IndexedDbPersistence> {
if (!IndexedDbPersistence.isAvailable()) {
throw new FirestoreError(
Expand All @@ -213,14 +214,15 @@ export class IndexedDbPersistence implements Persistence {
options.lruParams,
options.queue,
options.serializer,
options.sequenceNumberSyncer
options.sequenceNumberSyncer,
options.force
);
await persistence.start();
return persistence;
}

private readonly document: Document | null;
private readonly window: Window;
private readonly window: Window | null;

// Technically these types should be `| undefined` because they are
// initialized asynchronously by start(), but that would be more misleading
Expand Down Expand Up @@ -254,7 +256,7 @@ export class IndexedDbPersistence implements Persistence {
private readonly queryCache: IndexedDbQueryCache;
private readonly indexManager: IndexedDbIndexManager;
private readonly remoteDocumentCache: IndexedDbRemoteDocumentCache;
private readonly webStorage: Storage;
private readonly webStorage: Storage | null;
readonly referenceDelegate: IndexedDbLruDelegate;

private constructor(
Expand All @@ -265,7 +267,8 @@ export class IndexedDbPersistence implements Persistence {
lruParams: LruParams,
private readonly queue: AsyncQueue,
serializer: JsonProtoSerializer,
private readonly sequenceNumberSyncer: SequenceNumberSyncer
private readonly sequenceNumberSyncer: SequenceNumberSyncer,
private readonly force: boolean
) {
this.referenceDelegate = new IndexedDbLruDelegate(this, lruParams);
this.dbName = persistenceKey + IndexedDbPersistence.MAIN_DATABASE;
Expand All @@ -281,14 +284,19 @@ export class IndexedDbPersistence implements Persistence {
this.indexManager,
/*keepDocumentChangeLog=*/ this.allowTabSynchronization
);
this.window = platform.window;
if (platform.window && platform.window.localStorage) {
this.window = platform.window;
this.webStorage = this.window.localStorage;
this.webStorage = platform.window.localStorage;
} else {
throw new FirestoreError(
Code.UNIMPLEMENTED,
'IndexedDB persistence is only available on platforms that support LocalStorage.'
);
this.webStorage = null;
if (force === false) {
log.error(
LOG_TAG,
'LocalStorage is unavailable. As a result, persistence may not work ' +
'reliably. In particular enablePersistence() could fail immediately ' +
'after refreshing the page.'
);
}
}
}

Expand All @@ -299,7 +307,6 @@ export class IndexedDbPersistence implements Persistence {
*/
private start(): Promise<void> {
assert(!this.started, 'IndexedDbPersistence double-started!');
assert(this.window !== null, "Expected 'window' to be defined");

return SimpleDb.openOrCreate(
this.dbName,
Expand All @@ -310,7 +317,7 @@ export class IndexedDbPersistence implements Persistence {
this.simpleDb = db;
// NOTE: This is expected to fail sometimes (in the case of another tab already
// having the persistence lock), so it's the first thing we should do.
return this.updateClientMetadataAndTryBecomePrimary();
return this.updateClientMetadataAndTryBecomePrimary(this.force);
})
.then(() => {
this.attachVisibilityHandler();
Expand Down Expand Up @@ -392,7 +399,9 @@ export class IndexedDbPersistence implements Persistence {
* primary state listener if the client either newly obtained or released its
* primary lease.
*/
private updateClientMetadataAndTryBecomePrimary(): Promise<void> {
private updateClientMetadataAndTryBecomePrimary(
force = false
): Promise<void> {
return this.simpleDb.runTransaction('readwrite', ALL_STORES, txn => {
const metadataStore = clientMetadataStore(txn);
return metadataStore
Expand All @@ -417,7 +426,7 @@ export class IndexedDbPersistence implements Persistence {
});
}
})
.next(() => this.canActAsPrimary(txn))
.next(() => (force ? true : this.canActAsPrimary(txn)))
.next(canActAsPrimary => {
const wasPrimary = this.isPrimary;
this.isPrimary = canActAsPrimary;
Expand Down Expand Up @@ -525,11 +534,13 @@ export class IndexedDbPersistence implements Persistence {
// Ideally we'd delete the IndexedDb and LocalStorage zombie entries for
// the client atomically, but we can't. So we opt to delete the IndexedDb
// entries first to avoid potentially reviving a zombied client.
inactiveClients.forEach(inactiveClient => {
this.window.localStorage.removeItem(
this.zombiedClientLocalStorageKey(inactiveClient.clientId)
);
});
if (this.webStorage) {
inactiveClients.forEach(inactiveClient => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Turn this into a for loop and get rid of bang on this.webStorage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

this.webStorage!.removeItem(
this.zombiedClientLocalStorageKey(inactiveClient.clientId)
);
});
}
}
}

Expand Down Expand Up @@ -590,6 +601,9 @@ export class IndexedDbPersistence implements Persistence {
}

if (!this.isLocalClient(currentPrimary)) {
if (this.force) {
return true;
}
if (!currentPrimary!.allowTabSynchronization) {
// Fail the `canActAsPrimary` check if the current leaseholder has
// not opted into multi-tab synchronization. If this happens at
Expand Down Expand Up @@ -974,6 +988,9 @@ export class IndexedDbPersistence implements Persistence {
* handler.
*/
private attachWindowUnloadHook(): void {
if (!this.window) {
return;
}
if (typeof this.window.addEventListener === 'function') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what effect this has on code size, but you could remove some of extra lines in this PR but using optional chaining:

if (typeof this.window?.addEventListener === 'function') {}
this.webStorage?.setValue(...)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed some of the if statements, but the linter complains if you use optional chaining without assigning it to a variable that gets used.

this.windowUnloadHandler = () => {
// Note: In theory, this should be scheduled on the AsyncQueue since it
Expand All @@ -992,6 +1009,9 @@ export class IndexedDbPersistence implements Persistence {
}

private detachWindowUnloadHook(): void {
if (!this.window) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be an assert in the if block below.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

return;
}
if (this.windowUnloadHandler) {
assert(
typeof this.window.removeEventListener === 'function',
Expand All @@ -1008,6 +1028,9 @@ export class IndexedDbPersistence implements Persistence {
* cleanup logic in `shutdown()`.
*/
private isClientZombied(clientId: ClientId): boolean {
if (!this.webStorage) {
return false;
}
try {
const isZombied =
this.webStorage.getItem(this.zombiedClientLocalStorageKey(clientId)) !==
Expand All @@ -1031,6 +1054,9 @@ export class IndexedDbPersistence implements Persistence {
* clients are ignored during primary tab selection.
*/
private markClientZombied(): void {
if (!this.webStorage) {
return;
}
try {
this.webStorage.setItem(
this.zombiedClientLocalStorageKey(this.clientId),
Expand All @@ -1044,6 +1070,9 @@ export class IndexedDbPersistence implements Persistence {

/** Removes the zombied client entry if it exists. */
private removeClientZombiedEntry(): void {
if (!this.webStorage) {
return;
}
try {
this.webStorage.removeItem(
this.zombiedClientLocalStorageKey(this.clientId)
Expand Down
13 changes: 12 additions & 1 deletion packages/firestore/src/local/simple_db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,14 @@ export class SimpleDb {
// suggests IE9 and older WebKit browsers handle upgrade
// differently. They expect setVersion, as described here:
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
const request = window.indexedDB.open(name, version);

let request: IDBOpenDBRequest;
// Support Web Workers, which don't have `window` global variable.
if (typeof indexedDB !== 'undefined') {
request = indexedDB.open(name, version);
} else {
request = window.indexedDB.open(name, version);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since window is usually the global object and we've already checked for the indexedDB global directly, is there a case where we will ever hit this?

Maybe we have a fake window object in node tests or something? Do you know?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I demand answers too :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked into this some more and since window is bound to globalThis in the dom, we won't ever hit this line unless window has been mocked out and the indexedDB global doesn't exist.

Deleted.

}

request.onsuccess = (event: Event) => {
const db = (event.target as IDBOpenDBRequest).result;
Expand Down Expand Up @@ -132,6 +139,10 @@ export class SimpleDb {

/** Returns true if IndexedDB is available in the current environment. */
static isAvailable(): boolean {
// If IndexedDb is available directly (ex: in case of web workers).
if (typeof indexedDB !== 'undefined') {
return true;
}
if (typeof window === 'undefined' || window.indexedDB == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per above, since window is usually the global object and we've already checked for the indexedDB global directly, is there a case where we will ever hit this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need this for cases where indexedDB and window aren't available and return false, or else an environment without a global indexedDB that passes the other checks will incorrectly return true for isAvailable().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we just need to register the IndexedDb shim directly on globalAny in node_persistence.ts.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment for why it's already registered.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that mean that "if (typeof window === 'undefined' || window.indexedDB == null)" is now no longer needed? Is your distinction from your first response here still relevant? if so, we should probably talk this through offline.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per offline conversation, removed the window logic, and flipped the indexedDB check to return false. By returning true automatically when IndexedDB is available, we are skipping all of the UA checks.

Also removed the window.navigator check, to make sure workers aren't rejected early.

return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ async function withCustomPersistence(
queue,
serializer,
lruParams: LruParams.DEFAULT,
sequenceNumberSyncer: MOCK_SEQUENCE_NUMBER_SYNCER
sequenceNumberSyncer: MOCK_SEQUENCE_NUMBER_SYNCER,
force: false
});

await fn(persistence, platform, queue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ export async function testIndexedDbPersistence(
queue,
serializer: JSON_SERIALIZER,
lruParams,
sequenceNumberSyncer: MOCK_SEQUENCE_NUMBER_SYNCER
sequenceNumberSyncer: MOCK_SEQUENCE_NUMBER_SYNCER,
force: false
});
}

Expand Down
3 changes: 2 additions & 1 deletion packages/firestore/test/unit/specs/spec_test_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,8 @@ class IndexedDbTestRunner extends TestRunner {
queue: this.queue,
serializer,
lruParams: LruParams.DEFAULT,
sequenceNumberSyncer: this.sharedClientState
sequenceNumberSyncer: this.sharedClientState,
force: false
});
}

Expand Down