export const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"; export const PROTOCOL_SCHEMA = 1; function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } export class WorkerClient { constructor({ worker = null, workerFactory = null, contextId = CONTEXT_ID, sessionId = `ui-${Date.now().toString(36)}`, timeoutMs = 10000 } = {}) { this.contextId = contextId; this.sessionId = sessionId; this.timeoutMs = timeoutMs; this.worker = worker ?? (workerFactory ? workerFactory() : new Worker("../worker/sim-worker.mjs", { type: "module" })); this.seq = 0; this.pending = new Map(); this.completed = new Map(); this.listeners = new Set(); this.closed = false; this.worker.onmessage = (event) => this.#receive(event.data); this.worker.onerror = (event) => this.#failAll(new Error(event?.message ?? "worker_exception")); } onMessage(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } #emit(message) { for (const listener of this.listeners) listener(message); } #receive(message) { this.#emit(message); if (!isObject(message)) return; const operationId = message.operation_id ?? message.payload?.operation_id; const pending = operationId ? this.pending.get(operationId) : this.pending.get(`seq:${message.seq}`); if (!pending) return; clearTimeout(pending.timer); this.pending.delete(operationId ?? `seq:${message.seq}`); this.completed.set(operationId, message); if (message.kind === "ERROR" || message.payload?.status === "error" || message.payload?.status === "blocked") { const error = new Error(message.payload?.message_key ?? message.payload?.code ?? "worker_error"); error.response = message; pending.reject(error); } else pending.resolve(message); } #failAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); } this.pending.clear(); } send(kind, payload = {}, { operationId = null, timeoutMs = this.timeoutMs } = {}) { if (this.closed) return Promise.reject(new Error("worker_client_closed")); if (operationId && this.completed.has(operationId)) return Promise.resolve(this.completed.get(operationId)); if (operationId && this.pending.has(operationId)) return this.pending.get(operationId).promise; const seq = ++this.seq; const message = { schema_version: PROTOCOL_SCHEMA, context_id: this.contextId, session_id: this.sessionId, seq, kind, ...(operationId ? { operation_id: operationId } : {}), payload, }; let resolvePromise; let rejectPromise; const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; }); const timer = setTimeout(() => { this.pending.delete(operationId); const error = new Error("worker_timeout"); error.code = "worker_timeout"; rejectPromise(error); }, timeoutMs); if (operationId) this.pending.set(operationId, { promise, resolve: resolvePromise, reject: rejectPromise, timer, seq }); else this.pending.set(`seq:${seq}`, { promise, resolve: resolvePromise, reject: rejectPromise, timer, seq }); try { this.worker.postMessage(message); } catch (error) { clearTimeout(timer); this.pending.delete(operationId ?? `seq:${seq}`); rejectPromise(error); } return promise; } hello() { return this.send("HELLO", {}); } attach(operationId = "session-attach") { return this.send("SESSION_ATTACH", {}, { operationId }); } loadWorkspace(rootPath = "/workspace", configuration = undefined, hal = undefined, operationId = "workspace-load") { return this.send("LOAD_WORKSPACE", { root_path: rootPath, ...(configuration ? { configuration } : {}), ...(hal ? { hal } : {}) }, { operationId }); } step(cycles = 1, operationId = `step-${this.seq + 1}`) { return this.send("SIM_STEP", { cycles }, { operationId }); } snapshot(operationId = `snapshot-${this.seq + 1}`) { return this.send("SNAPSHOT_REQUEST", {}, { operationId }); } checkpoint(operationId = `checkpoint-${this.seq + 1}`) { return this.send("CHECKPOINT", {}, { operationId }); } close() { if (this.closed) return; this.closed = true; this.#failAll(new Error("worker_client_closed")); this.worker.terminate?.(); } }