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]>
This commit is contained in:
ekklesia
2026-10-05 21:31:55 -05:00
co-authored by Claude Opus 5.5
parent 518fd101a2
commit fbc8af7510
4 changed files with 184 additions and 44 deletions
+4 -3
View File
@@ -248,8 +248,9 @@ describe("a display recovers from every failed request without a reload", () =>
const display = build();
await display.start();
await settle();
// An hour of virtual time: enough for every backoff step, and the cap, to have been reached.
await advance(60 * 60_000);
// Eight failures back off for at most 1 + 2 + 4 + 8 + 16 + 32 + 60 + 60 seconds, the last two at
// the one-minute ceiling. Five virtual minutes is enough for all of them and the success after.
await advance(5 * 60_000);
expect(server.sessionRequests - sessionsBefore).toBe(9);
expect(root.dataset.offline).toBe("false");
@@ -299,7 +300,7 @@ describe("a display recovers from every failed request without a reload", () =>
await display.stop();
const sessionsAtStop = server.sessionRequests;
await advance(60 * 60_000);
await advance(5 * 60_000);
expect(server.sessionRequests).toBe(sessionsAtStop);
});