/** * WebSocket client for real-time domain updates */ class PeerDirectoryWebSocket { constructor() { this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; this.reconnectDelay = 1000; this.listeners = new Map(); this.isConnected = false; this.reconnectTimeout = null; } /** * Connect to WebSocket server */ connect() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { this.isConnected = true; this.reconnectAttempts = 0; this.emit('connected'); this.updateStatus(true); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.handleMessage(message); } catch (err) { console.error('Error parsing WebSocket message:', err); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.emit('error', error); }; this.ws.onclose = () => { this.isConnected = false; this.updateStatus(false); this.emit('disconnected'); this.attemptReconnect(); }; } catch (err) { console.error('Error connecting to WebSocket:', err); this.attemptReconnect(); } } /** * Handle incoming messages */ handleMessage(message) { const { type, data } = message; switch (type) { case 'init': this.emit('init', data); break; case 'update': this.emit('update', data); break; case 'domain-added': this.emit('domain-added', data); break; case 'domain-removed': this.emit('domain-removed', data); break; default: // Silently ignore unknown message types from other plugins // This is normal - the WebSocket server broadcasts to all clients } } /** * Send message to server */ send(type, data = {}) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type, ...data })); } } /** * Request initial domain data from server */ requestDomains() { this.send('request-domains'); } /** * Attempt to reconnect */ attemptReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('Max reconnection attempts reached'); this.emit('reconnect-failed'); return; } if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); } this.reconnectAttempts++; const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000); this.reconnectTimeout = setTimeout(() => { console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); this.emit('reconnecting'); this.connect(); }, delay); } /** * Update connection status indicator */ updateStatus(connected) { const indicator = document.getElementById('statusIndicator'); const statusText = document.getElementById('statusText'); if (indicator && statusText) { if (connected) { indicator.classList.remove('disconnected'); statusText.textContent = 'Connected'; } else { indicator.classList.add('disconnected'); statusText.textContent = 'Disconnected - Reconnecting...'; } } } /** * Add event listener */ on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); } /** * Remove event listener */ off(event, callback) { if (this.listeners.has(event)) { const callbacks = this.listeners.get(event); const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } } /** * Emit event to listeners */ emit(event, data) { if (this.listeners.has(event)) { this.listeners.get(event).forEach(callback => { try { callback(data); } catch (err) { console.error(`Error in WebSocket event listener for ${event}:`, err); } }); } } /** * Disconnect */ disconnect() { if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } if (this.ws) { this.ws.close(); this.ws = null; } this.isConnected = false; this.listeners.clear(); } } // Create singleton instance window.wsClient = new PeerDirectoryWebSocket();