Files
Signage/tests/browser/backoff.test.ts
T
ekklesiaandClaude Opus 5.5 fbc8af7510 fix(display): retry every failed request with backoff, and never give up
A display tried each of its network paths once:
- A registration that failed left it continuing from its persisted
  state, behind the offline banner, with no realtime, heartbeat or
  retry, until someone reloaded it. This is what the production kiosk
  showed after a restart.
- A failed state refresh scheduled no further refresh, so a display
  whose realtime was also down never fetched state again.
- A realtime stream the host reported closed was never subscribed
  again.

Each path now retries on its own backoff: about 1 s, doubling to a
one-minute ceiling, with jitter so a building's screens do not return
in lockstep. There is no attempt limit, because a television has
nobody to press Retry. Each resets when its request works again.
- Registration retries until it works, then brings the display live
  exactly as a first-time success does.
- A failed refresh schedules its own retry, with no realtime needed.
- A closed realtime subscription is replaced.
- Stopping clears every retry.

The recovery test's virtual window shrinks from an hour to five
minutes: eight failures back off for at most about 183 s, and an hour
of offline-continuation timers made the test slow without proving more.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 21:31:55 -05:00

35 lines
1.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
DISPLAY_RETRY_BASE_MS,
DISPLAY_RETRY_MAX_MS,
displayRetryDelayMs,
} from "../../src/browser/backoff.js";
describe("display retry backoff", () => {
it("doubles from one second and stops at one minute", () => {
const longest = (attempt: number) => displayRetryDelayMs(attempt, () => 1);
expect([0, 1, 2, 3, 4, 5, 6, 7, 50].map(longest)).toEqual([
1_000, 2_000, 4_000, 8_000, 16_000, 32_000, 60_000, 60_000, 60_000,
]);
});
it("jitters between half and all of each step, and never waits zero", () => {
for (const attempt of [0, 3, 6, 40]) {
const step = Math.min(DISPLAY_RETRY_MAX_MS, DISPLAY_RETRY_BASE_MS * 2 ** attempt);
expect(displayRetryDelayMs(attempt, () => 0)).toBe(step / 2);
expect(displayRetryDelayMs(attempt, () => 1)).toBe(step);
for (let index = 0; index < 50; index += 1) {
const delay = displayRetryDelayMs(attempt);
expect(delay).toBeGreaterThanOrEqual(step / 2);
expect(delay).toBeLessThanOrEqual(step);
}
}
});
it("treats a nonsensical attempt or random value as the nearest sensible one", () => {
expect(displayRetryDelayMs(-3, () => 1)).toBe(1_000);
expect(displayRetryDelayMs(2, () => 7)).toBe(4_000);
expect(displayRetryDelayMs(2, () => -1)).toBe(2_000);
});
});