Files
PecHub/frontend/src/hooks/useWebSocket.ts
T
2026-03-19 16:58:23 +01:00

173 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Hook useWebSocket connessione WebSocket al backend PEChub.
*
* Il backend usa FastAPI WebSocket nativo (non Socket.io).
* Endpoint: /api/v1/ws/{tenant_id}?token=<access_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<WebSocket | null>(null)
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | 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 }
}