@@ -97,6 +97,13 @@ const enum PersistentStreamState {
97
97
*/
98
98
Open ,
99
99
100
+ /**
101
+ * The stream is healthy and has been connected for more than 10 seconds. We
102
+ * therefore assume that the credentials we passed were valid. Both
103
+ * isStarted() and isOpen() will return true.
104
+ */
105
+ Healthy ,
106
+
100
107
/**
101
108
* The stream encountered an error. The next start attempt will back off.
102
109
* While in this state isStarted() will return false.
@@ -132,6 +139,9 @@ export interface PersistentStreamListener {
132
139
/** The time a stream stays open after it is marked idle. */
133
140
const IDLE_TIMEOUT_MS = 60 * 1000 ;
134
141
142
+ /** The time a stream stays open until we consider it healthy. */
143
+ const HEALTHY_TIMEOUT_MS = 10 * 1000 ;
144
+
135
145
/**
136
146
* A PersistentStream is an abstract base class that represents a streaming RPC
137
147
* to the Firestore backend. It's built on top of the connections own support
@@ -178,6 +188,7 @@ export abstract class PersistentStream<
178
188
private closeCount = 0 ;
179
189
180
190
private idleTimer : DelayedOperation < void > | null = null ;
191
+ private healthCheck : DelayedOperation < void > | null = null ;
181
192
private stream : Stream < SendType , ReceiveType > | null = null ;
182
193
183
194
protected backoff : ExponentialBackoff ;
@@ -186,6 +197,7 @@ export abstract class PersistentStream<
186
197
private queue : AsyncQueue ,
187
198
connectionTimerId : TimerId ,
188
199
private idleTimerId : TimerId ,
200
+ private healthTimerId : TimerId ,
189
201
protected connection : Connection ,
190
202
private credentialsProvider : CredentialsProvider ,
191
203
protected listener : ListenerType
@@ -203,8 +215,8 @@ export abstract class PersistentStream<
203
215
isStarted ( ) : boolean {
204
216
return (
205
217
this . state === PersistentStreamState . Starting ||
206
- this . state === PersistentStreamState . Open ||
207
- this . state === PersistentStreamState . Backoff
218
+ this . state === PersistentStreamState . Backoff ||
219
+ this . isOpen ( )
208
220
) ;
209
221
}
210
222
@@ -213,7 +225,8 @@ export abstract class PersistentStream<
213
225
* called) and the stream is ready for outbound requests.
214
226
*/
215
227
isOpen ( ) : boolean {
216
- return this . state === PersistentStreamState . Open ;
228
+ return this . state === PersistentStreamState . Open ||
229
+ this . state === PersistentStreamState . Healthy ;
217
230
}
218
231
219
232
/**
@@ -291,6 +304,7 @@ export abstract class PersistentStream<
291
304
/** Sends a message to the underlying stream. */
292
305
protected sendRequest ( msg : SendType ) : void {
293
306
this . cancelIdleCheck ( ) ;
307
+ this . cancelHealthCheck ( ) ;
294
308
this . stream ! . send ( msg ) ;
295
309
}
296
310
@@ -311,6 +325,14 @@ export abstract class PersistentStream<
311
325
}
312
326
}
313
327
328
+ /** Cancels the health check delayed operation. */
329
+ private cancelHealthCheck ( ) : void {
330
+ if ( this . healthCheck ) {
331
+ this . healthCheck . cancel ( ) ;
332
+ this . healthCheck = null ;
333
+ }
334
+ }
335
+
314
336
/**
315
337
* Closes the stream and cleans up as necessary:
316
338
*
@@ -352,9 +374,14 @@ export abstract class PersistentStream<
352
374
'Using maximum backoff delay to prevent overloading the backend.'
353
375
) ;
354
376
this . backoff . resetToMax ( ) ;
355
- } else if ( error && error . code === Code . UNAUTHENTICATED ) {
356
- // "unauthenticated" error means the token was rejected. Try force refreshing it in case it
357
- // just expired.
377
+ } else if ( error && error . code === Code . UNAUTHENTICATED &&
378
+ this . state !== PersistentStreamState . Healthy ) {
379
+ // "unauthenticated" error means the token was rejected. This should rarely
380
+ // happen since both Auth and AppCheck ensure a sufficient TTL when we
381
+ // request a token. If a user manually resets their system clock this can
382
+ // fail, however. In this case, we should get a Code.UNAUTHENTICATED error
383
+ // before we received the first message and we need to invalidate the token
384
+ // to ensure that we fetch a new token.
358
385
this . credentialsProvider . invalidateToken ( ) ;
359
386
}
360
387
@@ -450,6 +477,19 @@ export abstract class PersistentStream<
450
477
this . state = PersistentStreamState . Open ;
451
478
return this . listener ! . onOpen ( ) ;
452
479
} ) ;
480
+
481
+ if ( this . healthCheck === null ) {
482
+ this . healthCheck = this . queue . enqueueAfterDelay (
483
+ this . healthTimerId ,
484
+ HEALTHY_TIMEOUT_MS ,
485
+ ( ) => {
486
+ if ( this . isOpen ( ) ) {
487
+ this . state = PersistentStreamState . Healthy ;
488
+ }
489
+ return Promise . resolve ( ) ;
490
+ }
491
+ ) ;
492
+ }
453
493
} ) ;
454
494
this . stream . onClose ( ( error ?: FirestoreError ) => {
455
495
dispatchIfNotClosed ( ( ) => {
@@ -559,6 +599,7 @@ export class PersistentListenStream extends PersistentStream<
559
599
queue ,
560
600
TimerId . ListenStreamConnectionBackoff ,
561
601
TimerId . ListenStreamIdle ,
602
+ TimerId . HealthCheckTimeout ,
562
603
connection ,
563
604
credentials ,
564
605
listener
@@ -667,6 +708,7 @@ export class PersistentWriteStream extends PersistentStream<
667
708
queue ,
668
709
TimerId . WriteStreamConnectionBackoff ,
669
710
TimerId . WriteStreamIdle ,
711
+ TimerId . HealthCheckTimeout ,
670
712
connection ,
671
713
credentials ,
672
714
listener
0 commit comments