import type { ServerMessage } from "./types"; /** * Client command types sent to the monitor server. */ type ClientCommand = | { readonly cmd: "load"; readonly source: string } | { readonly cmd: "load_file"; readonly path: string } | { readonly cmd: "step_tick" } | { readonly cmd: "step_event" } | { readonly cmd: "run_until"; readonly until: number } | { readonly cmd: "send"; readonly target: number; readonly offset: number; readonly ctx: number; readonly data: number } | { readonly cmd: "inject"; readonly target: number; readonly offset: number; readonly ctx: number; readonly data: number } | { readonly cmd: "reset" } | { readonly cmd: "reset"; readonly reload: true }; /** * Options for creating a WebSocket connection. */ type ConnectionOptions = { readonly url: string; readonly onMessage: (message: ServerMessage) => void; readonly onConnect: () => void; readonly onDisconnect: () => void; }; /** * Monitor connection interface. */ type MonitorConnection = { readonly send: (cmd: ClientCommand) => void; readonly close: () => void; readonly isConnected: () => boolean; }; /** * Create a WebSocket connection to the monitor server with auto-reconnect. */ function createConnection(options: ConnectionOptions): MonitorConnection { let ws: WebSocket | null = null; let reconnectDelay = 1000; // Start at 1 second const maxReconnectDelay = 30000; // Cap at 30 seconds let reconnectTimeout: ReturnType | null = null; let manualClose = false; /** * Attempt to establish a WebSocket connection. */ function connect(): void { if (ws !== null && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) { return; // Already connected or connecting } try { ws = new WebSocket(options.url); ws.addEventListener("open", () => { console.log("[MonitorConnection] Connected"); reconnectDelay = 1000; // Reset backoff on successful connect options.onConnect(); }); ws.addEventListener("message", (event) => { try { const message: ServerMessage = JSON.parse(event.data); options.onMessage(message); } catch (err) { console.error("[MonitorConnection] Failed to parse message:", err); } }); ws.addEventListener("close", () => { console.log("[MonitorConnection] Disconnected"); options.onDisconnect(); if (!manualClose) { // Auto-reconnect with exponential backoff reconnectTimeout = setTimeout(() => { console.log(`[MonitorConnection] Reconnecting in ${reconnectDelay}ms...`); reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay); connect(); }, reconnectDelay); } }); ws.addEventListener("error", (event) => { console.error("[MonitorConnection] WebSocket error:", event); }); } catch (err) { console.error("[MonitorConnection] Failed to create WebSocket:", err); if (!manualClose) { reconnectTimeout = setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay); connect(); }, reconnectDelay); } } } // Establish initial connection connect(); /** * Send a command to the server. */ function send(cmd: ClientCommand): void { if (ws === null || ws.readyState !== WebSocket.OPEN) { console.warn("[MonitorConnection] Not connected; command will be dropped:", cmd); return; } try { ws.send(JSON.stringify(cmd)); } catch (err) { console.error("[MonitorConnection] Failed to send command:", err); } } /** * Close the connection and prevent reconnection. */ function close(): void { manualClose = true; if (reconnectTimeout !== null) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } if (ws !== null) { ws.close(); ws = null; } } /** * Check if the connection is currently open. */ function isConnected(): boolean { return ws !== null && ws.readyState === WebSocket.OPEN; } return { send, close, isConnected, }; } export type { ClientCommand, ConnectionOptions, MonitorConnection, }; export { createConnection, };