/** * Connection Manager - Handles WebSocket connection lifecycle */ import { ConnectionState, ConnectionState as ConnectionStateEnum } from '../types'; export interface ConnectionManagerConfig { streamUrl: string; autoReconnect: boolean; reconnectDelay: number; maxReconnectAttempts: number; debug: boolean; } export interface ConnectionManagerEvents { connected: () => void; disconnected: (code: number, reason: string) => void; reconnecting: (attempt: number) => void; error: (error: Error) => void; message: (data: string | ArrayBuffer) => void; stateChange: (state: ConnectionState) => void; } /** * Manages WebSocket connections with automatic reconnection */ export class ConnectionManager { private ws: WebSocket | null = null; private state: ConnectionState = ConnectionStateEnum.DISCONNECTED; private reconnectAttempts: number = 0; private reconnectTimer: ReturnType | null = null; private reconnectionLock: boolean = false; private readonly config: ConnectionManagerConfig; private readonly eventHandlers: Partial = {}; constructor(config: ConnectionManagerConfig) { this.config = config; } /** * Register event handler */ public on( event: K, handler: ConnectionManagerEvents[K] ): void { this.eventHandlers[event] = handler; } /** * Emit event */ private emit( event: K, ...args: Parameters ): void { const handler = this.eventHandlers[event]; if (handler) { // @ts-expect-error - Dynamic event handling handler(...args); } } /** * Connect to WebSocket */ public async connect(): Promise { if (this.state === ConnectionStateEnum.CONNECTING || this.state === ConnectionStateEnum.CONNECTED) { return; } this.setState(ConnectionStateEnum.CONNECTING); this.clearReconnectTimer(); try { await this.createWebSocketConnection(); this.setState(ConnectionStateEnum.CONNECTED); this.emit('connected'); this.reconnectAttempts = 0; } catch (error) { this.setState(ConnectionStateEnum.DISCONNECTED); throw error; } } /** * Disconnect WebSocket */ public disconnect(): void { this.clearReconnectTimer(); if (this.ws) { this.ws.close(1000, 'Normal closure'); this.ws = null; } this.setState(ConnectionStateEnum.DISCONNECTED); this.reconnectAttempts = 0; this.reconnectionLock = false; } /** * Send message through WebSocket */ public send(data: string | ArrayBuffer): void { if (this.state !== ConnectionStateEnum.CONNECTED || !this.ws) { throw new Error('WebSocket is not connected'); } this.ws.send(data); } /** * Get current connection state */ public getState(): ConnectionState { return this.state; } /** * Check if connected */ public isConnected(): boolean { return this.state === ConnectionStateEnum.CONNECTED; } /** * Create WebSocket connection */ private async createWebSocketConnection(): Promise { return new Promise((resolve, reject) => { try { this.ws = new WebSocket(this.config.streamUrl); this.ws.onopen = () => { resolve(); }; this.ws.onclose = (event) => { this.handleDisconnection(event.code, event.reason); }; this.ws.onerror = () => { const error = new Error('WebSocket connection failed'); reject(error); }; this.ws.onmessage = (event) => { this.emit('message', event.data); }; } catch (error) { reject(error); } }); } /** * Handle disconnection */ private handleDisconnection(code: number, reason: string): void { this.ws = null; this.setState(ConnectionStateEnum.DISCONNECTED); this.emit('disconnected', code, reason); // Attempt reconnection if enabled and not manually disconnected if (this.config.autoReconnect && code !== 1000) { this.attemptReconnect(); } } /** * Attempt reconnection with backoff */ private attemptReconnect(): void { // Prevent duplicate reconnection attempts with atomic lock if (this.reconnectionLock) { return; } this.reconnectionLock = true; if ( this.config.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.config.maxReconnectAttempts ) { this.setState(ConnectionStateEnum.FAILED); const err = new Error('Max reconnection attempts reached'); this.emit('error', err); this.reconnectionLock = false; return; } this.reconnectAttempts++; this.setState(ConnectionStateEnum.RECONNECTING); this.emit('reconnecting', this.reconnectAttempts); const delay = this.calculateReconnectDelay(); this.reconnectTimer = setTimeout(async () => { try { await this.connect(); this.reconnectionLock = false; } catch (err) { this.reconnectionLock = false; // Small delay to prevent tight recursive loop setTimeout(() => { this.attemptReconnect(); }, 100); } }, delay); } /** * Calculate adaptive reconnect delay */ private calculateReconnectDelay(): number { const baseDelay = this.config.reconnectDelay; const maxDelay = 30000; // 30 seconds // Exponential backoff with jitter const exponentialDelay = baseDelay * Math.pow(2, this.reconnectAttempts - 1); const jitter = Math.random() * 1000; // Up to 1 second jitter return Math.min(exponentialDelay + jitter, maxDelay); } /** * Clear reconnect timer */ private clearReconnectTimer(): void { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; this.reconnectionLock = false; } } /** * Set connection state and emit event */ private setState(state: ConnectionState): void { if (this.state !== state) { this.state = state; this.emit('stateChange', state); } } }