Share incremental replacement UTF-8 decoding across web and JavaScript workers, bound content before complete-line allocation and handle split CRLF consistently. Keep named schemas, request versus checkpoint failures, worker reuse and stream cleanup authority in their servers. Cover real server recovery and deterministic byte boundaries.
227 lines
7.0 KiB
TypeScript
227 lines
7.0 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { createHash, randomUUID } from "node:crypto";
|
|
import { once } from "node:events";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createInterface } from "node:readline";
|
|
import { expect, it } from "vitest";
|
|
import {
|
|
JAVASCRIPT_ANALYSIS_WORKER_DEFAULTS,
|
|
javaScriptWorkerAdmissionSchema,
|
|
javaScriptWorkerMessageSchema,
|
|
javaScriptWorkerRequestSchema,
|
|
} from "./JavaScriptAnalysisWorkerProtocol.js";
|
|
|
|
const source = "export const observed = 1;";
|
|
const digest = createHash("sha256").update(source).digest("hex");
|
|
const frame = (value: unknown): string => `${JSON.stringify(value)}\n`;
|
|
const admission = (id: string) =>
|
|
javaScriptWorkerAdmissionSchema.parse({
|
|
id,
|
|
semantic_result_budget_bytes: 0,
|
|
});
|
|
|
|
const startServer = async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-js-server-framing-"));
|
|
const serverUrl = new URL(
|
|
"./JavaScriptAnalysisWorkerServer.ts",
|
|
import.meta.url,
|
|
).href;
|
|
const analyzerUrl = new URL(
|
|
"../../domain/javascript/javascriptStaticAnalysis.ts",
|
|
import.meta.url,
|
|
).href;
|
|
const child = spawn(
|
|
process.execPath,
|
|
[
|
|
"--import",
|
|
import.meta.resolve("tsx"),
|
|
"--input-type=module",
|
|
"-e",
|
|
`const { serveJavaScriptAnalysisWorker } = await import(${JSON.stringify(serverUrl)});
|
|
const { analyzeJavaScriptStaticSource } = await import(${JSON.stringify(analyzerUrl)});
|
|
await serveJavaScriptAnalysisWorker(async (file, capacity, callbacks) => {
|
|
await callbacks.checkpoint(analyzeJavaScriptStaticSource(file.text.value));
|
|
return null;
|
|
});`,
|
|
],
|
|
{ cwd: root, stdio: ["pipe", "pipe", "pipe"] },
|
|
);
|
|
const closed = once(child, "close");
|
|
let stderr = "";
|
|
child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
|
|
stderr += chunk;
|
|
});
|
|
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
const messages = lines[Symbol.asyncIterator]();
|
|
return {
|
|
child,
|
|
closed,
|
|
stderr: () => stderr,
|
|
next: async () => {
|
|
const next = await messages.next();
|
|
if (next.done) throw new Error(`Worker output ended: ${stderr}`);
|
|
return javaScriptWorkerMessageSchema.parse(JSON.parse(next.value));
|
|
},
|
|
request: async () => {
|
|
const id = randomUUID();
|
|
await writeFile(join(root, `${id}.source`), source, { flag: "wx" });
|
|
return javaScriptWorkerRequestSchema.parse({
|
|
id,
|
|
unknown_budget: 100,
|
|
node_budget: 100,
|
|
text_sha256: digest,
|
|
file: {
|
|
path: "app.mjs",
|
|
sha256: digest,
|
|
container_sha256: digest,
|
|
bytes: Buffer.byteLength(source),
|
|
inventory_artifact_id: "server-framing-source",
|
|
kind: "javascript",
|
|
unpacked: false,
|
|
},
|
|
});
|
|
},
|
|
close: async () => {
|
|
if (child.exitCode === null && child.signalCode === null) child.kill();
|
|
await closed;
|
|
lines.close();
|
|
await rm(root, { recursive: true, force: true });
|
|
},
|
|
};
|
|
};
|
|
|
|
it("consumes fragmented CRLF admission and coalesced next-job frames in order", async () => {
|
|
const server = await startServer();
|
|
try {
|
|
expect(await server.next()).toMatchObject({ kind: "ready" });
|
|
const first = await server.request();
|
|
const second = await server.request();
|
|
server.child.stdin.write(frame(first));
|
|
expect(await server.next()).toMatchObject({ kind: "static", id: first.id });
|
|
const encoded = JSON.stringify(admission(first.id));
|
|
server.child.stdin.write(encoded.slice(0, 12));
|
|
server.child.stdin.write(`${encoded.slice(12)}\r`);
|
|
server.child.stdin.write(
|
|
`\n${frame(second)}${frame(admission(second.id))}`,
|
|
);
|
|
expect(await server.next()).toMatchObject({
|
|
kind: "phase",
|
|
id: first.id,
|
|
phase: "transfer",
|
|
});
|
|
expect(await server.next()).toEqual({
|
|
kind: "complete",
|
|
id: first.id,
|
|
descriptor: null,
|
|
});
|
|
expect(await server.next()).toMatchObject({
|
|
kind: "static",
|
|
id: second.id,
|
|
});
|
|
expect(await server.next()).toMatchObject({
|
|
kind: "phase",
|
|
id: second.id,
|
|
phase: "transfer",
|
|
});
|
|
expect(await server.next()).toEqual({
|
|
kind: "complete",
|
|
id: second.id,
|
|
descriptor: null,
|
|
});
|
|
server.child.stdin.end();
|
|
expect((await server.closed)[0]).toBe(0);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("drains oversized checkpoint admission, reports output failure, and serves the next job", async () => {
|
|
const server = await startServer();
|
|
try {
|
|
await server.next();
|
|
const first = await server.request();
|
|
const second = await server.request();
|
|
server.child.stdin.write(frame(first));
|
|
expect(await server.next()).toMatchObject({ kind: "static", id: first.id });
|
|
server.child.stdin.write(
|
|
"x".repeat(JAVASCRIPT_ANALYSIS_WORKER_DEFAULTS.protocolBytes),
|
|
);
|
|
server.child.stdin.write(
|
|
`x\n${frame(second)}${frame(admission(second.id))}`,
|
|
);
|
|
expect(await server.next()).toEqual({
|
|
kind: "failure",
|
|
id: first.id,
|
|
reason: "output",
|
|
resource: null,
|
|
limits: null,
|
|
message: "JavaScript worker admission frame is missing or oversized",
|
|
});
|
|
expect(await server.next()).toMatchObject({
|
|
kind: "static",
|
|
id: second.id,
|
|
});
|
|
expect(await server.next()).toMatchObject({
|
|
kind: "phase",
|
|
id: second.id,
|
|
phase: "transfer",
|
|
});
|
|
expect(await server.next()).toEqual({
|
|
kind: "complete",
|
|
id: second.id,
|
|
descriptor: null,
|
|
});
|
|
server.child.stdin.end();
|
|
expect((await server.closed)[0]).toBe(0);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("reports missing checkpoint admission as output failure at stdin EOF", async () => {
|
|
const server = await startServer();
|
|
try {
|
|
await server.next();
|
|
const request = await server.request();
|
|
server.child.stdin.write(frame(request));
|
|
expect(await server.next()).toMatchObject({
|
|
kind: "static",
|
|
id: request.id,
|
|
});
|
|
server.child.stdin.end();
|
|
expect(await server.next()).toMatchObject({
|
|
kind: "failure",
|
|
id: request.id,
|
|
reason: "output",
|
|
resource: null,
|
|
message: "JavaScript worker admission frame is missing or oversized",
|
|
});
|
|
expect((await server.closed)[0]).toBe(0);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it.each(["lf", "eof", "open"] as const)(
|
|
"rejects oversized initial requests with %s input",
|
|
async (boundary) => {
|
|
const server = await startServer();
|
|
try {
|
|
await server.next();
|
|
const oversized = "x".repeat(
|
|
JAVASCRIPT_ANALYSIS_WORKER_DEFAULTS.protocolBytes + 1,
|
|
);
|
|
if (boundary === "open") server.child.stdin.write(oversized);
|
|
else server.child.stdin.end(oversized + (boundary === "lf" ? "\n" : ""));
|
|
expect((await server.closed)[0]).not.toBe(0);
|
|
expect(server.stderr()).toContain(
|
|
"RangeError: JavaScript worker request exceeds its byte budget",
|
|
);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
},
|
|
);
|