Skip to content

Commit f682044

Browse files
authored
Merge 51a279d into 86155b3
2 parents 86155b3 + 51a279d commit f682044

File tree

4 files changed

+151
-26
lines changed

4 files changed

+151
-26
lines changed

.changeset/hot-birds-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/firestore': patch
3+
---
4+
5+
Terminate Firestore more gracefully when "Clear Site Data" button is pressed in a web browser

packages/firestore/src/core/firestore_client.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,22 +231,49 @@ export async function setOfflineComponentProvider(
231231
}
232232
});
233233

234-
offlineComponentProvider.persistence.setDatabaseDeletedListener(() => {
235-
logWarn('Terminating Firestore due to IndexedDb database deletion');
234+
offlineComponentProvider.persistence.setDatabaseDeletedListener(event => {
235+
let error: FirestoreError | undefined;
236+
237+
if (event.type === 'ClearSiteDataDatabaseDeletedEvent') {
238+
// Throw FirestoreError rather than just Error so that the error will
239+
// be treated as "non-retryable".
240+
error = new FirestoreError(
241+
'failed-precondition',
242+
`Terminating Firestore in response to "${event.type}" event ` +
243+
`to prevent potential IndexedDB database corruption. ` +
244+
`This situation could be caused by clicking the ` +
245+
`"Clear Site Data" button in a web browser. ` +
246+
`Try reloading the web page to re-initialize the ` +
247+
`IndexedDB database.`
248+
);
249+
logWarn(error.message, event.data);
250+
} else {
251+
logWarn(
252+
`Terminating Firestore in response to "${event.type}" event`,
253+
event.data
254+
);
255+
}
256+
236257
client
237258
.terminate()
238259
.then(() => {
239260
logDebug(
240-
'Terminating Firestore due to IndexedDb database deletion ' +
241-
'completed successfully'
261+
`Terminating Firestore in response to "${event.type}" event ` +
262+
'completed successfully',
263+
event.data
242264
);
243265
})
244266
.catch(error => {
245267
logWarn(
246-
'Terminating Firestore due to IndexedDb database deletion failed',
247-
error
268+
`Terminating Firestore in response to "${event.type}" event failed:`,
269+
error,
270+
event.data
248271
);
249272
});
273+
274+
if (error) {
275+
throw error;
276+
}
250277
});
251278

252279
client._offlineComponents = offlineComponentProvider;

packages/firestore/src/local/persistence.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,74 @@ export interface ReferenceDelegate {
9898
): PersistencePromise<void>;
9999
}
100100

101-
export type DatabaseDeletedListener = () => void;
101+
/**
102+
* A {@link DatabaseDeletedListener} event indicating that the IndexedDB
103+
* database received a "versionchange" event with a null value for "newVersion".
104+
* This event indicates that another tab in multi-tab IndexedDB persistence mode
105+
* has called `clearIndexedDbPersistence()` and requires this tab to close its
106+
* IndexedDB connection in order to allow the "clear" operation to proceed.
107+
*/
108+
export class VersionChangeDatabaseDeletedEvent {
109+
/** A type discriminator. */
110+
readonly type = 'VersionChangeDatabaseDeletedEvent' as const;
111+
112+
constructor(
113+
readonly data: {
114+
/** A unique ID for this event. */
115+
eventId: string;
116+
/**
117+
* The value of the "newVersion" property of the "versionchange" event
118+
* that triggered this event. Its value is _always_ `null`, but is kept
119+
* here for posterity.
120+
*/
121+
eventNewVersion: null;
122+
}
123+
) {}
124+
}
125+
126+
/**
127+
* A {@link DatabaseDeletedListener} event indicating that the "Clear Site Data"
128+
* button in a web browser was (likely) clicked, deleting the IndexedDB
129+
* database.
130+
*/
131+
export class ClearSiteDataDatabaseDeletedEvent {
132+
/** A type discriminator. */
133+
readonly type = 'ClearSiteDataDatabaseDeletedEvent' as const;
134+
135+
constructor(
136+
readonly data: {
137+
/** A unique ID for this event. */
138+
eventId: string;
139+
/** The IndexedDB version that was last reported by the database. */
140+
lastClosedVersion: number;
141+
/**
142+
* The value of the "oldVersion" property of the "onupgradeneeded"
143+
* IndexedDB event that triggered this event.
144+
*/
145+
eventOldVersion: number;
146+
/**
147+
* The value of the "newVersion" property of the "onupgradeneeded"
148+
* IndexedDB event that triggered this event.
149+
*/
150+
eventNewVersion: number | null;
151+
/**
152+
* The value of the "version" property of the "IDBDatabase" object.
153+
*/
154+
dbVersion: number;
155+
}
156+
) {}
157+
}
158+
159+
/**
160+
* The type of the "event" parameter of {@link DatabaseDeletedListener}.
161+
*/
162+
export type DatabaseDeletedListenerEvent =
163+
| VersionChangeDatabaseDeletedEvent
164+
| ClearSiteDataDatabaseDeletedEvent;
165+
166+
export type DatabaseDeletedListener = (
167+
event: DatabaseDeletedListenerEvent
168+
) => void;
102169

103170
/**
104171
* Persistence is the lowest-level shared interface to persistent storage in

packages/firestore/src/local/simple_db.ts

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@
1818
import { getGlobal, getUA, isIndexedDBAvailable } from '@firebase/util';
1919

2020
import { debugAssert } from '../util/assert';
21+
import { generateUniqueDebugId } from '../util/debug_uid';
2122
import { Code, FirestoreError } from '../util/error';
22-
import { logDebug, logError, logWarn } from '../util/log';
23+
import { logDebug, logError } from '../util/log';
2324
import { Deferred } from '../util/promise';
2425

25-
import { DatabaseDeletedListener } from './persistence';
26+
import {
27+
ClearSiteDataDatabaseDeletedEvent,
28+
DatabaseDeletedListener,
29+
VersionChangeDatabaseDeletedEvent
30+
} from './persistence';
2631
import { PersistencePromise } from './persistence_promise';
2732

2833
// References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal()
@@ -299,9 +304,33 @@ export class SimpleDb {
299304
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
300305
const request = indexedDB.open(this.name, this.version);
301306

307+
// Store information about "Clear Site Data" being detected in the
308+
// "onupgradeneeded" event listener and handle it in the "onsuccess"
309+
// event listener, as opposed to throwing directly from the
310+
// "onupgradeneeded" event listener. Do this because throwing from the
311+
// "onupgradeneeded" event listener results in a generic error being
312+
// reported to the "onerror" event listener that cannot be distinguished
313+
// from other errors.
314+
const clearSiteDataEvent: ClearSiteDataDatabaseDeletedEvent[] = [];
315+
302316
request.onsuccess = (event: Event) => {
317+
let error: unknown;
318+
if (clearSiteDataEvent[0]) {
319+
try {
320+
this.databaseDeletedListener?.(clearSiteDataEvent[0]);
321+
} catch (e) {
322+
error = e;
323+
}
324+
}
325+
303326
const db = (event.target as IDBOpenDBRequest).result;
304-
resolve(db);
327+
328+
if (error) {
329+
reject(error);
330+
db.close();
331+
} else {
332+
resolve(db);
333+
}
305334
};
306335

307336
request.onblocked = () => {
@@ -353,18 +382,14 @@ export class SimpleDb {
353382
this.lastClosedDbVersion !== null &&
354383
this.lastClosedDbVersion !== event.oldVersion
355384
) {
356-
// This thrown error will get passed to the `onerror` callback
357-
// registered above, and will then be propagated correctly.
358-
throw new Error(
359-
`refusing to open IndexedDB database due to potential ` +
360-
`corruption of the IndexedDB database data; this corruption ` +
361-
`could be caused by clicking the "clear site data" button in ` +
362-
`a web browser; try reloading the web page to re-initialize ` +
363-
`the IndexedDB database: ` +
364-
`lastClosedDbVersion=${this.lastClosedDbVersion}, ` +
365-
`event.oldVersion=${event.oldVersion}, ` +
366-
`event.newVersion=${event.newVersion}, ` +
367-
`db.version=${db.version}`
385+
clearSiteDataEvent.push(
386+
new ClearSiteDataDatabaseDeletedEvent({
387+
eventId: generateUniqueDebugId(),
388+
lastClosedVersion: this.lastClosedDbVersion,
389+
eventOldVersion: event.oldVersion,
390+
eventNewVersion: event.newVersion,
391+
dbVersion: db.version
392+
})
368393
);
369394
}
370395
this.schemaConverter
@@ -399,11 +424,12 @@ export class SimpleDb {
399424
// Notify the listener if another tab attempted to delete the IndexedDb
400425
// database, such as by calling clearIndexedDbPersistence().
401426
if (event.newVersion === null) {
402-
logWarn(
403-
`Received "versionchange" event with newVersion===null; ` +
404-
'notifying the registered DatabaseDeletedListener, if any'
427+
this.databaseDeletedListener?.(
428+
new VersionChangeDatabaseDeletedEvent({
429+
eventId: generateUniqueDebugId(),
430+
eventNewVersion: event.newVersion
431+
})
405432
);
406-
this.databaseDeletedListener?.();
407433
}
408434
},
409435
{ passive: true }

0 commit comments

Comments
 (0)