Skip to content

firestore: minor refactor of listener registration of "versionchange" indexedb events #9087

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/long-pets-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/firestore': patch
---

Internal listener registration change for IndexedDB "versionchange" events.
22 changes: 17 additions & 5 deletions packages/firestore/src/core/firestore_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,23 @@ export async function setOfflineComponentProvider(
}
});

// When a user calls clearPersistence() in one client, all other clients
// need to be terminated to allow the delete to succeed.
offlineComponentProvider.persistence.setDatabaseDeletedListener(() =>
client.terminate()
);
offlineComponentProvider.persistence.setDatabaseDeletedListener(() => {
logWarn('Terminating Firestore due to IndexedDb database deletion');
client
.terminate()
.then(() => {
logDebug(
'Terminating Firestore due to IndexedDb database deletion ' +
'completed successfully'
);
})
.catch(error => {
logWarn(
'Terminating Firestore due to IndexedDb database deletion failed',
error
);
});
});

client._offlineComponents = offlineComponentProvider;
}
Expand Down
29 changes: 19 additions & 10 deletions packages/firestore/src/local/indexeddb_persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ import { IndexedDbTargetCache } from './indexeddb_target_cache';
import { getStore, IndexedDbTransaction } from './indexeddb_transaction';
import { LocalSerializer } from './local_serializer';
import { LruParams } from './lru_garbage_collector';
import { Persistence, PrimaryStateListener } from './persistence';
import {
DatabaseDeletedListener,
Persistence,
PrimaryStateListener
} from './persistence';
import { PersistencePromise } from './persistence_promise';
import {
PersistenceTransaction,
Expand Down Expand Up @@ -324,20 +328,25 @@ export class IndexedDbPersistence implements Persistence {
}

/**
* Registers a listener that gets called when the database receives a
* version change event indicating that it has deleted.
* Registers a listener that gets called when the underlying database receives
* an event indicating that it either has been deleted or is pending deletion
* and must be closed.
*
* For example, this callback will be called in the case that multi-tab
* IndexedDB persistence is in use and another tab calls
* clearIndexedDbPersistence(). In that case, this Firestore instance must
* close its IndexedDB connection in order to allow the deletion initiated by
* the other tab to proceed.
*
* This method may only be called once; subsequent invocations will result in
* an exception, refusing to supersede the previously-registered listener.
*
* PORTING NOTE: This is only used for Web multi-tab.
*/
setDatabaseDeletedListener(
databaseDeletedListener: () => Promise<void>
databaseDeletedListener: DatabaseDeletedListener
): void {
this.simpleDb.setVersionChangeListener(async event => {
// Check if an attempt is made to delete IndexedDB.
if (event.newVersion === null) {
await databaseDeletedListener();
}
});
this.simpleDb.setDatabaseDeletedListener(databaseDeletedListener);
}

/**
Expand Down
18 changes: 15 additions & 3 deletions packages/firestore/src/local/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ export interface ReferenceDelegate {
): PersistencePromise<void>;
}

export type DatabaseDeletedListener = () => void;

/**
* Persistence is the lowest-level shared interface to persistent storage in
* Firestore.
Expand Down Expand Up @@ -151,13 +153,23 @@ export interface Persistence {
shutdown(): Promise<void>;

/**
* Registers a listener that gets called when the database receives a
* version change event indicating that it has deleted.
* Registers a listener that gets called when the underlying database receives
* an event indicating that it either has been deleted or is pending deletion
* and must be closed.
*
* For example, this callback will be called in the case that multi-tab
* IndexedDB persistence is in use and another tab calls
* clearIndexedDbPersistence(). In that case, this Firestore instance must
* close its IndexedDB connection in order to allow the deletion initiated by
* the other tab to proceed.
*
* This method may only be called once; subsequent invocations will result in
* an exception, refusing to supersede the previously-registered listener.
*
* PORTING NOTE: This is only used for Web multi-tab.
*/
setDatabaseDeletedListener(
databaseDeletedListener: () => Promise<void>
databaseDeletedListener: DatabaseDeletedListener
): void;

/**
Expand Down
38 changes: 26 additions & 12 deletions packages/firestore/src/local/simple_db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { getGlobal, getUA, isIndexedDBAvailable } from '@firebase/util';

import { debugAssert } from '../util/assert';
import { Code, FirestoreError } from '../util/error';
import { logDebug, logError } from '../util/log';
import { logDebug, logError, logWarn } from '../util/log';
import { Deferred } from '../util/promise';

import { DatabaseDeletedListener } from './persistence';
import { PersistencePromise } from './persistence_promise';

// References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal()
Expand Down Expand Up @@ -158,8 +159,8 @@ export class SimpleDbTransaction {
*/
export class SimpleDb {
private db?: IDBDatabase;
private databaseDeletedListener?: DatabaseDeletedListener;
private lastClosedDbVersion: number | null = null;
private versionchangelistener?: (event: IDBVersionChangeEvent) => void;

/** Deletes the specified database. */
static delete(name: string): Promise<void> {
Expand Down Expand Up @@ -392,22 +393,35 @@ export class SimpleDb {
);
}

if (this.versionchangelistener) {
this.db.onversionchange = event => this.versionchangelistener!(event);
}
this.db.addEventListener(
'versionchange',
event => {
// Notify the listener if another tab attempted to delete the IndexedDb
// database, such as by calling clearIndexedDbPersistence().
if (event.newVersion === null) {
logWarn(
`Received "versionchange" event with newVersion===null; ` +
'notifying the registered DatabaseDeletedListener, if any'
);
this.databaseDeletedListener?.();
}
},
{ passive: true }
);

return this.db;
}

setVersionChangeListener(
versionChangeListener: (event: IDBVersionChangeEvent) => void
setDatabaseDeletedListener(
databaseDeletedListener: DatabaseDeletedListener
): void {
this.versionchangelistener = versionChangeListener;
if (this.db) {
this.db.onversionchange = (event: IDBVersionChangeEvent) => {
return versionChangeListener(event);
};
if (this.databaseDeletedListener) {
throw new Error(
'setDatabaseDeletedListener() may only be called once, ' +
'and it has already been called'
);
}
this.databaseDeletedListener = databaseDeletedListener;
}

async runTransaction<T>(
Expand Down
10 changes: 8 additions & 2 deletions packages/firestore/test/unit/specs/spec_test_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,14 @@ abstract class TestRunner {
this.eventManager.onLastRemoteStoreUnlisten =
triggerRemoteStoreUnlisten.bind(null, this.syncEngine);

await this.persistence.setDatabaseDeletedListener(async () => {
await this.shutdown();
this.persistence.setDatabaseDeletedListener(() => {
this.shutdown().catch(error => {
console.warn(
'WARNING: this.shutdown() failed in callback ' +
'specified to persistence.setDatabaseDeletedListener',
error
);
});
});

this.started = true;
Expand Down
Loading