|
|
import { WebSocketEvent } from '@/types/agent';
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
|
|
interface UseWebSocketProps {
|
|
|
url: string;
|
|
|
onMessage: (event: WebSocketEvent) => void;
|
|
|
onError?: (error: Event) => void;
|
|
|
}
|
|
|
|
|
|
export const useWebSocket = ({ url, onMessage, onError }: UseWebSocketProps) => {
|
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
|
const [connectionState, setConnectionState] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected');
|
|
|
const wsRef = useRef<WebSocket | null>(null);
|
|
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
|
|
const reconnectAttemptsRef = useRef(0);
|
|
|
const maxReconnectAttempts = 3;
|
|
|
const baseReconnectDelay = 3000;
|
|
|
const maxReconnectDelay = 5000;
|
|
|
const lastErrorTimeRef = useRef(0);
|
|
|
const errorThrottleMs = 5000;
|
|
|
const isInitialConnectionRef = useRef(true);
|
|
|
|
|
|
const getReconnectDelay = () => {
|
|
|
|
|
|
const delay = Math.min(
|
|
|
baseReconnectDelay * Math.pow(2, reconnectAttemptsRef.current),
|
|
|
maxReconnectDelay
|
|
|
);
|
|
|
return delay + Math.random() * 1000;
|
|
|
};
|
|
|
|
|
|
const connect = useCallback(() => {
|
|
|
if (wsRef.current?.readyState === WebSocket.OPEN || wsRef.current?.readyState === WebSocket.CONNECTING) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
setConnectionState('connecting');
|
|
|
const ws = new WebSocket(url);
|
|
|
|
|
|
ws.onopen = () => {
|
|
|
console.log('WebSocket connected');
|
|
|
setIsConnected(true);
|
|
|
setConnectionState('connected');
|
|
|
reconnectAttemptsRef.current = 0;
|
|
|
isInitialConnectionRef.current = false;
|
|
|
};
|
|
|
|
|
|
ws.onmessage = (event) => {
|
|
|
try {
|
|
|
const data = JSON.parse(event.data) as WebSocketEvent;
|
|
|
onMessage(data);
|
|
|
} catch (error) {
|
|
|
console.error('Failed to parse WebSocket message:', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
ws.onerror = (error) => {
|
|
|
console.error('WebSocket error:', error);
|
|
|
setConnectionState('error');
|
|
|
|
|
|
|
|
|
|
|
|
if (!isInitialConnectionRef.current) {
|
|
|
|
|
|
const now = Date.now();
|
|
|
if (now - lastErrorTimeRef.current > errorThrottleMs) {
|
|
|
lastErrorTimeRef.current = now;
|
|
|
onError?.(error);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
ws.onclose = (event) => {
|
|
|
console.log('WebSocket disconnected', { code: event.code, reason: event.reason });
|
|
|
setIsConnected(false);
|
|
|
setConnectionState('disconnected');
|
|
|
|
|
|
|
|
|
if (event.code !== 1000 && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
|
|
const delay = getReconnectDelay();
|
|
|
console.log(`Attempting to reconnect in ${Math.round(delay)}ms (attempt ${reconnectAttemptsRef.current + 1}/${maxReconnectAttempts})`);
|
|
|
|
|
|
reconnectTimeoutRef.current = setTimeout(() => {
|
|
|
reconnectAttemptsRef.current++;
|
|
|
connect();
|
|
|
}, delay);
|
|
|
} else if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
|
|
|
console.log('Max reconnection attempts reached');
|
|
|
setConnectionState('error');
|
|
|
} else if (event.code === 1000) {
|
|
|
|
|
|
setConnectionState('disconnected');
|
|
|
console.log('WebSocket closed normally, not reconnecting');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
wsRef.current = ws;
|
|
|
} catch (error) {
|
|
|
console.error('Failed to create WebSocket connection:', error);
|
|
|
setConnectionState('error');
|
|
|
}
|
|
|
}, [url, onMessage, onError]);
|
|
|
|
|
|
const disconnect = useCallback(() => {
|
|
|
if (reconnectTimeoutRef.current) {
|
|
|
clearTimeout(reconnectTimeoutRef.current);
|
|
|
}
|
|
|
if (wsRef.current) {
|
|
|
wsRef.current.close(1000, 'Manual disconnect');
|
|
|
wsRef.current = null;
|
|
|
}
|
|
|
setIsConnected(false);
|
|
|
setConnectionState('disconnected');
|
|
|
reconnectAttemptsRef.current = 0;
|
|
|
}, []);
|
|
|
|
|
|
const manualReconnect = useCallback(() => {
|
|
|
console.log('Manual reconnect requested');
|
|
|
disconnect();
|
|
|
reconnectAttemptsRef.current = 0;
|
|
|
isInitialConnectionRef.current = false;
|
|
|
setTimeout(() => connect(), 1000);
|
|
|
}, [disconnect, connect]);
|
|
|
|
|
|
const sendMessage = (message: unknown) => {
|
|
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
|
try {
|
|
|
wsRef.current.send(JSON.stringify(message));
|
|
|
} catch (error) {
|
|
|
console.error('Failed to send WebSocket message:', error);
|
|
|
}
|
|
|
} else {
|
|
|
console.warn('WebSocket is not connected');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
|
connect();
|
|
|
|
|
|
return () => {
|
|
|
disconnect();
|
|
|
};
|
|
|
}, [url]);
|
|
|
|
|
|
return {
|
|
|
isConnected,
|
|
|
connectionState,
|
|
|
sendMessage,
|
|
|
reconnect: connect,
|
|
|
disconnect,
|
|
|
manualReconnect
|
|
|
};
|
|
|
};
|
|
|
|