import { useEffect, useRef } from 'react'; import type { GameState } from '@shared/types'; import type { TurnResult } from '@shared/protocol'; interface SseHandlers { onState?: (state: GameState) => void; onTurn?: (state: GameState, result: TurnResult) => void; /** * Fired for a server-sent NAMED `event: error` (a MessageEvent with a JSON * body `{ message }`) — e.g. the game was deleted or no longer exists. Pure * connection errors (no data) are ignored and EventSource auto-reconnects. */ onError?: (message: string) => void; } /** * Subscribe to a game's SSE stream. The server sends a `state` event on connect * and a `turn` event after every turn; EventSource auto-reconnects on drop, so a * reload always resyncs to the persisted server state. * * Handlers are kept in a ref so the (id-keyed) effect always invokes the latest * callbacks without re-subscribing on every render. */ export function useSse(id: string | null, handlers: SseHandlers): void { const handlersRef = useRef(handlers); handlersRef.current = handlers; useEffect(() => { if (!id) return; const es = new EventSource(`/sse/${id}`); const onState = (e: MessageEvent) => { try { handlersRef.current.onState?.(JSON.parse(e.data).state as GameState); } catch { /* ignore malformed */ } }; const onTurn = (e: MessageEvent) => { try { const d = JSON.parse(e.data) as { state: GameState; result: TurnResult }; handlersRef.current.onTurn?.(d.state, d.result); } catch { /* ignore malformed */ } }; const onError = (e: Event) => { if (e instanceof MessageEvent && e.data) { try { const d = JSON.parse(e.data) as { message?: string }; if (d.message) handlersRef.current.onError?.(d.message); } catch { /* ignore malformed */ } } }; es.addEventListener('state', onState); es.addEventListener('turn', onTurn); es.addEventListener('error', onError); return () => es.close(); }, [id]); }