// ISS-129 (v0.57.0): in-process fan-out registry for the SSE state stream (/api/events). The app // runs as one long-lived server process (`next start`; instrumentation.ts already keeps background // sim state alive) — no new dependencies, no WS upgrade needed. The Map lives on globalThis because // Next.js gives instrumentation.ts (where broadcast() fires from the background tick) and route // handlers (where subscribe() happens in /api/events) separate module instances; a plain `let` would // give each its own empty map and no push would ever be delivered. v1.4.0 (ISS-112): subscribers are // keyed by PLAYER ID — the engine projects one snapshot per connected player and pushes each frame // only to that player's group, so every client sees its own account row in every pushed frame. type Push = (text: string) => void; const g = globalThis as unknown as { __flightSseSubscribersByPlayer?: Map> }; const subscribers: Map> = (g.__flightSseSubscribersByPlayer ??= new Map()); export function subscribe(playerId: string, fn: Push): () => void { let group = subscribers.get(playerId); if (!group) { group = new Set(); subscribers.set(playerId, group); } group.add(fn); return () => { const s = subscribers.get(playerId); if (!s) return; // already unsubscribed — idempotent s.delete(fn); if (s.size === 0) subscribers.delete(playerId); // drop the empty group so projections stop for it }; } // v1.4.0 (ISS-112): which player ids currently have at least one connected stream — the engine // projects exactly these per tick (N is tiny; a projection is skipped when nobody listens). export function subscribedPlayerIds(): string[] { return [...subscribers.keys()]; } // Call each subscriber safely — one dead stream must not take the others down. `frames` maps // player id → pre-serialized frame text (the engine projects per player, this only fans out). export function broadcast(frames: Map): void { for (const [playerId, text] of frames) { const group = subscribers.get(playerId); if (!group) continue; // unsubscribed between projection and push — fine for (const fn of [...group]) { try { fn(text); } catch { // subscriber's stream is gone; its own cleanup unsubscribes it } } } }