107 lines
5.1 KiB
JavaScript
107 lines
5.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import vm from "node:vm";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const registrySource = fs.readFileSync(new URL("../src/module-registry.generated.ts", import.meta.url), "utf8");
|
|
const registryAst = ts.createSourceFile("registry.ts", registrySource, ts.ScriptTarget.Latest, true);
|
|
const routes = new Map();
|
|
const mapping = registryAst.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === "getWebModuleWorkerForCommand");
|
|
const inspect = node => {
|
|
if (ts.isCaseClause(node) && ts.isStringLiteral(node.expression)) {
|
|
const result = node.statements.find(ts.isReturnStatement)?.expression;
|
|
if (result && ts.isStringLiteral(result)) routes.set(node.expression.text, result.text);
|
|
}
|
|
ts.forEachChild(node, inspect);
|
|
};
|
|
inspect(mapping);
|
|
|
|
function fixture() {
|
|
const workers = [];
|
|
const route = { module: "TechDraw", primaryWorker: "techdraw-native.worker.ts", commands: [
|
|
"techdraw.createRecompute", "techdraw.desktopCommand", "techdraw.nativeSaveReopen",
|
|
] };
|
|
const registry = {
|
|
getWebModuleRoute: module => module === "TechDraw" ? route : undefined,
|
|
getWebModuleWorkerForCommand: (module, command) => routes.get(`${module}:${command}`) ?? route.primaryWorker,
|
|
createWebModuleWorker: (_module, workerFile) => {
|
|
const worker = { workerFile, sent: [], terminated: 0,
|
|
postMessage(message) { this.sent.push(message); },
|
|
terminate() { this.terminated += 1; },
|
|
emit(data) { this.onmessage({ data }); },
|
|
};
|
|
workers.push(worker);
|
|
return worker;
|
|
},
|
|
};
|
|
const cache = new Map([
|
|
["./module-registry.generated.js", registry],
|
|
["./module-loader.js", {}],
|
|
["./native-source-policy.js", { nativeSourceDecision: () => undefined }],
|
|
]);
|
|
function load(name) {
|
|
if (cache.has(name)) return cache.get(name);
|
|
const source = fs.readFileSync(new URL(`../src/${name.replace(/^\.\//, "").replace(/\.js$/, ".ts")}`, import.meta.url), "utf8");
|
|
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText;
|
|
const module = { exports: {} };
|
|
vm.runInNewContext("(function(exports,require,module){" + compiled + "})", { AbortController })(module.exports, load, module);
|
|
cache.set(name, module.exports);
|
|
return module.exports;
|
|
}
|
|
return { router: new (load("./module-router.js").WebModuleRouter)(), workers };
|
|
}
|
|
|
|
test("drawing, desktop commands and save use one existing native document worker", async () => {
|
|
const { router, workers } = fixture();
|
|
try {
|
|
for (const command of ["techdraw.createRecompute", "techdraw.desktopCommand", "techdraw.nativeSaveReopen"]) {
|
|
assert.equal(routes.get(`TechDraw:${command}`), "techdraw-python.worker.ts");
|
|
const result = router.request("TechDraw", command, { documentId: "CurrentDrawing" });
|
|
if (workers.length === 1 && workers[0].sent.length === 0) workers[0].emit({ kind: "ready" });
|
|
await Promise.resolve();
|
|
const request = workers[0].sent.at(-1);
|
|
workers[0].emit({ kind: "result", requestId: request.requestId, result: { documentId: "CurrentDrawing" } });
|
|
assert.equal((await result).result.documentId, "CurrentDrawing");
|
|
}
|
|
assert.equal(workers.length, 1);
|
|
assert.equal(workers[0].workerFile, "techdraw-python.worker.ts");
|
|
} finally { router.dispose(); }
|
|
});
|
|
|
|
test("uncorrelated worker errors reject pending commands instead of waiting forever", async () => {
|
|
const { router, workers } = fixture();
|
|
const result = router.request("TechDraw", "techdraw.createRecompute", {});
|
|
workers[0].emit({ kind: "ready" });
|
|
await Promise.resolve();
|
|
const rejected = assert.rejects(result, /invalid_request: wrong envelope/);
|
|
workers[0].emit({ kind: "error", requestId: "invalid", error: { code: "invalid_request", message: "wrong envelope" } });
|
|
await rejected;
|
|
assert.equal(router.listLoaded().length, 0);
|
|
assert.equal(workers[0].terminated, 1);
|
|
});
|
|
|
|
test("initialization failure rejects readiness and permits a fresh worker", async () => {
|
|
const { router, workers } = fixture();
|
|
const first = router.preload("TechDraw", "techdraw-python.worker.ts");
|
|
const rejected = assert.rejects(first, /kernel_load_failed: missing wasm/);
|
|
workers[0].emit({ kind: "error", requestId: "initialization", error: { code: "kernel_load_failed", message: "missing wasm" } });
|
|
await rejected;
|
|
const retry = router.preload("TechDraw", "techdraw-python.worker.ts");
|
|
assert.equal(workers.length, 2);
|
|
workers[1].emit({ kind: "ready" });
|
|
await retry;
|
|
router.dispose();
|
|
});
|
|
|
|
test("correlated business failure keeps the healthy worker available", async () => {
|
|
const { router, workers } = fixture();
|
|
const result = router.request("TechDraw", "techdraw.createRecompute", {});
|
|
workers[0].emit({ kind: "ready" });
|
|
await Promise.resolve();
|
|
workers[0].emit({ kind: "error", requestId: workers[0].sent[0].requestId, error: { code: "invalid_argument", message: "bad size" } });
|
|
assert.equal((await result).error.code, "invalid_argument");
|
|
assert.equal(router.listLoaded().length, 1);
|
|
router.dispose();
|
|
});
|