@@ -272,17 +272,26 @@ const scheduleRefresh = (expires: number): void => {
272272 } , delayMs )
273273}
274274
275+ /**
276+ * Current failure backoff: base `FAILURE_BACKOFF_MS`, doubling per consecutive
277+ * failure up to `MAX_BACKOFF_MS`. Shared by the background retry timer and the
278+ * gated-call gate in `runHandshake` so plugin traffic cannot outpace the
279+ * exponential policy.
280+ */
281+ const failureBackoffMs = ( ) : number =>
282+ Math . min (
283+ FAILURE_BACKOFF_MS * 2 ** Math . max ( 0 , consecutiveFailures - 1 ) ,
284+ MAX_BACKOFF_MS
285+ )
286+
275287/**
276288 * Schedule the next handshake after a failed or hung attempt. `scheduleRefresh`
277289 * only runs on success, so without this the engine would sit idle until a gated
278290 * call or an app restart. The wait doubles with each consecutive failure up to
279291 * `MAX_BACKOFF_MS` to keep a hopeless device from re-attesting forever.
280292 */
281293const scheduleRetryAfterFailure = ( ) : void => {
282- const backoffMs = Math . min (
283- FAILURE_BACKOFF_MS * 2 ** Math . max ( 0 , consecutiveFailures - 1 ) ,
284- MAX_BACKOFF_MS
285- )
294+ const backoffMs = failureBackoffMs ( )
286295 // A cached token may still have most of its life left - a stale handshake can
287296 // land one while a newer attempt is failing. Never retry sooner than
288297 // `scheduleRefresh` would have, or a failing device would re-attest every
@@ -304,7 +313,7 @@ const scheduleRetryAfterFailure = (): void => {
304313 */
305314const runHandshake = ( ) : void => {
306315 if ( inFlight != null ) return
307- if ( Date . now ( ) - lastFailureAt < FAILURE_BACKOFF_MS ) return
316+ if ( Date . now ( ) - lastFailureAt < failureBackoffMs ( ) ) return
308317 // A handshake whose native call hangs past the watchdog has its `inFlight`
309318 // lock released so a newer handshake can start. Tag each attempt so a stale
310319 // one that finally resolves cannot clobber a token a newer handshake already
0 commit comments