// WebSocket client for real-time features const WS = { _ws: null, _reconnectTimer: null, _listeners: {}, _connected: false, connect() { if (this._ws && this._ws.readyState === WebSocket.OPEN) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = `${protocol}//${window.location.host}/ws`; try { this._ws = new WebSocket(url); } catch (e) { console.warn('WS connection failed, retrying in 5s'); this._scheduleReconnect(); return; } this._ws.onopen = () => { this._connected = true; this._emit('connected'); }; this._ws.onclose = () => { this._connected = false; this._emit('disconnected'); this._scheduleReconnect(); }; this._ws.onerror = () => { this._emit('error'); }; this._ws.onmessage = (event) => { try { const data = JSON.parse(event.data); this._emit(data.type, data); } catch (e) { // ignore non-JSON messages } }; // Keepalive ping every 30s this._pingInterval = setInterval(() => { if (this._ws && this._ws.readyState === WebSocket.OPEN) { this._ws.send(JSON.stringify({ type: 'ping' })); } }, 30000); }, disconnect() { if (this._pingInterval) clearInterval(this._pingInterval); if (this._reconnectTimer) clearTimeout(this._reconnectTimer); if (this._ws) { this._ws.onclose = null; this._ws.close(); this._ws = null; } this._connected = false; }, _scheduleReconnect() { if (this._reconnectTimer) return; this._reconnectTimer = setTimeout(() => { this._reconnectTimer = null; this.connect(); }, 5000); }, on(event, callback) { if (!this._listeners[event]) this._listeners[event] = []; this._listeners[event].push(callback); return () => { this._listeners[event] = this._listeners[event].filter(cb => cb !== callback); }; }, _emit(event, data) { (this._listeners[event] || []).forEach(cb => cb(data)); // Also emit all for catch-all listeners (this._listeners['*'] || []).forEach(cb => cb(event, data)); }, isConnected() { return this._connected; } }; // Auto-connect on page load document.addEventListener('DOMContentLoaded', () => WS.connect());