import { ref } from "vue"; export type RealtimeNewMessage = { contactId: string; contactName: string; text: string; channel: string; direction: string; at: string; }; export type RealtimePilotTrace = { text: string; at: string }; export function useCrmRealtime(opts: { isAuthenticated: () => boolean; onDashboardChanged: () => Promise; onNewMessage?: (msg: RealtimeNewMessage) => void; onPilotTrace?: (log: RealtimePilotTrace) => void; onPilotFinished?: () => void; }) { const crmRealtimeState = ref<"idle" | "connecting" | "open" | "error">("idle"); let crmRealtimeSocket: WebSocket | null = null; let crmRealtimeReconnectTimer: ReturnType | null = null; let crmRealtimeRefreshTimer: ReturnType | null = null; let crmRealtimeRefreshInFlight = false; let crmRealtimeReconnectAttempt = 0; function clearCrmRealtimeReconnectTimer() { if (!crmRealtimeReconnectTimer) return; clearTimeout(crmRealtimeReconnectTimer); crmRealtimeReconnectTimer = null; } function clearCrmRealtimeRefreshTimer() { if (!crmRealtimeRefreshTimer) return; clearTimeout(crmRealtimeRefreshTimer); crmRealtimeRefreshTimer = null; } async function runCrmRealtimeRefresh() { if (!opts.isAuthenticated() || crmRealtimeRefreshInFlight) return; crmRealtimeRefreshInFlight = true; try { await opts.onDashboardChanged(); } catch { // ignore transient realtime refresh errors } finally { crmRealtimeRefreshInFlight = false; } } function scheduleCrmRealtimeRefresh(delayMs = 250) { clearCrmRealtimeRefreshTimer(); crmRealtimeRefreshTimer = setTimeout(() => { crmRealtimeRefreshTimer = null; void runCrmRealtimeRefresh(); }, delayMs); } function scheduleCrmRealtimeReconnect() { clearCrmRealtimeReconnectTimer(); const attempt = Math.min(crmRealtimeReconnectAttempt + 1, 8); crmRealtimeReconnectAttempt = attempt; const delayMs = Math.min(1000 * 2 ** (attempt - 1), 15000); crmRealtimeReconnectTimer = setTimeout(() => { crmRealtimeReconnectTimer = null; startCrmRealtime(); }, delayMs); } function stopCrmRealtime() { clearCrmRealtimeReconnectTimer(); clearCrmRealtimeRefreshTimer(); if (crmRealtimeSocket) { const socket = crmRealtimeSocket; crmRealtimeSocket = null; socket.onopen = null; socket.onmessage = null; socket.onerror = null; socket.onclose = null; try { socket.close(1000, "client stop"); } catch { // ignore socket close errors } } crmRealtimeState.value = "idle"; } function startCrmRealtime() { if (process.server || !opts.isAuthenticated()) return; if (crmRealtimeSocket) { const state = crmRealtimeSocket.readyState; if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) return; } clearCrmRealtimeReconnectTimer(); const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const url = `${protocol}//${window.location.host}/ws/crm-updates`; const socket = new WebSocket(url); crmRealtimeSocket = socket; crmRealtimeState.value = "connecting"; socket.onopen = () => { crmRealtimeState.value = "open"; crmRealtimeReconnectAttempt = 0; }; socket.onmessage = (event) => { const raw = typeof event.data === "string" ? event.data : ""; if (!raw) return; try { const payload = JSON.parse(raw) as { type?: string; [key: string]: any }; if (payload.type === "dashboard.changed") { scheduleCrmRealtimeRefresh(); } if (payload.type === "message.new" && opts.onNewMessage) { opts.onNewMessage(payload as unknown as RealtimeNewMessage); } if (payload.type === "pilot.trace" && opts.onPilotTrace) { opts.onPilotTrace({ text: String(payload.text ?? ""), at: String(payload.at ?? "") }); } if (payload.type === "pilot.catchup" && opts.onPilotTrace && Array.isArray(payload.logs)) { for (const log of payload.logs) { opts.onPilotTrace({ text: String((log as any).text ?? ""), at: String((log as any).at ?? "") }); } } if (payload.type === "pilot.finished" && opts.onPilotFinished) { opts.onPilotFinished(); } } catch { // ignore malformed realtime payloads } }; socket.onerror = () => { crmRealtimeState.value = "error"; }; socket.onclose = () => { const wasActive = crmRealtimeSocket === socket; if (wasActive) { crmRealtimeSocket = null; } if (!opts.isAuthenticated()) { crmRealtimeState.value = "idle"; return; } crmRealtimeState.value = "error"; scheduleCrmRealtimeReconnect(); }; } return { crmRealtimeState, startCrmRealtime, stopCrmRealtime }; }