Skip to content

Commit 36be62a

Browse files
Re-open IndexedDB if closed (#3535)
1 parent cf3401d commit 36be62a

File tree

7 files changed

+208
-223
lines changed

7 files changed

+208
-223
lines changed

.changeset/quick-drinks-cheat.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/firestore": patch
3+
---
4+
5+
The SDK no longer crashes with the error "The database connection is closing". Instead, the individual operations that cause this error may be rejected.

packages/firestore/src/local/indexeddb_persistence.ts

+10-15
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,7 @@ export class IndexedDbPersistence implements Persistence {
191191
}
192192
}
193193

194-
// Technically `simpleDb` should be `| undefined` because it is
195-
// initialized asynchronously by start(), but that would be more misleading
196-
// than useful.
197-
private simpleDb!: SimpleDb;
194+
private simpleDb: SimpleDb;
198195

199196
private listenSequence: ListenSequence | null = null;
200197

@@ -259,6 +256,11 @@ export class IndexedDbPersistence implements Persistence {
259256
this.referenceDelegate = new IndexedDbLruDelegate(this, lruParams);
260257
this.dbName = persistenceKey + MAIN_DATABASE;
261258
this.serializer = new LocalSerializer(serializer);
259+
this.simpleDb = new SimpleDb(
260+
this.dbName,
261+
SCHEMA_VERSION,
262+
new SchemaConverter(this.serializer)
263+
);
262264
this.targetCache = new IndexedDbTargetCache(
263265
this.referenceDelegate,
264266
this.serializer
@@ -292,17 +294,10 @@ export class IndexedDbPersistence implements Persistence {
292294
debugAssert(!this.started, 'IndexedDbPersistence double-started!');
293295
debugAssert(this.window !== null, "Expected 'window' to be defined");
294296

295-
return SimpleDb.openOrCreate(
296-
this.dbName,
297-
SCHEMA_VERSION,
298-
new SchemaConverter(this.serializer)
299-
)
300-
.then(db => {
301-
this.simpleDb = db;
302-
// NOTE: This is expected to fail sometimes (in the case of another tab already
303-
// having the persistence lock), so it's the first thing we should do.
304-
return this.updateClientMetadataAndTryBecomePrimary();
305-
})
297+
// NOTE: This is expected to fail sometimes (in the case of another tab
298+
// already having the persistence lock), so it's the first thing we should
299+
// do.
300+
return this.updateClientMetadataAndTryBecomePrimary()
306301
.then(() => {
307302
if (!this.isPrimary && !this.allowTabSynchronization) {
308303
// Fail `start()` if `synchronizeTabs` is disabled and we cannot

packages/firestore/src/local/simple_db.ts

+122-95
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { debugAssert } from '../util/assert';
2020
import { Code, FirestoreError } from '../util/error';
2121
import { logDebug, logError } from '../util/log';
2222
import { Deferred } from '../util/promise';
23-
import { SCHEMA_VERSION } from './indexeddb_schema';
2423
import { PersistencePromise } from './persistence_promise';
2524

2625
// References to `window` are guarded by SimpleDb.isAvailable()
@@ -54,88 +53,8 @@ export interface SimpleDbSchemaConverter {
5453
* See PersistencePromise for more details.
5554
*/
5655
export class SimpleDb {
57-
/**
58-
* Opens the specified database, creating or upgrading it if necessary.
59-
*
60-
* Note that `version` must not be a downgrade. IndexedDB does not support downgrading the schema
61-
* version. We currently do not support any way to do versioning outside of IndexedDB's versioning
62-
* mechanism, as only version-upgrade transactions are allowed to do things like create
63-
* objectstores.
64-
*/
65-
static openOrCreate(
66-
name: string,
67-
version: number,
68-
schemaConverter: SimpleDbSchemaConverter
69-
): Promise<SimpleDb> {
70-
debugAssert(
71-
SimpleDb.isAvailable(),
72-
'IndexedDB not supported in current environment.'
73-
);
74-
logDebug(LOG_TAG, 'Opening database:', name);
75-
return new PersistencePromise<SimpleDb>((resolve, reject) => {
76-
// TODO(mikelehen): Investigate browser compatibility.
77-
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
78-
// suggests IE9 and older WebKit browsers handle upgrade
79-
// differently. They expect setVersion, as described here:
80-
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
81-
const request = indexedDB.open(name, version);
82-
83-
request.onsuccess = (event: Event) => {
84-
const db = (event.target as IDBOpenDBRequest).result;
85-
resolve(new SimpleDb(db));
86-
};
87-
88-
request.onblocked = () => {
89-
reject(
90-
new FirestoreError(
91-
Code.FAILED_PRECONDITION,
92-
'Cannot upgrade IndexedDB schema while another tab is open. ' +
93-
'Close all tabs that access Firestore and reload this page to proceed.'
94-
)
95-
);
96-
};
97-
98-
request.onerror = (event: Event) => {
99-
const error: DOMException = (event.target as IDBOpenDBRequest).error!;
100-
if (error.name === 'VersionError') {
101-
reject(
102-
new FirestoreError(
103-
Code.FAILED_PRECONDITION,
104-
'A newer version of the Firestore SDK was previously used and so the persisted ' +
105-
'data is not compatible with the version of the SDK you are now using. The SDK ' +
106-
'will operate with persistence disabled. If you need persistence, please ' +
107-
're-upgrade to a newer version of the SDK or else clear the persisted IndexedDB ' +
108-
'data for your app to start fresh.'
109-
)
110-
);
111-
} else {
112-
reject(error);
113-
}
114-
};
115-
116-
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
117-
logDebug(
118-
LOG_TAG,
119-
'Database "' + name + '" requires upgrade from version:',
120-
event.oldVersion
121-
);
122-
const db = (event.target as IDBOpenDBRequest).result;
123-
schemaConverter
124-
.createOrUpgrade(
125-
db,
126-
request.transaction!,
127-
event.oldVersion,
128-
SCHEMA_VERSION
129-
)
130-
.next(() => {
131-
logDebug(
132-
LOG_TAG,
133-
'Database upgrade to version ' + SCHEMA_VERSION + ' complete'
134-
);
135-
});
136-
};
137-
}).toPromise();
138-
}
56+
private db?: IDBDatabase;
57+
private versionchangelistener?: (event: IDBVersionChangeEvent) => void;
13958

14059
/** Deletes the specified database. */
14160
static delete(name: string): Promise<void> {
@@ -233,7 +152,25 @@ export class SimpleDb {
233152
return Number(version);
234153
}
235154

236-
constructor(private db: IDBDatabase) {
155+
/*
156+
* Creates a new SimpleDb wrapper for IndexedDb database `name`.
157+
*
158+
* Note that `version` must not be a downgrade. IndexedDB does not support
159+
* downgrading the schema version. We currently do not support any way to do
160+
* versioning outside of IndexedDB's versioning mechanism, as only
161+
* version-upgrade transactions are allowed to do things like create
162+
* objectstores.
163+
*/
164+
constructor(
165+
private readonly name: string,
166+
private readonly version: number,
167+
private readonly schemaConverter: SimpleDbSchemaConverter
168+
) {
169+
debugAssert(
170+
SimpleDb.isAvailable(),
171+
'IndexedDB not supported in current environment.'
172+
);
173+
237174
const iOSVersion = SimpleDb.getIOSVersion(getUA());
238175
// NOTE: According to https://bugs.webkit.org/show_bug.cgi?id=197050, the
239176
// bug we're checking for should exist in iOS >= 12.2 and < 13, but for
@@ -249,12 +186,91 @@ export class SimpleDb {
249186
}
250187
}
251188

189+
/**
190+
* Opens the specified database, creating or upgrading it if necessary.
191+
*/
192+
async ensureDb(): Promise<IDBDatabase> {
193+
if (!this.db) {
194+
logDebug(LOG_TAG, 'Opening database:', this.name);
195+
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
196+
// TODO(mikelehen): Investigate browser compatibility.
197+
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
198+
// suggests IE9 and older WebKit browsers handle upgrade
199+
// differently. They expect setVersion, as described here:
200+
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
201+
const request = indexedDB.open(this.name, this.version);
202+
203+
request.onsuccess = (event: Event) => {
204+
const db = (event.target as IDBOpenDBRequest).result;
205+
resolve(db);
206+
};
207+
208+
request.onblocked = () => {
209+
reject(
210+
new IndexedDbTransactionError(
211+
'Cannot upgrade IndexedDB schema while another tab is open. ' +
212+
'Close all tabs that access Firestore and reload this page to proceed.'
213+
)
214+
);
215+
};
216+
217+
request.onerror = (event: Event) => {
218+
const error: DOMException = (event.target as IDBOpenDBRequest).error!;
219+
if (error.name === 'VersionError') {
220+
reject(
221+
new FirestoreError(
222+
Code.FAILED_PRECONDITION,
223+
'A newer version of the Firestore SDK was previously used and so the persisted ' +
224+
'data is not compatible with the version of the SDK you are now using. The SDK ' +
225+
'will operate with persistence disabled. If you need persistence, please ' +
226+
're-upgrade to a newer version of the SDK or else clear the persisted IndexedDB ' +
227+
'data for your app to start fresh.'
228+
)
229+
);
230+
} else {
231+
reject(new IndexedDbTransactionError(error));
232+
}
233+
};
234+
235+
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
236+
logDebug(
237+
LOG_TAG,
238+
'Database "' + this.name + '" requires upgrade from version:',
239+
event.oldVersion
240+
);
241+
const db = (event.target as IDBOpenDBRequest).result;
242+
this.schemaConverter
243+
.createOrUpgrade(
244+
db,
245+
request.transaction!,
246+
event.oldVersion,
247+
this.version
248+
)
249+
.next(() => {
250+
logDebug(
251+
LOG_TAG,
252+
'Database upgrade to version ' + this.version + ' complete'
253+
);
254+
});
255+
};
256+
});
257+
}
258+
259+
if (this.versionchangelistener) {
260+
this.db.onversionchange = event => this.versionchangelistener!(event);
261+
}
262+
return this.db;
263+
}
264+
252265
setVersionChangeListener(
253266
versionChangeListener: (event: IDBVersionChangeEvent) => void
254267
): void {
255-
this.db.onversionchange = (event: IDBVersionChangeEvent) => {
256-
return versionChangeListener(event);
257-
};
268+
this.versionchangelistener = versionChangeListener;
269+
if (this.db) {
270+
this.db.onversionchange = (event: IDBVersionChangeEvent) => {
271+
return versionChangeListener(event);
272+
};
273+
}
258274
}
259275

260276
async runTransaction<T>(
@@ -268,12 +284,14 @@ export class SimpleDb {
268284
while (true) {
269285
++attemptNumber;
270286

271-
const transaction = SimpleDbTransaction.open(
272-
this.db,
273-
readonly ? 'readonly' : 'readwrite',
274-
objectStores
275-
);
276287
try {
288+
this.db = await this.ensureDb();
289+
290+
const transaction = SimpleDbTransaction.open(
291+
this.db,
292+
readonly ? 'readonly' : 'readwrite',
293+
objectStores
294+
);
277295
const transactionFnResult = transactionFn(transaction)
278296
.catch(error => {
279297
// Abort the transaction if there was an error.
@@ -312,6 +330,8 @@ export class SimpleDb {
312330
retryable
313331
);
314332

333+
this.close();
334+
315335
if (!retryable) {
316336
return Promise.reject(error);
317337
}
@@ -320,7 +340,10 @@ export class SimpleDb {
320340
}
321341

322342
close(): void {
323-
this.db.close();
343+
if (this.db) {
344+
this.db.close();
345+
}
346+
this.db = undefined;
324347
}
325348
}
326349

@@ -400,7 +423,7 @@ export interface IterateOptions {
400423
export class IndexedDbTransactionError extends FirestoreError {
401424
name = 'IndexedDbTransactionError';
402425

403-
constructor(cause: Error) {
426+
constructor(cause: Error | string) {
404427
super(Code.UNAVAILABLE, 'IndexedDB transaction failed: ' + cause);
405428
}
406429
}
@@ -429,7 +452,11 @@ export class SimpleDbTransaction {
429452
mode: IDBTransactionMode,
430453
objectStoreNames: string[]
431454
): SimpleDbTransaction {
432-
return new SimpleDbTransaction(db.transaction(objectStoreNames, mode));
455+
try {
456+
return new SimpleDbTransaction(db.transaction(objectStoreNames, mode));
457+
} catch (e) {
458+
throw new IndexedDbTransactionError(e);
459+
}
433460
}
434461

435462
constructor(private readonly transaction: IDBTransaction) {

packages/firestore/test/unit/local/encoded_resource_path.test.ts

+3-12
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,9 @@ describe('EncodedResourcePath', () => {
5353

5454
const dbName = 'resource-path-tests';
5555

56-
beforeEach(() => {
57-
return SimpleDb.delete(dbName)
58-
.then(() => {
59-
return SimpleDb.openOrCreate(
60-
dbName,
61-
1,
62-
new EncodedResourcePathSchemaConverter()
63-
);
64-
})
65-
.then(simpleDb => {
66-
db = simpleDb;
67-
});
56+
beforeEach(async () => {
57+
await SimpleDb.delete(dbName);
58+
db = new SimpleDb(dbName, 1, new EncodedResourcePathSchemaConverter());
6859
});
6960

7061
afterEach(() => {

0 commit comments

Comments
 (0)