// ── ws.js — WebSocket connection and subscription ──────── let ws = null; let wsStatus = 'disconnected'; let wsReconnectTimeout = null; let wsReconnectDelay = 1000; const wsCallbacks = new Set(); export function getWsURL() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return protocol + '//' + window.location.host + '/api/v1/ws'; } export function connectWS() { if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return; } try { ws = new WebSocket(getWsURL()); ws.onopen = () => { console.log('WebSocket connected'); wsStatus = 'connected'; wsReconnectDelay = 1000; updateWSIndicator(); wsCallbacks.forEach(cb => cb({ type: 'connected' })); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); wsCallbacks.forEach(cb => cb({ type: 'message', data })); } catch (e) { console.error('Failed to parse WS message:', e); } }; ws.onclose = () => { console.log('WebSocket disconnected'); wsStatus = 'reconnecting'; updateWSIndicator(); wsCallbacks.forEach(cb => cb({ type: 'disconnected' })); wsReconnectTimeout = setTimeout(connectWS, wsReconnectDelay); wsReconnectDelay = Math.min(wsReconnectDelay * 1.5, 30000); }; ws.onerror = (err) => { console.error('WebSocket error:', err); }; } catch (e) { console.error('Failed to connect WebSocket:', e); wsReconnectTimeout = setTimeout(connectWS, wsReconnectDelay); wsReconnectDelay = Math.min(wsReconnectDelay * 1.5, 30000); } } export function subscribeWS(callback) { wsCallbacks.add(callback); if (!ws || ws.readyState !== WebSocket.OPEN) { connectWS(); } return () => wsCallbacks.delete(callback); } export function updateWSIndicator() { const dot = document.getElementById('ws-dot'); if (!dot) return; dot.className = 'ws-dot ' + wsStatus; const labels = { connected: 'Live — WebSocket connected', reconnecting: 'Reconnecting…', disconnected: 'Disconnected', }; dot.title = labels[wsStatus] || 'Unknown'; }