Expand description
Node worker_threads: real OS-thread workers with fully isolated heaps.
§Model (matches Node: workers do NOT share the JS heap)
node-js’s entire runtime — the JsHost heap, the module cache, the event
loop channel — lives in thread_local!s (host::HOST, module’s statics).
So spawning a fresh OS thread automatically gives that thread its OWN
isolated interpreter and heap. A Worker here is therefore a real
std::thread that calls crate::eval_file/eval_str on the worker file,
running it against that thread’s own clean thread_local host. Nothing on
the JS heap is shared between the main thread and a worker, or between two
workers — exactly Node’s isolation guarantee.
§Why messages cross as JSON strings, never Values
fusevm::Value is a per-thread heap handle (Value::Obj(u32) indexes the
calling thread’s JsHost.heap); it is neither Send nor meaningful on
another thread. So a message can NEVER be a Value. Every message crosses
the thread boundary as a plain String of JSON:
sender thread: value ─JSON.stringify([value])→ String (on sender's heap)
channel: String (Send)
receiver thread: String ─JSON.parse(s)[0]→ value (on receiver's heap)The value is wrapped in a one-element array before JSON.stringify so that
top-level primitives AND undefined round-trip through a single always-valid
JSON document (JSON.stringify(undefined) is itself undefined, not a
string — the array wrapper avoids that). Deserialization unwraps [0] on the
receiving thread’s own heap.
§Serialization is a JSON subset of structured clone (documented limitation)
Only JSON-serializable data transfers: objects, arrays, strings, numbers,
booleans, null. undefined becomes null (JSON semantics), and functions,
symbols, Map/Set, cycles, BigInt, and ArrayBuffer transfers are NOT
supported (a BigInt makes JSON.stringify throw, surfaced as a thrown
error from postMessage, matching Node’s DataCloneError in spirit). This
is an honest subset, never a silent fake.
§Bidirectional message flow (both directions are real)
- worker → main (
parentPort.postMessage): the worker serializes on its own heap and posts anIoTaskonto the MAIN loop’sio_sender(captured at construction). The task runs on the main thread, deserializes into a freshValueon the main heap, and emits'message'on theWorkerobject. - main → worker (
worker.postMessage): the main thread serializes and sends the JSON string over anmpscchannel to the worker. A per-worker “bridge” thread (started when the worker adds aparentPort'message'listener) forwards each string as anIoTaskonto the WORKER loop’sio_sender; the task runs on the worker thread, deserializes on the worker heap, and emits'message'onparentPort. The bridge is required because the worker’s event loop (host::run_event_loop) blocks only on its own I/O channel and this module cannot modify it — same patternnetuses for socket reads.
§Liveness
new Worker incr_handles the MAIN loop so the process stays alive while the
worker runs; the worker’s 'exit' decr_handles it. On the worker side,
registering a parentPort 'message' listener incr_handles the WORKER loop
(keeping the worker alive to receive messages), and terminate releases it.
§terminate is cooperative (documented limitation)
Rust has no safe thread cancellation, so terminate signals the worker (via
the bridge) to decr_handle and let its event loop unwind; a worker parked in
its message loop exits promptly. A worker spinning in a tight synchronous JS
loop is not force-killed — there is no safe preemption point. terminate
returns undefined (awaiting it resolves to undefined).
Constants§
- BROADCAST_
CHANNEL_ METHODS - Instance methods on a
BroadcastChannelobject. - METHODS
- Module-level
worker_threadsfunctions. NOTE: these route only if the parent adds a"worker_threads"arm tostdlib::is_methodandstdlib::call(the module previously had no callable methods) — see the report. - PORT_
METHODS - Instance methods on a
MessagePort(the worker-sideparentPort), beyond the shared EventEmitter surface. - WORKER_
METHODS - Instance methods on a
Worker(main-side handle), beyond the shared EventEmitter surface (on/once/emit/…).
Functions§
- call
- Module-level dispatch. Routes only if the parent adds a
"worker_threads"arm tostdlib::call(see the report). - constant
- Non-function members of the
worker_threadsnamespace:isMainThread/threadId/parentPort/workerData, and theWorkerconstructor (as aBuiltin("Worker")sonew Worker(...)reachesconstruct_worker). - construct_
broadcast_ channel new BroadcastChannel(name)→ an object that broadcastspostMessagedata to every OTHERBroadcastChannelof the same name.- construct_
message_ channel new MessageChannel()→{ port1, port2 }, twoMessagePortobjects linked so thatport1.postMessage(v)is delivered toport2(and vice versa). Both ports live on the calling thread and share its heap; messages are cloned through the JSON subset (serialize/deserialize) so a posted object is a copy, not a shared reference — matching structured clone’s copy semantics. Requires the parent to wireMessageChannelconstruction (see the report).- construct_
worker - Build a
Workerand spawn its OS thread (runs on the MAIN thread). The worker thread runsfilename(a file path, or the code itself whenoptions.evalis truthy) against its own freshthread_localhost. - instance_
call