diff --git a/desktop/src/shared/api/relayReconnectController.test.mjs b/desktop/src/shared/api/relayReconnectController.test.mjs index 37b9424345..0a3a1c9355 100644 --- a/desktop/src/shared/api/relayReconnectController.test.mjs +++ b/desktop/src/shared/api/relayReconnectController.test.mjs @@ -9,7 +9,10 @@ import assert from "node:assert/strict"; import test, { mock } from "node:test"; -import { RelayReconnectController } from "./relayReconnectController.ts"; +import { + DEFAULT_RECONNECT_TIMING_POLICY, + RelayReconnectController, +} from "./relayReconnectController.ts"; // ── Dep builder helpers ─────────────────────────────────────────────────────── @@ -62,6 +65,36 @@ function makeDeps({ return deps; } +// ── Timing policy ───────────────────────────────────────────────────────────── + +test("default timing policy preserves current reconnect timings", () => { + assert.deepEqual(DEFAULT_RECONNECT_TIMING_POLICY, { + fastPathTimeoutMs: 4_000, + pollIntervalMs: 3_000, + backstopMs: 120_000, + }); +}); + +test("injected timing policy drives fast-path, poll, and backstop timers", async () => { + const ctrl = new RelayReconnectController({ + fastPathTimeoutMs: 11, + pollIntervalMs: 22, + backstopMs: 33, + }); + const deps = makeDeps({ + preconnectResult: async () => { + throw new Error("relay unreachable"); + }, + hookConfiguredResult: async () => false, + }); + + await ctrl.start(deps); + + assert.equal(deps.setTimeout.mock.calls[0].arguments[1], 11); + assert.equal(deps.setInterval.mock.calls[0].arguments[1], 22); + assert.equal(deps.setTimeout.mock.calls[1].arguments[1], 33); +}); + // ── Phase 1: fast path ──────────────────────────────────────────────────────── test("fast-path success — hook never invoked, onSuccess fires, state resets", async () => { diff --git a/desktop/src/shared/api/relayReconnectController.ts b/desktop/src/shared/api/relayReconnectController.ts index 45bba3666c..fcea2f29dc 100644 --- a/desktop/src/shared/api/relayReconnectController.ts +++ b/desktop/src/shared/api/relayReconnectController.ts @@ -28,12 +28,22 @@ * testable without React or Tauri. */ -/** Short deadline for the optimistic fast-path attempt. */ -const FAST_PATH_TIMEOUT_MS = 4_000; -/** Interval between poll attempts during phase 3. */ -const POLL_INTERVAL_MS = 3_000; -/** Maximum total time to keep polling before giving up. */ -const BACKSTOP_MS = 120_000; +/** Timer durations used by the reconnect controller. */ +export type ReconnectTimingPolicy = { + /** Short deadline for the optimistic fast-path attempt. */ + fastPathTimeoutMs: number; + /** Interval between poll attempts during phase 3. */ + pollIntervalMs: number; + /** Maximum total time to keep polling before giving up. */ + backstopMs: number; +}; + +/** Current production reconnect timings. */ +export const DEFAULT_RECONNECT_TIMING_POLICY: ReconnectTimingPolicy = { + fastPathTimeoutMs: 4_000, + pollIntervalMs: 3_000, + backstopMs: 120_000, +}; export type ReconnectState = { isPending: boolean; @@ -74,6 +84,14 @@ function withDeadline( } export class RelayReconnectController { + private readonly timingPolicy: ReconnectTimingPolicy; + + constructor( + timingPolicy: ReconnectTimingPolicy = DEFAULT_RECONNECT_TIMING_POLICY, + ) { + this.timingPolicy = timingPolicy; + } + private state: ReconnectState = { isPending: false, isWaitingOnReconnectHook: false, @@ -136,7 +154,7 @@ export class RelayReconnectController { try { await withDeadline( deps.preconnect(), - FAST_PATH_TIMEOUT_MS, + this.timingPolicy.fastPathTimeoutMs, "fast-path", deps.setTimeout, deps.clearTimeout, @@ -226,7 +244,7 @@ export class RelayReconnectController { .catch(() => { // Poll failed — keep trying. }); - }, POLL_INTERVAL_MS); + }, this.timingPolicy.pollIntervalMs); this.backstopId = deps.setTimeout(() => { if (resolved || cancelled()) return; @@ -235,7 +253,7 @@ export class RelayReconnectController { // background retry loop keeps running. The notification is soft. onBackstop(); this.finish(() => {}, false); - }, BACKSTOP_MS); + }, this.timingPolicy.backstopMs); // Return false now; async success is delivered via state updates. return false;