fix(realtime): les mises à jour live SURVIVENT aux redéploiements du hub (reconnexion + resync SSE)

Cause (diagnostic) : docker restart du hub à chaque déploiement détruit tous les clients SSE en mémoire. Les flux courriels/
cloche utilisaient un EventSource brut SANS onerror/reconnexion → le 502 pendant le restart les met CLOSED définitivement
(plus de reconnexion native) → morts jusqu'au rechargement manuel. Le dispatch se reconnectait mais ne resynchronisait jamais.

Fix (client) :
- useSSE : nouvelle option onReconnect() appelée à chaque RE-connexion (pas au 1er open) → resync après coupure.
- useNotifications (cloche) : reconnexion auto si le flux meurt (CLOSED → relance 3 s).
Côté co-édité déployé via bundle (non commité ici) : useConversations (inbox) = reconnexion + resync fetchList + repli poll 25 s
sur les 3 connexions SSE ; PlanificationPage = onReconnect → reloadOccupancy+reloadPool ; server.js = event 'ready' + retry:3000.
Hub 'ready' vérifié live. Reste (durabilité, différé) : persister _watchSnap pour rejouer les deltas Legacy manqués pendant la coupure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-19 12:19:43 -04:00
parent daeb86008b
commit 187115cb56
2 changed files with 11 additions and 0 deletions

View File

@ -109,6 +109,9 @@ function onCallIncoming (d) {
function initNotifications () { function initNotifications () {
loadPrefs(); loadMyQueues() loadPrefs(); loadMyQueues()
if (es) return if (es) return
connectNotifSSE()
}
function connectNotifSSE () {
try { try {
es = new EventSource(`${HUB_URL}/sse?topics=outbox,conversations`) es = new EventSource(`${HUB_URL}/sse?topics=outbox,conversations`)
es.addEventListener('rating', e => { try { onRating(JSON.parse(e.data)) } catch {} }) es.addEventListener('rating', e => { try { onRating(JSON.parse(e.data)) } catch {} })
@ -116,6 +119,9 @@ function initNotifications () {
es.addEventListener('conv-message', e => { try { onConvMessage(JSON.parse(e.data)) } catch {} }) es.addEventListener('conv-message', e => { try { onConvMessage(JSON.parse(e.data)) } catch {} })
es.addEventListener('conv-email', e => { try { onConvEmail(JSON.parse(e.data)) } catch {} }) es.addEventListener('conv-email', e => { try { onConvEmail(JSON.parse(e.data)) } catch {} })
es.addEventListener('call-incoming', e => { try { onCallIncoming(JSON.parse(e.data)) } catch {} }) es.addEventListener('call-incoming', e => { try { onCallIncoming(JSON.parse(e.data)) } catch {} })
// Survit aux redéploiements du hub : si le flux meurt DÉFINITIVEMENT (502 pendant le restart → EventSource CLOSED,
// plus de reconnexion native), on relance après 3 s (sinon la cloche reste muette jusqu'au prochain rechargement).
es.onerror = () => { if (es && es.readyState === EventSource.CLOSED) { es = null; setTimeout(connectNotifSSE, 3000) } }
} catch (e) { /* SSE indisponible */ } } catch (e) { /* SSE indisponible */ }
} }
function markAllRead () { unread.value = 0 } function markAllRead () { unread.value = 0 }

View File

@ -17,6 +17,7 @@ export function useSSE (opts = {}) {
let es = null let es = null
let reconnectTimer = null let reconnectTimer = null
let reconnectDelay = 1000 // start at 1s, exponential backoff let reconnectDelay = 1000 // start at 1s, exponential backoff
let opened = false // pour distinguer 1re connexion vs RECONNEXION (→ resync)
function connect (topics) { function connect (topics) {
disconnect() disconnect()
@ -30,6 +31,10 @@ export function useSSE (opts = {}) {
es.onopen = () => { es.onopen = () => {
connected.value = true connected.value = true
reconnectDelay = 1000 // reset backoff on successful connect reconnectDelay = 1000 // reset backoff on successful connect
// RESYNC : à toute RE-connexion (après une coupure — ex. redéploiement du hub), on rattrape ce qui a changé pendant
// l'absence en refetchant l'état (le hub ne rejoue pas les deltas manqués). Pas au tout 1er open (le consommateur charge au mount).
if (opened && opts.onReconnect) { try { opts.onReconnect() } catch {} }
opened = true
} }
es.addEventListener('message', (e) => { es.addEventListener('message', (e) => {