ws.ts (2319B)
1 export interface WsMessage { 2 type: string; 3 data?: unknown; 4 } 5 6 interface WsClientOptions { 7 maxReconnectAttempts?: number; 8 baseReconnectDelay?: number; 9 maxReconnectDelay?: number; 10 } 11 12 export class WsClient { 13 private ws: WebSocket | null = null; 14 private handlers: ((msg: WsMessage) => void)[] = []; 15 private reconnectAttempts = 0; 16 private reconnectTimer: ReturnType<typeof setTimeout> | null = null; 17 private destroyed = false; 18 private maxReconnectAttempts: number; 19 private baseReconnectDelay: number; 20 private maxReconnectDelay: number; 21 22 constructor( 23 private url: string, 24 options?: WsClientOptions, 25 ) { 26 this.maxReconnectAttempts = options?.maxReconnectAttempts ?? 10; 27 this.baseReconnectDelay = options?.baseReconnectDelay ?? 1000; 28 this.maxReconnectDelay = options?.maxReconnectDelay ?? 30000; 29 } 30 31 connect() { 32 this.destroyed = false; 33 this.ws = new WebSocket(this.url); 34 35 this.ws.onopen = () => { 36 this.reconnectAttempts = 0; 37 }; 38 39 this.ws.onmessage = (event: MessageEvent) => { 40 try { 41 const msg = JSON.parse(event.data) as WsMessage; 42 for (const handler of this.handlers) { 43 handler(msg); 44 } 45 } catch { 46 // Ignore malformed messages 47 } 48 }; 49 50 this.ws.onclose = () => { 51 if (!this.destroyed) { 52 this.scheduleReconnect(); 53 } 54 }; 55 56 this.ws.onerror = () => { 57 // onclose will fire after onerror 58 }; 59 } 60 61 onMessage(handler: (msg: WsMessage) => void) { 62 this.handlers.push(handler); 63 } 64 65 send(type: string, data?: unknown) { 66 if (this.ws !== null && this.ws.readyState === 1) { 67 this.ws.send(JSON.stringify({ type, data })); 68 } 69 } 70 71 destroy() { 72 this.destroyed = true; 73 if (this.reconnectTimer !== null) { 74 clearTimeout(this.reconnectTimer); 75 this.reconnectTimer = null; 76 } 77 this.ws?.close(); 78 this.ws = null; 79 } 80 81 private scheduleReconnect() { 82 if (this.reconnectAttempts >= this.maxReconnectAttempts) { 83 return; 84 } 85 86 const delay = Math.min( 87 this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts), 88 this.maxReconnectDelay, 89 ); 90 this.reconnectAttempts++; 91 92 this.reconnectTimer = setTimeout(() => { 93 if (!this.destroyed) { 94 this.connect(); 95 } 96 }, delay); 97 } 98 }