Skip to content

Commit 5563417

Browse files
committed
Treat "Clear Site Data" event the same way as clearIndexedDbPersistence() from another tab.
1 parent 86155b3 commit 5563417

File tree

3 files changed

+133
-25
lines changed

3 files changed

+133
-25
lines changed

packages/firestore/src/core/firestore_client.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,22 +231,47 @@ 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 | null;
236+
237+
if (event.type === 'ClearSiteDataDatabaseDeletedEvent') {
238+
const message =
239+
`Terminating Firestore in response to "${event.type}" event ` +
240+
`to prevent potential IndexedDB database corruption. ` +
241+
`This situation could be caused by clicking the ` +
242+
`"Clear Site Data" button in a web browser. ` +
243+
`Try reloading the web page to re-initialize the ` +
244+
`IndexedDB database.`;
245+
// Throw FirestoreError rather than just Error so that the error will
246+
// be treated as "non-retryable".
247+
error = new FirestoreError('failed-precondition', message);
248+
logWarn(message, event.data);
249+
} else {
250+
error = null;
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 ' +
261+
`Terminating Firestore in response to "${event.type}" event ` +
241262
'completed successfully'
242263
);
243264
})
244265
.catch(error => {
245266
logWarn(
246-
'Terminating Firestore due to IndexedDb database deletion failed',
247-
error
267+
`Terminating Firestore in response to "${event.type}" event failed:`,
268+
error
248269
);
249270
});
271+
272+
if (error) {
273+
throw error;
274+
}
250275
});
251276

252277
client._offlineComponents = offlineComponentProvider;

packages/firestore/src/local/persistence.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,70 @@ 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+
/**
115+
* The value of the "newVersion" property of the "versionchange" event
116+
* that triggered this event. Its value is _always_ `null`, but is kept here
117+
* for posterity.
118+
*/
119+
eventNewVersion: null;
120+
}
121+
) {}
122+
}
123+
124+
/**
125+
* A {@link DatabaseDeletedListener} event indicating that the "Clear Site Data"
126+
* button in a web browser was (likely) clicked, deleting the IndexedDB
127+
* database.
128+
*/
129+
export class ClearSiteDataDatabaseDeletedEvent {
130+
/** A type discriminator. */
131+
readonly type = 'ClearSiteDataDatabaseDeletedEvent' as const;
132+
133+
constructor(
134+
readonly data: {
135+
/** The IndexedDB version that was last reported by the database. */
136+
lastClosedVersion: number;
137+
/**
138+
* The value of the "oldVersion" property of the "onupgradeneeded"
139+
* IndexedDB event that triggered this event.
140+
*/
141+
eventOldVersion: number;
142+
/**
143+
* The value of the "newVersion" property of the "onupgradeneeded"
144+
* IndexedDB event that triggered this event.
145+
*/
146+
eventNewVersion: number | null;
147+
/**
148+
* The value of the "version" property of the "IDBDatabase" object.
149+
*/
150+
dbVersion: number;
151+
}
152+
) {}
153+
}
154+
155+
/**
156+
* The type of the "event" parameter of {@link DatabaseDeletedListener}.
157+
*/
158+
export type DatabaseDeletedListenerEvent =
159+
| VersionChangeDatabaseDeletedEvent
160+
| ClearSiteDataDatabaseDeletedEvent;
161+
162+
export type DatabaseDeletedListener = (
163+
event: DatabaseDeletedListenerEvent
164+
) => void;
102165

103166
/**
104167
* Persistence is the lowest-level shared interface to persistent storage in

packages/firestore/src/local/simple_db.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@ import { getGlobal, getUA, isIndexedDBAvailable } from '@firebase/util';
1919

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

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

2832
// References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal()
@@ -299,8 +303,31 @@ export class SimpleDb {
299303
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
300304
const request = indexedDB.open(this.name, this.version);
301305

306+
// Store information about "Clear Site Data" being detected in the
307+
// "onupgradeneeded" event and check it in the "onsuccess" event
308+
// rather than throwing directly from the "onupgradeneeded" event
309+
// since throwing directly from the listener results in a generic
310+
// exception that cannot be distinguished from other errors.
311+
const clearSiteDataEvent = {
312+
event: null as ClearSiteDataDatabaseDeletedEvent | null
313+
};
314+
302315
request.onsuccess = (event: Event) => {
303316
const db = (event.target as IDBOpenDBRequest).result;
317+
318+
if (clearSiteDataEvent.event) {
319+
try {
320+
this.databaseDeletedListener?.(clearSiteDataEvent.event);
321+
} catch (e) {
322+
try {
323+
db.close();
324+
} finally {
325+
reject(e);
326+
}
327+
return;
328+
}
329+
}
330+
304331
resolve(db);
305332
};
306333

@@ -353,19 +380,12 @@ export class SimpleDb {
353380
this.lastClosedDbVersion !== null &&
354381
this.lastClosedDbVersion !== event.oldVersion
355382
) {
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}`
368-
);
383+
clearSiteDataEvent.event = new ClearSiteDataDatabaseDeletedEvent({
384+
lastClosedVersion: this.lastClosedDbVersion,
385+
eventOldVersion: event.oldVersion,
386+
eventNewVersion: event.newVersion,
387+
dbVersion: db.version
388+
});
369389
}
370390
this.schemaConverter
371391
.createOrUpgrade(
@@ -399,11 +419,11 @@ export class SimpleDb {
399419
// Notify the listener if another tab attempted to delete the IndexedDb
400420
// database, such as by calling clearIndexedDbPersistence().
401421
if (event.newVersion === null) {
402-
logWarn(
403-
`Received "versionchange" event with newVersion===null; ` +
404-
'notifying the registered DatabaseDeletedListener, if any'
422+
this.databaseDeletedListener?.(
423+
new VersionChangeDatabaseDeletedEvent({
424+
eventNewVersion: event.newVersion
425+
})
405426
);
406-
this.databaseDeletedListener?.();
407427
}
408428
},
409429
{ passive: true }

0 commit comments

Comments
 (0)