@@ -125,14 +125,21 @@ export class RemoteStore implements TargetMetadataProvider {
125
125
126
126
private onlineStateTracker : OnlineStateTracker ;
127
127
128
+ /**
129
+ * A barrier to track unresolved operations that block the restart of the
130
+ * write stream. This is used to remove writes from the mutation queue if the
131
+ * initial removal attempt failed.
132
+ */
133
+ private writeStreamBarrier = 0 ;
134
+
128
135
constructor (
129
136
/**
130
137
* The local store, used to fill the write pipeline with outbound mutations.
131
138
*/
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
+ async enableNetworkInternal ( ) : Promise < void > {
190
200
if ( this . canUseNetwork ( ) ) {
191
201
if ( this . shouldStartWatchStream ( ) ) {
192
202
this . startWatchStream ( ) ;
@@ -226,6 +236,29 @@ export class RemoteStore implements TargetMetadataProvider {
226
236
this . cleanUpWatchStreamState ( ) ;
227
237
}
228
238
239
+ /**
240
+ * Recovery logic for IndexedDB errors that takes the network offline until
241
+ * `op` succeeds. Retries are scheduled with backoff using `enqueueRetryable()`.
242
+ */
243
+ private async disableNetworkUntilRecovery (
244
+ e : FirestoreError ,
245
+ op : ( ) => Promise < void >
246
+ ) : Promise < void > {
247
+ if ( e . name === 'IndexedDbTransactionError' ) {
248
+ // Increment the write stream barrier to prevent out of band stream
249
+ // restarts.
250
+ ++ this . writeStreamBarrier ;
251
+ await this . disableNetworkInternal ( ) ;
252
+ this . asyncQueue . enqueueRetryable ( async ( ) => {
253
+ await op ( ) ;
254
+ -- this . writeStreamBarrier ;
255
+ await this . enableNetworkInternal ( ) ;
256
+ } ) ;
257
+ } else {
258
+ throw e ;
259
+ }
260
+ }
261
+
229
262
async shutdown ( ) : Promise < void > {
230
263
logDebug ( LOG_TAG , 'RemoteStore shutting down.' ) ;
231
264
this . networkEnabled = false ;
@@ -337,7 +370,9 @@ export class RemoteStore implements TargetMetadataProvider {
337
370
}
338
371
339
372
canUseNetwork ( ) : boolean {
340
- return this . isPrimary && this . networkEnabled ;
373
+ return (
374
+ this . writeStreamBarrier === 0 && this . isPrimary && this . networkEnabled
375
+ ) ;
341
376
}
342
377
343
378
private cleanUpWatchStreamState ( ) : void {
@@ -510,27 +545,31 @@ export class RemoteStore implements TargetMetadataProvider {
510
545
* Starts the write stream if necessary.
511
546
*/
512
547
async fillWritePipeline ( ) : Promise < void > {
513
- if ( this . canAddToWritePipeline ( ) ) {
514
- const lastBatchIdRetrieved =
515
- this . writePipeline . length > 0
516
- ? this . writePipeline [ this . writePipeline . length - 1 ] . batchId
517
- : BATCHID_UNKNOWN ;
518
- const batch = await this . localStore . nextMutationBatch (
519
- lastBatchIdRetrieved
520
- ) ;
521
-
522
- if ( batch === null ) {
523
- if ( this . writePipeline . length === 0 ) {
524
- this . writeStream . markIdle ( ) ;
548
+ try {
549
+ while ( this . canAddToWritePipeline ( ) ) {
550
+ const lastBatchIdRetrieved =
551
+ this . writePipeline . length > 0
552
+ ? this . writePipeline [ this . writePipeline . length - 1 ] . batchId
553
+ : BATCHID_UNKNOWN ;
554
+
555
+ const batch = await this . localStore . nextMutationBatch (
556
+ lastBatchIdRetrieved
557
+ ) ;
558
+
559
+ if ( batch ) {
560
+ this . addToWritePipeline ( batch ) ;
561
+ } else {
562
+ break ;
525
563
}
526
- } else {
527
- this . addToWritePipeline ( batch ) ;
528
- await this . fillWritePipeline ( ) ;
529
564
}
530
- }
531
565
532
- if ( this . shouldStartWriteStream ( ) ) {
533
- this . startWriteStream ( ) ;
566
+ if ( this . shouldStartWriteStream ( ) ) {
567
+ this . startWriteStream ( ) ;
568
+ } else if ( this . writePipeline . length === 0 ) {
569
+ this . writeStream . markIdle ( ) ;
570
+ }
571
+ } catch ( e ) {
572
+ await this . disableNetworkUntilRecovery ( e , ( ) => Promise . resolve ( ) ) ;
534
573
}
535
574
}
536
575
@@ -568,6 +607,7 @@ export class RemoteStore implements TargetMetadataProvider {
568
607
private shouldStartWriteStream ( ) : boolean {
569
608
return (
570
609
this . canUseNetwork ( ) &&
610
+ this . writeStreamBarrier === 0 &&
571
611
! this . writeStream . isStarted ( ) &&
572
612
this . writePipeline . length > 0
573
613
) ;
@@ -592,7 +632,7 @@ export class RemoteStore implements TargetMetadataProvider {
592
632
}
593
633
}
594
634
595
- private onMutationResult (
635
+ private async onMutationResult (
596
636
commitVersion : SnapshotVersion ,
597
637
results : MutationResult [ ]
598
638
) : Promise < void > {
@@ -604,11 +644,16 @@ export class RemoteStore implements TargetMetadataProvider {
604
644
) ;
605
645
const batch = this . writePipeline . shift ( ) ! ;
606
646
const success = MutationBatchResult . from ( batch , commitVersion , results ) ;
607
- return this . syncEngine . applySuccessfulWrite ( success ) . then ( ( ) => {
647
+ try {
648
+ await this . syncEngine . applySuccessfulWrite ( success ) ;
608
649
// It's possible that with the completion of this mutation another
609
650
// slot has freed up.
610
- return this . fillWritePipeline ( ) ;
611
- } ) ;
651
+ await this . fillWritePipeline ( ) ;
652
+ } catch ( e ) {
653
+ await this . disableNetworkUntilRecovery ( e , ( ) =>
654
+ this . syncEngine . applySuccessfulWrite ( success )
655
+ ) ;
656
+ }
612
657
}
613
658
614
659
private async onWriteStreamClose ( error ?: FirestoreError ) : Promise < void > {
@@ -621,8 +666,8 @@ export class RemoteStore implements TargetMetadataProvider {
621
666
) ;
622
667
}
623
668
624
- // If the write stream closed after the write handshake completes, a write
625
- // operation failed and we fail the pending operation.
669
+ // An error that occurs after the write handshake completes is an indication
670
+ // that the write operation itself failed .
626
671
if ( error && this . writeStream . handshakeComplete ) {
627
672
// This error affects the actual write.
628
673
await this . handleWriteError ( error ! ) ;
@@ -648,13 +693,16 @@ export class RemoteStore implements TargetMetadataProvider {
648
693
// restart.
649
694
this . writeStream . inhibitBackoff ( ) ;
650
695
651
- return this . syncEngine
652
- . rejectFailedWrite ( batch . batchId , error )
653
- . then ( ( ) => {
654
- // It's possible that with the completion of this mutation
655
- // another slot has freed up.
656
- return this . fillWritePipeline ( ) ;
657
- } ) ;
696
+ try {
697
+ await this . syncEngine . rejectFailedWrite ( batch . batchId , error ) ;
698
+ // It's possible that with the completion of this mutation
699
+ // another slot has freed up.
700
+ await this . fillWritePipeline ( ) ;
701
+ } catch ( e ) {
702
+ await this . disableNetworkUntilRecovery ( e , ( ) =>
703
+ this . syncEngine . rejectFailedWrite ( batch . batchId , error )
704
+ ) ;
705
+ }
658
706
} else {
659
707
// Transient error, just let the retry logic kick in.
660
708
}
0 commit comments