Files
VPS 4 Builder 092635be5a fix(realtime): guarda cleanup de WebSocket em CONNECTING (StrictMode)
Fechar o socket ainda em CONNECTING (double-mount do StrictMode em dev) gerava
"WebSocket is closed before the connection is established". closeSocket() adia o
close para o onopen e neutraliza handlers; reconexão por queda inesperada intacta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 20:54:24 +02:00

94 lines
3.9 KiB
TypeScript

// ─────────────────────────────────────────────────────────────────────────────
// 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<RTHandler>();
let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | 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();
},
};