/** * WebSocket client for real-time updates */ class WebSocketClient { constructor(basePath = '') { this.basePath = basePath; this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; this.reconnectDelay = 1000; this.listeners = new Map(); this.isConnected = false; this.shouldReconnect = true; } connect() { if (this.ws && this.ws.readyState === WebSocket.OPEN) { return; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // Ensure basePath ends with '/' for proper URL construction let basePath = this.basePath || ''; if (basePath && !basePath.endsWith('/')) { basePath += '/'; } const wsUrl = `${protocol}//${window.location.host}${basePath}ws`; console.log('WebSocket connecting to:', wsUrl, 'basePath:', this.basePath); try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connected'); this.isConnected = true; this.reconnectAttempts = 0; this.emit('connected'); }; this.ws.onmessage = (event) => { try { // Validate message size (prevent memory issues) const maxMessageSize = 10 * 1024 * 1024; // 10MB if (event.data && event.data.length > maxMessageSize) { console.error('WebSocket message too large:', event.data.length, 'bytes'); this.ws.close(); return; } const message = JSON.parse(event.data); if (!message || typeof message !== 'object') { console.error('Invalid WebSocket message format'); return; } this.handleMessage(message); } catch (e) { console.error('Failed to parse WebSocket message:', e); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.emit('error', error); }; this.ws.onclose = () => { console.log('WebSocket disconnected'); this.isConnected = false; this.emit('disconnected'); if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); setTimeout(() => this.connect(), delay); } }; } catch (e) { console.error('Failed to create WebSocket connection:', e); this.emit('error', e); } } handleMessage(message) { const { type, payload, time } = message; // Emit to specific type listeners this.emit(type, payload, time); // Emit to all listeners this.emit('message', { type, payload, time }); } on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); } off(event, callback) { if (!this.listeners.has(event)) { return; } const callbacks = this.listeners.get(event); const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } emit(event, ...args) { if (this.listeners.has(event)) { this.listeners.get(event).forEach(callback => { try { callback(...args); } catch (e) { console.error('Error in WebSocket event handler:', e); } }); } } disconnect() { this.shouldReconnect = false; if (this.ws) { this.ws.close(); this.ws = null; } } send(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } else { console.warn('WebSocket is not connected'); } } } // Create global WebSocket client instance // Safely get basePath from global scope (defined in page.html) window.wsClient = new WebSocketClient(typeof basePath !== 'undefined' ? basePath : '');