// Optional WebSocket producer — pipes external JSON commands into the bus. // Server -> client message shapes: // { id?: string, command: Command } // { id?: string, script: Command[] } // { id?: string, dsl: string } // Client -> server replies: { id, ok: true } | { id, ok: false, error: string } import { commandBus, type Command } from "./bus" import { runScriptText } from "./script" type ServerMessage = | { id?: string; command: Command } | { id?: string; script: Command[] } | { id?: string; dsl: string } export type CommandSocket = { close: () => void socket: () => WebSocket | null } export function connectCommandSocket( url: string, opts: { reconnect?: boolean; reconnectDelayMs?: number } = {}, ): CommandSocket { const { reconnect = true, reconnectDelayMs = 2000 } = opts let ws: WebSocket | null = null let closed = false const open = () => { ws = new WebSocket(url) ws.addEventListener("open", () => { console.log("[command-ws] connected", url) }) ws.addEventListener("message", async (event) => { let msg: ServerMessage try { msg = JSON.parse(event.data) } catch (err) { ws?.send(JSON.stringify({ error: "invalid_json", detail: String(err) })) return } try { if ("dsl" in msg) await runScriptText(msg.dsl) else if ("script" in msg) await commandBus.run(msg.script) else if ("command" in msg) await commandBus.dispatch(msg.command) else throw new Error("missing 'command', 'script', or 'dsl'") ws?.send(JSON.stringify({ id: msg.id, ok: true })) } catch (err) { ws?.send(JSON.stringify({ id: msg.id, ok: false, error: String(err) })) } }) ws.addEventListener("close", () => { if (closed || !reconnect) return setTimeout(open, reconnectDelayMs) }) ws.addEventListener("error", () => ws?.close()) } open() return { close: () => { closed = true ws?.close() }, socket: () => ws, } }