092635be5a
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>
94 lines
3.9 KiB
TypeScript
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();
|
|
},
|
|
};
|