@@ -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,10 @@ 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 (
229
+ this . state === PersistentStreamState . Open ||
230
+ this . state === PersistentStreamState . Healthy
231
+ ) ;
217
232
}
218
233
219
234
/**
@@ -311,6 +326,14 @@ export abstract class PersistentStream<
311
326
}
312
327
}
313
328
329
+ /** Cancels the health check delayed operation. */
330
+ private cancelHealthCheck ( ) : void {
331
+ if ( this . healthCheck ) {
332
+ this . healthCheck . cancel ( ) ;
333
+ this . healthCheck = null ;
334
+ }
335
+ }
336
+
314
337
/**
315
338
* Closes the stream and cleans up as necessary:
316
339
*
@@ -336,6 +359,7 @@ export abstract class PersistentStream<
336
359
337
360
// Cancel any outstanding timers (they're guaranteed not to execute).
338
361
this . cancelIdleCheck ( ) ;
362
+ this . cancelHealthCheck ( ) ;
339
363
this . backoff . cancel ( ) ;
340
364
341
365
// Invalidates any stream-related callbacks (e.g. from auth or the
@@ -352,9 +376,17 @@ export abstract class PersistentStream<
352
376
'Using maximum backoff delay to prevent overloading the backend.'
353
377
) ;
354
378
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.
379
+ } else if (
380
+ error &&
381
+ error . code === Code . UNAUTHENTICATED &&
382
+ this . state !== PersistentStreamState . Healthy
383
+ ) {
384
+ // "unauthenticated" error means the token was rejected. This should rarely
385
+ // happen since both Auth and AppCheck ensure a sufficient TTL when we
386
+ // request a token. If a user manually resets their system clock this can
387
+ // fail, however. In this case, we should get a Code.UNAUTHENTICATED error
388
+ // before we received the first message and we need to invalidate the token
389
+ // to ensure that we fetch a new token.
358
390
this . credentialsProvider . invalidateToken ( ) ;
359
391
}
360
392
@@ -448,6 +480,20 @@ export abstract class PersistentStream<
448
480
'Expected stream to be in state Starting, but was ' + this . state
449
481
) ;
450
482
this . state = PersistentStreamState . Open ;
483
+ debugAssert (
484
+ this . healthCheck === null ,
485
+ 'Expected healthCheck to be null'
486
+ ) ;
487
+ this . healthCheck = this . queue . enqueueAfterDelay (
488
+ this . healthTimerId ,
489
+ HEALTHY_TIMEOUT_MS ,
490
+ ( ) => {
491
+ if ( this . isOpen ( ) ) {
492
+ this . state = PersistentStreamState . Healthy ;
493
+ }
494
+ return Promise . resolve ( ) ;
495
+ }
496
+ ) ;
451
497
return this . listener ! . onOpen ( ) ;
452
498
} ) ;
453
499
} ) ;
@@ -559,6 +605,7 @@ export class PersistentListenStream extends PersistentStream<
559
605
queue ,
560
606
TimerId . ListenStreamConnectionBackoff ,
561
607
TimerId . ListenStreamIdle ,
608
+ TimerId . HealthCheckTimeout ,
562
609
connection ,
563
610
credentials ,
564
611
listener
@@ -667,6 +714,7 @@ export class PersistentWriteStream extends PersistentStream<
667
714
queue ,
668
715
TimerId . WriteStreamConnectionBackoff ,
669
716
TimerId . WriteStreamIdle ,
717
+ TimerId . HealthCheckTimeout ,
670
718
connection ,
671
719
credentials ,
672
720
listener
0 commit comments