diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index fcdbf7be3fa..ebdf20b5a16 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -1021,6 +1021,43 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(TestClock.layer())), ); + it.effect("resets the tolerant budget across a network drop", () => + Effect.gen(function* () { + // A slow timeout arms the tolerant 45s budget. A network drop is not + // backend slowness, so the reconnect after the network returns must start + // fresh on the normal 15s budget rather than carry the enlarged one. + const harness = yield* makeHarness({ ready: () => Effect.never }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState( + supervisor.state, + (state) => + state.phase === "connecting" && state.stage === "synchronizing" && state.attempt === 1, + ); + yield* TestClock.adjust("15 seconds"); + yield* eventuallyState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + + // Network blips offline (which resets the tolerant classification) then back. + yield* harness.setNetworkStatus("offline"); + yield* awaitState(supervisor.state, (state) => state.phase === "offline"); + yield* harness.setNetworkStatus("online"); + + // The reconnect must time out at the normal 15s budget — a leaked tolerant + // flag would keep it "connecting" until 45s. + yield* awaitState( + supervisor.state, + (state) => state.phase === "connecting" && state.stage === "synchronizing", + ); + yield* TestClock.adjust("15 seconds"); + yield* eventuallyState(supervisor.state, (state) => state.phase === "backoff"); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("reconnects a relay session on a credentials change during a background probe", () => Effect.gen(function* () { // The first session's heartbeat probe stalls; a credentials change that diff --git a/packages/client-runtime/src/connection/supervisor.ts b/packages/client-runtime/src/connection/supervisor.ts index fac4660586c..b109cf1d759 100644 --- a/packages/client-runtime/src/connection/supervisor.ts +++ b/packages/client-runtime/src/connection/supervisor.ts @@ -698,6 +698,12 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( continue; } if (currentIntent.network === "offline") { + // A network drop is not backend slowness — don't carry a "slow" budget + // across the outage, so the first attempt after the network returns + // starts fresh on the normal establishment budget. (failureCount is kept + // so backoff continuity survives a transient blip.) + tolerant = false; + consecutiveSlowTimeouts = 0; yield* clearLease; yield* setState(offlineState(currentIntent, generation, failureCount + 1, latestFailure)); yield* waitForSignal;