// A liveness watchdog for the live event stream. The gateway pushes a `heartbeat` event on a // fixed cadence (GATEWAY_PUSH_HEARTBEAT_INTERVAL, default 10 s), so a healthy stream delivers at // least one event well inside the timeout even when the game itself is idle. A network that dies // silently — e.g. airplane mode cutting the radio — leaves the streaming fetch neither erroring // nor delivering; without this watchdog the machine would stay "online" until the next active // call fails (often only on a foreground resync). The watchdog resets on every delivered event // and fires onDead once the stream has been silent for the whole timeout, so the app can report // the drop and reconnect. /** StreamWatchdog is the control surface returned by {@link createStreamWatchdog}. */ export interface StreamWatchdog { /** reset (re)starts the countdown; call on stream open and on every delivered event. It also * clears a prior pause, so a reopen after a suspend resumes watching. */ reset(): void; /** pause stops the countdown while the app is backgrounded (timers are throttled while hidden, * and a suspended stream legitimately delivers nothing — neither should trip a false drop). */ pause(): void; /** resume restarts the countdown after a pause (on returning to the foreground). */ resume(): void; /** clear stops the countdown without re-arming; call when the stream is closed or has errored. */ clear(): void; } /** * createStreamWatchdog builds a watchdog that calls onDead once no reset has happened for * timeoutMs. timeoutMs must exceed the server heartbeat interval plus proxy/jitter slack, or a * healthy but idle stream would trip it. */ export function createStreamWatchdog(opts: { timeoutMs: number; onDead: () => void }): StreamWatchdog { let timer: ReturnType | null = null; let paused = false; function stop(): void { if (timer !== null) { clearTimeout(timer); timer = null; } } function arm(): void { stop(); timer = setTimeout(opts.onDead, opts.timeoutMs); } return { reset(): void { paused = false; arm(); }, pause(): void { paused = true; stop(); }, resume(): void { if (paused) { paused = false; arm(); } }, clear(): void { stop(); }, }; }