// Poll the local daemon for real wire events. Offline-tolerant by design: // the sim is fully alive without a daemon, so failures are silent and the // poll backs off until the daemon answers again. import { isWireEvent, type WireEvent } from "../engine"; const POLL_MS = 3000; const OFFLINE_BACKOFF_MS = 15000; export function startDaemonPoll(onEvents: (events: WireEvent[]) => void): () => void { let timer: ReturnType | undefined; let stopped = false; const tick = async () => { if (stopped) return; // unmounted before first tick — never drain for a dead poller let delay = POLL_MS; try { const res = await fetch("/api/events/pending", { signal: AbortSignal.timeout(2000) }); if (res.ok) { const body: unknown = await res.json(); const list = (body as { events?: unknown[] }).events ?? []; const valid = list.filter(isWireEvent); // a malformed event on this path means the daemon let contract drift through — be loud if (valid.length !== list.length) console.error("puck: daemon sent events violating the wire schema", list); // drain is destructive — a stopped poller (StrictMode's first mount) must not // swallow events meant for the live one. Losing one drain to an unlucky unmount // race is acceptable; silently eating every event on dev double-mount is not. if (valid.length && !stopped) onEvents(valid); } else { delay = OFFLINE_BACKOFF_MS; } } catch { delay = OFFLINE_BACKOFF_MS; // daemon not running — normal in Space/demo mode } if (!stopped) timer = setTimeout(tick, delay); }; // drain is destructive, so the first fetch must not race React StrictMode's // mount→unmount→mount: the doomed first instance is cleaned up synchronously, // well inside this delay, so only the surviving poller ever touches the queue. timer = setTimeout(tick, 350); return () => { stopped = true; clearTimeout(timer); }; }