Spaces:
Running
Running
| /** | |
| * 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; | |
| } | |