first commit
This commit is contained in:
151
frontend/src/hooks/useWebSocket.ts
Normal file
151
frontend/src/hooks/useWebSocket.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const WS_URL = (import.meta as any).env?.VITE_WS_URL || 'ws://localhost:8000/ws'
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: string
|
||||
channel?: string
|
||||
timestamp?: string
|
||||
data?: Record<string, unknown>
|
||||
payload?: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface UseWebSocketOptions {
|
||||
autoConnect?: boolean
|
||||
autoSubscribe?: string[]
|
||||
heartbeatInterval?: number
|
||||
onMessage?: (message: WebSocketMessage) => void
|
||||
onConnect?: () => void
|
||||
onDisconnect?: () => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
interface UseWebSocketReturn {
|
||||
connected: boolean
|
||||
lastMessage: WebSocketMessage | null
|
||||
sendMessage: (message: Record<string, unknown>) => void
|
||||
subscribe: (channels: string[]) => void
|
||||
connect: () => void
|
||||
disconnect: () => void
|
||||
}
|
||||
|
||||
export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketReturn {
|
||||
const {
|
||||
autoConnect = true,
|
||||
autoSubscribe = [],
|
||||
onMessage,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onError,
|
||||
} = options
|
||||
|
||||
const { token } = useAuthStore()
|
||||
console.log('[WebSocket] Token present:', !!token, token ? token.substring(0, 20) + '...' : 'none')
|
||||
console.log('[WebSocket] autoConnect:', autoConnect)
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const [connected, setConnected] = useState(false)
|
||||
const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null)
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const heartbeatTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!token) {
|
||||
console.log('[WebSocket] No token, skipping connect')
|
||||
return
|
||||
}
|
||||
|
||||
const wsUrl = `${WS_URL}?token=${token}`
|
||||
console.log('[WebSocket] Connecting to:', wsUrl)
|
||||
const ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[WebSocket] Connected!')
|
||||
setConnected(true)
|
||||
if (autoSubscribe.length > 0) {
|
||||
ws.send(JSON.stringify({ type: 'subscribe', data: { channels: autoSubscribe } }))
|
||||
}
|
||||
onConnect?.()
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
console.log('[WebSocket] Received:', event.data)
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(event.data)
|
||||
setLastMessage(message)
|
||||
onMessage?.(message)
|
||||
} catch {
|
||||
console.error('Failed to parse WebSocket message')
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('[WebSocket] Disconnected:', event.code, event.reason)
|
||||
setConnected(false)
|
||||
if (heartbeatTimerRef.current) {
|
||||
clearInterval(heartbeatTimerRef.current)
|
||||
heartbeatTimerRef.current = null
|
||||
}
|
||||
onDisconnect?.()
|
||||
|
||||
if (autoConnect && token) {
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
console.log('[WebSocket] Reconnecting...')
|
||||
connect()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WebSocket] Error:', error)
|
||||
onError?.(new Error('WebSocket error'))
|
||||
}
|
||||
|
||||
wsRef.current = ws
|
||||
}, [token, autoConnect, autoSubscribe, onConnect, onDisconnect, onError])
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
if (heartbeatTimerRef.current) {
|
||||
clearInterval(heartbeatTimerRef.current)
|
||||
heartbeatTimerRef.current = null
|
||||
}
|
||||
wsRef.current?.close()
|
||||
wsRef.current = null
|
||||
setConnected(false)
|
||||
}, [])
|
||||
|
||||
const sendMessage = useCallback((message: Record<string, unknown>) => {
|
||||
wsRef.current?.send(JSON.stringify(message))
|
||||
}, [])
|
||||
|
||||
const subscribe = useCallback((channels: string[]) => {
|
||||
wsRef.current?.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
data: { channels }
|
||||
}))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[WebSocket] useEffect triggered, autoConnect:', autoConnect, 'token:', !!token)
|
||||
if (autoConnect && token) {
|
||||
connect()
|
||||
}
|
||||
return () => {
|
||||
disconnect()
|
||||
}
|
||||
}, [autoConnect, token, connect, disconnect])
|
||||
|
||||
return {
|
||||
connected,
|
||||
lastMessage,
|
||||
sendMessage,
|
||||
subscribe,
|
||||
connect,
|
||||
disconnect,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user