Skip to content

Commit 8fae067

Browse files
authored
Merge 013d821 into 0f891d8
2 parents 0f891d8 + 013d821 commit 8fae067

File tree

5 files changed

+135
-41
lines changed

5 files changed

+135
-41
lines changed

packages/firestore/src/core/firestore_client.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ import {
3333
localStoreReadDocument,
3434
localStoreSetIndexAutoCreationEnabled
3535
} from '../local/local_store_impl';
36-
import { Persistence } from '../local/persistence';
36+
import {
37+
Persistence,
38+
DatabaseDeletedListenerAbortResult,
39+
DatabaseDeletedListenerContinueResult
40+
} from '../local/persistence';
3741
import { Document } from '../model/document';
3842
import { DocumentKey } from '../model/document_key';
3943
import { FieldIndex } from '../model/field_index';
@@ -232,9 +236,17 @@ export async function setOfflineComponentProvider(
232236

233237
// When a user calls clearPersistence() in one client, all other clients
234238
// need to be terminated to allow the delete to succeed.
235-
offlineComponentProvider.persistence.setDatabaseDeletedListener(() =>
236-
client.terminate()
237-
);
239+
offlineComponentProvider.persistence.setDatabaseDeletedListener(reason => {
240+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
241+
client.terminate();
242+
if (reason === 'site data cleared') {
243+
return new DatabaseDeletedListenerAbortResult(
244+
'protecting against database corruption'
245+
);
246+
} else {
247+
return new DatabaseDeletedListenerContinueResult();
248+
}
249+
});
238250

239251
client._offlineComponents = offlineComponentProvider;
240252
}

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ 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 {
62+
Persistence,
63+
PrimaryStateListener,
64+
DatabaseDeletedListener
65+
} from './persistence';
6266
import { PersistencePromise } from './persistence_promise';
6367
import {
6468
PersistenceTransaction,
@@ -324,20 +328,18 @@ export class IndexedDbPersistence implements Persistence {
324328
}
325329

326330
/**
327-
* Registers a listener that gets called when the database receives a
328-
* version change event indicating that it has deleted.
331+
* Registers a listener that gets called when the database receives an
332+
* event indicating that it has deleted. This could be, for example, another
333+
* tab in multi-tab persistence mode having its `clearIndexedDbPersistence()`
334+
* function called, or a user manually clicking "Clear Site Data" in a
335+
* browser.
329336
*
330337
* PORTING NOTE: This is only used for Web multi-tab.
331338
*/
332339
setDatabaseDeletedListener(
333-
databaseDeletedListener: () => Promise<void>
340+
databaseDeletedListener: DatabaseDeletedListener
334341
): 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-
});
342+
this.simpleDb.setDatabaseDeletedListener(databaseDeletedListener);
341343
}
342344

343345
/**

packages/firestore/src/local/persistence.ts

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

101+
export type DatabaseDeletedReason = 'persistence cleared' | 'site data cleared';
102+
103+
export class DatabaseDeletedListenerContinueResult {
104+
readonly type = 'continue' as const;
105+
}
106+
107+
export class DatabaseDeletedListenerAbortResult {
108+
readonly type = 'abort' as const;
109+
constructor(readonly abortReason: string) {}
110+
}
111+
112+
export type DatabaseDeletedListenerResult =
113+
| DatabaseDeletedListenerContinueResult
114+
| DatabaseDeletedListenerAbortResult;
115+
116+
export type DatabaseDeletedListener = (
117+
reason: DatabaseDeletedReason
118+
) => DatabaseDeletedListenerResult;
119+
101120
/**
102121
* Persistence is the lowest-level shared interface to persistent storage in
103122
* Firestore.
@@ -151,13 +170,16 @@ export interface Persistence {
151170
shutdown(): Promise<void>;
152171

153172
/**
154-
* Registers a listener that gets called when the database receives a
155-
* version change event indicating that it has deleted.
173+
* Registers a listener that gets called when the database receives an
174+
* event indicating that it has deleted. This could be, for example, another
175+
* tab in multi-tab persistence mode having its `clearIndexedDbPersistence()`
176+
* function called, or a user manually clicking "Clear Site Data" in a
177+
* browser.
156178
*
157179
* PORTING NOTE: This is only used for Web multi-tab.
158180
*/
159181
setDatabaseDeletedListener(
160-
databaseDeletedListener: () => Promise<void>
182+
databaseDeletedListener: DatabaseDeletedListener
161183
): void;
162184

163185
/**

packages/firestore/src/local/simple_db.ts

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ 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 } from '../util/log';
22+
import { logDebug, logError, logWarn } from '../util/log';
2323
import { Deferred } from '../util/promise';
2424

25+
import { type DatabaseDeletedListener } from './persistence';
2526
import { PersistencePromise } from './persistence_promise';
2627

2728
// References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal()
@@ -159,7 +160,7 @@ export class SimpleDbTransaction {
159160
export class SimpleDb {
160161
private db?: IDBDatabase;
161162
private lastClosedDbVersion: number | null = null;
162-
private versionchangelistener?: (event: IDBVersionChangeEvent) => void;
163+
private databaseDeletedListener?: DatabaseDeletedListener;
163164

164165
/** Deletes the specified database. */
165166
static delete(name: string): Promise<void> {
@@ -352,19 +353,36 @@ export class SimpleDb {
352353
this.lastClosedDbVersion !== null &&
353354
this.lastClosedDbVersion !== event.oldVersion
354355
) {
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: ` +
356+
logWarn(
357+
`IndexedDB onupgradeneeded indicates that the ` +
358+
`database contents may have been cleared, such as by clicking ` +
359+
`the "clear site data" button in a browser. This _could_ cause ` +
360+
`corruption of the IndexeDB database data if the clear ` +
361+
`operation happened in the middle of Firestore operations. (` +
362+
`db.name=${db.name}, ` +
363+
`db.version=${db.version}, ` +
363364
`lastClosedDbVersion=${this.lastClosedDbVersion}, ` +
364365
`event.oldVersion=${event.oldVersion}, ` +
365-
`event.newVersion=${event.newVersion}, ` +
366-
`db.version=${db.version}`
366+
`event.newVersion=${event.newVersion}` +
367+
`)`
367368
);
369+
if (this.databaseDeletedListener) {
370+
const listenerResult =
371+
this.databaseDeletedListener('site data cleared');
372+
if (listenerResult.type !== 'continue') {
373+
throw new Error(
374+
`Refusing to open IndexedDB database after having been ` +
375+
`cleared, such as by clicking the "clear site data" button ` +
376+
`in a web browser: ${listenerResult.abortReason} (` +
377+
`db.name=${db.name}, ` +
378+
`db.version=${db.version}, ` +
379+
`lastClosedDbVersion=${this.lastClosedDbVersion}, ` +
380+
`event.oldVersion=${event.oldVersion}, ` +
381+
`event.newVersion=${event.newVersion}` +
382+
`)`
383+
);
384+
}
385+
}
368386
}
369387
this.schemaConverter
370388
.createOrUpgrade(
@@ -387,27 +405,64 @@ export class SimpleDb {
387405
event => {
388406
const db = event.target as IDBDatabase;
389407
this.lastClosedDbVersion = db.version;
408+
logWarn(
409+
`IndexedDB "close" event received, indicating abnormal database ` +
410+
`closure. The database contents may have been cleared, such as ` +
411+
`by clicking the "clear site data" button in a browser. ` +
412+
`Re-opening the IndexedDB database may fail to avoid IndexedDB ` +
413+
`database data corruption (` +
414+
`db.name=${db.name}, ` +
415+
`db.version=${db.version}` +
416+
`)`
417+
);
390418
},
391419
{ passive: true }
392420
);
393421
}
394422

395-
if (this.versionchangelistener) {
396-
this.db.onversionchange = event => this.versionchangelistener!(event);
397-
}
423+
this.db.addEventListener(
424+
'versionchange',
425+
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(
438+
'persistence cleared'
439+
);
440+
if (listenerResult.type !== 'continue') {
441+
logWarn(
442+
`Closing IndexedDB database "${db.name}" in response to ` +
443+
`"versionchange" event with newVersion===null: ` +
444+
`${listenerResult.abortReason}`
445+
);
446+
db.close();
447+
if (db === this.db) {
448+
this.db = undefined;
449+
}
450+
}
451+
}
452+
},
453+
{ passive: true }
454+
);
398455

399456
return this.db;
400457
}
401458

402-
setVersionChangeListener(
403-
versionChangeListener: (event: IDBVersionChangeEvent) => void
459+
setDatabaseDeletedListener(
460+
databaseDeletedListener: DatabaseDeletedListener
404461
): void {
405-
this.versionchangelistener = versionChangeListener;
406-
if (this.db) {
407-
this.db.onversionchange = (event: IDBVersionChangeEvent) => {
408-
return versionChangeListener(event);
409-
};
462+
if (this.databaseDeletedListener) {
463+
throw new Error('setOnDatabaseDeletedListener() has already been called');
410464
}
465+
this.databaseDeletedListener = databaseDeletedListener;
411466
}
412467

413468
async runTransaction<T>(

packages/firestore/test/unit/specs/spec_test_runner.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import { LocalStore } from '../../../src/local/local_store';
8585
import { localStoreConfigureFieldIndexes } from '../../../src/local/local_store_impl';
8686
import { LruGarbageCollector } from '../../../src/local/lru_garbage_collector';
8787
import { MemoryLruDelegate } from '../../../src/local/memory_persistence';
88+
import { DatabaseDeletedListenerContinueResult } from '../../../src/local/persistence';
8889
import {
8990
ClientId,
9091
SharedClientState
@@ -365,8 +366,10 @@ abstract class TestRunner {
365366
this.eventManager.onLastRemoteStoreUnlisten =
366367
triggerRemoteStoreUnlisten.bind(null, this.syncEngine);
367368

368-
await this.persistence.setDatabaseDeletedListener(async () => {
369-
await this.shutdown();
369+
this.persistence.setDatabaseDeletedListener(() => {
370+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
371+
this.shutdown();
372+
return new DatabaseDeletedListenerContinueResult();
370373
});
371374

372375
this.started = true;

0 commit comments

Comments
 (0)