Skip to content

node:worker_threads: +48 Node.js tests passing — MessagePort, stdio, SHARE_ENV, exit codes, transfer semantics, postMessageToThread + inspector - #31216

Merged
Jarred-Sumner merged 67 commits into
mainfrom
ciro/worker-threads-compat
Jul 10, 2026
Merged

Conversation

@cirospaciari

@cirospaciari cirospaciari commented May 22, 2026

Copy link
Copy Markdown
Member

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/…) off MessagePort.prototype onto an intermediate prototype so Object.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.
  • moveMessagePortToContext throws ERR_CLOSED_MESSAGE_PORT for a closed port.
  • postMessage transfer-list errors match Node: source/peer port → DataCloneError "Transfer list contains source port"; already-detached port → "...already detached" (validated before any ArrayBuffer detach); a MessagePort in 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.inspect output.
  • postMessage to 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 DataCloneError before 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 control MessagePort to every thread; each worker keeps one port to the hub, so any thread can reach any other. Errors map to ERR_WORKER_MESSAGING_* (same-thread / failed / errored / timeout).
  • Fixed MessagePort.unref() to release the event-loop ref taken by a message listener (previously only the onmessage/ref() keepalive was released), so an always-listening control port no longer pins a worker's loop.

env: SHARE_ENV

  • A Worker created with env: SHARE_ENV shares a live, process-wide environment with the parent (write-through process.env over a lock-guarded store; reads/writes/deletes/enumeration). The normal snapshot path is unchanged; env: 42 still throws ERR_INVALID_ARG_TYPE.

Captured stdio

  • new Worker(f, { stdin/stdout/stderr: true }): worker.stdout/stderr are Readables fed by the worker's process.stdout/stderr (including console.log); worker.stdin is 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 + threadName export; validate the filename like Node (ERR_WORKER_PATH / ERR_INVALID_URL_SCHEME); terminate() keeps the loop alive until it resolves; normalize options for new Worker(f, null); eval: false validates the path.

Worker introspection (Node 24)

  • worker.getHeapStatistics(), worker.cpuUsage([prevValue]), worker.startCpuProfile([options]) (returns a handle with an idempotent stop()), and worker.startHeapProfile([options]) — sampled from the worker thread across threads (mirroring getHeapSnapshot), with Node-matching synchronous option validation and ERR_WORKER_NOT_RUNNING once 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 SyntaxError to the parent's error event.

Worker process surface

  • On a worker thread process.abort/chdir (and setuid/seteuid/setgid/setegid/setgroups/initgroups off Windows) are disabled stubs that throw ERR_WORKER_UNSUPPORTED_OPERATION (with .disabled === true), process.umask(setMask) throws (the getter still works), debugPort defaults to 9229, and the main-only _startProfilerIdleNotifier/_stopProfilerIdleNotifier/_debugProcess/_debugEnd internals are removed — fixing process.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_FD set), so if (process.send) still works in a normal worker.

Crash & teardown safety

  • Serializing an error whose stack getter throws (a throwing Error.prepareStackTrace) no longer crashes postMessage.
  • A worker that throws during a beforeExit handler exits cleanly with code 1 instead of SIGTRAP.
  • 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 Worker pointed at a missing entry file reports Cannot find module '<path>' (code MODULE_NOT_FOUND), matching Node, instead of the native loader's ModuleNotFound message.

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

@robobun

robobun commented May 22, 2026

Copy link
Copy Markdown
Collaborator
Updated 4:16 PM PT - Jul 10th, 2026

@cirospaciari, your commit 13a7f81 is still building in Build #71656, but has 1 failures so far (All Failures):

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds C++ MessagePort peer-close and transfer validation; exposes threadName and port-activity to JS; refactors JS MessagePort emitter/close behavior; validates worker filenames/names; implements captured stdio via MessageChannel; updates moveMessagePortToContext and Worker lifecycle/getters; and adds comprehensive tests.

Changes

Worker Threads Node.js Compatibility

Layer / File(s) Summary
MessagePort C++ interface
src/jsc/bindings/webcore/MessagePort.h, src/jsc/bindings/webcore/MessagePort.cpp
Adds MessagePort::peerClosed() and rejects detached JSMessagePort in transfer lists; refines DataCloneError messages.
MessagePortPipe peer notification
src/jsc/bindings/webcore/MessagePortPipe.cpp, src/jsc/bindings/webcore/MessagePortPipe.h
Notify entangled peer on close by posting a task to the peer's script context and invoking peerClosed().
Worker native binding: threadName & port activity
src/jsc/bindings/webcore/Worker.cpp
Expose threadName and isMessagePortActive from native createNodeWorkerThreadsBinding and include MessagePortPipe for activity checks.
JS: name normalization & filename validation
src/js/node/worker_threads.ts
Add normalizeWorkerName and validateWorkerFilename; read _threadName/_isMessagePortActive from native binding.
JS: MessagePort emitter and captured stdio
src/js/node/worker_threads.ts
Refactor injectFakeEmitter to insert an intermediate prototype, override MessagePort.prototype.close to synchronously emit "close" and track closed ports, and implement captured-stdio plumbing with MessageChannel control ports; derive module threadName.
JS: moveMessagePortToContext and Worker state
src/js/node/worker_threads.ts
Change moveMessagePortToContext(port, context) to validate port instances/closed status; add Worker private fields (#name, #exited) and initialize #name from normalized options.
JS: Worker stdio getters and transfer wiring
src/js/node/worker_threads.ts
Worker constructor creates MessageChannel pairs for requested stdio, transfers worker-side ports via workerData/transferList, and implements lazy stdin/stdout/stderr getters backed by control ports.
JS: terminate and threadName lifecycle
src/js/node/worker_threads.ts
terminate() calls ref() before terminating; #onClose sets #exited = true; add Worker.threadName getter and export module threadName.
MessagePort peer/transfer tests
test/js/node/test/parallel/test-worker-message-*.js, test/js/node/test/parallel/test-worker-message-port-transfer-self.js
Add tests for ArrayBuffer neutering, port transferring, self-transfer DataCloneError, closed/detached transfer errors, SharedArrayBuffer lists, and message ordering around close.
Worker stdio & drain tests
test/js/node/test/parallel/test-worker-stdio*.js, test/js/node/test/parallel/test-worker-message-port-drain.js
Tests for stdout/stdin capture, flush ordering, piping, and per-worker stdout drain behaviour.
Worker lifecycle & termination tests
test/js/node/test/parallel/test-worker-thread-name.js, test/js/node/test/parallel/test-worker-terminate-*.js, test/js/node/test/parallel/test-worker-nexttick-terminate.js
Validate threadName behavior, termination during microtasks/nextTick/unref, and termination while async operations are in flight.
Worker init, path, and error tests
test/js/node/test/parallel/test-worker-unsupported-path.js, test/js/node/test/parallel/test-worker-heapdump-failure.js, test/js/node/test/parallel/test-worker-init-failure.js, test/js/node/test/parallel/test-worker-memory.js
Tests for invalid specifiers, heap snapshot behavior on non-running workers, init failures under FD limits, and memory-regression checks.
Worker exit/transfer semantics tests
test/js/node/test/parallel/test-worker-crypto-sign-transfer-result.js, test/js/node/test/parallel/test-worker-process-exit-async-module.js, test/js/node/test/parallel/test-worker-voluntarily-exit-*.js, test/js/node/test/parallel/test-worker-terminate-source-map.js
Verify crypto buffer transfers, process.exit ordering in async modules, voluntary exit semantics, and source-map shutdown isolation.
Sequential FD coordination tests
test/js/node/test/sequential/test-worker-fshandles-*.js
Sequential tests exercising fd open/close during recursive worker spawning and termination.
Unit test adjustments
test/js/node/worker_threads/worker_threads.test.ts
Adjust moveMessagePortToContext test to pass a real MessagePort and relax expected MessagePort string form in serialization test.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is clearly about the worker_threads compatibility and test batch, so it matches the main change despite being verbose.
Description check ✅ Passed The description is detailed and covers the PR goals plus some verification context, so it is mostly complete.

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Found 5 issues this PR may fix:

  1. Bun doesn't finish the execution when all workers' ports are closed #11760 - PR adds peer close events so closing one side of a MessageChannel now releases the entangled peer's event-loop ref, fixing the hang
  2. MessagePort missing removeListener method (Node.js EventEmitter incompatibility) #29022 - PR moves EventEmitter-style helpers onto MessagePort's intermediate prototype, adding the missing removeListener method
  3. worker_threads stdout/stderr not implemented for Worker #28039 - PR implements { stdin: true, stdout: true, stderr: true } options for Worker using MessageChannel pairs
  4. worker_threads.Worker option "stdout" is not yet implemented in Bun #23875 - PR implements the stdout option for worker_threads.Worker
  5. Worker STDIN and STDOUT throw NotImplemented Exceptions #14563 - PR implements Worker stdin/stdout/stderr support, resolving the NotImplemented exceptions

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #11760
Fixes #29022
Fixes #28039
Fixes #23875
Fixes #14563

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. worker_threads: implement parentPort.close()/ref()/unref()/hasRef() #30549 - Implements parentPort.close()/ref()/unref()/hasRef(), which overlaps with this PR's MessagePort close(callback) and ref/unref lifecycle changes
  2. worker_threads: implement Worker#stdout/stderr/stdin (1hw6ic) #29826 - Implements Worker#stdout/stderr/stdin, which overlaps with this PR's worker stdio capture via MessageChannel control ports
  3. feat(worker_threads): implement stdout/stderr for Worker #26599 - Implements Worker stdout/stderr, which overlaps with this PR's worker stdio implementation
  4. worker_threads: give MessagePort node's EventEmitter surface #29024 - Adds Node EventEmitter aliases to MessagePort, which overlaps with this PR's MessagePort EventEmitter restructuring
  5. fix(MessagePort): emit "close" event on both ports when one side is closed #27691 - Emits close event on both ports when one side is closed, which overlaps with this PR's MessagePort peer close event

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/worker_threads.ts`:
- Around line 389-398: The parameter "context" in function
moveMessagePortToContext is declared but unused; to satisfy the linter and
indicate intentional non-use, rename it to "_context" (i.e., change the
parameter from context to _context) in the moveMessagePortToContext signature
and keep the rest of the function unchanged (retaining the MessagePort checks
and the throwNotImplemented("worker_threads.moveMessagePortToContext") call).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1bf0df59-8c8c-40f9-aecb-b2e02d6868a9

📥 Commits

Reviewing files that changed from the base of the PR and between 346ce08 and b47da01.

📒 Files selected for processing (35)
  • src/js/node/worker_threads.ts
  • src/jsc/bindings/webcore/MessagePort.cpp
  • src/jsc/bindings/webcore/MessagePort.h
  • src/jsc/bindings/webcore/MessagePortPipe.cpp
  • src/jsc/bindings/webcore/MessagePortPipe.h
  • src/jsc/bindings/webcore/Worker.cpp
  • test/js/node/test/parallel/test-worker-crypto-sign-transfer-result.js
  • test/js/node/test/parallel/test-worker-dns-terminate.js
  • test/js/node/test/parallel/test-worker-heapdump-failure.js
  • test/js/node/test/parallel/test-worker-init-failure.js
  • test/js/node/test/parallel/test-worker-memory.js
  • test/js/node/test/parallel/test-worker-message-channel.js
  • test/js/node/test/parallel/test-worker-message-port-arraybuffer.js
  • test/js/node/test/parallel/test-worker-message-port-close.js
  • test/js/node/test/parallel/test-worker-message-port-drain.js
  • test/js/node/test/parallel/test-worker-message-port-message-before-close.js
  • test/js/node/test/parallel/test-worker-message-port-message-port-transferring.js
  • test/js/node/test/parallel/test-worker-message-port-multiple-sharedarraybuffers.js
  • test/js/node/test/parallel/test-worker-nexttick-terminate.js
  • test/js/node/test/parallel/test-worker-process-exit-async-module.js
  • test/js/node/test/parallel/test-worker-stdio-flush-inflight.js
  • test/js/node/test/parallel/test-worker-stdio-flush.js
  • test/js/node/test/parallel/test-worker-stdio.js
  • test/js/node/test/parallel/test-worker-terminate-microtask-loop.js
  • test/js/node/test/parallel/test-worker-terminate-ref-public-port.js
  • test/js/node/test/parallel/test-worker-terminate-source-map.js
  • test/js/node/test/parallel/test-worker-terminate-unrefed.js
  • test/js/node/test/parallel/test-worker-thread-name.js
  • test/js/node/test/parallel/test-worker-uncaught-exception.js
  • test/js/node/test/parallel/test-worker-unsupported-path.js
  • test/js/node/test/parallel/test-worker-voluntarily-exit-followed-by-addition.js
  • test/js/node/test/parallel/test-worker-voluntarily-exit-followed-by-throw.js
  • test/js/node/test/sequential/test-worker-fshandles-error-on-termination.js
  • test/js/node/test/sequential/test-worker-fshandles-open-close-on-termination.js
  • test/js/node/worker_threads/worker_threads.test.ts

Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts
Comment thread src/jsc/bindings/webcore/MessagePortPipe.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/worker_threads.ts`:
- Around line 216-226: The custom inspect function attached to MessagePort via
the kInspectCustom symbol declares unused parameters depth and options which
trigger the linter; update the function signature on MessagePort.prototype (the
value function assigned to kInspectCustom) to prefix those parameters with
underscores (e.g., _depth, _options) so the signature still matches
util.inspect.custom while satisfying the linter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ee3c8ab1-4b97-4c4f-8c3a-4c199254153d

📥 Commits

Reviewing files that changed from the base of the PR and between b47da01 and 26f1da0.

📒 Files selected for processing (5)
  • src/js/node/worker_threads.ts
  • src/jsc/bindings/webcore/MessagePort.cpp
  • src/jsc/bindings/webcore/Worker.cpp
  • test/js/node/test/parallel/test-worker-message-port-transfer-self.js
  • test/js/node/worker_threads/worker_threads.test.ts

Comment thread src/js/node/worker_threads.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/node/worker_threads.ts (1)

405-409: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

moveMessagePortToContext() only recognizes JS-initiated closes.

closedMessagePorts is updated only by the JS MessagePort.prototype.close() wrapper, but this PR also closes ports from native code (MessagePort::peerClosed() in src/jsc/bindings/webcore/MessagePort.cpp, plus teardown paths). Those ports will now be observably closed without ever entering this WeakSet, so this branch still falls through to throwNotImplemented() instead of ERR_CLOSED_MESSAGE_PORT.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/node/worker_threads.ts` around lines 405 - 409,
moveMessagePortToContext currently only checks the JS-side WeakSet
closedMessagePorts and misses ports closed from native (e.g.,
MessagePort::peerClosed()), causing the code path to fall through instead of
throwing ERR_CLOSED_MESSAGE_PORT; update moveMessagePortToContext (and related
MessagePort close handling) to detect native-initiated closes by reading a
JS-visible closed flag that native code sets (e.g., a new internal slot or
property on the MessagePort object that native MessagePort::peerClosed()
updates), or alternatively ensure native close paths also add the port to the
existing closedMessagePorts WeakSet; reference moveMessagePortToContext and
closedMessagePorts and make sure the native MessagePort::peerClosed() path
updates the same JS-visible state so moveMessagePortToContext throws
$ERR_CLOSED_MESSAGE_PORT for native-closed ports.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/worker_threads.ts`:
- Around line 208-209: The callback passed to close(cb) is queued with
queueMicrotask(cb) which loses the MessagePort receiver; update the close
implementation (MessagePort.prototype.close / close(cb)) to invoke the queued
microtask as a wrapper that calls the original cb with the MessagePort as
receiver (e.g., queueMicrotask(() => cb.call(this))) after the existing typeof
cb check so the callback runs with this === MessagePort while still being
scheduled asynchronously.

---

Outside diff comments:
In `@src/js/node/worker_threads.ts`:
- Around line 405-409: moveMessagePortToContext currently only checks the
JS-side WeakSet closedMessagePorts and misses ports closed from native (e.g.,
MessagePort::peerClosed()), causing the code path to fall through instead of
throwing ERR_CLOSED_MESSAGE_PORT; update moveMessagePortToContext (and related
MessagePort close handling) to detect native-initiated closes by reading a
JS-visible closed flag that native code sets (e.g., a new internal slot or
property on the MessagePort object that native MessagePort::peerClosed()
updates), or alternatively ensure native close paths also add the port to the
existing closedMessagePorts WeakSet; reference moveMessagePortToContext and
closedMessagePorts and make sure the native MessagePort::peerClosed() path
updates the same JS-visible state so moveMessagePortToContext throws
$ERR_CLOSED_MESSAGE_PORT for native-closed ports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a69db20-fe42-49c1-bafd-1dc766a79ccd

📥 Commits

Reviewing files that changed from the base of the PR and between 26f1da0 and f092f3b.

📒 Files selected for processing (3)
  • src/js/node/worker_threads.ts
  • src/jsc/bindings/webcore/MessagePort.cpp
  • test/js/node/test/parallel/test-worker-message-port-transfer-closed.js

Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/jsc/bindings/webcore/MessagePortPipe.cpp
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts
Comment thread src/js/node/worker_threads.ts
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
@cirospaciari cirospaciari changed the title node: worker_threads compatibility improvements node:worker_threads: MessagePort, captured stdio, and error-handling compatibility May 22, 2026
Comment thread src/js/node/worker_threads.ts
Comment thread src/js/node/worker_threads.ts
Comment thread test/js/web/workers/worker.test.ts
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/jsc/bindings/webcore/SerializedScriptValue.cpp Outdated
Comment thread src/jsc/bindings/JSEnvironmentVariableMap.cpp Outdated
@cirospaciari
cirospaciari force-pushed the ciro/worker-threads-compat branch from 462b948 to 6267c8e Compare May 24, 2026 19:52
Comment thread src/jsc/bindings/webcore/MessagePort.cpp Outdated
Comment thread src/jsc/bindings/JSEnvironmentVariableMap.cpp Outdated
Comment thread src/jsc/bindings/JSEnvironmentVariableMap.cpp Outdated
Comment thread src/jsc/lib.rs Outdated
Comment thread src/js/node/worker_threads.ts
Comment thread src/jsc/web_worker.rs Outdated
Comment thread src/jsc/bindings/webcore/JSWorker.cpp
Comment thread test/js/node/test/parallel/test-worker-dns-terminate.js Outdated
Comment thread test/js/node/worker_threads/worker_threads.test.ts
Comment thread test/js/node/test/parallel/test-worker-init-failure.js Outdated
Comment thread src/jsc/bindings/BunProcess.cpp
@cirospaciari cirospaciari changed the title node:worker_threads: MessagePort, captured stdio, and error-handling compatibility node:worker_threads: MessagePort, stdio, SHARE_ENV, exit codes, transfer semantics, postMessageToThread + inspector May 24, 2026
@cirospaciari
cirospaciari force-pushed the ciro/worker-threads-compat branch from 75c4056 to 10ace2f Compare May 24, 2026 22:22
Comment thread src/js/node/worker_threads.ts
Comment thread src/jsc/bindings/webcore/MessagePort.cpp Outdated
Comment thread test/js/node/test/parallel/test-worker-terminate-null-handler.js
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/jsc/bindings/webcore/MessagePortPipe.cpp
robobun added a commit that referenced this pull request Jul 28, 2026
Since #31216 every node:worker_threads Worker implicitly preloads
node:worker_threads, which pulls in node:stream and reads process.nextTick
before the worker's own entry runs. That consumed the one-shot
onEachMicrotaskTick hook the same way an explicit --preload does, so a CJS
worker entry saw microtasks before nextTick callbacks.
robobun added a commit that referenced this pull request Jul 29, 2026
…askTick nextTick hook

Node.js evaluates a CommonJS entry synchronously from Module.runMain and then
runs processTicksAndRejections, so nextTick callbacks scheduled at top level
run before microtasks. An ESM entry runs from inside the microtask queue, so
its top level sees the reverse. Bun ran every entry through the ESM loader's
promise chain and relied on a one-shot onEachMicrotaskTick hook to drain the
nextTick queue after the entry body's microtask. The one-shot fired once and
never re-armed, so whichever preload (explicit or the implicit
node:worker_threads preload every worker_threads Worker gets since #31216)
touched process.nextTick first consumed it, and the entry's own nextTicks ran
after its microtasks.

F1: gate the bun:main load on the entry kind. A known-ESM entry (.mjs/.mts,
or .js/.ts/.jsx/.tsx under "type": "module") keeps the async loader path so
its top level sees microtasks first (Node's ESM ordering). Every other entry
drains JSC's SynchronousModuleQueue before returning, so the CommonJS body
runs before the first microtask checkpoint and nextTick drains first. Preloads
apply the same gate (Node's --require is synchronous, --import is async). One
tick() after a synchronously-loaded body drains its nextTick queue then its
microtasks, matching processTicksAndRejections after Module.runMain.

F2: drop the onEachMicrotaskTick nextTick hook. The hook interleaved nextTick
between individual microtasks, so a process.nextTick queued from inside a
microtask jumped ahead of that microtask's siblings (Node drains the whole
FIFO first, then the tick queue). With F1 it is no longer needed for entry
ordering. resetOnEachMicrotaskTick / cleanupAsyncHooksData now only serve
AsyncLocalStorage.enterWith. GlobalObject::drainMicrotasks re-checks the
queue after vm.drainMicrotasks so ticks scheduled by the lazy-first
process.nextTick access still run in the same checkpoint, and
processTicksAndRejections clears the "has work" field so
JSNextTickQueue::isEmpty reflects the drained state.

The fs-callback ordering (fs.readFile(...).then(cb) runs cb from a promise
reaction) needs the same treatment as F1 applied to completion dispatch;
that is #33366.

Fixes #34115
robobun added a commit that referenced this pull request Jul 29, 2026
Since #31216 every node:worker_threads Worker implicitly preloads
node:worker_threads, which pulls in node:stream and reads process.nextTick
before the worker's own entry runs. That consumed the one-shot
onEachMicrotaskTick hook the same way an explicit --preload does, so a CJS
worker entry saw microtasks before nextTick callbacks.
robobun added a commit that referenced this pull request Jul 30, 2026
Since #31216 worker stdout/stderr is always port-backed, so the
process.exit() drop also hits workers created without {stdout: true};
a worker that logs N lines then exits surfaces only the first.
robobun added a commit that referenced this pull request Jul 30, 2026
Since #31216 worker stdout/stderr is always port-backed, so the
process.exit() drop also hits workers created without {stdout: true};
a worker that logs N lines then exits surfaces only the first.
robobun added a commit that referenced this pull request Aug 5, 2026
… stale 120s timeout

Review follow-ups: the fixture header now describes the current reqId-map
design (#31216) instead of the superseded raw-pointer fix, the mid-loop
comment no longer claims a GC/teardown overlap this PR measured to be
gone, every iteration fails on an empty snapshot stream (one payload per
process is parsed as JSON), and the 120s release timeout arm sized for
the old 15x300 workload collapses into a single 60s ceiling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants