接续L4-TOOL-DB浏览器Worker边界

结论:浏览器 Tool DB Worker 传输层已接入并明确阻塞缺失的 Python/WASM runtime,未冒充 DB_PROGRAM 执行就绪。
This commit is contained in:
2026-06-19 07:09:25 +08:00
parent d323511f8b
commit 63870fd6d7
10 changed files with 248 additions and 4 deletions

View File

@@ -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

View File

@@ -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";

View File

@@ -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;
},
};
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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);
}
});