"use client"; import { useEffect, useRef, useState } from "react"; /** True after the first client paint — gate time-relative / random content to avoid hydration mismatch. */ export function useMounted(): boolean { const [m, setM] = useState(false); useEffect(() => setM(true), []); return m; } /** requestAnimationFrame loop. `cb` receives the high-res timestamp. */ export function useRaf(cb: (t: number) => void, active = true): void { const ref = useRef(cb); ref.current = cb; useEffect(() => { if (!active) return; let raf = 0; const tick = (t: number) => { ref.current(t); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [active]); } /** setInterval that survives re-renders. Pass ms=null to pause. */ export function useInterval(cb: () => void, ms: number | null): void { const ref = useRef(cb); ref.current = cb; useEffect(() => { if (ms == null) return; const id = setInterval(() => ref.current(), ms); return () => clearInterval(id); }, [ms]); } /** A 1s ticker that re-renders the component — for live clocks driven by Date.now(). */ export function useNow(active = true, ms = 1000): number { const [now, setNow] = useState(() => Date.now()); useInterval(() => setNow(Date.now()), active ? ms : null); return now; } /** Reactive media query. */ export function useMediaQuery(query: string): boolean { const [match, setMatch] = useState(false); useEffect(() => { const m = window.matchMedia(query); const on = () => setMatch(m.matches); on(); m.addEventListener("change", on); return () => m.removeEventListener("change", on); }, [query]); return match; } export type LiveStatus = "connecting" | "open" | "closed"; export interface UseLiveSocket { /** Last parsed message received. */ last: T | null; status: LiveStatus; } /** * Generic auto-reconnecting JSON WebSocket subscription. The server pushes; the client * only receives. Promoted from F1Peak's LiveLink to serve any ZQ live feed. * Pass `url=null` to stay disconnected (e.g. nothing live to watch). */ export function useLiveSocket( url: string | null, opts?: { onMessage?: (msg: T) => void; reconnectMs?: number }, ): UseLiveSocket { const [last, setLast] = useState(null); const [status, setStatus] = useState("connecting"); const cb = useRef(opts?.onMessage); cb.current = opts?.onMessage; const reconnectMs = opts?.reconnectMs ?? 3000; useEffect(() => { if (!url) { setStatus("closed"); return; } let ws: WebSocket | null = null; let timer: ReturnType | null = null; let killed = false; const connect = () => { setStatus("connecting"); try { ws = new WebSocket(url); } catch { timer = setTimeout(connect, reconnectMs); return; } ws.onopen = () => !killed && setStatus("open"); ws.onmessage = (e) => { let msg: T; try { msg = JSON.parse(e.data as string) as T; } catch { return; } if (killed) return; setLast(msg); cb.current?.(msg); }; ws.onclose = () => { if (killed) return; setStatus("closed"); timer = setTimeout(connect, reconnectMs); }; ws.onerror = () => ws?.close(); }; connect(); return () => { killed = true; if (timer) clearTimeout(timer); ws?.close(); }; }, [url, reconnectMs]); return { last, status }; }