Spaces:
Sleeping
Sleeping
| /** | |
| * WebSocket Client for Real-time Communication | |
| * Manages WebSocket connections with automatic reconnection and exponential backoff | |
| * Supports message routing to type-specific subscribers | |
| */ | |
| class WSClient { | |
| constructor() { | |
| this.socket = null; | |
| this.status = 'disconnected'; | |
| this.statusSubscribers = new Set(); | |
| this.globalSubscribers = new Set(); | |
| this.typeSubscribers = new Map(); | |
| this.eventLog = []; | |
| this.backoff = 1000; // Initial backoff delay in ms | |
| this.maxBackoff = 16000; // Maximum backoff delay in ms | |
| this.shouldReconnect = true; | |
| this.reconnectAttempts = 0; | |
| this.connectionStartTime = null; | |
| } | |
| /** | |
| * Automatically determine WebSocket URL based on current window location | |
| * Always uses the current origin to avoid hardcoded URLs | |
| */ | |
| get url() { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const host = window.location.host; | |
| return `${protocol}//${host}/ws`; | |
| } | |
| /** | |
| * Log WebSocket events for debugging and monitoring | |
| * Maintains a rolling buffer of the last 100 events | |
| * @param {Object} event - Event object to log | |
| */ | |
| logEvent(event) { | |
| const entry = { | |
| ...event, | |
| time: new Date().toISOString(), | |
| attempt: this.reconnectAttempts | |
| }; | |
| this.eventLog.push(entry); | |
| // Keep only last 100 events | |
| if (this.eventLog.length > 100) { | |
| this.eventLog = this.eventLog.slice(-100); | |
| } | |
| console.log('[WSClient]', entry); | |
| } | |
| /** | |
| * Subscribe to connection status changes | |
| * @param {Function} callback - Called with new status ('connecting', 'connected', 'disconnected', 'error') | |
| * @returns {Function} Unsubscribe function | |
| */ | |
| onStatusChange(callback) { | |
| if (typeof callback !== 'function') { | |
| throw new Error('Callback must be a function'); | |
| } | |
| this.statusSubscribers.add(callback); | |
| // Immediately call with current status | |
| callback(this.status); | |
| return () => this.statusSubscribers.delete(callback); | |
| } | |
| /** | |
| * Subscribe to all WebSocket messages | |
| * @param {Function} callback - Called with parsed message data | |
| * @returns {Function} Unsubscribe function | |
| */ | |
| onMessage(callback) { | |
| if (typeof callback !== 'function') { | |
| throw new Error('Callback must be a function'); | |
| } | |
| this.globalSubscribers.add(callback); | |
| return () => this.globalSubscribers.delete(callback); | |
| } | |
| /** | |
| * Subscribe to specific message types | |
| * @param {string} type - Message type to subscribe to (e.g., 'market_update', 'news_update') | |
| * @param {Function} callback - Called with messages of the specified type | |
| * @returns {Function} Unsubscribe function | |
| */ | |
| subscribe(type, callback) { | |
| if (typeof callback !== 'function') { | |
| throw new Error('Callback must be a function'); | |
| } | |
| if (!this.typeSubscribers.has(type)) { | |
| this.typeSubscribers.set(type, new Set()); | |
| } | |
| const set = this.typeSubscribers.get(type); | |
| set.add(callback); | |
| return () => set.delete(callback); | |
| } | |
| /** | |
| * Update connection status and notify all subscribers | |
| * @param {string} newStatus - New status value | |
| */ | |
| updateStatus(newStatus) { | |
| if (this.status !== newStatus) { | |
| const oldStatus = this.status; | |
| this.status = newStatus; | |
| this.logEvent({ | |
| type: 'status_change', | |
| from: oldStatus, | |
| to: newStatus | |
| }); | |
| this.statusSubscribers.forEach(cb => { | |
| try { | |
| cb(newStatus); | |
| } catch (error) { | |
| console.error('[WSClient] Error in status subscriber:', error); | |
| } | |
| }); | |
| } | |
| } | |
| /** | |
| * Establish WebSocket connection with automatic reconnection | |
| * Implements exponential backoff for reconnection attempts | |
| */ | |
| connect() { | |
| // Prevent multiple simultaneous connection attempts | |
| if (this.socket && (this.socket.readyState === WebSocket.CONNECTING || this.socket.readyState === WebSocket.OPEN)) { | |
| console.log('[WSClient] Already connected or connecting'); | |
| return; | |
| } | |
| this.connectionStartTime = Date.now(); | |
| this.updateStatus('connecting'); | |
| try { | |
| this.socket = new WebSocket(this.url); | |
| this.logEvent({ | |
| type: 'connection_attempt', | |
| url: this.url, | |
| attempt: this.reconnectAttempts + 1 | |
| }); | |
| this.socket.onopen = () => { | |
| const connectionTime = Date.now() - this.connectionStartTime; | |
| this.backoff = 1000; // Reset backoff on successful connection | |
| this.reconnectAttempts = 0; | |
| this.updateStatus('connected'); | |
| this.logEvent({ | |
| type: 'connection_established', | |
| connectionTime: `${connectionTime}ms` | |
| }); | |
| console.log(`[WSClient] Connected to ${this.url} in ${connectionTime}ms`); | |
| }; | |
| this.socket.onmessage = (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| this.logEvent({ | |
| type: 'message_received', | |
| messageType: data.type || 'unknown', | |
| size: event.data.length | |
| }); | |
| // Notify global subscribers | |
| this.globalSubscribers.forEach(cb => { | |
| try { | |
| cb(data); | |
| } catch (error) { | |
| console.error('[WSClient] Error in global subscriber:', error); | |
| } | |
| }); | |
| // Notify type-specific subscribers | |
| if (data.type && this.typeSubscribers.has(data.type)) { | |
| this.typeSubscribers.get(data.type).forEach(cb => { | |
| try { | |
| cb(data); | |
| } catch (error) { | |
| console.error(`[WSClient] Error in ${data.type} subscriber:`, error); | |
| } | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('[WSClient] Message parse error:', error); | |
| this.logEvent({ | |
| type: 'parse_error', | |
| error: error.message, | |
| rawData: event.data.substring(0, 100) | |
| }); | |
| } | |
| }; | |
| this.socket.onclose = (event) => { | |
| const wasConnected = this.status === 'connected'; | |
| this.updateStatus('disconnected'); | |
| this.logEvent({ | |
| type: 'connection_closed', | |
| code: event.code, | |
| reason: event.reason || 'No reason provided', | |
| wasClean: event.wasClean | |
| }); | |
| // Attempt reconnection if enabled | |
| if (this.shouldReconnect) { | |
| this.reconnectAttempts++; | |
| const delay = this.backoff; | |
| this.backoff = Math.min(this.backoff * 2, this.maxBackoff); | |
| console.log(`[WSClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})...`); | |
| this.logEvent({ | |
| type: 'reconnect_scheduled', | |
| delay: `${delay}ms`, | |
| nextBackoff: `${this.backoff}ms` | |
| }); | |
| setTimeout(() => this.connect(), delay); | |
| } | |
| }; | |
| this.socket.onerror = (error) => { | |
| console.error('[WSClient] WebSocket error:', error); | |
| this.updateStatus('error'); | |
| this.logEvent({ | |
| type: 'connection_error', | |
| error: error.message || 'Unknown error', | |
| readyState: this.socket ? this.socket.readyState : 'null' | |
| }); | |
| }; | |
| } catch (error) { | |
| console.error('[WSClient] Failed to create WebSocket:', error); | |
| this.updateStatus('error'); | |
| this.logEvent({ | |
| type: 'creation_error', | |
| error: error.message | |
| }); | |
| // Retry connection if enabled | |
| if (this.shouldReconnect) { | |
| this.reconnectAttempts++; | |
| const delay = this.backoff; | |
| this.backoff = Math.min(this.backoff * 2, this.maxBackoff); | |
| setTimeout(() => this.connect(), delay); | |
| } | |
| } | |
| } | |
| /** | |
| * Gracefully disconnect WebSocket and disable automatic reconnection | |
| */ | |
| disconnect() { | |
| this.shouldReconnect = false; | |
| if (this.socket) { | |
| this.logEvent({ type: 'manual_disconnect' }); | |
| this.socket.close(1000, 'Client disconnect'); | |
| this.socket = null; | |
| } | |
| } | |
| /** | |
| * Manually trigger reconnection (useful for testing or recovery) | |
| */ | |
| reconnect() { | |
| this.disconnect(); | |
| this.shouldReconnect = true; | |
| this.backoff = 1000; // Reset backoff | |
| this.reconnectAttempts = 0; | |
| this.connect(); | |
| } | |
| /** | |
| * Send a message through the WebSocket connection | |
| * @param {Object} data - Data to send (will be JSON stringified) | |
| * @returns {boolean} True if sent successfully, false otherwise | |
| */ | |
| send(data) { | |
| if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { | |
| console.error('[WSClient] Cannot send message: not connected'); | |
| this.logEvent({ | |
| type: 'send_failed', | |
| reason: 'not_connected', | |
| readyState: this.socket ? this.socket.readyState : 'null' | |
| }); | |
| return false; | |
| } | |
| try { | |
| const message = JSON.stringify(data); | |
| this.socket.send(message); | |
| this.logEvent({ | |
| type: 'message_sent', | |
| messageType: data.type || 'unknown', | |
| size: message.length | |
| }); | |
| return true; | |
| } catch (error) { | |
| console.error('[WSClient] Failed to send message:', error); | |
| this.logEvent({ | |
| type: 'send_error', | |
| error: error.message | |
| }); | |
| return false; | |
| } | |
| } | |
| /** | |
| * Get a copy of the event log | |
| * @returns {Array} Array of logged events | |
| */ | |
| getEvents() { | |
| return [...this.eventLog]; | |
| } | |
| /** | |
| * Get current connection statistics | |
| * @returns {Object} Connection statistics | |
| */ | |
| getStats() { | |
| return { | |
| status: this.status, | |
| reconnectAttempts: this.reconnectAttempts, | |
| currentBackoff: this.backoff, | |
| maxBackoff: this.maxBackoff, | |
| shouldReconnect: this.shouldReconnect, | |
| subscriberCounts: { | |
| status: this.statusSubscribers.size, | |
| global: this.globalSubscribers.size, | |
| typed: Array.from(this.typeSubscribers.entries()).map(([type, subs]) => ({ | |
| type, | |
| count: subs.size | |
| })) | |
| }, | |
| eventLogSize: this.eventLog.length, | |
| url: this.url | |
| }; | |
| } | |
| /** | |
| * Check if WebSocket is currently connected | |
| * @returns {boolean} True if connected | |
| */ | |
| isConnected() { | |
| return this.socket && this.socket.readyState === WebSocket.OPEN; | |
| } | |
| /** | |
| * Clear all subscribers (useful for cleanup) | |
| */ | |
| clearSubscribers() { | |
| this.statusSubscribers.clear(); | |
| this.globalSubscribers.clear(); | |
| this.typeSubscribers.clear(); | |
| this.logEvent({ type: 'subscribers_cleared' }); | |
| } | |
| } | |
| // Create singleton instance | |
| const wsClient = new WSClient(); | |
| // Auto-connect on module load | |
| wsClient.connect(); | |
| // Export singleton instance | |
| export default wsClient; |