From 43aae1a8f012e5be35feb51fbf288fe5d6ce8d19 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Jun 2025 16:38:13 -0400 Subject: [PATCH 1/4] Attempt #1 at a fix --- .../firestore/src/core/firestore_client.ts | 13 ++- .../src/local/indexeddb_persistence.ts | 18 ++- packages/firestore/src/local/persistence.ts | 21 +++- packages/firestore/src/local/simple_db.ts | 109 +++++++++++++----- 4 files changed, 115 insertions(+), 46 deletions(-) diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index e2aa19aaba8..77ab79c6874 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -232,9 +232,16 @@ 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(reason => { + client.terminate(); + if (reason === "persistence cleared") { + return { reason: `allowing another tab's "clear persistence" attempt to succeed` }; + } else if (reason === "site data cleared") { + return { reason: `protecting against database corruption` }; + } else { + return { reason: `unknown (code: vpfvjqeqvn)` }; + } + }); client._offlineComponents = offlineComponentProvider; } diff --git a/packages/firestore/src/local/indexeddb_persistence.ts b/packages/firestore/src/local/indexeddb_persistence.ts index 57c26ea5baa..5b1c2973499 100644 --- a/packages/firestore/src/local/indexeddb_persistence.ts +++ b/packages/firestore/src/local/indexeddb_persistence.ts @@ -58,7 +58,7 @@ 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 { Persistence, PrimaryStateListener, DatabaseDeletedListener } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { PersistenceTransaction, @@ -324,20 +324,18 @@ 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 database receives an + * event indicating that it has deleted. This could be, for example, another + * tab in multi-tab persistence mode having its `clearIndexedDbPersistence()` + * function called, or a user manually clicking "Clear Site Data" in a + * browser. * * PORTING NOTE: This is only used for Web multi-tab. */ setDatabaseDeletedListener( - databaseDeletedListener: () => Promise + 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); } /** diff --git a/packages/firestore/src/local/persistence.ts b/packages/firestore/src/local/persistence.ts index b014a6479ac..cff54d1ac9a 100644 --- a/packages/firestore/src/local/persistence.ts +++ b/packages/firestore/src/local/persistence.ts @@ -98,6 +98,18 @@ export interface ReferenceDelegate { ): PersistencePromise; } +export type DatabaseDeletedReason = "persistence cleared" | "site data cleared"; + +export const DatabaseDeletedListenerContinueResult: unique symbol = Symbol("DatabaseDeletedListenerContinueResult"); + +export interface DatabaseDeletedListenerAbortResult { + reason: string; +} + +export type DatabaseDeletedListenerResult = typeof DatabaseDeletedListenerContinueResult | DatabaseDeletedListenerAbortResult; + +export type DatabaseDeletedListener = (reason: DatabaseDeletedReason) => DatabaseDeletedListenerResult; + /** * Persistence is the lowest-level shared interface to persistent storage in * Firestore. @@ -151,13 +163,16 @@ export interface Persistence { shutdown(): Promise; /** - * 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 database receives an + * event indicating that it has deleted. This could be, for example, another + * tab in multi-tab persistence mode having its `clearIndexedDbPersistence()` + * function called, or a user manually clicking "Clear Site Data" in a + * browser. * * PORTING NOTE: This is only used for Web multi-tab. */ setDatabaseDeletedListener( - databaseDeletedListener: () => Promise + databaseDeletedListener: DatabaseDeletedListener ): void; /** diff --git a/packages/firestore/src/local/simple_db.ts b/packages/firestore/src/local/simple_db.ts index 6d27702e725..38085c5c495 100644 --- a/packages/firestore/src/local/simple_db.ts +++ b/packages/firestore/src/local/simple_db.ts @@ -15,14 +15,18 @@ * limitations under the License. */ -import { getGlobal, getUA, isIndexedDBAvailable } from '@firebase/util'; +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 { Deferred } from '../util/promise'; +import {debugAssert} from '../util/assert'; +import {Code, FirestoreError} from '../util/error'; +import {logDebug, logError, logWarn} from '../util/log'; +import {Deferred} from '../util/promise'; -import { PersistencePromise } from './persistence_promise'; +import {PersistencePromise} from './persistence_promise'; +import { + type DatabaseDeletedListener, + DatabaseDeletedListenerContinueResult +} from './persistence'; // References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal() /* eslint-disable no-restricted-globals */ @@ -159,7 +163,7 @@ export class SimpleDbTransaction { export class SimpleDb { private db?: IDBDatabase; private lastClosedDbVersion: number | null = null; - private versionchangelistener?: (event: IDBVersionChangeEvent) => void; + private databaseDeletedListener?: DatabaseDeletedListener; /** Deletes the specified database. */ static delete(name: string): Promise { @@ -352,19 +356,35 @@ export class SimpleDb { this.lastClosedDbVersion !== null && this.lastClosedDbVersion !== event.oldVersion ) { - // This thrown error will get passed to the `onerror` callback - // registered above, and will then be propagated correctly. - throw new Error( - `refusing to open IndexedDB database due to potential ` + - `corruption of the IndexedDB database data; this corruption ` + - `could be caused by clicking the "clear site data" button in ` + - `a web browser; try reloading the web page to re-initialize ` + - `the IndexedDB database: ` + - `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + - `event.oldVersion=${event.oldVersion}, ` + - `event.newVersion=${event.newVersion}, ` + - `db.version=${db.version}` + logWarn( + `IndexedDB onupgradeneeded indicates that the ` + + `database contents may have been cleared, such as by clicking ` + + `the "clear site data" button in a browser. This _could_ cause ` + + `corruption of the IndexeDB database data if the clear ` + + `operation happened in the middle of Firestore operations. (` + + `db.name=${db.name}, ` + + `db.version=${db.version}, ` + + `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + + `event.oldVersion=${event.oldVersion}, ` + + `event.newVersion=${event.newVersion}` + + `)` ); + if (this.databaseDeletedListener) { + const listenerResult = this.databaseDeletedListener("site data cleared"); + if (listenerResult !== DatabaseDeletedListenerContinueResult) { + throw new Error( + `Refusing to open IndexedDB database after having been ` + + `cleared, such as by clicking the "clear site data" button ` + + `in a web browser: ${listenerResult.reason} (` + + `db.name=${db.name}, ` + + `db.version=${db.version}, ` + + `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + + `event.oldVersion=${event.oldVersion}, ` + + `event.newVersion=${event.newVersion}` + + `)` + ); + } + } } this.schemaConverter .createOrUpgrade( @@ -387,27 +407,56 @@ export class SimpleDb { event => { const db = event.target as IDBDatabase; this.lastClosedDbVersion = db.version; + logWarn( + `IndexedDB "close" event received, indicating abnormal database ` + + `closure. The database contents may have been cleared, such as ` + + `by clicking the "clear site data" button in a browser. ` + + `Re-opening the IndexedDB database may fail to avoid IndexedDB ` + + `database data corruption (` + + `db.name=${db.name}, ` + + `db.version=${db.version}` + + `)` + ); }, { passive: true } ); } - if (this.versionchangelistener) { - this.db.onversionchange = event => this.versionchangelistener!(event); - } + this.db.addEventListener("versionchange", event => { + const db = event.target as IDBDatabase; + if (event.newVersion !== null) { + return; + } + + logDebug( + `IndexedDB "versionchange" event with newVersion===null received; ` + + `this is likely because clearIndexedDbPersistence() was called, ` + + `possibly in another tab if multi-tab persistence is enabled.` + ); + if (this.databaseDeletedListener) { + const listenerResult = this.databaseDeletedListener("persistence cleared"); + if (listenerResult !== DatabaseDeletedListenerContinueResult) { + logWarn( + `Closing IndexedDB database "${db.name}" in response to ` + + `"versionchange" event with newVersion===null: ` + + `${listenerResult.reason}` + ); + db.close(); + if (db === this.db) { + this.db = undefined; + } + } + } + }, {passive:true}); return this.db; } - setVersionChangeListener( - versionChangeListener: (event: IDBVersionChangeEvent) => void - ): void { - this.versionchangelistener = versionChangeListener; - if (this.db) { - this.db.onversionchange = (event: IDBVersionChangeEvent) => { - return versionChangeListener(event); - }; + setDatabaseDeletedListener(databaseDeletedListener: DatabaseDeletedListener): void { + if (this.databaseDeletedListener) { + throw new Error("setOnDatabaseDeletedListener() has already been called"); } + this.databaseDeletedListener = databaseDeletedListener; } async runTransaction( From eaa8ae828a7dbc8710c51e2cd59071aeca06f3d1 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Jun 2025 17:02:02 -0400 Subject: [PATCH 2/4] fix spec_test_runner.ts --- packages/firestore/src/core/firestore_client.ts | 12 +++++------- packages/firestore/src/local/persistence.ts | 11 +++++++---- packages/firestore/src/local/simple_db.ts | 13 +++++-------- .../firestore/test/unit/specs/spec_test_runner.ts | 6 ++++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 77ab79c6874..71371cdfd41 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -33,7 +33,7 @@ import { localStoreReadDocument, localStoreSetIndexAutoCreationEnabled } from '../local/local_store_impl'; -import { Persistence } from '../local/persistence'; +import { Persistence, DatabaseDeletedListenerAbortResult, DatabaseDeletedListenerContinueResult } from '../local/persistence'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { FieldIndex } from '../model/field_index'; @@ -234,12 +234,10 @@ export async function setOfflineComponentProvider( // need to be terminated to allow the delete to succeed. offlineComponentProvider.persistence.setDatabaseDeletedListener(reason => { client.terminate(); - if (reason === "persistence cleared") { - return { reason: `allowing another tab's "clear persistence" attempt to succeed` }; - } else if (reason === "site data cleared") { - return { reason: `protecting against database corruption` }; - } else { - return { reason: `unknown (code: vpfvjqeqvn)` }; + if (reason === "site data cleared") { + return new DatabaseDeletedListenerAbortResult("protecting against database corruption"); + } else { + return new DatabaseDeletedListenerContinueResult(); } }); diff --git a/packages/firestore/src/local/persistence.ts b/packages/firestore/src/local/persistence.ts index cff54d1ac9a..8bc33b34406 100644 --- a/packages/firestore/src/local/persistence.ts +++ b/packages/firestore/src/local/persistence.ts @@ -100,13 +100,16 @@ export interface ReferenceDelegate { export type DatabaseDeletedReason = "persistence cleared" | "site data cleared"; -export const DatabaseDeletedListenerContinueResult: unique symbol = Symbol("DatabaseDeletedListenerContinueResult"); +export class DatabaseDeletedListenerContinueResult { + readonly type = "continue" as const; +} -export interface DatabaseDeletedListenerAbortResult { - reason: string; +export class DatabaseDeletedListenerAbortResult { + readonly type = "abort" as const; + constructor(readonly abortReason: string) {} } -export type DatabaseDeletedListenerResult = typeof DatabaseDeletedListenerContinueResult | DatabaseDeletedListenerAbortResult; +export type DatabaseDeletedListenerResult = DatabaseDeletedListenerContinueResult | DatabaseDeletedListenerAbortResult; export type DatabaseDeletedListener = (reason: DatabaseDeletedReason) => DatabaseDeletedListenerResult; diff --git a/packages/firestore/src/local/simple_db.ts b/packages/firestore/src/local/simple_db.ts index 38085c5c495..2143735a156 100644 --- a/packages/firestore/src/local/simple_db.ts +++ b/packages/firestore/src/local/simple_db.ts @@ -23,10 +23,7 @@ import {logDebug, logError, logWarn} from '../util/log'; import {Deferred} from '../util/promise'; import {PersistencePromise} from './persistence_promise'; -import { - type DatabaseDeletedListener, - DatabaseDeletedListenerContinueResult -} from './persistence'; +import {type DatabaseDeletedListener} from './persistence'; // References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal() /* eslint-disable no-restricted-globals */ @@ -371,11 +368,11 @@ export class SimpleDb { ); if (this.databaseDeletedListener) { const listenerResult = this.databaseDeletedListener("site data cleared"); - if (listenerResult !== DatabaseDeletedListenerContinueResult) { + if (listenerResult.type !== "continue") { throw new Error( `Refusing to open IndexedDB database after having been ` + `cleared, such as by clicking the "clear site data" button ` + - `in a web browser: ${listenerResult.reason} (` + + `in a web browser: ${listenerResult.abortReason} (` + `db.name=${db.name}, ` + `db.version=${db.version}, ` + `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + @@ -435,11 +432,11 @@ export class SimpleDb { ); if (this.databaseDeletedListener) { const listenerResult = this.databaseDeletedListener("persistence cleared"); - if (listenerResult !== DatabaseDeletedListenerContinueResult) { + if (listenerResult.type !== "continue") { logWarn( `Closing IndexedDB database "${db.name}" in response to ` + `"versionchange" event with newVersion===null: ` + - `${listenerResult.reason}` + `${listenerResult.abortReason}` ); db.close(); if (db === this.db) { diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index 51d2229b8a1..e7ac0689453 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -77,6 +77,7 @@ import { SCHEMA_VERSION } from '../../../src/local/indexeddb_schema'; import { SchemaConverter } from '../../../src/local/indexeddb_schema_converter'; +import { DatabaseDeletedListenerContinueResult } from '../../../src/local/persistence'; import { DbPrimaryClientKey, DbPrimaryClientStore @@ -365,8 +366,9 @@ abstract class TestRunner { this.eventManager.onLastRemoteStoreUnlisten = triggerRemoteStoreUnlisten.bind(null, this.syncEngine); - await this.persistence.setDatabaseDeletedListener(async () => { - await this.shutdown(); + this.persistence.setDatabaseDeletedListener(() => { + this.shutdown(); + return new DatabaseDeletedListenerContinueResult(); }); this.started = true; From 5f5bbb6c125cc559ccb0399a936c11ead994d2af Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Jun 2025 17:02:22 -0400 Subject: [PATCH 3/4] prettier --- .../firestore/src/core/firestore_client.ts | 14 +- .../src/local/indexeddb_persistence.ts | 6 +- packages/firestore/src/local/persistence.ts | 14 +- packages/firestore/src/local/simple_db.ts | 125 ++++++++++-------- 4 files changed, 91 insertions(+), 68 deletions(-) diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 71371cdfd41..6f062732e21 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -33,7 +33,11 @@ import { localStoreReadDocument, localStoreSetIndexAutoCreationEnabled } from '../local/local_store_impl'; -import { Persistence, DatabaseDeletedListenerAbortResult, DatabaseDeletedListenerContinueResult } from '../local/persistence'; +import { + Persistence, + DatabaseDeletedListenerAbortResult, + DatabaseDeletedListenerContinueResult +} from '../local/persistence'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { FieldIndex } from '../model/field_index'; @@ -234,9 +238,11 @@ export async function setOfflineComponentProvider( // need to be terminated to allow the delete to succeed. offlineComponentProvider.persistence.setDatabaseDeletedListener(reason => { client.terminate(); - if (reason === "site data cleared") { - return new DatabaseDeletedListenerAbortResult("protecting against database corruption"); - } else { + if (reason === 'site data cleared') { + return new DatabaseDeletedListenerAbortResult( + 'protecting against database corruption' + ); + } else { return new DatabaseDeletedListenerContinueResult(); } }); diff --git a/packages/firestore/src/local/indexeddb_persistence.ts b/packages/firestore/src/local/indexeddb_persistence.ts index 5b1c2973499..3a845732674 100644 --- a/packages/firestore/src/local/indexeddb_persistence.ts +++ b/packages/firestore/src/local/indexeddb_persistence.ts @@ -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, DatabaseDeletedListener } from './persistence'; +import { + Persistence, + PrimaryStateListener, + DatabaseDeletedListener +} from './persistence'; import { PersistencePromise } from './persistence_promise'; import { PersistenceTransaction, diff --git a/packages/firestore/src/local/persistence.ts b/packages/firestore/src/local/persistence.ts index 8bc33b34406..2d174ebc651 100644 --- a/packages/firestore/src/local/persistence.ts +++ b/packages/firestore/src/local/persistence.ts @@ -98,20 +98,24 @@ export interface ReferenceDelegate { ): PersistencePromise; } -export type DatabaseDeletedReason = "persistence cleared" | "site data cleared"; +export type DatabaseDeletedReason = 'persistence cleared' | 'site data cleared'; export class DatabaseDeletedListenerContinueResult { - readonly type = "continue" as const; + readonly type = 'continue' as const; } export class DatabaseDeletedListenerAbortResult { - readonly type = "abort" as const; + readonly type = 'abort' as const; constructor(readonly abortReason: string) {} } -export type DatabaseDeletedListenerResult = DatabaseDeletedListenerContinueResult | DatabaseDeletedListenerAbortResult; +export type DatabaseDeletedListenerResult = + | DatabaseDeletedListenerContinueResult + | DatabaseDeletedListenerAbortResult; -export type DatabaseDeletedListener = (reason: DatabaseDeletedReason) => DatabaseDeletedListenerResult; +export type DatabaseDeletedListener = ( + reason: DatabaseDeletedReason +) => DatabaseDeletedListenerResult; /** * Persistence is the lowest-level shared interface to persistent storage in diff --git a/packages/firestore/src/local/simple_db.ts b/packages/firestore/src/local/simple_db.ts index 2143735a156..43f1e84334e 100644 --- a/packages/firestore/src/local/simple_db.ts +++ b/packages/firestore/src/local/simple_db.ts @@ -15,15 +15,15 @@ * limitations under the License. */ -import {getGlobal, getUA, isIndexedDBAvailable} from '@firebase/util'; +import { getGlobal, getUA, isIndexedDBAvailable } from '@firebase/util'; -import {debugAssert} from '../util/assert'; -import {Code, FirestoreError} from '../util/error'; -import {logDebug, logError, logWarn} from '../util/log'; -import {Deferred} from '../util/promise'; +import { debugAssert } from '../util/assert'; +import { Code, FirestoreError } from '../util/error'; +import { logDebug, logError, logWarn } from '../util/log'; +import { Deferred } from '../util/promise'; -import {PersistencePromise} from './persistence_promise'; -import {type DatabaseDeletedListener} from './persistence'; +import { PersistencePromise } from './persistence_promise'; +import { type DatabaseDeletedListener } from './persistence'; // References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal() /* eslint-disable no-restricted-globals */ @@ -355,30 +355,31 @@ export class SimpleDb { ) { logWarn( `IndexedDB onupgradeneeded indicates that the ` + - `database contents may have been cleared, such as by clicking ` + - `the "clear site data" button in a browser. This _could_ cause ` + - `corruption of the IndexeDB database data if the clear ` + - `operation happened in the middle of Firestore operations. (` + - `db.name=${db.name}, ` + - `db.version=${db.version}, ` + - `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + - `event.oldVersion=${event.oldVersion}, ` + - `event.newVersion=${event.newVersion}` + - `)` + `database contents may have been cleared, such as by clicking ` + + `the "clear site data" button in a browser. This _could_ cause ` + + `corruption of the IndexeDB database data if the clear ` + + `operation happened in the middle of Firestore operations. (` + + `db.name=${db.name}, ` + + `db.version=${db.version}, ` + + `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + + `event.oldVersion=${event.oldVersion}, ` + + `event.newVersion=${event.newVersion}` + + `)` ); if (this.databaseDeletedListener) { - const listenerResult = this.databaseDeletedListener("site data cleared"); - if (listenerResult.type !== "continue") { + const listenerResult = + this.databaseDeletedListener('site data cleared'); + if (listenerResult.type !== 'continue') { throw new Error( `Refusing to open IndexedDB database after having been ` + - `cleared, such as by clicking the "clear site data" button ` + - `in a web browser: ${listenerResult.abortReason} (` + - `db.name=${db.name}, ` + - `db.version=${db.version}, ` + - `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + - `event.oldVersion=${event.oldVersion}, ` + - `event.newVersion=${event.newVersion}` + - `)` + `cleared, such as by clicking the "clear site data" button ` + + `in a web browser: ${listenerResult.abortReason} (` + + `db.name=${db.name}, ` + + `db.version=${db.version}, ` + + `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + + `event.oldVersion=${event.oldVersion}, ` + + `event.newVersion=${event.newVersion}` + + `)` ); } } @@ -406,52 +407,60 @@ export class SimpleDb { this.lastClosedDbVersion = db.version; logWarn( `IndexedDB "close" event received, indicating abnormal database ` + - `closure. The database contents may have been cleared, such as ` + - `by clicking the "clear site data" button in a browser. ` + - `Re-opening the IndexedDB database may fail to avoid IndexedDB ` + - `database data corruption (` + - `db.name=${db.name}, ` + - `db.version=${db.version}` + - `)` + `closure. The database contents may have been cleared, such as ` + + `by clicking the "clear site data" button in a browser. ` + + `Re-opening the IndexedDB database may fail to avoid IndexedDB ` + + `database data corruption (` + + `db.name=${db.name}, ` + + `db.version=${db.version}` + + `)` ); }, { passive: true } ); } - this.db.addEventListener("versionchange", event => { - const db = event.target as IDBDatabase; - if (event.newVersion !== null) { - return; - } + this.db.addEventListener( + 'versionchange', + event => { + const db = event.target as IDBDatabase; + if (event.newVersion !== null) { + return; + } - logDebug( - `IndexedDB "versionchange" event with newVersion===null received; ` + - `this is likely because clearIndexedDbPersistence() was called, ` + - `possibly in another tab if multi-tab persistence is enabled.` - ); - if (this.databaseDeletedListener) { - const listenerResult = this.databaseDeletedListener("persistence cleared"); - if (listenerResult.type !== "continue") { - logWarn( - `Closing IndexedDB database "${db.name}" in response to ` + - `"versionchange" event with newVersion===null: ` + - `${listenerResult.abortReason}` + logDebug( + `IndexedDB "versionchange" event with newVersion===null received; ` + + `this is likely because clearIndexedDbPersistence() was called, ` + + `possibly in another tab if multi-tab persistence is enabled.` + ); + if (this.databaseDeletedListener) { + const listenerResult = this.databaseDeletedListener( + 'persistence cleared' ); - db.close(); - if (db === this.db) { - this.db = undefined; + if (listenerResult.type !== 'continue') { + logWarn( + `Closing IndexedDB database "${db.name}" in response to ` + + `"versionchange" event with newVersion===null: ` + + `${listenerResult.abortReason}` + ); + db.close(); + if (db === this.db) { + this.db = undefined; + } } } - } - }, {passive:true}); + }, + { passive: true } + ); return this.db; } - setDatabaseDeletedListener(databaseDeletedListener: DatabaseDeletedListener): void { + setDatabaseDeletedListener( + databaseDeletedListener: DatabaseDeletedListener + ): void { if (this.databaseDeletedListener) { - throw new Error("setOnDatabaseDeletedListener() has already been called"); + throw new Error('setOnDatabaseDeletedListener() has already been called'); } this.databaseDeletedListener = databaseDeletedListener; } From 013d8213e83571e33a41da4bf3d83d184e30da54 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Jun 2025 17:04:52 -0400 Subject: [PATCH 4/4] yarn lint:fix --- packages/firestore/src/core/firestore_client.ts | 1 + packages/firestore/src/local/simple_db.ts | 2 +- packages/firestore/test/unit/specs/spec_test_runner.ts | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 6f062732e21..0d28f925ce4 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -237,6 +237,7 @@ 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(reason => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises client.terminate(); if (reason === 'site data cleared') { return new DatabaseDeletedListenerAbortResult( diff --git a/packages/firestore/src/local/simple_db.ts b/packages/firestore/src/local/simple_db.ts index 43f1e84334e..421615d6239 100644 --- a/packages/firestore/src/local/simple_db.ts +++ b/packages/firestore/src/local/simple_db.ts @@ -22,8 +22,8 @@ import { Code, FirestoreError } from '../util/error'; import { logDebug, logError, logWarn } from '../util/log'; import { Deferred } from '../util/promise'; -import { PersistencePromise } from './persistence_promise'; import { type DatabaseDeletedListener } from './persistence'; +import { PersistencePromise } from './persistence_promise'; // References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal() /* eslint-disable no-restricted-globals */ diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index e7ac0689453..d4e7f7db250 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -77,7 +77,6 @@ import { SCHEMA_VERSION } from '../../../src/local/indexeddb_schema'; import { SchemaConverter } from '../../../src/local/indexeddb_schema_converter'; -import { DatabaseDeletedListenerContinueResult } from '../../../src/local/persistence'; import { DbPrimaryClientKey, DbPrimaryClientStore @@ -86,6 +85,7 @@ import { LocalStore } from '../../../src/local/local_store'; import { localStoreConfigureFieldIndexes } from '../../../src/local/local_store_impl'; import { LruGarbageCollector } from '../../../src/local/lru_garbage_collector'; import { MemoryLruDelegate } from '../../../src/local/memory_persistence'; +import { DatabaseDeletedListenerContinueResult } from '../../../src/local/persistence'; import { ClientId, SharedClientState @@ -367,6 +367,7 @@ abstract class TestRunner { triggerRemoteStoreUnlisten.bind(null, this.syncEngine); this.persistence.setDatabaseDeletedListener(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this.shutdown(); return new DatabaseDeletedListenerContinueResult(); });