import { useEffect, useRef } from "react"; import { useApi } from "@/contexts/ApiContext"; import { JobProgressSnapshot } from "@/lib/jobsApi"; /** * Subscribe to backend job events on the shared /ws/joint-data channel. * * Two event types flow on this socket: * - `jobs_changed` → fired on submit / watchdog finalisation / delete. * Triggers `onChange` so the caller can refetch /jobs. * - `job_progress` → fired by the watchdog (~1Hz) while jobs are running. * Triggers `onProgress` with per-job snapshots so the * UI can update the progress bar in place — no fetch. * * Callback refs are captured so identity changes don't tear down the socket. * Auto-reconnects with a 3s delay if the server bounces. */ export const useJobsChangedSignal = ( onChange: () => void, onProgress?: (snapshots: JobProgressSnapshot[]) => void, ) => { const { wsBaseUrl } = useApi(); const changeRef = useRef(onChange); changeRef.current = onChange; const progressRef = useRef(onProgress); progressRef.current = onProgress; useEffect(() => { let cancelled = false; let ws: WebSocket | null = null; let reconnectTimer: ReturnType | null = null; const connect = () => { if (cancelled) return; try { ws = new WebSocket(`${wsBaseUrl}/ws/joint-data`); } catch { reconnectTimer = setTimeout(connect, 3000); return; } ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data?.type === "jobs_changed") { changeRef.current(); } else if ( data?.type === "job_progress" && progressRef.current && Array.isArray(data?.jobs) ) { progressRef.current(data.jobs as JobProgressSnapshot[]); } } catch { /* ignore non-JSON or unexpected payloads */ } }; ws.onclose = () => { if (cancelled) return; reconnectTimer = setTimeout(connect, 3000); }; }; connect(); return () => { cancelled = true; if (reconnectTimer) clearTimeout(reconnectTimer); if (ws) ws.close(); }; }, [wsBaseUrl]); };