/** * Hook useWebSocket – connessione WebSocket al backend PEChub. * * Il backend usa FastAPI WebSocket nativo (non Socket.io). * Endpoint: /api/v1/ws/{tenant_id}?token= * * Gestisce: * - Connessione automatica all'autenticazione * - Riconnessione con backoff esponenziale * - Dispatch degli eventi all'inbox/mailbox store */ import { useEffect, useRef, useCallback } from 'react' import { useAuthStore } from '@/store/auth.store' import { useInboxStore } from '@/store/inbox.store' import { useMailboxStore } from '@/store/mailbox.store' import type { MessageResponse, WsEvent } from '@/types/api.types' import toast from 'react-hot-toast' const MAX_RECONNECT_DELAY = 30000 const BASE_RECONNECT_DELAY = 1000 export function useWebSocket() { const wsRef = useRef(null) const reconnectTimeoutRef = useRef | null>(null) const reconnectAttemptsRef = useRef(0) const isUnmountedRef = useRef(false) const { user, isAuthenticated } = useAuthStore() const prependMessage = useInboxStore((s) => s.prependMessage) const updateMailboxStatus = useMailboxStore((s) => s.updateMailboxStatus) const handleEvent = useCallback( (event: WsEvent) => { switch (event.type) { case 'mailbox:new_message': { const message = event.payload as unknown as MessageResponse prependMessage(message) if (!message.is_read) { const from = message.from_address || 'Mittente sconosciuto' const subject = message.subject || '(nessun oggetto)' toast.success(` Nuova PEC da ${from}: ${subject}`, { duration: 5000, id: `new-msg-${message.id}`, }) } break } case 'mailbox:status_changed': { const { mailbox_id, status, error_msg } = event.payload as { mailbox_id: string status: string error_msg?: string } updateMailboxStatus(mailbox_id, status, error_msg) if (status === 'error') { toast.error(` Errore sincronizzazione casella`, { duration: 8000 }) } break } case 'send_job:status_changed': { const { job_id: _jobId, status, mailbox_id: _mid } = event.payload as { job_id: string status: string mailbox_id: string } if (status === 'sent') { toast.success(' PEC inviata con successo', { duration: 4000 }) } else if (status === 'failed') { toast.error(' Invio PEC fallito definitivamente', { duration: 8000 }) } break } case 'send_job:anomaly': { toast.error( ' Anomalia invio PEC: nessuna ricevuta di accettazione entro 24h', { duration: 10000 }, ) break } } }, [prependMessage, updateMailboxStatus], ) const connect = useCallback(() => { if (!isAuthenticated || !user) return const token = localStorage.getItem('access_token') if (!token) return // Costruisce l'URL WebSocket const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const host = window.location.host const wsUrl = `${protocol}//${host}/api/v1/ws/${user.tenant_id}?token=${token}` try { const ws = new WebSocket(wsUrl) wsRef.current = ws ws.onopen = () => { console.log('[WS] Connessione stabilita') reconnectAttemptsRef.current = 0 } ws.onmessage = (event) => { try { const data = JSON.parse(event.data) as WsEvent handleEvent(data) } catch { console.warn('[WS] Messaggio non valido:', event.data) } } ws.onerror = (error) => { console.error('[WS] Errore:', error) } ws.onclose = (event) => { console.log('[WS] Connessione chiusa', event.code, event.reason) wsRef.current = null if (isUnmountedRef.current || !isAuthenticated) return // Backoff esponenziale per la riconnessione const delay = Math.min( BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttemptsRef.current), MAX_RECONNECT_DELAY, ) reconnectAttemptsRef.current += 1 console.log(`[WS] Riconnessione in ${delay}ms (tentativo ${reconnectAttemptsRef.current})`) reconnectTimeoutRef.current = setTimeout(() => { if (!isUnmountedRef.current) connect() }, delay) } } catch (error) { console.error('[WS] Errore durante la connessione:', error) } }, [isAuthenticated, user, handleEvent]) const disconnect = useCallback(() => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current) reconnectTimeoutRef.current = null } if (wsRef.current) { wsRef.current.close(1000, 'Logout') wsRef.current = null } }, []) useEffect(() => { isUnmountedRef.current = false if (isAuthenticated && user) { connect() } else { disconnect() } return () => { isUnmountedRef.current = true disconnect() } }, [isAuthenticated, user?.id, connect, disconnect]) return { isConnected: wsRef.current?.readyState === WebSocket.OPEN } }