/** * useSimState — subscribes to the simulation WebSocket. * * Connects to /ws/sim, stores the latest JSON snapshot, and auto-reconnects * every 3 seconds on drop so the dashboard survives brief network hiccups. * * Architecture: the single data source for the whole dashboard; consumed * by App.jsx and distributed to panels via props. * * Design: last-write-wins snapshot updates keep rendering stateless. */ import { useEffect, useRef, useState, useCallback } from "react"; import { wsUrl } from "../utils/api"; export default function useSimState() { const [snapshot, setSnapshot] = useState(null); const wsRef = useRef(null); const reconnectTimer = useRef(null); const disposedRef = useRef(false); const connect = useCallback(() => { if (disposedRef.current) return; clearTimeout(reconnectTimer.current); const url = wsUrl("/ws/sim"); const ws = new WebSocket(url); ws.onmessage = (e) => { try { const data = JSON.parse(e.data); setSnapshot(data); } catch { /* ignore parse errors */ } }; ws.onclose = () => { // that case creates an orphan connection after the view has unmounted. if (disposedRef.current || wsRef.current !== ws) return; reconnectTimer.current = setTimeout(connect, 3000); }; ws.onerror = () => ws.close(); wsRef.current = ws; }, []); useEffect(() => { disposedRef.current = false; connect(); return () => { clearTimeout(reconnectTimer.current); disposedRef.current = true; const ws = wsRef.current; wsRef.current = null; if (ws) { ws.onclose = null; ws.close(); } }; }, [connect]); return snapshot; }