Skip to content

Commit 43aae1a

Browse files
committed
Attempt #1 at a fix
1 parent 0f891d8 commit 43aae1a

File tree

4 files changed

+115
-46
lines changed

4 files changed

+115
-46
lines changed

packages/firestore/src/core/firestore_client.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,16 @@ export async function setOfflineComponentProvider(
232232

233233
// When a user calls clearPersistence() in one client, all other clients
234234
// need to be terminated to allow the delete to succeed.
235-
offlineComponentProvider.persistence.setDatabaseDeletedListener(() =>
236-
client.terminate()
237-
);
235+
offlineComponentProvider.persistence.setDatabaseDeletedListener(reason => {
236+
client.terminate();
237+
if (reason === "persistence cleared") {
238+
return { reason: `allowing another tab's "clear persistence" attempt to succeed` };
239+
} else if (reason === "site data cleared") {
240+
return { reason: `protecting against database corruption` };
241+
} else {
242+
return { reason: `unknown (code: vpfvjqeqvn)` };
243+
}
244+
});
238245

239246
client._offlineComponents = offlineComponentProvider;
240247
}

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import { IndexedDbTargetCache } from './indexeddb_target_cache';
5858
import { getStore, IndexedDbTransaction } from './indexeddb_transaction';
5959
import { LocalSerializer } from './local_serializer';
6060
import { LruParams } from './lru_garbage_collector';
61-
import { Persistence, PrimaryStateListener } from './persistence';
61+
import { Persistence, PrimaryStateListener, DatabaseDeletedListener } from './persistence';
6262
import { PersistencePromise } from './persistence_promise';
6363
import {
6464
PersistenceTransaction,
@@ -324,20 +324,18 @@ export class IndexedDbPersistence implements Persistence {
324324
}
325325

326326
/**
327-
* Registers a listener that gets called when the database receives a
328-
* version change event indicating that it has deleted.
327+
* Registers a listener that gets called when the database receives an
328+
* event indicating that it has deleted. This could be, for example, another
329+
* tab in multi-tab persistence mode having its `clearIndexedDbPersistence()`
330+
* function called, or a user manually clicking "Clear Site Data" in a
331+
* browser.
329332
*
330333
* PORTING NOTE: This is only used for Web multi-tab.
331334
*/
332335
setDatabaseDeletedListener(
333-
databaseDeletedListener: () => Promise<void>
336+
databaseDeletedListener: DatabaseDeletedListener
334337
): void {
335-
this.simpleDb.setVersionChangeListener(async event => {
336-
// Check if an attempt is made to delete IndexedDB.
337-
if (event.newVersion === null) {
338-
await databaseDeletedListener();
339-
}
340-
});
338+
this.simpleDb.setDatabaseDeletedListener(databaseDeletedListener);
341339
}
342340

343341
/**

packages/firestore/src/local/persistence.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ export interface ReferenceDelegate {
9898
): PersistencePromise<void>;
9999
}
100100

101+
export type DatabaseDeletedReason = "persistence cleared" | "site data cleared";
102+
103+
export const DatabaseDeletedListenerContinueResult: unique symbol = Symbol("DatabaseDeletedListenerContinueResult");
104+
105+
export interface DatabaseDeletedListenerAbortResult {
106+
reason: string;
107+
}
108+
109+
export type DatabaseDeletedListenerResult = typeof DatabaseDeletedListenerContinueResult | DatabaseDeletedListenerAbortResult;
110+
111+
export type DatabaseDeletedListener = (reason: DatabaseDeletedReason) => DatabaseDeletedListenerResult;
112+
101113
/**
102114
* Persistence is the lowest-level shared interface to persistent storage in
103115
* Firestore.
@@ -151,13 +163,16 @@ export interface Persistence {
151163
shutdown(): Promise<void>;
152164

153165
/**
154-
* Registers a listener that gets called when the database receives a
155-
* version change event indicating that it has deleted.
166+
* Registers a listener that gets called when the database receives an
167+
* event indicating that it has deleted. This could be, for example, another
168+
* tab in multi-tab persistence mode having its `clearIndexedDbPersistence()`
169+
* function called, or a user manually clicking "Clear Site Data" in a
170+
* browser.
156171
*
157172
* PORTING NOTE: This is only used for Web multi-tab.
158173
*/
159174
setDatabaseDeletedListener(
160-
databaseDeletedListener: () => Promise<void>
175+
databaseDeletedListener: DatabaseDeletedListener
161176
): void;
162177

163178
/**

packages/firestore/src/local/simple_db.ts

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { getGlobal, getUA, isIndexedDBAvailable } from '@firebase/util';
18+
import {getGlobal, getUA, isIndexedDBAvailable} from '@firebase/util';
1919

20-
import { debugAssert } from '../util/assert';
21-
import { Code, FirestoreError } from '../util/error';
22-
import { logDebug, logError } from '../util/log';
23-
import { Deferred } from '../util/promise';
20+
import {debugAssert} from '../util/assert';
21+
import {Code, FirestoreError} from '../util/error';
22+
import {logDebug, logError, logWarn} from '../util/log';
23+
import {Deferred} from '../util/promise';
2424

25-
import { PersistencePromise } from './persistence_promise';
25+
import {PersistencePromise} from './persistence_promise';
26+
import {
27+
type DatabaseDeletedListener,
28+
DatabaseDeletedListenerContinueResult
29+
} from './persistence';
2630

2731
// References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal()
2832
/* eslint-disable no-restricted-globals */
@@ -159,7 +163,7 @@ export class SimpleDbTransaction {
159163
export class SimpleDb {
160164
private db?: IDBDatabase;
161165
private lastClosedDbVersion: number | null = null;
162-
private versionchangelistener?: (event: IDBVersionChangeEvent) => void;
166+
private databaseDeletedListener?: DatabaseDeletedListener;
163167

164168
/** Deletes the specified database. */
165169
static delete(name: string): Promise<void> {
@@ -352,19 +356,35 @@ export class SimpleDb {
352356
this.lastClosedDbVersion !== null &&
353357
this.lastClosedDbVersion !== event.oldVersion
354358
) {
355-
// This thrown error will get passed to the `onerror` callback
356-
// registered above, and will then be propagated correctly.
357-
throw new Error(
358-
`refusing to open IndexedDB database due to potential ` +
359-
`corruption of the IndexedDB database data; this corruption ` +
360-
`could be caused by clicking the "clear site data" button in ` +
361-
`a web browser; try reloading the web page to re-initialize ` +
362-
`the IndexedDB database: ` +
363-
`lastClosedDbVersion=${this.lastClosedDbVersion}, ` +
364-
`event.oldVersion=${event.oldVersion}, ` +
365-
`event.newVersion=${event.newVersion}, ` +
366-
`db.version=${db.version}`
359+
logWarn(
360+
`IndexedDB onupgradeneeded indicates that the ` +
361+
`database contents may have been cleared, such as by clicking ` +
362+
`the "clear site data" button in a browser. This _could_ cause ` +
363+
`corruption of the IndexeDB database data if the clear ` +
364+
`operation happened in the middle of Firestore operations. (` +
365+
`db.name=${db.name}, ` +
366+
`db.version=${db.version}, ` +
367+
`lastClosedDbVersion=${this.lastClosedDbVersion}, ` +
368+
`event.oldVersion=${event.oldVersion}, ` +
369+
`event.newVersion=${event.newVersion}` +
370+
`)`
367371
);
372+
if (this.databaseDeletedListener) {
373+
const listenerResult = this.databaseDeletedListener("site data cleared");
374+
if (listenerResult !== DatabaseDeletedListenerContinueResult) {
375+
throw new Error(
376+
`Refusing to open IndexedDB database after having been ` +
377+
`cleared, such as by clicking the "clear site data" button ` +
378+
`in a web browser: ${listenerResult.reason} (` +
379+
`db.name=${db.name}, ` +
380+
`db.version=${db.version}, ` +
381+
`lastClosedDbVersion=${this.lastClosedDbVersion}, ` +
382+
`event.oldVersion=${event.oldVersion}, ` +
383+
`event.newVersion=${event.newVersion}` +
384+
`)`
385+
);
386+
}
387+
}
368388
}
369389
this.schemaConverter
370390
.createOrUpgrade(
@@ -387,27 +407,56 @@ export class SimpleDb {
387407
event => {
388408
const db = event.target as IDBDatabase;
389409
this.lastClosedDbVersion = db.version;
410+
logWarn(
411+
`IndexedDB "close" event received, indicating abnormal database ` +
412+
`closure. The database contents may have been cleared, such as ` +
413+
`by clicking the "clear site data" button in a browser. ` +
414+
`Re-opening the IndexedDB database may fail to avoid IndexedDB ` +
415+
`database data corruption (` +
416+
`db.name=${db.name}, ` +
417+
`db.version=${db.version}` +
418+
`)`
419+
);
390420
},
391421
{ passive: true }
392422
);
393423
}
394424

395-
if (this.versionchangelistener) {
396-
this.db.onversionchange = event => this.versionchangelistener!(event);
397-
}
425+
this.db.addEventListener("versionchange", event => {
426+
const db = event.target as IDBDatabase;
427+
if (event.newVersion !== null) {
428+
return;
429+
}
430+
431+
logDebug(
432+
`IndexedDB "versionchange" event with newVersion===null received; ` +
433+
`this is likely because clearIndexedDbPersistence() was called, ` +
434+
`possibly in another tab if multi-tab persistence is enabled.`
435+
);
436+
if (this.databaseDeletedListener) {
437+
const listenerResult = this.databaseDeletedListener("persistence cleared");
438+
if (listenerResult !== DatabaseDeletedListenerContinueResult) {
439+
logWarn(
440+
`Closing IndexedDB database "${db.name}" in response to ` +
441+
`"versionchange" event with newVersion===null: ` +
442+
`${listenerResult.reason}`
443+
);
444+
db.close();
445+
if (db === this.db) {
446+
this.db = undefined;
447+
}
448+
}
449+
}
450+
}, {passive:true});
398451

399452
return this.db;
400453
}
401454

402-
setVersionChangeListener(
403-
versionChangeListener: (event: IDBVersionChangeEvent) => void
404-
): void {
405-
this.versionchangelistener = versionChangeListener;
406-
if (this.db) {
407-
this.db.onversionchange = (event: IDBVersionChangeEvent) => {
408-
return versionChangeListener(event);
409-
};
455+
setDatabaseDeletedListener(databaseDeletedListener: DatabaseDeletedListener): void {
456+
if (this.databaseDeletedListener) {
457+
throw new Error("setOnDatabaseDeletedListener() has already been called");
410458
}
459+
this.databaseDeletedListener = databaseDeletedListener;
411460
}
412461

413462
async runTransaction<T>(

0 commit comments

Comments
 (0)