@@ -140,20 +140,24 @@ export function createStreamingBase(config: {
140140 }
141141
142142 /**
143- * Promotes a directive deferred from `onopen` (`pendingFallback`) into the
144- * committed `fdv1Fallback`/`fdv1FallbackTtlMs` state, clears the pending
145- * pair, and returns the current committed fallback state.
146- *
147- * Called via putWithFallback() by every path that can queue a payload
148- * result, a stream error, a network failure, or a ping-triggered poll
149- * result (including one that succeeds without its own fallback signal),
150- * so a directive deferred at onopen surfaces no matter which path fires
151- * next. The one exception is the plain goodbye path, which only runs
152- * when nothing is pending and reads the committed flag directly instead.
153- * Safe to call when nothing is pending; it just returns the current state
154- * unchanged.
143+ * Resolves the current fallback directive, optionally overridden by an
144+ * `incoming` directive from a different source (an in-band goodbye payload
145+ * or an error-response header). When `incoming` signals fallback, it wins
146+ * outright - it carries its own TTL, which takes precedence over anything
147+ * deferred at `onopen`, and the pending pair is cleared since it is now
148+ * superseded. Otherwise, a directive deferred at `onopen` (`pendingFallback`)
149+ * is promoted into the committed `fdv1Fallback`/`fdv1FallbackTtlMs` state and
150+ * the pending pair is cleared. Safe to call with neither; it just returns the
151+ * current committed state unchanged.
155152 */
156- function resolveFallback ( ) : FallbackDirective {
153+ function resolveFallback ( incoming ?: FallbackDirective ) : FallbackDirective {
154+ if ( incoming ?. fdv1Fallback ) {
155+ fdv1Fallback = true ;
156+ fdv1FallbackTtlMs = incoming . fdv1FallbackTtlMs ;
157+ pendingFallback = false ;
158+ pendingFallbackTtlMs = undefined ;
159+ return { fdv1Fallback, fdv1FallbackTtlMs } ;
160+ }
157161 if ( pendingFallback ) {
158162 fdv1Fallback = true ;
159163 fdv1FallbackTtlMs = pendingFallbackTtlMs ;
@@ -164,15 +168,18 @@ export function createStreamingBase(config: {
164168 }
165169
166170 /**
167- * The single place a result derived from the committed/pending stream state
168- * is enqueued. It resolves the current fallback directive once (promoting any
169- * directive deferred at onopen) and hands it to `build`, which stamps it onto
170- * the result. Call sites that carry their own directive source - an in-band
171- * goodbye directive, an error-response header, or a plain no- fallback goodbye
172- * or shutdown - enqueue directly instead .
171+ * The single place a result derived from the fallback directive state is
172+ * enqueued. It resolves the current directive once via `resolveFallback()`
173+ * - passing `incoming` through when the call site has its own directive
174+ * source - and hands the result to `build`, which stamps it onto the
175+ * result. `shutdown()` is the only path with no fallback state to resolve,
176+ * so it still enqueues directly.
173177 */
174- function putWithFallback ( build : ( fallback : FallbackDirective ) => FDv2SourceResult ) : void {
175- resultQueue . put ( build ( resolveFallback ( ) ) ) ;
178+ function putWithFallback (
179+ build : ( fallback : FallbackDirective ) => FDv2SourceResult ,
180+ incoming ?: FallbackDirective ,
181+ ) : void {
182+ resultQueue . put ( build ( resolveFallback ( incoming ) ) ) ;
176183 }
177184
178185 function handleAction ( action : internal . ProtocolAction , rawData ?: unknown ) : void {
@@ -183,22 +190,16 @@ export function createStreamingBase(config: {
183190 break ;
184191
185192 case 'goodbye' : {
193+ // An in-band fallback signal in the goodbye data (its own TTL) takes
194+ // precedence over a directive deferred at onopen; putWithFallback()
195+ // passes it through to resolveFallback() as the incoming override.
186196 const goodbyeDirective = readGoodbyeFallbackDirective ( rawData ) ;
187- if ( goodbyeDirective . fdv1Fallback ) {
188- // An in-band fallback signal in the goodbye data overrides any pending
189- // or committed state and carries its own TTL, so it does not go through
190- // putWithFallback(). Clear the pending pair since it is superseded.
191- fdv1Fallback = true ;
192- pendingFallback = false ;
193- pendingFallbackTtlMs = undefined ;
194- resultQueue . put ( terminalError ( errorInfoFromUnknown ( action . reason ) , goodbyeDirective ) ) ;
195- } else if ( pendingFallback ) {
196- // No in-band signal, but a directive was deferred at onopen: let
197- // putWithFallback() consume the pending fallback and clear the values.
198- putWithFallback ( ( fallback ) => terminalError ( errorInfoFromUnknown ( action . reason ) , fallback ) ) ;
199- } else {
200- resultQueue . put ( goodbye ( action . reason , { fdv1Fallback } ) ) ;
201- }
197+ putWithFallback (
198+ ( fallback ) => ( fallback . fdv1Fallback
199+ ? terminalError ( errorInfoFromUnknown ( action . reason ) , fallback )
200+ : goodbye ( action . reason , fallback ) ) ,
201+ goodbyeDirective ,
202+ ) ;
202203 break ;
203204 }
204205
@@ -230,10 +231,11 @@ export function createStreamingBase(config: {
230231 // A fallback directive overrides normal retry handling, even for an
231232 // otherwise-recoverable HTTP status: the server is telling us to stop
232233 // trying FDv2 now rather than keep retrying this connection.
233- fdv1Fallback = true ;
234- fdv1FallbackTtlMs = directive . fdv1FallbackTtlMs ;
235234 logConnectionResult ( false ) ;
236- resultQueue . put ( terminalError ( errorInfoFromHttpError ( err . status ?? 0 ) , directive ) ) ;
235+ putWithFallback (
236+ ( fallback ) => terminalError ( errorInfoFromHttpError ( err . status ?? 0 ) , fallback ) ,
237+ directive ,
238+ ) ;
237239 return false ;
238240 }
239241
0 commit comments