trading/dashboard/src/hooks/useWebSocket.ts

109 lines
3.1 KiB
TypeScript
Raw Normal View History

import { useEffect, useRef, useState, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
export interface WebSocketEvent {
type: string;
data: Record<string, unknown>;
timestamp: string;
}
const WS_RECONNECT_DELAY = 3000;
const WS_MAX_RECONNECT_DELAY = 30000;
export function useWebSocket() {
const queryClient = useQueryClient();
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const reconnectDelayRef = useRef(WS_RECONNECT_DELAY);
const [lastEvent, setLastEvent] = useState<WebSocketEvent | null>(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 };
}