first
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
// Created by: Claude
|
||||
// Date: 2026-01-03
|
||||
// Purpose: Hook personnalisé pour la gestion WebSocket avec reconnexion
|
||||
// Refs: client/CLAUDE.md, docs/protocol_events_v_2.md
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import { useAuthStore } from '../stores/authStore'
|
||||
|
||||
/**
|
||||
* Événement WebSocket structuré selon le protocole Mesh.
|
||||
*/
|
||||
export interface WebSocketEvent {
|
||||
type: string
|
||||
id: string
|
||||
timestamp: string
|
||||
from: string
|
||||
to: string
|
||||
payload: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Options pour le hook useWebSocket.
|
||||
*/
|
||||
interface UseWebSocketOptions {
|
||||
url?: string
|
||||
autoConnect?: boolean
|
||||
reconnectDelay?: number
|
||||
maxReconnectAttempts?: number
|
||||
onMessage?: (event: WebSocketEvent) => void
|
||||
onConnect?: () => void
|
||||
onDisconnect?: () => void
|
||||
onError?: (error: Event) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* État de connexion WebSocket.
|
||||
*/
|
||||
export enum ConnectionStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook personnalisé pour gérer la connexion WebSocket.
|
||||
*
|
||||
* Fonctionnalités:
|
||||
* - Connexion automatique avec le token JWT
|
||||
* - Reconnexion automatique en cas de déconnexion
|
||||
* - Gestion des événements structurés
|
||||
* - Envoi d'événements typés
|
||||
*/
|
||||
export const useWebSocket = (options: UseWebSocketOptions = {}) => {
|
||||
const {
|
||||
url = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws',
|
||||
autoConnect = true,
|
||||
reconnectDelay = 3000,
|
||||
maxReconnectAttempts = 5,
|
||||
onMessage,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onError,
|
||||
} = options
|
||||
|
||||
const { token, logout } = useAuthStore()
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const reconnectAttemptsRef = useRef(0)
|
||||
const peerId = useRef<string | null>(null)
|
||||
|
||||
const [status, setStatus] = useState<ConnectionStatus>(ConnectionStatus.DISCONNECTED)
|
||||
const [lastError, setLastError] = useState<string | null>(null)
|
||||
|
||||
/**
|
||||
* Nettoyer les timeouts de reconnexion.
|
||||
*/
|
||||
const clearReconnectTimeout = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Connecter au serveur WebSocket.
|
||||
*/
|
||||
const connect = useCallback(() => {
|
||||
if (!token) {
|
||||
console.warn('Cannot connect to WebSocket: no token available')
|
||||
return
|
||||
}
|
||||
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
console.warn('WebSocket already connected')
|
||||
return
|
||||
}
|
||||
|
||||
setStatus(
|
||||
reconnectAttemptsRef.current > 0
|
||||
? ConnectionStatus.RECONNECTING
|
||||
: ConnectionStatus.CONNECTING
|
||||
)
|
||||
|
||||
try {
|
||||
// Construire l'URL avec le token en query parameter
|
||||
const wsUrl = `${url}?token=${token}`
|
||||
const ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected')
|
||||
setStatus(ConnectionStatus.CONNECTED)
|
||||
setLastError(null)
|
||||
reconnectAttemptsRef.current = 0
|
||||
|
||||
// Envoyer system.hello pour s'identifier
|
||||
const helloEvent: Partial<WebSocketEvent> = {
|
||||
type: 'system.hello',
|
||||
payload: {
|
||||
client_type: 'web',
|
||||
version: '1.0.0',
|
||||
},
|
||||
}
|
||||
ws.send(JSON.stringify(helloEvent))
|
||||
|
||||
onConnect?.()
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data: WebSocketEvent = JSON.parse(event.data)
|
||||
|
||||
// Stocker le peer_id depuis system.welcome
|
||||
if (data.type === 'system.welcome') {
|
||||
peerId.current = data.payload.peer_id
|
||||
console.log('Received peer_id:', peerId.current)
|
||||
}
|
||||
|
||||
onMessage?.(data)
|
||||
} catch (err) {
|
||||
console.error('Error parsing WebSocket message:', err)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
setLastError('WebSocket connection error')
|
||||
setStatus(ConnectionStatus.ERROR)
|
||||
onError?.(error)
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket closed:', event.code, event.reason)
|
||||
wsRef.current = null
|
||||
peerId.current = null
|
||||
|
||||
if (event.code === 1008) {
|
||||
// Invalid token - déconnecter l'utilisateur
|
||||
console.error('Invalid token, logging out')
|
||||
logout()
|
||||
setStatus(ConnectionStatus.DISCONNECTED)
|
||||
} else if (reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||
// Tenter une reconnexion
|
||||
setStatus(ConnectionStatus.RECONNECTING)
|
||||
reconnectAttemptsRef.current++
|
||||
console.log(
|
||||
`Reconnecting... (attempt ${reconnectAttemptsRef.current}/${maxReconnectAttempts})`
|
||||
)
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
connect()
|
||||
}, reconnectDelay)
|
||||
} else {
|
||||
setStatus(ConnectionStatus.DISCONNECTED)
|
||||
setLastError('Max reconnection attempts reached')
|
||||
}
|
||||
|
||||
onDisconnect?.()
|
||||
}
|
||||
|
||||
wsRef.current = ws
|
||||
} catch (err) {
|
||||
console.error('Error creating WebSocket:', err)
|
||||
setStatus(ConnectionStatus.ERROR)
|
||||
setLastError('Failed to create WebSocket connection')
|
||||
}
|
||||
}, [token, url, reconnectDelay, maxReconnectAttempts, onConnect, onMessage, onDisconnect, onError, logout])
|
||||
|
||||
/**
|
||||
* Déconnecter du serveur WebSocket.
|
||||
*/
|
||||
const disconnect = useCallback(() => {
|
||||
clearReconnectTimeout()
|
||||
reconnectAttemptsRef.current = maxReconnectAttempts // Empêcher la reconnexion auto
|
||||
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close()
|
||||
wsRef.current = null
|
||||
peerId.current = null
|
||||
}
|
||||
|
||||
setStatus(ConnectionStatus.DISCONNECTED)
|
||||
}, [clearReconnectTimeout, maxReconnectAttempts])
|
||||
|
||||
/**
|
||||
* Envoyer un événement WebSocket.
|
||||
*/
|
||||
const sendEvent = useCallback((event: Partial<WebSocketEvent>) => {
|
||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
|
||||
console.warn('WebSocket not connected, cannot send event')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// Ajouter les champs par défaut
|
||||
const fullEvent: WebSocketEvent = {
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
from: peerId.current || 'unknown',
|
||||
to: event.to || 'server',
|
||||
type: event.type || 'unknown',
|
||||
payload: event.payload || {},
|
||||
}
|
||||
|
||||
wsRef.current.send(JSON.stringify(fullEvent))
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error sending WebSocket event:', err)
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Connexion automatique au montage.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (autoConnect && token) {
|
||||
connect()
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearReconnectTimeout()
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close()
|
||||
}
|
||||
}
|
||||
}, [autoConnect, token, connect, clearReconnectTimeout])
|
||||
|
||||
return {
|
||||
status,
|
||||
lastError,
|
||||
peerId: peerId.current,
|
||||
isConnected: status === ConnectionStatus.CONNECTED,
|
||||
connect,
|
||||
disconnect,
|
||||
sendEvent,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user