Skip to content

fix(node:worker_threads): MessageChannel ports + transfer/clone markers - #3808

Merged
proggeramlug merged 4 commits into
mainfrom
fix-worker-channels-3157-3159-3156
May 31, 2026
Merged

proggeramlug merged 4 commits into
mainfrom
fix-worker-channels-3157-3159-3156

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Implements same-process MessageChannel / MessagePort delivery and the
worker_threads transfer/clone marker helpers.

Closes #3157
Closes #3159

Implementation

#3157 — MessageChannel ports + receiveMessageOnPort

  • new MessageChannel() now returns a real heap object { port1, port2 }
    backed by a same-process paired-port registry (MESSAGE_PORTS keyed by a
    monotonic port id). Each port object carries closure-valued
    postMessage / on / addListener / once / off / removeListener /
    close / start / ref / unref / hasRef, each capturing its port id.
  • port.postMessage(v) JSON-serializes v (structured-clone-like value
    semantics, matching the existing parentPort stdin/stdout IPC path) and
    enqueues it on the peer port's inbox.
  • receiveMessageOnPort(port) synchronously pops one queued message and
    returns { message: value }, or undefined when the inbox is empty.
  • port.on('message', cb) registers a listener and starts the port; the
    event-loop pump (js_worker_threads_channels_process_pending) drains
    inboxes and invokes listeners on the main thread. port.close() queues a
    close event on the port. The pump's "has active handles" check keeps the
    loop alive while a started port has queued messages or a pending close.
  • Listener closures are GC-rooted + rewritten across moves via the existing
    worker_threads mutable root scanner.

#3159 — transfer / clone markers

  • markAsUncloneable(obj) now records the object; port.postMessage of a
    marked-uncloneable value throws a DataCloneError.
    (markAsUntransferable / isMarkedAsUntransferable were already
    implemented.)

HIR lowering

  • new MessageChannel() via named import now routes to the same
    receiver-less worker_threads NativeMethodCall as the
    new worker_threads.MessageChannel() member form, so the runtime allocates
    the real { port1, port2 } object instead of an empty user-class object.
  • chan.port1 / chan.port2 on a MessageChannel-typed value lower to a
    plain object field read (returning the port object) rather than a zero-arg
    native getter. parentPort stays on its native-receiver dispatch path.

No new HIR variants added.

Validation

Gap test test-files/test_gap_worker_channels_3157plus.ts (MessageChannel
sync receiveMessageOnPort, async on('message') delivery, port close, and
the marker helpers) is byte-identical to
node --experimental-strip-types under the default auto-optimize compile.

./scripts/check_file_size.sh, cargo fmt --all -- --check, and
cargo test -p perry-hir are green.

Ralph Küpper and others added 4 commits May 31, 2026 18:14
Implement same-process MessageChannel/MessagePort delivery and the
transfer/clone marker helpers.

- new MessageChannel() returns {port1, port2} backed by a same-process
  paired-port registry. port.postMessage(v) JSON-serializes v and
  enqueues it on the peer's inbox.
- receiveMessageOnPort(port) synchronously pops one queued message and
  returns { message: value }, or undefined when empty.
- port.on('message', cb) / addListener / once register a listener; the
  event-loop pump drains inboxes and invokes the callback. port.start()
  enables delivery; port.close() fires 'close'.
- markAsUncloneable(obj) now records the object; postMessage of a marked
  object throws a DataCloneError.

Closes #3157
Closes #3159
… real object

- new MessageChannel() via named import now lowers to the worker_threads
  NativeMethodCall (like the namespace form), so the runtime allocates the
  real { port1, port2 } object instead of an empty user-class object.
- chan.port1 / chan.port2 on a MessageChannel-typed value lower to a plain
  object field read (returning the port object) rather than a zero-arg
  native getter. parentPort stays on native-receiver dispatch.
- port.on('message', cb) registers the runtime event-loop pump so a
  MessageChannel-only program ticks the loop and delivers queued messages.
…157-3159-3156

# Conflicts:
#	crates/perry-hir/src/lower/expr_new.rs
@proggeramlug
proggeramlug merged commit 61f0716 into main May 31, 2026
1 check passed
@proggeramlug
proggeramlug deleted the fix-worker-channels-3157-3159-3156 branch May 31, 2026 16:57
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.

node:worker_threads: implement transfer and clone marker helpers node:worker_threads: implement MessageChannel ports and receiveMessageOnPort

1 participant