// Runtime worker entry: installs the syscall client, event loop, and process object, // then evaluates the entry script. Runs inside a (nested) worker spawned by the kernel. // The kernel bumps the mailbox heartbeat; the sync watchdog here only reads it. import type { FsResult, KernelToRuntime, KernelToRuntimeInit, ProcessRuntime, Res, ResolvedModule, RuntimeToKernel, SyscallSpec } from "../shared/protocol"; import { awaitSyncReply, takeSignals, writeSyncRequest, SAB_BYTES } from "../shared/mailbox"; import { builtinUrl, cjsUrl, vfsUrl, type HostToken } from "../shared/vurls"; import { toNodeError, loaderError, requireNotFound, webEntryNotEsm } from "./errors"; import { compileCjs, makeRequire, loadCjsByPath, newModule, type RequireContext, type ModuleCacheEntry } from "./modules"; import { esmBegin, esmEnd, importEsm } from "./esm-sync"; import { capture, runInFrame } from "./async_hooks"; import { loadBuiltin } from "./builtins"; import { deliverWatchEvents } from "./fs"; import { installLoop, type Loop } from "./loop"; import { createHttp, type HttpRuntime } from "./http"; import { makeProcess, signalReceiver, uncaughtCaptures, type GuestProcess } from "./process"; import { StdinStream } from "./stdin"; import { StdioWriter } from "./stdout"; import { ReadStream, WriteStream, installTty } from "./tty"; import { join } from "./resolver"; import { useNextTick } from "./ticks"; import { createTimers, type Timers } from "./timers"; import { installNodeGlobals, installWebGlobals } from "./globals"; import { holdNativePromises } from "./native-holds"; import { installConsole, makeConsole } from "./console"; import { installPorts, type MessagePort as NodePort } from "./ports"; import { makeWorkerThreads, type WorkerThreads } from "./worker_threads"; import { makeChildProcess, type ChildProcesses } from "./child_process"; let kernelPort: MessagePort; let sab: SharedArrayBuffer; let loop!: Loop; let http!: HttpRuntime; let proc!: GuestProcess; let timers!: Timers; let threads!: WorkerThreads; let children!: ChildProcesses; /** A thread's channel to its parent: its stdio and its uncaught error go there. */ let parentPort: NodePort | null = null; let takePendingSignals: () => void = () => {}; let cwdValue = "/"; /** The host page that owns this process; module URLs name it. Set by init. */ let host!: HostToken; let nextId = 1; const pending = new Map void>(); // ---- typed syscall client ---- // call() resolves a promise for runtime-internal awaits; submit() is the guest-facing // async form, whose result runs as a poll-phase loop callback, as libuv delivers it. function handleReply(res: Res): void { const settle = pending.get(res.id); if (!settle) return; pending.delete(res.id); settle(res); loop.release(); // the in-flight syscall no longer holds the loop } function send(op: keyof SyscallSpec, args: unknown, settle: (res: Res) => void): void { const id = nextId++; loop.hold(); pending.set(id, settle); kernelPort.postMessage({ t: "req", id, op, args }); } function call(op: K, args: SyscallSpec[K]["args"]): Promise { return new Promise((resolve, reject) => { send(op, args, (res) => (res.ok ? resolve(res.val as SyscallSpec[K]["ret"]) : reject(toNodeError(res.err!)))); }); } function submit( op: K, args: SyscallSpec[K]["args"], done: (result: FsResult) => void, ): void { // Captured here, synchronously in the guest's call (fs.stat(), fs.watch(), ...): // the reply arrives over kernelPort.onmessage, a native callback with no frame of // its own, so without this the fs callback would always land in the root frame. const frame = capture(); send(op, args, (res) => { const result: FsResult = res.ok ? { ok: true, val: res.val as SyscallSpec[K]["ret"] } : { ok: false, err: res.err! }; loop.enqueueCompletion(() => runInFrame(frame, done, undefined, [result])); }); } function trySync(op: K, args: SyscallSpec[K]["args"]): FsResult { writeSyncRequest(sab, op as string, args); // Doorbell: the kernel reads the real request from the SAB on receiving this message. kernelPort.postMessage({ t: "req", id: 0, op, args: null, sync: true }); return awaitSyncReply(sab); } function callSync(op: K, args: SyscallSpec[K]["args"]): SyscallSpec[K]["ret"] { const reply = trySync(op, args); if (!reply.ok) throw toNodeError(reply.err); return reply.val; } // ---- stdio capture (guest console → kernel → host streams) ---- const enc = new TextEncoder(); function emitStdout(bytes: Uint8Array): void { if (parentPort) return parentPort.send({ k: "stdio", fd: 1, bytes }); kernelPort.postMessage({ t: "stdio", stream: "stdout", bytes } satisfies RuntimeToKernel); } function emitStderr(bytes: Uint8Array): void { if (parentPort) return parentPort.send({ k: "stdio", fd: 2, bytes }); kernelPort.postMessage({ t: "stdio", stream: "stderr", bytes } satisfies RuntimeToKernel); } // ---- module context ---- const cache = new Map(); const ctx: RequireContext = { call: call as RequireContext["call"], callSync: callSync as RequireContext["callSync"], trySync: trySync as RequireContext["trySync"], submit: submit as RequireContext["submit"], cache, dirnameOf: (path) => { const i = path.lastIndexOf("/"); return i <= 0 ? "/" : path.slice(0, i); }, get cwd() { return cwdValue; }, resolvePath(p: string): string { return p.startsWith("/") ? p : join(cwdValue, p); }, get http() { return http.module; }, get timers() { return timers; }, get loop() { return loop; }, get process() { return proc; }, get workerThreads() { return threads; }, get childProcess() { return children; }, }; self.onmessage = (ev: MessageEvent): void => { const msg = ev.data; if (msg.t !== "init") return; // From here on the kernel speaks only over msg.port; the worker scope is the guest's. self.onmessage = null; kernelPort = msg.port; sab = new SharedArrayBuffer(SAB_BYTES); kernelPort.postMessage({ t: "attach", mailbox: sab } satisfies RuntimeToKernel); cwdValue = msg.cwd; host = msg.host; loop = installLoop({ beforeExit: () => proc.emit("beforeExit", proc.exitCode ?? 0), exit: () => proc.exit(), onUncaught: (e) => uncaught(e), beforePoll: () => takePendingSignals(), }); useNextTick(loop.nextTick); holdNativePromises(loop); // Errors that escape the loop (microtasks, async functions) take the same path: // an uncaughtException listener, else Node's fatal report and exit 1. Rejections // follow Node's default --unhandled-rejections=throw. self.addEventListener("error", (ev) => { ev.preventDefault(); uncaught(ev.error ?? ev.message); }); self.addEventListener("unhandledrejection", (ev) => { ev.preventDefault(); unhandledRejection(ev.reason, ev.promise); }); http = createHttp({ call, loop, respond: (id, res) => kernelPort.postMessage({ t: "httpResponse", id, res } satisfies RuntimeToKernel), closed: (id) => kernelPort.postMessage({ t: "connectionClosed", id } satisfies RuntimeToKernel), }); installPorts(loop); threads = makeWorkerThreads({ loop, init: msg, host, callSync, post: (m, transfer) => kernelPort.postMessage(m, transfer), process: () => proc, resolvePath: (p) => ctx.resolvePath(p), }); parentPort = threads.parentPort; children = makeChildProcess({ loop, callSync, post: (m) => kernelPort.postMessage(m), process: () => proc, resolvePath: (p) => ctx.resolvePath(p), }); kernelPort.onmessage = (rev: MessageEvent) => { const m = rev.data; switch (m.t) { case "res": handleReply(m); break; case "httpRequest": http.dispatch(m); break; case "connect": http.connect(m); break; case "signal": loop.interrupt(); break; case "stdin": stdin.receive(m.bytes); break; case "threadExited": threads.threadExited(m); break; case "childStdio": children.childStdio(m.pid, m.stream, m.bytes); break; case "childExited": children.childExited(m.pid, m.status); break; case "fsEvents": deliverWatchEvents(m.wid, m.events); break; } }; const emitFd = (fd: number, bytes: Uint8Array): void => (fd === 2 ? emitStderr(bytes) : emitStdout(bytes)); installTty({ loop, terminal: msg.tty !== null, callSync, write: emitFd, env: () => proc.env, emitWarning: (message, type) => proc.emitWarning(message, type), }); const stdin = msg.tty ? new ReadStream(0) : new StdinStream(loop); // A thread's stdin is empty unless asked for (worker stdin: true is not modelled). if (msg.thread && stdin instanceof StdinStream) stdin.receive(null); const [stdout, stderr] = msg.tty ? [new WriteStream(1), new WriteStream(2)] : [new StdioWriter(1, emitStdout, loop.nextTick), new StdioWriter(2, emitStderr, loop.nextTick)]; proc = makeProcess({ msg, isThread: msg.thread !== null, loop, stdin, stdout, stderr, callSync, cwd: { get: () => cwdValue, set: (dir) => (cwdValue = dir) }, emitStderr, reportFatal: (e) => emitStderr(enc.encode(stackOf(e))), exitWorker, getBuiltinModule: (id) => loadBuiltin(id, ctx), }); takePendingSignals = signalReceiver(proc, loop, () => takeSignals(sab)); // Node's getStdout/getStderr: a terminal's streams track its size through SIGWINCH. if (stdout instanceof WriteStream && stderr instanceof WriteStream) { proc.on("SIGWINCH", function refreshStdoutOnSigWinch() { stdout._refreshSize(); }); proc.on("SIGWINCH", function refreshStderrOnSigWinch() { stderr._refreshSize(); }); } timers = createTimers(loop, (message, type) => proc.emitWarning(message, type)); // Node's lib/timers.js: util.promisify(setTimeout) and util.promisify(setImmediate) are // the timers/promises forms, resolved lazily so they are the same functions. for (const name of ["setTimeout", "setImmediate"] as const) { Object.defineProperty(timers[name], Symbol.for("nodejs.util.promisify.custom"), { enumerable: true, get: () => (loadBuiltin("timers/promises", ctx) as Record)[name], }); } const setup = processRuntimes[msg.runtime]; setup.installGlobals(timers, proc); // ---- guest ESM hooks (installed before the entry runs) ---- // Facade modules served from the SW virtual origin reach back into the runtime // through this one well-known symbol. builtin()/cjs() share require()'s loader // and cache, so an ESM graph and a CJS require() of the same module agree on a // single evaluation per worker. const typelessWarned = new Set(); (globalThis as Record)[Symbol.for("nib.runtime")] = { builtin: (id: string) => loadBuiltin(id, ctx), cjs: (path: string) => loadCjsByPath(ctx, path), meta: (path: string) => ({ url: `file://${path}`, filename: path, dirname: ctx.dirnameOf(path), resolve: (specifier: string): string => moduleUrlFor( ctx.callSync("resolve", { specifier, parent: ctx.dirnameOf(path), kind: "import" }), specifier, path, ), }), // The guest npm command's way into the kernel's installer (src/npm-cli). Async, and // holding the loop while the install runs, as any pending syscall does. npm: (request: SyscallSpec["npm"]["args"]) => call("npm", request), npmLookup: (request: SyscallSpec["npmLookup"]["args"]) => call("npmLookup", request), // A module whose format syntax detection chose opens with this call (kernel // rewrite). Node warns once per package.json, so the process remembers which. typeless: (pjson: string, path: string): void => { if (typelessWarned.has(pjson)) return; typelessWarned.add(pjson); proc.emitWarning( `Module type of file://${path} is not specified and it doesn't parse as CommonJS.\n` + "Reparsing as ES module because module syntax was detected. This incurs a performance overhead.\n" + `To eliminate this warning, add "type": "module" to ${pjson}.`, { code: "MODULE_TYPELESS_PACKAGE_JSON" }, ); }, // Every guest import() lands here (kernel rewrite). The hold keeps the process // alive while the browser fetches and links the target, as Node does. importFrom: (from: string) => async (specifier: unknown): Promise => { loop.hold(); try { const spec = String(specifier); return await importEsm( from, spec, () => call("resolve", { specifier: spec, parent: ctx.dirnameOf(from), kind: "import" }), (resolved) => import(moduleUrlFor(resolved, spec, from)), ); } finally { loop.release(); } }, // Every served ES module opens and closes with these (kernel rewrite), so a // require() of a natively imported module gets that same instance. esmBegin, esmEnd, }; // Guest console → stdio: Node's own Console, bound to process.stdout/stderr, so a // patched write() sees console output and %-formatting/colours/grouping/tables all // behave like Node's (not the browser's native console). installConsole({ emitWarning: (msg) => proc.emitWarning(msg) }); console = makeConsole(proc.stdout, proc.stderr) as unknown as Console; // Node emits 'online' when the thread starts running its entry. parentPort?.send({ k: "online" }); void runEntry(msg, setup.runEntry); }; /** What differs between process runtimes (VISION D2). Everything else (the loop, stdio, * console capture, node: builtins, the exit path) is the same for both. */ interface ProcessRuntimeSetup { installGlobals(timers: Timers, proc: GuestProcess): void; /** Load and evaluate the entry, then mark the main script done. */ runEntry(msg: KernelToRuntimeInit): Promise; } const processRuntimes: Record = { node: { installGlobals: installNodeGlobals, runEntry: runNodeEntry }, web: { installGlobals: installWebGlobals, runEntry: runWebEntry }, }; /** Map a resolved module record to its virtual-origin URL — the same mapping the * kernel's transformer applies when rewriting static specifiers (shared/vurls.ts). */ function moduleUrlFor(resolved: ResolvedModule, specifier: string, from: string): string { if (resolved.kind === "builtin") return builtinUrl(host, resolved.id); if (resolved.kind === "missing") throw loaderError(resolved.code, specifier, from); if (resolved.format === "cjs") return cjsUrl(host, resolved.path); // esm (and json, wrapped) ride the vfs URL; the version query keeps the browser's // module map coherent across re-seeds. const st = callSync("stat", { path: resolved.path, follow: true }); return vfsUrl(host, resolved.path, st); } async function runEntry(msg: KernelToRuntimeInit, run: ProcessRuntimeSetup["runEntry"]): Promise { try { await run(msg); } catch (e) { // A throw from the entry is an uncaught exception like any other: listeners it // registered before throwing keep the process running. if (uncaught(e)) loop.setMainDone(); } } async function runWebEntry(msg: KernelToRuntimeInit): Promise { if (msg.thread?.eval != null) throw webEntryNotEsm("the eval source", "a CommonJS script"); const resolved = await call("resolve", { specifier: msg.entry, parent: msg.cwd, kind: "import" }); if (resolved.kind === "file" && resolved.format !== "esm") { throw webEntryNotEsm(resolved.path, resolved.format === "cjs" ? "CommonJS" : "JSON"); } await import(moduleUrlFor(resolved, msg.entry, msg.cwd)); loop.setMainDone(); } async function runNodeEntry(msg: KernelToRuntimeInit): Promise { if (msg.thread?.eval != null) { // An eval worker runs its source as a CJS script whose require() starts at cwd. const fn = compileCjs(msg.thread.eval, msg.entry, ["exports", "require", "module", "__filename", "__dirname", "process"]); const moduleObj = { exports: {}, id: msg.entry, filename: msg.entry, loaded: false, children: [] as unknown[] }; loop.runMain(() => fn.call(moduleObj.exports, moduleObj.exports, makeRequire(ctx, `${msg.cwd}/${msg.entry}`), moduleObj, msg.entry, ".", proc)); loop.setMainDone(); return; } const resolved = await call("resolve", { specifier: msg.entry, parent: msg.cwd, kind: "require" }); // Node resolves the entry through its CJS loader, whatever the entry's format. if (resolved.kind === "missing" && resolved.code === "ERR_MODULE_NOT_FOUND") throw requireNotFound(msg.entry, []); if (resolved.kind === "missing") throw loaderError(resolved.code, msg.entry, msg.cwd); if (resolved.kind !== "file") throw requireNotFound(msg.entry, []); const { source, format } = await call("loadModule", { path: resolved.path }); if (format === "esm") { // Guest ESM executes through the browser's native module linker: the SW // virtual origin serves the kernel-transformed source, so cycles, live // bindings and TLA come from the engine (DESIGN §Module system). const st = callSync("stat", { path: resolved.path, follow: true }); await import(vfsUrl(host, resolved.path, st)); loop.setMainDone(); return; } if (format !== "cjs") throw loaderError("ERR_REQUIRE_ESM", msg.entry, msg.cwd); const mod = newModule(ctx, resolved.path); ctx.cache.set(resolved.path, mod); const text = new TextDecoder().decode(source); const fn = compileCjs(text, resolved.path, ["exports", "require", "module", "__filename", "__dirname", "process"]); loop.runMain(() => fn.call(mod.exports, mod.exports, makeRequire(ctx, resolved.path), mod, resolved.path, ctx.dirnameOf(resolved.path), proc)); mod.loaded = true; loop.setMainDone(); } /** Route an uncaught error. True when an uncaughtException listener took it; * otherwise the process dies and this does not return. */ function uncaught(e: unknown, origin: "uncaughtException" | "unhandledRejection" = "uncaughtException"): boolean { const capture = proc && uncaughtCaptures.get(proc); if (capture) { try { capture(e); } catch (inner) { die(stackOf(inner), 7); } return true; } if (!proc || proc.listenerCount("uncaughtException") === 0) { if (parentPort) dieInThread(e); die(stackOf(e)); } try { proc.emit("uncaughtException", e, origin); } catch (inner) { die(stackOf(inner), 7); // Node's exit code for a throwing uncaughtException handler } return true; } function unhandledRejection(reason: unknown, promise: Promise): void { if (proc.listenerCount("unhandledRejection") > 0) { try { proc.emit("unhandledRejection", reason, promise); } catch (e) { uncaught(e); } return; } // No unhandledRejection listener: Node raises the rejection as an uncaught exception. if (proc.listenerCount("uncaughtException") > 0 || uncaughtCaptures.has(proc)) { uncaught(reason instanceof Error ? reason : unhandledRejectionError(reason), "unhandledRejection"); return; } if (parentPort) dieInThread(reason instanceof Error ? reason : unhandledRejectionError(reason)); die(reason instanceof Error ? stackOf(reason) : unhandledRejectionReport(reason)); } let dying = false; /** A thread's uncaught error goes to its parent's 'error' event, cloned as Node clones * it, instead of stderr; then the thread exits 1. */ function dieInThread(e: unknown): never { if (!dying) { dying = true; try { parentPort!.send({ k: "error", v: e }); } catch { parentPort!.send({ k: "error", v: String(e) }); } } if (proc) proc.exit(1); return exitWorker(1); } /** Node's fatal path: the report on stderr, then the exit listeners, then exit. */ function die(report: string, code = 1): never { if (!dying) { dying = true; emitStderr(enc.encode(report)); } if (proc) proc.exit(code); return exitWorker(code); } /** `exit` never replies (DESIGN invariant): the kernel terminates this worker. Blocking * until then keeps process.exit() from returning into guest code, as in Node. */ function exitWorker(code: number): never { kernelPort.postMessage({ t: "req", id: nextId++, op: "exit", args: { code } }); self.close(); for (;;) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0); } function stackOf(e: unknown): string { return `${(e as Error)?.stack ?? String(e)}\n`; } function unhandledRejectionError(reason: unknown): Error & { code: string } { const shown = typeof reason === "string" ? JSON.stringify(reason) : String(reason); const e = new Error("This error originated either by throwing inside of an async function without a catch " + "block, or by rejecting a promise which was not handled with .catch(). " + `The promise rejected with the reason ${shown}.`); e.name = "UnhandledPromiseRejection"; return Object.assign(e, { code: "ERR_UNHANDLED_REJECTION" }); } function unhandledRejectionReport(reason: unknown): string { const e = unhandledRejectionError(reason); return `[${e.name}: ${e.message}] {\n code: '${e.code}'\n}\n`; } export {};