@@ -123,6 +123,13 @@ export class RemoteStore implements TargetMetadataProvider {
123
123
124
124
private isPrimary = false ;
125
125
126
+ /**
127
+ * When set to `true`, the network was taken offline due to an IndexedDB
128
+ * failure. The state is flipped to `false` when access becomes available
129
+ * again.
130
+ */
131
+ private indexedDbFailed = false ;
132
+
126
133
private onlineStateTracker : OnlineStateTracker ;
127
134
128
135
constructor (
@@ -132,7 +139,7 @@ export class RemoteStore implements TargetMetadataProvider {
132
139
private localStore : LocalStore ,
133
140
/** The client-side proxy for interacting with the backend. */
134
141
private datastore : Datastore ,
135
- asyncQueue : AsyncQueue ,
142
+ private asyncQueue : AsyncQueue ,
136
143
onlineStateHandler : ( onlineState : OnlineState ) => void ,
137
144
connectivityMonitor : ConnectivityMonitor
138
145
) {
@@ -184,9 +191,12 @@ export class RemoteStore implements TargetMetadataProvider {
184
191
}
185
192
186
193
/** Re-enables the network. Idempotent. */
187
- async enableNetwork ( ) : Promise < void > {
194
+ enableNetwork ( ) : Promise < void > {
188
195
this . networkEnabled = true ;
196
+ return this . enableNetworkInternal ( ) ;
197
+ }
189
198
199
+ private async enableNetworkInternal ( ) : Promise < void > {
190
200
if ( this . canUseNetwork ( ) ) {
191
201
this . writeStream . lastStreamToken = await this . localStore . getLastStreamToken ( ) ;
192
202
@@ -339,7 +349,7 @@ export class RemoteStore implements TargetMetadataProvider {
339
349
}
340
350
341
351
canUseNetwork ( ) : boolean {
342
- return this . isPrimary && this . networkEnabled ;
352
+ return ! this . indexedDbFailed && this . isPrimary && this . networkEnabled ;
343
353
}
344
354
345
355
private cleanUpWatchStreamState ( ) : void {
@@ -391,7 +401,18 @@ export class RemoteStore implements TargetMetadataProvider {
391
401
) {
392
402
// There was an error on a target, don't wait for a consistent snapshot
393
403
// to raise events
394
- return this . handleTargetError ( watchChange ) ;
404
+ try {
405
+ await this . handleTargetError ( watchChange ) ;
406
+ } catch ( e ) {
407
+ logDebug (
408
+ LOG_TAG ,
409
+ 'Failed to remove targets %s: %s ' ,
410
+ watchChange . targetIds . join ( ',' ) ,
411
+ e
412
+ ) ;
413
+ await this . disableNetworkUntilRecovery ( e ) ;
414
+ }
415
+ return ;
395
416
}
396
417
397
418
if ( watchChange instanceof DocumentWatchChange ) {
@@ -407,15 +428,52 @@ export class RemoteStore implements TargetMetadataProvider {
407
428
}
408
429
409
430
if ( ! snapshotVersion . isEqual ( SnapshotVersion . min ( ) ) ) {
410
- const lastRemoteSnapshotVersion = await this . localStore . getLastRemoteSnapshotVersion ( ) ;
411
- if ( snapshotVersion . compareTo ( lastRemoteSnapshotVersion ) >= 0 ) {
412
- // We have received a target change with a global snapshot if the snapshot
413
- // version is not equal to SnapshotVersion.min().
414
- await this . raiseWatchSnapshot ( snapshotVersion ) ;
431
+ try {
432
+ const lastRemoteSnapshotVersion = await this . localStore . getLastRemoteSnapshotVersion ( ) ;
433
+ if ( snapshotVersion . compareTo ( lastRemoteSnapshotVersion ) >= 0 ) {
434
+ // We have received a target change with a global snapshot if the snapshot
435
+ // version is not equal to SnapshotVersion.min().
436
+ await this . raiseWatchSnapshot ( snapshotVersion ) ;
437
+ }
438
+ } catch ( e ) {
439
+ logDebug ( LOG_TAG , 'Failed to raise snapshot:' , e ) ;
440
+ await this . disableNetworkUntilRecovery ( e ) ;
415
441
}
416
442
}
417
443
}
418
444
445
+ /**
446
+ * Recovery logic for IndexedDB errors that takes the network offline until
447
+ * IndexedDb probing succeeds. Retries are scheduled with backoff using
448
+ * `enqueueRetryable()`.
449
+ */
450
+ private async disableNetworkUntilRecovery ( e : FirestoreError ) : Promise < void > {
451
+ if ( e . name === 'IndexedDbTransactionError' ) {
452
+ debugAssert (
453
+ ! this . indexedDbFailed ,
454
+ 'Unexpected network event when IndexedDB was marked failed.'
455
+ ) ;
456
+ this . indexedDbFailed = true ;
457
+
458
+ // Disable network and raise offline snapshots
459
+ await this . disableNetworkInternal ( ) ;
460
+ this . onlineStateTracker . set ( OnlineState . Offline ) ;
461
+
462
+ // Probe IndexedDB periodically and re-enable network
463
+ this . asyncQueue . enqueueRetryable ( async ( ) => {
464
+ logDebug ( LOG_TAG , 'Retrying IndexedDB access' ) ;
465
+ // Issue a simple read operation to determine if IndexedDB recovered.
466
+ // Ideally, we would expose a health check directly on SimpleDb, but
467
+ // RemoteStore only has access to persistence through LocalStore.
468
+ await this . localStore . getLastRemoteSnapshotVersion ( ) ;
469
+ this . indexedDbFailed = false ;
470
+ await this . enableNetworkInternal ( ) ;
471
+ } ) ;
472
+ } else {
473
+ throw e ;
474
+ }
475
+ }
476
+
419
477
/**
420
478
* Takes a batch of changes from the Datastore, repackages them as a
421
479
* RemoteEvent, and passes that on to the listener, which is typically the
@@ -486,21 +544,19 @@ export class RemoteStore implements TargetMetadataProvider {
486
544
}
487
545
488
546
/** Handles an error on a target */
489
- private handleTargetError ( watchChange : WatchTargetChange ) : Promise < void > {
547
+ private async handleTargetError (
548
+ watchChange : WatchTargetChange
549
+ ) : Promise < void > {
490
550
debugAssert ( ! ! watchChange . cause , 'Handling target error without a cause' ) ;
491
551
const error = watchChange . cause ! ;
492
- let promiseChain = Promise . resolve ( ) ;
493
- watchChange . targetIds . forEach ( targetId => {
494
- promiseChain = promiseChain . then ( async ( ) => {
495
- // A watched target might have been removed already.
496
- if ( this . listenTargets . has ( targetId ) ) {
497
- this . listenTargets . delete ( targetId ) ;
498
- this . watchChangeAggregator ! . removeTarget ( targetId ) ;
499
- return this . syncEngine . rejectListen ( targetId , error ) ;
500
- }
501
- } ) ;
502
- } ) ;
503
- return promiseChain ;
552
+ for ( const targetId of watchChange . targetIds ) {
553
+ // A watched target might have been removed already.
554
+ if ( this . listenTargets . has ( targetId ) ) {
555
+ await this . syncEngine . rejectListen ( targetId , error ) ;
556
+ this . listenTargets . delete ( targetId ) ;
557
+ this . watchChangeAggregator ! . removeTarget ( targetId ) ;
558
+ }
559
+ }
504
560
}
505
561
506
562
/**
@@ -637,25 +693,21 @@ export class RemoteStore implements TargetMetadataProvider {
637
693
// If the write stream closed due to an error, invoke the error callbacks if
638
694
// there are pending writes.
639
695
if ( error && this . writePipeline . length > 0 ) {
640
- // A promise that is resolved after we processed the error
641
- let errorHandling : Promise < void > ;
642
696
if ( this . writeStream . handshakeComplete ) {
643
697
// This error affects the actual write.
644
- errorHandling = this . handleWriteError ( error ! ) ;
698
+ await this . handleWriteError ( error ! ) ;
645
699
} else {
646
700
// If there was an error before the handshake has finished, it's
647
701
// possible that the server is unable to process the stream token
648
702
// we're sending. (Perhaps it's too old?)
649
- errorHandling = this . handleHandshakeError ( error ! ) ;
703
+ await this . handleHandshakeError ( error ! ) ;
650
704
}
651
705
652
- return errorHandling . then ( ( ) => {
653
- // The write stream might have been started by refilling the write
654
- // pipeline for failed writes
655
- if ( this . shouldStartWriteStream ( ) ) {
656
- this . startWriteStream ( ) ;
657
- }
658
- } ) ;
706
+ // The write stream might have been started by refilling the write
707
+ // pipeline for failed writes
708
+ if ( this . shouldStartWriteStream ( ) ) {
709
+ this . startWriteStream ( ) ;
710
+ }
659
711
}
660
712
// No pending writes, nothing to do
661
713
}
0 commit comments