theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }