Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion desktop/src/shared/api/relayReconnectController.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────────────────

Expand Down Expand Up @@ -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 () => {
Expand Down
36 changes: 27 additions & 9 deletions desktop/src/shared/api/relayReconnectController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,6 +84,14 @@ function withDeadline<T>(
}

export class RelayReconnectController {
private readonly timingPolicy: ReconnectTimingPolicy;

constructor(
timingPolicy: ReconnectTimingPolicy = DEFAULT_RECONNECT_TIMING_POLICY,
) {
this.timingPolicy = timingPolicy;
}

private state: ReconnectState = {
isPending: false,
isWaitingOnReconnectHook: false,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Loading