Skip to content

Codegen: new MessageChannel() global constructor unlinked/non-constructible — routes to stdlib symbol, breaks React scheduler init #4873

Closed

Description

@proggeramlug

Summary

new MessageChannel() is not usable as a global constructor. Despite #2597 ("expose MessageChannel/MessagePort/BroadcastChannel as globals", closed 2026-06-02), a bare new MessageChannel():

  • fails to link standalone — Undefined symbols: _js_worker_threads_message_channel_new, referenced from _main; and
  • throws TypeError: is not a constructor at runtime when it does link (i.e. when something else in the module graph pulls node:worker_threads so the symbol resolves).

This blocks React's scheduler, which runs new MessageChannel() at module-init:

// node_modules/scheduler/cjs/scheduler.production.js:197
else if ("undefined" !== typeof MessageChannel) {
  var channel = new MessageChannel(), ...   // throws here, before any user code

Because typeof MessageChannel !== "undefined", scheduler takes the MessageChannel branch instead of its setTimeout fallback — then dies. Any React/React-DOM/react-reconciler app hits this. Surfaced re-evaluating ink (#348): with the full ink + react@19 + react-reconciler + scheduler graph now compiling and linking cleanly (yoga's WASM entry stubbed), scheduler init is the first runtime wall.

Repro

// mc.ts
const c = new MessageChannel();
console.log(typeof c, typeof c.port1, typeof c.port2);
$ perry compile mc.ts -o mc
Undefined symbols for architecture arm64:
  "_js_worker_threads_message_channel_new", referenced from:
      _main in ....o
ld: symbol(s) not found for architecture arm64
Error: Linking failed

In a graph that also imports node:worker_threads (e.g. via ws), the link succeeds but running it throws TypeError: is not a constructor.

Node reference (new MessageChannel() → { port1, port2 }, both MessagePort).

Likely root cause (hypothesis — two divergent code paths)

There appear to be two runtime constructors and the new-expression path picks the wrong/unanchored one:

  • js_message_channel_new — crates/perry-runtime/src/messaging.rs:180 — the global impl, wired for the call path in crates/perry-codegen/src/lower_call/builtin.rs:319 ("MessageChannel" => ... blk.call(DOUBLE, "js_message_channel_new", &[])).
  • js_worker_threads_message_channel_new — crates/perry-stdlib/src/worker_threads.rs:1141 — the module impl, mapped in crates/perry-codegen/src/lower_call/native_table/extras.rs:98-100 (method: "MessageChannel" → runtime: "js_worker_threads_message_channel_new").

The standalone link error names js_worker_threads_message_channel_new, so new MessageChannel() is routed to the stdlib/worker_threads symbol — which is only #[used]-anchored (worker_threads.rs:1975 KEEP_WT_MESSAGE_CHANNEL_NEW) and thus only present in the link when worker_threads is otherwise pulled in — rather than the always-available js_message_channel_new that the bare-call path uses. And even when it links, the result isn't a constructible object (is not a constructor). MessageChannel is in the new-expr builtin allowlist at crates/perry-codegen/src/expr/helpers.rs:321, so the new form is recognized but its lowering diverges from the call form.

Net: the new MessageChannel() path should route to the same always-linked global constructor (js_message_channel_new) the call path uses, and that object must be properly constructible.

Impact

Related

Activity

  1. added 3 commits that reference this issue on Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions