import { useCallback, useEffect, useRef, useState } from 'react'; export interface RealtimeLog { level: string; message: string; } interface ConnectOptions { clear?: boolean; onClose?: () => void; onError?: () => void; onResult?: () => void; } const MAX_LOGS = 1000; function toWebSocketUrl(pathOrUrl: string): string { if (pathOrUrl.startsWith('ws://') || pathOrUrl.startsWith('wss://')) { return pathOrUrl; } const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const path = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`; return `${protocol}://${window.location.host}${path}`; } export function useWebSocketLogs() { const [logs, setLogs] = useState([]); const [connected, setConnected] = useState(false); const wsRef = useRef(null); const callbacksRef = useRef({}); const suppressCloseRef = useRef(false); const clearLogs = useCallback(() => { setLogs([]); }, []); const close = useCallback((notify = false) => { if (!wsRef.current) { setConnected(false); return; } suppressCloseRef.current = !notify; wsRef.current.close(); wsRef.current = null; setConnected(false); }, []); const connect = useCallback((pathOrUrl: string, options: ConnectOptions = {}) => { close(false); suppressCloseRef.current = false; callbacksRef.current = options; if (options.clear ?? true) { setLogs([]); } const ws = new WebSocket(toWebSocketUrl(pathOrUrl)); wsRef.current = ws; setConnected(true); ws.onmessage = (event) => { try { const msg = JSON.parse(event.data) as RealtimeLog; if (msg.level === 'heartbeat') return; if (msg.level === 'result') { callbacksRef.current.onResult?.(); return; } setLogs((prev) => [...prev, msg].slice(-MAX_LOGS)); } catch { setLogs((prev) => [...prev, { level: 'info', message: String(event.data) }].slice(-MAX_LOGS)); } }; ws.onclose = () => { const isCurrent = wsRef.current === ws; const shouldNotify = isCurrent && !suppressCloseRef.current; if (isCurrent) { wsRef.current = null; setConnected(false); suppressCloseRef.current = false; } if (shouldNotify) { callbacksRef.current.onClose?.(); } }; ws.onerror = () => { setConnected(false); callbacksRef.current.onError?.(); }; }, [close]); useEffect(() => () => { close(false); }, [close]); return { logs, connected, clearLogs, connect, close, }; }