import { type ChildProcess, spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { Worker } from "node:worker_threads"; import type { KernelToHostMessage } from "../../bridge/protocol.ts"; import type { WorkerLike } from "./inline-worker.ts"; import type { JavaScriptKernelMode } from "./kernel-contract.ts"; export class WorkerStartupCancelledError extends Error { readonly name = "WorkerStartupCancelledError"; constructor(options?: { cause?: unknown }) { super("JavaScript worker startup was cancelled", options); } } export class JavaScriptWorkerExitedError extends Error { readonly name = "JavaScriptWorkerExitedError"; readonly exitCode: number; constructor(exitCode: number) { super(`JavaScript worker exited with code ${exitCode}`); this.exitCode = exitCode; } } export function spawnNodeWorker( url: URL, cwd: string, parallelPoolWidth: number, mode: JavaScriptKernelMode = "worker", hardenedCells = false ): WorkerLike { return wrapNodeWorker( new Worker(url, { workerData: { cwd, parallelPoolWidth, hardenedCells }, }), mode ); } /** * Spawns the JS kernel as a `--jitless` child process. V8 flags are fixed at * process start, so a worker thread cannot opt out of the JIT; only a child * process can. The child reads its configuration from environment variables * and speaks the same bridge protocol over the IPC channel. */ export function spawnJitlessWorker( cwd: string, parallelPoolWidth: number, hardenedCells = false ): WorkerLike { const child = spawn( process.execPath, [ "--jitless", fileURLToPath(new URL("./process-entry.mjs", import.meta.url)), ], { env: { ...process.env, PI_CODEMODE_JS_CWD: cwd, PI_CODEMODE_JS_POOL: String(parallelPoolWidth), PI_CODEMODE_JS_HARDENED: hardenedCells ? "1" : "0", }, stdio: ["inherit", "inherit", "inherit", "ipc"], } ); return wrapChildProcess(child); } export function waitForReady( worker: WorkerLike, signal: AbortSignal ): Promise { return new Promise((resolve, reject) => { let offMessage = (): void => {}; let offError = (): void => {}; const cleanup = (): void => { offMessage(); offError(); signal.removeEventListener("abort", onAbort); }; const onAbort = (): void => { cleanup(); reject(new WorkerStartupCancelledError()); }; offMessage = worker.onMessage((message) => { if (message.type === "ready") { cleanup(); resolve(); } else if (message.type === "init-failed") { cleanup(); reject(errorFromBridge(message.error)); } }); offError = worker.onError((error) => { cleanup(); reject(error); }); if (signal.aborted) { onAbort(); } else { signal.addEventListener("abort", onAbort, { once: true }); } }); } export function errorFromBridge(error: { readonly message: string; readonly name?: string; readonly stack?: string; }): Error { const result = new Error(error.message); if (error.name) { result.name = error.name; } if (error.stack) { result.stack = error.stack; } return result; } export function bridgeError(error: Error): { readonly message: string; readonly name?: string; readonly stack?: string; } { return { message: error.message, name: error.name, stack: error.stack }; } function wrapNodeWorker( worker: Worker, mode: JavaScriptKernelMode ): WorkerLike { return { mode, postMessage: (message) => worker.postMessage(message), onMessage(handler) { const listener = (message: KernelToHostMessage): void => handler(message); worker.on("message", listener); return () => worker.off("message", listener); }, onError(handler) { let reported = false; const report = (error: Error): void => { if (reported) { return; } reported = true; handler(error); }; const onError = (error: Error): void => report(error); const onExit = (code: number): void => report(new JavaScriptWorkerExitedError(code)); worker.on("error", onError); worker.on("exit", onExit); return () => { worker.off("error", onError); worker.off("exit", onExit); }; }, async terminate() { await worker.terminate(); }, }; } // Adapter that presents a `--jitless` child process as the same WorkerLike // contract: postMessage/onMessage over the IPC channel, error and exit // reporting mirroring wrapNodeWorker, and terminate() killing the child. function wrapChildProcess(child: ChildProcess): WorkerLike { return { mode: "process", postMessage: (message) => { child.send(message); }, onMessage(handler) { const listener = (message: KernelToHostMessage): void => handler(message); child.on("message", listener); return () => child.off("message", listener); }, onError(handler) { let reported = false; const report = (error: Error): void => { if (reported) { return; } reported = true; handler(error); }; const onError = (error: Error): void => report(error); const onExit = (code: number): void => report(new JavaScriptWorkerExitedError(code)); child.on("error", onError); child.on("exit", onExit); return () => { child.off("error", onError); child.off("exit", onExit); }; }, async terminate() { if (child.exitCode !== null || child.signalCode !== null) { return; } // Listen before killing: the child can only emit "exit" on a later // macrotask, so attaching the listener first cannot miss it. const exited = new Promise((resolve) => child.once("exit", () => resolve()) ); child.kill(); await exited; }, }; }