109 lines
3.1 KiB
TypeScript
109 lines
3.1 KiB
TypeScript
|
|
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 };
|
||
|
|
}
|