// Global maintenance signal + self-clearing poll. `active` is true while the edge is in a // planned deploy window (a caddy 503 carried `X-Scrabble-Maintenance`; see maintenance.ts). // A non-dismissable overlay (MaintenanceOverlay.svelte) covers the app while active, and a // cheap read is retried on a capped-backoff cadence (mirroring connection.svelte.ts) — the // first success lifts the overlay, so it can never get stuck even with no other traffic. // Mock mode never reports maintenance, so it simply stays inactive. import { backoffMs } from './retry'; let active = $state(false); let retryHintMs = $state(0); let pollTimer: ReturnType | null = null; let attempt = 0; let probing = false; let probe: (() => Promise) | null = null; export const maintenance = { /** active is true while the edge is in a planned maintenance window. */ get active(): boolean { return active; }, /** retryHintMs is the last Retry-After hint in milliseconds (0 when unknown). */ get retryHintMs(): number { return retryHintMs; }, }; /** registerMaintenanceProbe installs the reachability read the poll fires while active * (the transport wires a cheap authenticated read). */ export function registerMaintenanceProbe(fn: () => Promise): void { probe = fn; } /** reportMaintenance raises the overlay and starts the self-clearing poll. Idempotent: a * repeat hit only refreshes the hint, it never restarts a running poll. */ export function reportMaintenance(retryAfterMs: number): void { retryHintMs = retryAfterMs; if (active) return; active = true; attempt = 0; if (probe) schedulePoll(); } /** clearMaintenance lowers the overlay and stops the poll without reloading (a reset, or the * e2e hook). Prod recovery goes through maintenanceRecovered instead. */ export function clearMaintenance(): void { active = false; if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } } /** maintenanceRecovered runs when the edge answers again after a window we were showing. * The deploy that ended the window may have shipped a client-incompatible change (a wire / * schema bump), and the running bundle is the OLD one — so reload to pick up the fresh * client instead of merely hiding the overlay. A no-op unless the overlay was up; the cover * stays over the brief reload flash. In prod recovery is the only way the overlay clears. */ export function maintenanceRecovered(): void { if (!active) return; active = false; if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } if (typeof location !== 'undefined') location.reload(); } /** retryNow forces an immediate re-check (the overlay's button). It never closes the * overlay itself — only a successful probe does. A no-op while a probe is in flight. */ export function retryNow(): void { if (!active || !probe || probing) return; if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } attempt = 0; runProbe(); } function schedulePoll(): void { pollTimer = setTimeout(runProbe, backoffMs(++attempt)); } function runProbe(): void { pollTimer = null; if (!active || !probe || probing) return; probing = true; probe().then( () => { probing = false; maintenanceRecovered(); }, () => { probing = false; if (active) schedulePoll(); }, ); }