diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md index c682432..6d3fc9e 100644 --- a/wasm-port/runtime/sdk/README.md +++ b/wasm-port/runtime/sdk/README.md @@ -573,6 +573,11 @@ private module paths: process port, persisting its transcript and DB flat file through the OPFS tool DB store, and returning diagnostics. The helper is browser-safe glue; runtime semantics still come from the supplied port adapter. +- `createLinuxCncToolDbBrowserWorkerAdapter()` for the browser Worker transport + boundary. The bundled worker reports + `blocked_browser_python_wasm_runtime_missing` until a real Python/WASM runtime + is installed; the Worker shell alone must not mark `DB_PROGRAM` execution + ready. - `createLinuxCncToolDbNodeRuntimeAdapter()` in `runtime/sdk/src/tool-db-node-runtime-adapter.js` for Node/native verification of the real LinuxCNC `DB_PROGRAM` child process. This adapter is intentionally diff --git a/wasm-port/runtime/sdk/src/index.js b/wasm-port/runtime/sdk/src/index.js index 809702e..01aa9e0 100644 --- a/wasm-port/runtime/sdk/src/index.js +++ b/wasm-port/runtime/sdk/src/index.js @@ -99,6 +99,9 @@ export { createToolDbTransactionPlan, validateToolDbTranscript, } from "./tool-db-process-port.js"; +export { + createLinuxCncToolDbBrowserWorkerAdapter, +} from "./tool-db-browser-worker-adapter.js"; export { runToolDbProcessPortPersistenceSession, } from "./tool-db-runtime-session.js"; diff --git a/wasm-port/runtime/sdk/src/tool-db-browser-worker-adapter.js b/wasm-port/runtime/sdk/src/tool-db-browser-worker-adapter.js new file mode 100644 index 0000000..f0bebd0 --- /dev/null +++ b/wasm-port/runtime/sdk/src/tool-db-browser-worker-adapter.js @@ -0,0 +1,105 @@ +function requireWorkerUrl(value) { + if (typeof value === "string" && value.length > 0) { + return value; + } + if (value instanceof URL) { + return value; + } + throw new Error("workerUrl must be a non-empty string or URL."); +} + +function makeRequestId() { + return `tool-db-worker-${Date.now()}-${Math.random().toString(16).slice(2)}`; +} + +export function createLinuxCncToolDbBrowserWorkerAdapter({ + workerUrl, + workerFactory = (url) => new Worker(url, { type: "module" }), + timeoutMs = 5000, +} = {}) { + const normalizedWorkerUrl = requireWorkerUrl(workerUrl); + let worker = null; + const pending = new Map(); + let runtimeExecutionReady = false; + let startStatus = "blocked_browser_python_wasm_runtime_missing"; + + function rejectPending(error) { + for (const { reject, timer } of pending.values()) { + clearTimeout(timer); + reject(error); + } + pending.clear(); + } + + function request(type, payload = {}) { + if (!worker) { + return Promise.reject(new Error("Tool DB browser worker is not started.")); + } + const id = makeRequestId(); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + pending.delete(id); + reject(new Error(`timeout waiting for Tool DB browser worker ${type}.`)); + }, timeoutMs); + pending.set(id, { resolve, reject, timer }); + worker.postMessage({ id, type, ...payload }); + }); + } + + function handleMessage(event) { + const message = event.data ?? {}; + const waiter = pending.get(message.id); + if (!waiter) { + return; + } + pending.delete(message.id); + clearTimeout(waiter.timer); + if (message.ok === false) { + waiter.reject(new Error(message.error ?? `Tool DB browser worker ${message.type ?? "request"} failed.`)); + return; + } + waiter.resolve(message); + } + + return { + runtimeMode: "browser-python-wasm-worker", + + async start() { + worker = workerFactory(normalizedWorkerUrl); + worker.addEventListener("message", handleMessage); + worker.addEventListener("error", (event) => { + rejectPending(new Error(event.message ?? "Tool DB browser worker error.")); + }); + const result = await request("start"); + runtimeExecutionReady = result.runtimeExecutionReady === true; + startStatus = result.status ?? (runtimeExecutionReady + ? "browser_python_wasm_runtime_ready" + : "blocked_browser_python_wasm_runtime_missing"); + return { + status: startStatus, + runtimeExecutionReady, + }; + }, + + async writeLine(line) { + if (runtimeExecutionReady !== true) { + throw new Error(`${startStatus}: browser Tool DB runtime is not execution-ready.`); + } + await request("writeLine", { line }); + }, + + async readLine() { + if (runtimeExecutionReady !== true) { + throw new Error(`${startStatus}: browser Tool DB runtime is not execution-ready.`); + } + const result = await request("readLine"); + return result.line; + }, + + close() { + rejectPending(new Error("Tool DB browser worker closed.")); + worker?.terminate(); + worker = null; + }, + }; +} diff --git a/wasm-port/runtime/sdk/src/tool-db-node-runtime-adapter.js b/wasm-port/runtime/sdk/src/tool-db-node-runtime-adapter.js index 2d5303c..aabea91 100644 --- a/wasm-port/runtime/sdk/src/tool-db-node-runtime-adapter.js +++ b/wasm-port/runtime/sdk/src/tool-db-node-runtime-adapter.js @@ -103,6 +103,10 @@ export function createLinuxCncToolDbNodeRuntimeAdapter({ failWaiters(new Error(`DB_PROGRAM exited with code ${code}.`)); } }); + return { + status: "node_linuxcnc_db_program_started", + runtimeExecutionReady: true, + }; }, async writeLine(line) { diff --git a/wasm-port/runtime/sdk/src/tool-db-process-port.js b/wasm-port/runtime/sdk/src/tool-db-process-port.js index 4b8fafb..4e53dab 100644 --- a/wasm-port/runtime/sdk/src/tool-db-process-port.js +++ b/wasm-port/runtime/sdk/src/tool-db-process-port.js @@ -140,6 +140,8 @@ export function createLinuxCncToolDbProcessPort({ const transcript = []; let started = false; let closed = false; + let runtimeExecutionReady = false; + let startStatus = null; function assertOpen() { if (closed) { @@ -156,12 +158,17 @@ export function createLinuxCncToolDbProcessPort({ async start() { assertOpen(); started = true; + let adapterStartResult = {}; if (runtimeAdapter?.start) { - await runtimeAdapter.start(); + adapterStartResult = await runtimeAdapter.start(); } + runtimeExecutionReady = adapterStartResult.runtimeExecutionReady === true || + (Boolean(runtimeAdapter) && adapterStartResult.runtimeExecutionReady !== false && !adapterStartResult.status); + startStatus = adapterStartResult.status ?? (runtimeExecutionReady ? "runtime_adapter_started" : null); return { runtimeMode, - runtimeExecutionReady: Boolean(runtimeAdapter), + runtimeExecutionReady, + status: startStatus, executionEnabled: false, promotionAllowed: false, }; @@ -194,9 +201,9 @@ export function createLinuxCncToolDbProcessPort({ async runTransactionPlan(plan = createToolDbTransactionPlan()) { assertOpen(); - if (!runtimeAdapter) { + if (!runtimeAdapter || runtimeExecutionReady !== true) { return { - status: "blocked_runtime_adapter_required", + status: runtimeAdapter ? (startStatus ?? "blocked_runtime_execution_not_ready") : "blocked_runtime_adapter_required", plan, runtimeExecutionReady: false, executionEnabled: false, diff --git a/wasm-port/runtime/workers/tool-db-python-worker.js b/wasm-port/runtime/workers/tool-db-python-worker.js new file mode 100644 index 0000000..5e12229 --- /dev/null +++ b/wasm-port/runtime/workers/tool-db-python-worker.js @@ -0,0 +1,39 @@ +function postReply(id, type, payload) { + self.postMessage({ id, type, ok: true, ...payload }); +} + +function postError(id, type, error) { + self.postMessage({ + id, + type, + ok: false, + error: error?.message ?? String(error), + }); +} + +function pythonRuntimeAvailable() { + return typeof self.loadPyodide === "function" || Boolean(self.pyodide?.runPythonAsync); +} + +self.addEventListener("message", async (event) => { + const message = event.data ?? {}; + const { id, type } = message; + try { + if (type === "start") { + const ready = pythonRuntimeAvailable(); + postReply(id, type, { + runtimeMode: "browser-python-wasm-worker", + runtimeExecutionReady: ready, + status: ready + ? "browser_python_wasm_runtime_ready" + : "blocked_browser_python_wasm_runtime_missing", + executionEnabled: false, + promotionAllowed: false, + }); + return; + } + throw new Error("blocked_browser_python_wasm_runtime_missing"); + } catch (error) { + postError(id, type, error); + } +}); diff --git a/wasm-port/tests/browser/tool_db_process_browser_smoke.html b/wasm-port/tests/browser/tool_db_process_browser_smoke.html index 70578f4..11150d3 100644 --- a/wasm-port/tests/browser/tool_db_process_browser_smoke.html +++ b/wasm-port/tests/browser/tool_db_process_browser_smoke.html @@ -9,6 +9,7 @@