| 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; | |
| } | |
| get url() { | |
| const { protocol, host } = window.location; | |
| const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:'; | |
| return `${wsProtocol}//${host}/ws`; | |
| } | |
| logEvent(event) { | |
| const entry = { ...event, time: new Date().toISOString() }; | |
| this.eventLog.push(entry); | |
| this.eventLog = this.eventLog.slice(-100); | |
| } | |
| onStatusChange(callback) { | |
| this.statusSubscribers.add(callback); | |
| callback(this.status); | |
| return () => this.statusSubscribers.delete(callback); | |
| } | |
| onMessage(callback) { | |
| this.globalSubscribers.add(callback); | |
| return () => this.globalSubscribers.delete(callback); | |
| } | |
| subscribe(type, callback) { | |
| 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) { | |
| this.status = newStatus; | |
| this.statusSubscribers.forEach((cb) => cb(newStatus)); | |
| } | |
| connect() { | |
| if (this.socket && (this.status === 'connecting' || this.status === 'connected')) { | |
| return; | |
| } | |
| this.updateStatus('connecting'); | |
| this.socket = new WebSocket(this.url); | |
| this.logEvent({ type: 'status', status: 'connecting' }); | |
| this.socket.addEventListener('open', () => { | |
| this.backoff = 1000; | |
| this.updateStatus('connected'); | |
| this.logEvent({ type: 'status', status: 'connected' }); | |
| }); | |
| this.socket.addEventListener('message', (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| this.logEvent({ type: 'message', messageType: data.type || 'unknown' }); | |
| this.globalSubscribers.forEach((cb) => cb(data)); | |
| if (data.type && this.typeSubscribers.has(data.type)) { | |
| this.typeSubscribers.get(data.type).forEach((cb) => cb(data)); | |
| } | |
| } catch (error) { | |
| console.error('WS message parse error', error); | |
| } | |
| }); | |
| this.socket.addEventListener('close', () => { | |
| this.updateStatus('disconnected'); | |
| this.logEvent({ type: 'status', status: 'disconnected' }); | |
| if (this.shouldReconnect) { | |
| const delay = this.backoff; | |
| this.backoff = Math.min(this.backoff * 2, this.maxBackoff); | |
| setTimeout(() => this.connect(), delay); | |
| } | |
| }); | |
| this.socket.addEventListener('error', (error) => { | |
| console.error('WebSocket error', error); | |
| this.logEvent({ type: 'error', details: error.message || 'unknown' }); | |
| if (this.socket) { | |
| this.socket.close(); | |
| } | |
| }); | |
| } | |
| disconnect() { | |
| this.shouldReconnect = false; | |
| if (this.socket) { | |
| this.socket.close(); | |
| } | |
| } | |
| getEvents() { | |
| return [...this.eventLog]; | |
| } | |
| } | |
| const wsClient = new WSClient(); | |
| export default wsClient; | |