Skip to content

Commit fa7a185

Browse files
Re-open IndexedDB if closed
1 parent 0966416 commit fa7a185

File tree

6 files changed

+231
-254
lines changed

6 files changed

+231
-254
lines changed

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 40 additions & 47 deletions
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
@@ -288,54 +290,45 @@ export class IndexedDbPersistence implements Persistence {
288290
*
289291
* @return {Promise<void>} Whether persistence was enabled.
290292
*/
291-
start(): Promise<void> {
293+
async start(): Promise<void> {
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-
})
306-
.then(() => {
307-
if (!this.isPrimary && !this.allowTabSynchronization) {
308-
// Fail `start()` if `synchronizeTabs` is disabled and we cannot
309-
// obtain the primary lease.
310-
throw new FirestoreError(
311-
Code.FAILED_PRECONDITION,
312-
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
313-
);
314-
}
315-
this.attachVisibilityHandler();
316-
this.attachWindowUnloadHook();
297+
try {
298+
await this.simpleDb.ensureDb();
299+
300+
// NOTE: This is expected to fail sometimes (in the case of another tab already
301+
// having the persistence lock), so it's the first thing we should do.
302+
await this.updateClientMetadataAndTryBecomePrimary();
303+
304+
if (!this.isPrimary && !this.allowTabSynchronization) {
305+
// Fail `start()` if `synchronizeTabs` is disabled and we cannot
306+
// obtain the primary lease.
307+
throw new FirestoreError(
308+
Code.FAILED_PRECONDITION,
309+
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
310+
);
311+
}
312+
this.attachVisibilityHandler();
313+
this.attachWindowUnloadHook();
317314

318-
this.scheduleClientMetadataAndPrimaryLeaseRefreshes();
315+
this.scheduleClientMetadataAndPrimaryLeaseRefreshes();
319316

320-
return this.runTransaction(
321-
'getHighestListenSequenceNumber',
322-
'readonly',
323-
txn => this.targetCache.getHighestSequenceNumber(txn)
324-
);
325-
})
326-
.then(highestListenSequenceNumber => {
327-
this.listenSequence = new ListenSequence(
328-
highestListenSequenceNumber,
329-
this.sequenceNumberSyncer
330-
);
331-
})
332-
.then(() => {
333-
this._started = true;
334-
})
335-
.catch(reason => {
336-
this.simpleDb && this.simpleDb.close();
337-
return Promise.reject(reason);
338-
});
317+
const highestListenSequenceNumber = await this.runTransaction(
318+
'getHighestListenSequenceNumber',
319+
'readonly',
320+
txn => this.targetCache.getHighestSequenceNumber(txn)
321+
);
322+
323+
this.listenSequence = new ListenSequence(
324+
highestListenSequenceNumber,
325+
this.sequenceNumberSyncer
326+
);
327+
328+
this._started = true;
329+
} finally {
330+
this.simpleDb.close();
331+
}
339332
}
340333

341334
/**

packages/firestore/src/local/simple_db.ts

Lines changed: 122 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -54,88 +54,8 @@ export interface SimpleDbSchemaConverter {
5454
* See PersistencePromise for more details.
5555
*/
5656
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-
}
57+
private db?: IDBDatabase;
58+
private versionchangelistener?: (event: IDBVersionChangeEvent) => void;
13959

14060
/** Deletes the specified database. */
14161
static delete(name: string): Promise<void> {
@@ -233,7 +153,25 @@ export class SimpleDb {
233153
return Number(version);
234154
}
235155

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

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

260277
async runTransaction<T>(
@@ -268,12 +285,14 @@ export class SimpleDb {
268285
while (true) {
269286
++attemptNumber;
270287

271-
const transaction = SimpleDbTransaction.open(
272-
this.db,
273-
readonly ? 'readonly' : 'readwrite',
274-
objectStores
275-
);
276288
try {
289+
this.db = await this.ensureDb();
290+
291+
const transaction = SimpleDbTransaction.open(
292+
this.db,
293+
readonly ? 'readonly' : 'readwrite',
294+
objectStores
295+
);
277296
const transactionFnResult = transactionFn(transaction)
278297
.catch(error => {
279298
// Abort the transaction if there was an error.
@@ -312,6 +331,8 @@ export class SimpleDb {
312331
retryable
313332
);
314333

334+
this.close();
335+
315336
if (!retryable) {
316337
return Promise.reject(error);
317338
}
@@ -320,7 +341,10 @@ export class SimpleDb {
320341
}
321342

322343
close(): void {
323-
this.db.close();
344+
if (this.db) {
345+
this.db.close();
346+
}
347+
this.db = undefined;
324348
}
325349
}
326350

@@ -400,7 +424,7 @@ export interface IterateOptions {
400424
export class IndexedDbTransactionError extends FirestoreError {
401425
name = 'IndexedDbTransactionError';
402426

403-
constructor(cause: Error) {
427+
constructor(cause: Error | string) {
404428
super(Code.UNAVAILABLE, 'IndexedDB transaction failed: ' + cause);
405429
}
406430
}
@@ -429,7 +453,11 @@ export class SimpleDbTransaction {
429453
mode: IDBTransactionMode,
430454
objectStoreNames: string[]
431455
): SimpleDbTransaction {
432-
return new SimpleDbTransaction(db.transaction(objectStoreNames, mode));
456+
try {
457+
return new SimpleDbTransaction(db.transaction(objectStoreNames, mode));
458+
} catch (e) {
459+
throw new IndexedDbTransactionError(e);
460+
}
433461
}
434462

435463
constructor(private readonly transaction: IDBTransaction) {

0 commit comments

Comments
 (0)