| |
| |
| |
| |
| |
| 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; |
| this.maxBackoff = 16000; |
| this.shouldReconnect = true; |
| this.reconnectAttempts = 0; |
| this.connectionStartTime = null; |
| } |
|
|
| |
| |
| |
| |
| get url() { |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| const host = window.location.host; |
| return `${protocol}//${host}/ws`; |
| } |
|
|
| |
| |
| |
| |
| |
| logEvent(event) { |
| const entry = { |
| ...event, |
| time: new Date().toISOString(), |
| attempt: this.reconnectAttempts |
| }; |
| this.eventLog.push(entry); |
| |
| if (this.eventLog.length > 100) { |
| this.eventLog = this.eventLog.slice(-100); |
| } |
| console.log('[WSClient]', entry); |
| } |
|
|
| |
| |
| |
| |
| |
| onStatusChange(callback) { |
| if (typeof callback !== 'function') { |
| throw new Error('Callback must be a function'); |
| } |
| this.statusSubscribers.add(callback); |
| |
| callback(this.status); |
| return () => this.statusSubscribers.delete(callback); |
| } |
|
|
| |
| |
| |
| |
| |
| onMessage(callback) { |
| if (typeof callback !== 'function') { |
| throw new Error('Callback must be a function'); |
| } |
| this.globalSubscribers.add(callback); |
| return () => this.globalSubscribers.delete(callback); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| |
| 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); |
| } |
| }); |
| } |
| } |
|
|
| |
| |
| |
| |
| connect() { |
| |
| 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; |
| 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 |
| }); |
| |
| |
| this.globalSubscribers.forEach(cb => { |
| try { |
| cb(data); |
| } catch (error) { |
| console.error('[WSClient] Error in global subscriber:', error); |
| } |
| }); |
| |
| |
| 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 |
| }); |
| |
| |
| 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 |
| }); |
| |
| |
| if (this.shouldReconnect) { |
| this.reconnectAttempts++; |
| const delay = this.backoff; |
| this.backoff = Math.min(this.backoff * 2, this.maxBackoff); |
| setTimeout(() => this.connect(), delay); |
| } |
| } |
| } |
|
|
| |
| |
| |
| disconnect() { |
| this.shouldReconnect = false; |
| if (this.socket) { |
| this.logEvent({ type: 'manual_disconnect' }); |
| this.socket.close(1000, 'Client disconnect'); |
| this.socket = null; |
| } |
| } |
|
|
| |
| |
| |
| reconnect() { |
| this.disconnect(); |
| this.shouldReconnect = true; |
| this.backoff = 1000; |
| this.reconnectAttempts = 0; |
| this.connect(); |
| } |
|
|
| |
| |
| |
| |
| |
| 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; |
| } |
| } |
|
|
| |
| |
| |
| |
| getEvents() { |
| return [...this.eventLog]; |
| } |
|
|
| |
| |
| |
| |
| 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 |
| }; |
| } |
|
|
| |
| |
| |
| |
| isConnected() { |
| return this.socket && this.socket.readyState === WebSocket.OPEN; |
| } |
|
|
| |
| |
| |
| clearSubscribers() { |
| this.statusSubscribers.clear(); |
| this.globalSubscribers.clear(); |
| this.typeSubscribers.clear(); |
| this.logEvent({ type: 'subscribers_cleared' }); |
| } |
| } |
|
|
| |
| const wsClient = new WSClient(); |
|
|
| |
| wsClient.connect(); |
|
|
| |
| export default wsClient; |