|
|
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 |
|
|
}; |
|
|
}; |
|
|
|