import { useEffect, useRef, useState, useCallback } from 'react'; import { getWebSocketUrl, resetApiPortCache } from '@/utils/apiConfig'; interface WebSocketOptions { onMessage?: (data: any) => void; onConnect?: () => void; onDisconnect?: () => void; onError?: (error: Event) => void; reconnectInterval?: number; maxReconnectAttempts?: number; } export function useWebSocket(options: WebSocketOptions = {}) { const { onMessage, onConnect, onDisconnect, onError, reconnectInterval = 5000, maxReconnectAttempts = 10 } = options; const [isConnected, setIsConnected] = useState(false); const [reconnectAttempts, setReconnectAttempts] = useState(0); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const isManualClose = useRef(false); const lastPortRef = useRef(null); const clearReconnectTimeout = () => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } }; const connect = useCallback(async () => { // Prevent multiple simultaneous connections if (wsRef.current?.readyState === WebSocket.CONNECTING || wsRef.current?.readyState === WebSocket.OPEN) { return; } // Clear any existing reconnect timeout clearReconnectTimeout(); // Close existing connection if any if (wsRef.current) { wsRef.current.onclose = null; // Prevent triggering reconnect wsRef.current.close(); wsRef.current = null; } try { // Get WebSocket URL dynamically const wsUrl = await getWebSocketUrl(); // Extract port from URL for comparison const urlMatch = wsUrl.match(/:(\d+)/); const currentPort = urlMatch ? parseInt(urlMatch[1]) : null; // If port changed, reset API cache if (lastPortRef.current && currentPort && lastPortRef.current !== currentPort) { console.log(`🔄 Port changed from ${lastPortRef.current} to ${currentPort}, resetting cache`); resetApiPortCache(); } lastPortRef.current = currentPort; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { console.log('🔌 WebSocket connected'); setIsConnected(true); setReconnectAttempts(0); clearReconnectTimeout(); onConnect?.(); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); onMessage?.(data); } catch (error) { console.warn('Failed to parse WebSocket message:', error); } }; ws.onclose = (event) => { console.log('🔌 WebSocket disconnected'); setIsConnected(false); wsRef.current = null; onDisconnect?.(); // Only reconnect if not manually closed and under max attempts if (!isManualClose.current && reconnectAttempts < maxReconnectAttempts) { const delay = Math.min(reconnectInterval * Math.pow(1.5, reconnectAttempts), 30000); console.log(`⏳ Will reconnect in ${delay / 1000} seconds... (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})`); reconnectTimeoutRef.current = setTimeout(() => { setReconnectAttempts(prev => prev + 1); connect(); }, delay); } }; ws.onerror = (error) => { console.error('WebSocket error:', error); onError?.(error); // On error, trigger reconnect through close handler if (ws.readyState === WebSocket.OPEN) { ws.close(); } }; } catch (error) { console.error('Failed to create WebSocket:', error); setIsConnected(false); // Schedule reconnect on connection failure if (!isManualClose.current && reconnectAttempts < maxReconnectAttempts) { const delay = Math.min(reconnectInterval * Math.pow(1.5, reconnectAttempts), 30000); reconnectTimeoutRef.current = setTimeout(() => { setReconnectAttempts(prev => prev + 1); connect(); }, delay); } } }, [onMessage, onConnect, onDisconnect, onError, reconnectInterval, maxReconnectAttempts, reconnectAttempts]); const disconnect = useCallback(() => { isManualClose.current = true; clearReconnectTimeout(); if (wsRef.current) { wsRef.current.onclose = null; wsRef.current.close(); wsRef.current = null; } setIsConnected(false); setReconnectAttempts(0); }, []); const sendMessage = useCallback((data: any) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify(data)); return true; } return false; }, []); // Auto-connect on mount useEffect(() => { isManualClose.current = false; connect(); // Cleanup on unmount return () => { isManualClose.current = true; clearReconnectTimeout(); if (wsRef.current) { wsRef.current.close(); } }; }, []); // Only run on mount/unmount // Monitor for port changes by checking periodically useEffect(() => { const checkPortChange = async () => { if (!isConnected || !wsRef.current) return; try { const wsUrl = await getWebSocketUrl(); const urlMatch = wsUrl.match(/:(\d+)/); const currentPort = urlMatch ? parseInt(urlMatch[1]) : null; if (lastPortRef.current && currentPort && lastPortRef.current !== currentPort) { console.log(`🔄 Detected port change from ${lastPortRef.current} to ${currentPort}, reconnecting...`); // Force reconnect with new port isManualClose.current = false; disconnect(); setTimeout(() => { isManualClose.current = false; connect(); }, 100); } } catch (error) { // Ignore errors in port checking } }; const interval = setInterval(checkPortChange, 10000); // Check every 10 seconds return () => clearInterval(interval); }, [isConnected, connect, disconnect]); return { isConnected, connect, disconnect, sendMessage, reconnectAttempts }; }