import { useEffect, useRef, useState, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; export interface WebSocketEvent { type: string; data: Record; timestamp: string; } const WS_RECONNECT_DELAY = 3000; const WS_MAX_RECONNECT_DELAY = 30000; export function useWebSocket() { const queryClient = useQueryClient(); const wsRef = useRef(null); const reconnectTimeoutRef = useRef | null>(null); const reconnectDelayRef = useRef(WS_RECONNECT_DELAY); const [lastEvent, setLastEvent] = useState(null); const [isConnected, setIsConnected] = useState(false); const handleMessage = useCallback( (event: MessageEvent) => { try { const parsed: WebSocketEvent = JSON.parse(event.data); setLastEvent(parsed); // Invalidate relevant TanStack Query caches based on event type switch (parsed.type) { case 'trade_executed': queryClient.invalidateQueries({ queryKey: ['portfolio'] }); queryClient.invalidateQueries({ queryKey: ['positions'] }); queryClient.invalidateQueries({ queryKey: ['trades'] }); break; case 'signal_generated': queryClient.invalidateQueries({ queryKey: ['signals'] }); break; case 'portfolio_update': queryClient.invalidateQueries({ queryKey: ['portfolio'] }); queryClient.invalidateQueries({ queryKey: ['equity-curve'] }); break; case 'news_scored': queryClient.invalidateQueries({ queryKey: ['news'] }); break; case 'strategy_update': queryClient.invalidateQueries({ queryKey: ['strategies'] }); break; default: break; } } catch { // Ignore non-JSON messages } }, [queryClient] ); const connect = useCallback(() => { const token = localStorage.getItem('access_token'); if (!token) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; const wsUrl = `${protocol}//${host}/ws?token=${encodeURIComponent(token)}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { setIsConnected(true); reconnectDelayRef.current = WS_RECONNECT_DELAY; }; ws.onmessage = handleMessage; ws.onclose = () => { setIsConnected(false); wsRef.current = null; // Reconnect with exponential backoff reconnectTimeoutRef.current = setTimeout(() => { reconnectDelayRef.current = Math.min( reconnectDelayRef.current * 2, WS_MAX_RECONNECT_DELAY ); connect(); }, reconnectDelayRef.current); }; ws.onerror = () => { ws.close(); }; }, [handleMessage]); useEffect(() => { connect(); return () => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } if (wsRef.current) { wsRef.current.close(); } }; }, [connect]); return { lastEvent, isConnected }; }