import { useEffect, useRef, useState, useCallback } from 'react'; export interface WebSocketMessage { type: 'connected' | 'run_update' | 'progress' | 'error' | 'completed' | 'pong'; runId?: string; data?: any; timestamp?: string; } interface UseWebSocketOptions { runId: string | null; onMessage?: (message: WebSocketMessage) => void; onProgress?: (progress: number, currentDate?: string) => void; onError?: (error: string) => void; onCompleted?: (results?: any) => void; } export function useWebSocket({ runId, onMessage, onProgress, onError, onCompleted }: UseWebSocketOptions) { const [isConnected, setIsConnected] = useState(false); const [lastMessage, setLastMessage] = useState(null); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const pingIntervalRef = useRef(null); const isIntentionalDisconnect = useRef(false); const connect = useCallback(() => { if (!runId) { console.log('useWebSocket: No runId provided, skipping connection'); return; } // Check if already connected or connecting if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) { console.log('useWebSocket: Already connected or connecting to runId:', runId); return; } // Reset intentional disconnect flag isIntentionalDisconnect.current = false; // Clear any pending reconnect timeout if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } const wsUrl = `ws://localhost:2003/ws?runId=${runId}`; console.log('Connecting to WebSocket:', wsUrl); try { const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { console.log('WebSocket connected'); setIsConnected(true); // Start ping interval pingIntervalRef.current = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } }, 30000); // Ping every 30 seconds }; ws.onmessage = (event) => { try { const message: WebSocketMessage = JSON.parse(event.data); console.log('WebSocket message:', message); setLastMessage(message); // Call appropriate callbacks if (onMessage) { onMessage(message); } switch (message.type) { case 'progress': if (onProgress && message.data) { onProgress(message.data.progress, message.data.currentDate); } break; case 'error': if (onError && message.data?.error) { onError(message.data.error); console.error('Run Error:', message.data.error); } break; case 'completed': if (onCompleted) { onCompleted(message.data?.results); } console.log('Run Completed: The backtest run has completed successfully.'); break; } } catch (error) { console.error('Error parsing WebSocket message:', error); } }; ws.onerror = (error) => { console.error('WebSocket error:', error); setIsConnected(false); }; ws.onclose = () => { console.log('WebSocket disconnected'); setIsConnected(false); wsRef.current = null; // Clear ping interval if (pingIntervalRef.current) { clearInterval(pingIntervalRef.current); pingIntervalRef.current = null; } // Attempt to reconnect after 3 seconds // Only reconnect if not intentionally disconnected and still the same WebSocket if (runId && wsRef.current === ws && !isIntentionalDisconnect.current) { reconnectTimeoutRef.current = setTimeout(() => { console.log('Attempting to reconnect...'); connect(); }, 3000); } }; } catch (error) { console.error('Error creating WebSocket:', error); setIsConnected(false); } }, [runId, onMessage, onProgress, onError, onCompleted]); const disconnect = useCallback(() => { console.log('Disconnecting WebSocket...'); // Set flag to prevent automatic reconnection isIntentionalDisconnect.current = true; if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } if (pingIntervalRef.current) { clearInterval(pingIntervalRef.current); pingIntervalRef.current = null; } if (wsRef.current) { // Set a flag to prevent reconnection const ws = wsRef.current; wsRef.current = null; ws.close(); } setIsConnected(false); }, []); const sendMessage = useCallback((message: any) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify(message)); } else { console.warn('WebSocket is not connected'); } }, []); useEffect(() => { // Small delay to prevent rapid reconnections during React's render cycles const timeoutId = setTimeout(() => { if (runId) { connect(); } else { disconnect(); } }, 100); return () => { clearTimeout(timeoutId); disconnect(); }; }, [runId]); // Only depend on runId, not the functions return { isConnected, lastMessage, sendMessage, disconnect, connect }; }