"use client"; import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import { getTraceBuffer, TraceBuffer } from './trace-buffer'; import { getLegacyWsUrl } from './config'; import { TraceData, WebSocketState } from './types'; export type { TraceData, WebSocketState } from './types'; export function useWebSocket(url?: string) { const defaultUrl = url || getLegacyWsUrl(); const [state, setState] = useState({ isConnected: false, lastMessage: null, traces: [], }); const [isClient, setIsClient] = useState(false); const ws = useRef(null); const reconnectTimeout = useRef(null); const urlRef = useRef(defaultUrl); const bufferRef = useRef(null); const bufferUnsubscribe = useRef<(() => void) | null>(null); const isCleared = useRef(false); // Ensure we're on the client useEffect(() => { setIsClient(true); }, []); // Initialize trace buffer useEffect(() => { if (isClient && !bufferRef.current) { bufferRef.current = getTraceBuffer({ maxSize: 10000, updateInterval: 25, batchSize: 10, }); // Subscribe to buffer updates bufferUnsubscribe.current = bufferRef.current.subscribe((newTraces, bufferState) => { // If we've cleared and there are no new traces, don't restore the old lastMessage if (isCleared.current && newTraces.length === 0) { setState(prev => ({ ...prev, traces: bufferRef.current?.getRecentTraces(500) || [], lastMessage: null, // eslint-disable-next-line @typescript-eslint/no-explicit-any buffer: bufferRef.current as any, })); } else { // Reset cleared flag if we have new traces if (newTraces.length > 0) { isCleared.current = false; } setState(prev => ({ ...prev, traces: bufferRef.current?.getRecentTraces(500) || [], lastMessage: newTraces.length > 0 ? newTraces[newTraces.length - 1] : prev.lastMessage, // eslint-disable-next-line @typescript-eslint/no-explicit-any buffer: bufferRef.current as any, })); } }); // eslint-disable-next-line @typescript-eslint/no-explicit-any setState(prev => ({ ...prev, buffer: bufferRef.current as any })); } return () => { if (bufferUnsubscribe.current) { bufferUnsubscribe.current(); bufferUnsubscribe.current = null; } }; }, [isClient]); // Update URL ref when URL prop changes useEffect(() => { urlRef.current = defaultUrl; }, [defaultUrl]); const connect = useCallback(() => { // Double-check we're in browser and WebSocket is available if (typeof window === 'undefined' || !window.WebSocket) { console.log('WebSocket not available'); return; } try { console.log('Attempting to connect to:', urlRef.current); ws.current = new window.WebSocket(urlRef.current); ws.current.onopen = () => { console.log('WebSocket connected to:', urlRef.current); setState(prev => ({ ...prev, isConnected: true })); // Clear any reconnection timeout if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current); reconnectTimeout.current = null; } }; ws.current.onmessage = async (event) => { try { let messageData: string; // Handle both string and Blob data if (event.data instanceof Blob) { messageData = await event.data.text(); } else { messageData = event.data; } const data = JSON.parse(messageData) as TraceData; console.log('Received trace:', data.type); // Push to trace buffer instead of direct state update if (bufferRef.current) { bufferRef.current.push(data); } } catch (error) { console.error('Error parsing WebSocket message:', error, 'Data:', event.data); } }; ws.current.onerror = (error) => { console.warn('WebSocket error (this is normal if server is not running):', error); }; ws.current.onclose = () => { console.log('WebSocket disconnected'); setState(prev => ({ ...prev, isConnected: false })); // Attempt to reconnect after 3 seconds if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current); } reconnectTimeout.current = setTimeout(() => { console.log('Attempting to reconnect...'); connect(); }, 3000); }; } catch (error) { console.warn('Failed to connect WebSocket:', error); // Retry connection after delay if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current); } reconnectTimeout.current = setTimeout(() => { connect(); }, 3000); } }, []); // Remove url dependency to prevent reconnection loops const disconnect = useCallback(() => { if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current); reconnectTimeout.current = null; } if (ws.current && ws.current.readyState === ws.current.OPEN) { ws.current.close(); } ws.current = null; }, []); const sendMessage = useCallback((message: Record) => { if (ws.current && ws.current.readyState === ws.current.OPEN) { ws.current.send(JSON.stringify(message)); } else { console.warn('WebSocket is not connected'); } }, []); const clearTraces = useCallback(() => { if (bufferRef.current) { bufferRef.current.clear(); } isCleared.current = true; setState(prev => ({ ...prev, traces: [], lastMessage: null })); }, []); // Connect when component mounts and we're on client useEffect(() => { if (!isClient) return; const timer = setTimeout(() => { connect(); }, 100); // Small delay to ensure client is ready return () => { clearTimeout(timer); disconnect(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isClient]); // Only depend on isClient, not connect/disconnect to avoid loops // Memoize connect to prevent infinite loops const memoizedConnect = useMemo(() => connect, []); const memoizedDisconnect = useMemo(() => disconnect, []); return { ...state, isClient, sendMessage, clearTraces, reconnect: memoizedConnect, disconnect: memoizedDisconnect, // eslint-disable-next-line @typescript-eslint/no-explicit-any buffer: bufferRef.current as any, }; }