Intial Version

This commit is contained in:
2025-12-03 18:00:10 -05:00
parent 43c4227da7
commit 0b86c88eb4
55 changed files with 8938 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
import WebSocket from 'ws';
import { EventEmitter } from 'events';
import { PriceIndex, WebSocketMessage, HistoricalData } from '../shared/types';
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';
}
}
}