// ───────────────────────────────────────────────────────────────────────────── // Tempo real via WebSocket (singleton, por clínica). // O backend empurra { type: 'agenda' | 'notificacao' | 'connected', ... } quando // algo muda na clínica ativa → os componentes assinam e refazem o fetch na hora. // Reconexão automática com backoff; autentica via token+clinicaId na query string. // ───────────────────────────────────────────────────────────────────────────── type RTHandler = (msg: any) => void; const handlers = new Set(); let ws: WebSocket | null = null; let reconnectTimer: ReturnType | null = null; let backoff = 1000; let currentUrl = ''; function buildUrl(): string | null { const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); let clinicaId = ''; try { clinicaId = JSON.parse(localStorage.getItem('SCOREODONTO_ACTIVE_WORKSPACE') || '{}')?.id || ''; } catch { /* ignore */ } if (!token || !clinicaId) return null; const proto = location.protocol === 'https:' ? 'wss' : 'ws'; return `${proto}://${location.host}/api/ws?token=${encodeURIComponent(token)}&clinicaId=${encodeURIComponent(clinicaId)}`; } function scheduleReconnect() { if (reconnectTimer || handlers.size === 0) return; reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, backoff); backoff = Math.min(backoff * 2, 15000); } function connect() { const url = buildUrl(); if (!url) return; currentUrl = url; let sock: WebSocket; try { sock = new WebSocket(url); } catch { scheduleReconnect(); return; } ws = sock; sock.onopen = () => { backoff = 1000; }; sock.onmessage = (ev) => { let msg: any; try { msg = JSON.parse(ev.data); } catch { return; } handlers.forEach(h => { try { h(msg); } catch { /* ignore */ } }); }; sock.onclose = () => { if (ws === sock) ws = null; scheduleReconnect(); }; sock.onerror = () => { try { sock.close(); } catch { /* ignore */ } }; } // Fecha um socket que NÓS abrimos. Se ainda está em CONNECTING, chamar close() // agora dispara "WebSocket is closed before the connection is established" // (comum no double-mount do React StrictMode em DEV) → adiamos o close para o // onopen. Em todos os casos neutralizamos os handlers para o socket morto não // disparar reconexão (a reconexão automática por queda inesperada continua no // onclose original definido em connect()). function closeSocket(sock: WebSocket | null) { if (!sock) return; sock.onmessage = null; sock.onerror = null; sock.onclose = null; if (sock.readyState === WebSocket.CONNECTING) { sock.onopen = () => { try { sock.close(); } catch { /* ignore */ } }; } else { sock.onopen = null; try { sock.close(); } catch { /* ignore */ } } } function ensureConnected() { const url = buildUrl(); if (!url) return; // Clínica/token mudou → reconecta no canal certo. if (ws && url !== currentUrl) { closeSocket(ws); ws = null; } if (!ws || ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED) connect(); } export const Realtime = { /** Assina mensagens de tempo real. Retorna função de cancelamento. */ subscribe(handler: RTHandler): () => void { handlers.add(handler); ensureConnected(); return () => { handlers.delete(handler); if (handlers.size === 0) { if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } if (ws) { closeSocket(ws); ws = null; } } }; }, /** Força reconexão (ex.: ao trocar de workspace). */ reconnect() { if (ws) { closeSocket(ws); ws = null; } backoff = 1000; ensureConnected(); }, };