node:worker_threads: +48 Node.js tests passing — MessagePort, stdio, SHARE_ENV, exit codes, transfer semantics, postMessageToThread + inspector
Compatibility — node worker_threads test suites (debug build vs bun 1.3.13, which passes 51/138 parallel + 2/5 sequential):
| parallel | sequential | combined | |
|---|---|---|---|
| worker_threads | 102/138 | 2/5 | 104/143 (72.7%) |
Improves Node.js worker_threads compatibility across MessagePort, captured stdio, environment sharing, structured-clone transfer semantics, Worker construction and error handling, and ports the upstream Node worker tests these fix (plus a batch that already passed).
MessagePort
- Move the EventEmitter helpers (
on/off/once/emit/addListener/removeListener/…) offMessagePort.prototypeonto an intermediate prototype soObject.getOwnPropertyNames(MessagePort.prototype)matches Node. close([callback]): the callback runs asynchronously after the synchronous detach.- Dispatch the
'close'event natively, once (guarded), after delivering queued messages, to both the closed port and its entangled peer (including nested in-transit transferred ports); release the peer's event-loop ref. - Deliver messages already queued in a port's inbox when
close()is called mid-dispatch. moveMessagePortToContextthrowsERR_CLOSED_MESSAGE_PORTfor a closed port.postMessagetransfer-list errors match Node: source/peer port →DataCloneError "Transfer list contains source port"; already-detached port →"...already detached"(validated before any ArrayBuffer detach); aMessagePortin the message but not in the transfer list →DataCloneError "Object that needs transfer was found in message but not listed in transferList".- Node-compatible
util.inspectoutput. postMessageto a port's own entangled peer warns (not throws) and loses the channel, matching Node; every transfer-list port is still detached (transfer is atomic), and the source-port check is order-independent within the transfer list.
markAsUntransferable / markAsUncloneable / isMarkedAsUntransferable
- Mark objects with non-enumerable registry-symbol markers, enforced in the structured-clone serializer: a marked-untransferable object in a transfer list throws
DataCloneErrorbefore any detach; a marked-uncloneable object throws when cloned/posted (ArrayBuffers/views excepted, matching Node).
postMessageToThread
worker_threads.postMessageToThread(threadId, value[, transferList][, timeout])and the process'workerMessage'event (Node 22+). The main thread is a hub holding a controlMessagePortto every thread; each worker keeps one port to the hub, so any thread can reach any other. Errors map toERR_WORKER_MESSAGING_*(same-thread / failed / errored / timeout).- Fixed
MessagePort.unref()to release the event-loop ref taken by amessagelistener (previously only theonmessage/ref()keepalive was released), so an always-listening control port no longer pins a worker's loop.
env: SHARE_ENV
- A
Workercreated withenv: SHARE_ENVshares a live, process-wide environment with the parent (write-throughprocess.envover a lock-guarded store; reads/writes/deletes/enumeration). The normal snapshot path is unchanged;env: 42still throwsERR_INVALID_ARG_TYPE.
Captured stdio
new Worker(f, { stdin/stdout/stderr: true }):worker.stdout/stderrare Readables fed by the worker'sprocess.stdout/stderr(includingconsole.log);worker.stdinis a Writable. Streams are ended when the worker exits; the stdin listener attaches lazily so an unread{stdin:true}worker doesn't stay alive.
Worker construction & lifecycle
Worker#threadName+threadNameexport; validate the filename like Node (ERR_WORKER_PATH/ERR_INVALID_URL_SCHEME);terminate()keeps the loop alive until it resolves; normalizeoptionsfornew Worker(f, null);eval: falsevalidates the path.
Worker introspection (Node 24)
worker.getHeapStatistics(),worker.cpuUsage([prevValue]),worker.startCpuProfile([options])(returns a handle with an idempotentstop()), andworker.startHeapProfile([options])— sampled from the worker thread across threads (mirroringgetHeapSnapshot), with Node-matching synchronous option validation andERR_WORKER_NOT_RUNNINGonce the worker has exited.
Top-level await
- A worker whose entry module has an unsettled top-level await now exits with code 13 instead of hanging; a top-level await still waiting on real work is unaffected.
Errors
- A worker entry/import parse failure surfaces a real
SyntaxErrorto the parent'serrorevent.
Worker process surface
- On a worker thread
process.abort/chdir(andsetuid/seteuid/setgid/setegid/setgroups/initgroupsoff Windows) are disabled stubs that throwERR_WORKER_UNSUPPORTED_OPERATION(with.disabled === true),process.umask(setMask)throws (the getter still works),debugPortdefaults to 9229, and the main-only_startProfilerIdleNotifier/_stopProfilerIdleNotifier/_debugProcess/_debugEndinternals are removed — fixingprocess.abort()taking down the whole process from a worker. The IPC surface (send/disconnect/channel/connected) is disabled only when an IPC channel was inherited (NODE_CHANNEL_FDset), soif (process.send)still works in a normal worker.
Crash & teardown safety
- Serializing an error whose
stackgetter throws (a throwingError.prepareStackTrace) no longer crashespostMessage. - A worker that throws during a
beforeExithandler exits cleanly with code 1 instead ofSIGTRAP. - Terminating a worker mid-HTTP/2 no longer crashes the process: the HTTP/2 frame parser stops dispatching to JS once the worker is tearing down (no empty-value or pending-exception JS calls).
Worker entry resolution
- A
Workerpointed at a missing entry file reportsCannot find module '<path>'(codeMODULE_NOT_FOUND), matching Node, instead of the native loader'sModuleNotFoundmessage.
Tests
Ports the upstream Node worker tests for all of the above, plus a set that already passed. Existing Bun worker tests were updated where behavior is now Node-aligned (workers constructed from URL objects, not file:// strings; close delivers queued messages; parse errors are SyntaxError; eval:false validates the path).
no test proof · iteration 131 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/worker_threads/worker_heap_snapshot_gc.test.ts