import WebSocket from 'ws'; import { EventEmitter } from 'events'; import { PriceIndex, WebSocketMessage, HistoricalData } from '../shared/types.js'; export class WebSocketClient extends EventEmitter { private ws: WebSocket | null = null; private url: string; private reconnectInterval: number = 5000; private reconnectTimer: NodeJS.Timeout | null = null; private isIntentionallyClosed: boolean = false; private maxReconnectAttempts: number = 10; private reconnectAttempts: number = 0; constructor(url: string) { super(); this.url = url; } connect(): void { if (this.ws?.readyState === WebSocket.OPEN) { console.log('WebSocket already connected'); return; } this.isIntentionallyClosed = false; console.log(`Connecting to WebSocket: ${this.url}`); try { this.ws = new WebSocket(this.url); this.ws.on('open', () => { console.log('WebSocket connected'); this.reconnectAttempts = 0; this.emit('connected'); this.sendMessage({ type: 'subscribe', data: { channel: 'price_updates' } }); }); this.ws.on('message', (data: WebSocket.Data) => { try { const message: WebSocketMessage = JSON.parse(data.toString()); this.handleMessage(message); } catch (error) { console.error('Failed to parse WebSocket message:', error); } }); this.ws.on('close', (code: number, reason: Buffer) => { console.log(`WebSocket closed: ${code} - ${reason.toString()}`); this.emit('disconnected'); if (!this.isIntentionallyClosed) { this.scheduleReconnect(); } }); this.ws.on('error', (error: Error) => { console.error('WebSocket error:', error); this.emit('error', error); }); } catch (error) { console.error('Failed to create WebSocket connection:', error); this.scheduleReconnect(); } } private handleMessage(message: WebSocketMessage): void { switch (message.type) { case 'price_update': this.emit('priceUpdate', message.data as PriceIndex); break; case 'history_data': this.emit('historyData', message.data as HistoricalData); break; case 'connection_status': this.emit('status', message.data); break; case 'error': this.emit('error', new Error(message.data)); break; default: console.warn('Unknown message type:', message.type); } } private scheduleReconnect(): void { if (this.reconnectTimer || this.isIntentionallyClosed) { return; } if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('Max reconnection attempts reached'); this.emit('maxReconnectAttemptsReached'); return; } this.reconnectAttempts++; const delay = Math.min(this.reconnectInterval * this.reconnectAttempts, 30000); console.log(`Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.connect(); }, delay); } sendMessage(message: any): void { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); } else { console.warn('WebSocket not connected, cannot send message'); } } disconnect(): void { this.isIntentionallyClosed = true; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } } isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN; } getConnectionState(): string { if (!this.ws) return 'DISCONNECTED'; switch (this.ws.readyState) { case WebSocket.CONNECTING: return 'CONNECTING'; case WebSocket.OPEN: return 'CONNECTED'; case WebSocket.CLOSING: return 'CLOSING'; case WebSocket.CLOSED: return 'DISCONNECTED'; default: return 'UNKNOWN'; } } }