Skip to content

Commit fb90059

Browse files
IndexedDB recovery for waitForPendingWrites() (#3038)
1 parent ac215cf commit fb90059

File tree

8 files changed

+73
-48
lines changed

8 files changed

+73
-48
lines changed

packages/firestore/src/core/event_manager.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ import { Query } from './query';
2222
import { SyncEngine, SyncEngineListener } from './sync_engine';
2323
import { OnlineState } from './types';
2424
import { ChangeType, DocumentViewChange, ViewSnapshot } from './view_snapshot';
25-
import { logError } from '../util/log';
26-
import { Code, FirestoreError } from '../util/error';
27-
28-
const LOG_TAG = 'EventManager';
25+
import { wrapInUserErrorIfRecoverable } from '../util/async_queue';
2926

3027
/**
3128
* Holds the listeners and the last received ViewSnapshot for a query being
@@ -76,13 +73,11 @@ export class EventManager implements SyncEngineListener {
7673
try {
7774
queryInfo.viewSnap = await this.syncEngine.listen(query);
7875
} catch (e) {
79-
const msg = `Initialization of query '${query}' failed: ${e}`;
80-
logError(LOG_TAG, msg);
81-
if (e.name === 'IndexedDbTransactionError') {
82-
listener.onError(new FirestoreError(Code.UNAVAILABLE, msg));
83-
} else {
84-
throw e;
85-
}
76+
const firestoreError = wrapInUserErrorIfRecoverable(
77+
e,
78+
`Initialization of query '${listener.query}' failed`
79+
);
80+
listener.onError(firestoreError);
8681
return;
8782
}
8883
}

packages/firestore/src/core/sync_engine.ts

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { User } from '../auth/user';
1919
import {
2020
ignoreIfPrimaryLeaseLoss,
2121
LocalStore,
22-
LocalWriteResult,
2322
MultiTabLocalStore
2423
} from '../local/local_store';
2524
import { LocalViewChanges } from '../local/local_view_changes';
@@ -39,7 +38,7 @@ import { RemoteStore } from '../remote/remote_store';
3938
import { RemoteSyncer } from '../remote/remote_syncer';
4039
import { debugAssert, fail, hardAssert } from '../util/assert';
4140
import { Code, FirestoreError } from '../util/error';
42-
import { logDebug, logError } from '../util/log';
41+
import { logDebug } from '../util/log';
4342
import { primitiveComparator } from '../util/misc';
4443
import { ObjectMap } from '../util/obj_map';
4544
import { Deferred } from '../util/promise';
@@ -73,7 +72,7 @@ import {
7372
ViewDocumentChanges
7473
} from './view';
7574
import { ViewSnapshot } from './view_snapshot';
76-
import { AsyncQueue } from '../util/async_queue';
75+
import { AsyncQueue, wrapInUserErrorIfRecoverable } from '../util/async_queue';
7776
import { TransactionRunner } from './transaction_runner';
7877

7978
const LOG_TAG = 'SyncEngine';
@@ -353,27 +352,18 @@ export class SyncEngine implements RemoteSyncer {
353352
async write(batch: Mutation[], userCallback: Deferred<void>): Promise<void> {
354353
this.assertSubscribed('write()');
355354

356-
let result: LocalWriteResult;
357355
try {
358-
result = await this.localStore.localWrite(batch);
356+
const result = await this.localStore.localWrite(batch);
357+
this.sharedClientState.addPendingMutation(result.batchId);
358+
this.addMutationCallback(result.batchId, userCallback);
359+
await this.emitNewSnapsAndNotifyLocalStore(result.changes);
360+
await this.remoteStore.fillWritePipeline();
359361
} catch (e) {
360-
if (e.name === 'IndexedDbTransactionError') {
361-
// If we can't persist the mutation, we reject the user callback and
362-
// don't send the mutation. The user can then retry the write.
363-
logError(LOG_TAG, 'Dropping write that cannot be persisted: ' + e);
364-
userCallback.reject(
365-
new FirestoreError(Code.UNAVAILABLE, 'Failed to persist write: ' + e)
366-
);
367-
return;
368-
} else {
369-
throw e;
370-
}
362+
// If we can't persist the mutation, we reject the user callback and
363+
// don't send the mutation. The user can then retry the write.
364+
const error = wrapInUserErrorIfRecoverable(e, `Failed to persist write`);
365+
userCallback.reject(error);
371366
}
372-
373-
this.sharedClientState.addPendingMutation(result.batchId);
374-
this.addMutationCallback(result.batchId, userCallback);
375-
await this.emitNewSnapsAndNotifyLocalStore(result.changes);
376-
await this.remoteStore.fillWritePipeline();
377367
}
378368

379369
/**
@@ -588,16 +578,24 @@ export class SyncEngine implements RemoteSyncer {
588578
);
589579
}
590580

591-
const highestBatchId = await this.localStore.getHighestUnacknowledgedBatchId();
592-
if (highestBatchId === BATCHID_UNKNOWN) {
593-
// Trigger the callback right away if there is no pending writes at the moment.
594-
callback.resolve();
595-
return;
596-
}
581+
try {
582+
const highestBatchId = await this.localStore.getHighestUnacknowledgedBatchId();
583+
if (highestBatchId === BATCHID_UNKNOWN) {
584+
// Trigger the callback right away if there is no pending writes at the moment.
585+
callback.resolve();
586+
return;
587+
}
597588

598-
const callbacks = this.pendingWritesCallbacks.get(highestBatchId) || [];
599-
callbacks.push(callback);
600-
this.pendingWritesCallbacks.set(highestBatchId, callbacks);
589+
const callbacks = this.pendingWritesCallbacks.get(highestBatchId) || [];
590+
callbacks.push(callback);
591+
this.pendingWritesCallbacks.set(highestBatchId, callbacks);
592+
} catch (e) {
593+
const firestoreError = wrapInUserErrorIfRecoverable(
594+
e,
595+
'Initialization of waitForPendingWrites() operation failed'
596+
);
597+
callback.reject(firestoreError);
598+
}
601599
}
602600

603601
/**

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ import {
7272
import { PersistencePromise } from './persistence_promise';
7373
import { ClientId } from './shared_client_state';
7474
import { TargetData } from './target_data';
75-
import { SimpleDb, SimpleDbStore, SimpleDbTransaction } from './simple_db';
75+
import {
76+
isIndexedDbTransactionError,
77+
SimpleDb,
78+
SimpleDbStore,
79+
SimpleDbTransaction
80+
} from './simple_db';
7681

7782
const LOG_TAG = 'IndexedDbPersistence';
7883

@@ -287,7 +292,7 @@ export class IndexedDbPersistence implements Persistence {
287292
// having the persistence lock), so it's the first thing we should do.
288293
return this.updateClientMetadataAndTryBecomePrimary();
289294
})
290-
.then(e => {
295+
.then(() => {
291296
if (!this.isPrimary && !this.allowTabSynchronization) {
292297
// Fail `start()` if `synchronizeTabs` is disabled and we cannot
293298
// obtain the primary lease.
@@ -423,7 +428,7 @@ export class IndexedDbPersistence implements Persistence {
423428
)
424429
.catch(e => {
425430
if (!this.allowTabSynchronization) {
426-
if (e.name === 'IndexedDbTransactionError') {
431+
if (isIndexedDbTransactionError(e)) {
427432
logDebug(LOG_TAG, 'Failed to extend owner lease: ', e);
428433
// Proceed with the existing state. Any subsequent access to
429434
// IndexedDB will verify the lease.

packages/firestore/src/local/local_store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { IndexedDbMutationQueue } from './indexeddb_mutation_queue';
6666
import { IndexedDbRemoteDocumentCache } from './indexeddb_remote_document_cache';
6767
import { IndexedDbTargetCache } from './indexeddb_target_cache';
6868
import { extractFieldMask } from '../model/object_value';
69+
import { isIndexedDbTransactionError } from './simple_db';
6970

7071
const LOG_TAG = 'LocalStore';
7172

@@ -727,7 +728,7 @@ export class LocalStore {
727728
}
728729
);
729730
} catch (e) {
730-
if (e.name === 'IndexedDbTransactionError') {
731+
if (isIndexedDbTransactionError(e)) {
731732
// If `notifyLocalViewChanges` fails, we did not advance the sequence
732733
// number for the documents that were included in this transaction.
733734
// This might trigger them to be deleted earlier than they otherwise

packages/firestore/src/local/lru_garbage_collector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from './persistence';
3232
import { PersistencePromise } from './persistence_promise';
3333
import { TargetData } from './target_data';
34+
import { isIndexedDbTransactionError } from './simple_db';
3435

3536
const LOG_TAG = 'LruGarbageCollector';
3637

@@ -273,7 +274,7 @@ export class LruScheduler implements GarbageCollectionScheduler {
273274
try {
274275
await localStore.collectGarbage(this.garbageCollector);
275276
} catch (e) {
276-
if (e.name === 'IndexedDbTransactionError') {
277+
if (isIndexedDbTransactionError(e)) {
277278
logDebug(
278279
LOG_TAG,
279280
'Ignoring IndexedDB error during garbage collection: ',

packages/firestore/src/local/simple_db.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,13 @@ export class IndexedDbTransactionError extends FirestoreError {
418418
}
419419
}
420420

421+
/** Verifies whether `e` is an IndexedDbTransactionError. */
422+
export function isIndexedDbTransactionError(e: Error): boolean {
423+
// Use name equality, as instanceof checks on errors don't work with errors
424+
// that wrap other errors.
425+
return e.name === 'IndexedDbTransactionError';
426+
}
427+
421428
/**
422429
* Wraps an IDBTransaction and exposes a store() method to get a handle to a
423430
* specific object store.

packages/firestore/src/remote/remote_store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
WatchTargetChangeState
5555
} from './watch_change';
5656
import { ByteString } from '../util/byte_string';
57+
import { isIndexedDbTransactionError } from '../local/simple_db';
5758

5859
const LOG_TAG = 'RemoteStore';
5960

@@ -448,7 +449,7 @@ export class RemoteStore implements TargetMetadataProvider {
448449
* `enqueueRetryable()`.
449450
*/
450451
private async disableNetworkUntilRecovery(e: FirestoreError): Promise<void> {
451-
if (e.name === 'IndexedDbTransactionError') {
452+
if (isIndexedDbTransactionError(e)) {
452453
debugAssert(
453454
!this.indexedDbFailed,
454455
'Unexpected network event when IndexedDB was marked failed.'

packages/firestore/src/util/async_queue.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { logDebug, logError } from './log';
2121
import { CancelablePromise, Deferred } from './promise';
2222
import { ExponentialBackoff } from '../remote/backoff';
2323
import { PlatformSupport } from '../platform/platform';
24+
import { isIndexedDbTransactionError } from '../local/simple_db';
2425

2526
const LOG_TAG = 'AsyncQueue';
2627

@@ -338,7 +339,7 @@ export class AsyncQueue {
338339
deferred.resolve();
339340
this.backoff.reset();
340341
} catch (e) {
341-
if (e.name === 'IndexedDbTransactionError') {
342+
if (isIndexedDbTransactionError(e)) {
342343
logDebug(LOG_TAG, 'Operation failed with retryable error: ' + e);
343344
this.backoff.backoffAndRun(retryingOp);
344345
} else {
@@ -501,3 +502,19 @@ export class AsyncQueue {
501502
this.delayedOperations.splice(index, 1);
502503
}
503504
}
505+
506+
/**
507+
* Returns a FirestoreError that can be surfaced to the user if the provided
508+
* error is an IndexedDbTransactionError. Re-throws the error otherwise.
509+
*/
510+
export function wrapInUserErrorIfRecoverable(
511+
e: Error,
512+
msg: string
513+
): FirestoreError {
514+
logError(LOG_TAG, `${msg}: ${e}`);
515+
if (isIndexedDbTransactionError(e)) {
516+
return new FirestoreError(Code.UNAVAILABLE, `${msg}: ${e}`);
517+
} else {
518+
throw e;
519+
}
520+
}

0 commit comments

Comments
 (0)