Repository navigation
Codegen: new MessageChannel() global constructor unlinked/non-constructible — routes to stdlib symbol, breaks React scheduler init #4873
Description
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 constructorat runtime when it does link (i.e. when something else in the module graph pullsnode:worker_threadsso 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 codeBecause 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 incrates/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 incrates/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
- React / react-reconciler / scheduler — every app, at init. (Primary.)
- ink end-to-end (Compile
ink(React-based TUI framework) end-to-end viaperry.compilePackages#348) — this is the current first runtime wall, ahead of the known yoga-WASM rock. - Any browser-compat lib that feature-detects
MessageChannelviatypeofthennews it.
Related
- globals: expose MessageChannel, MessagePort, and BroadcastChannel #2597 (exposed the globals — the constructor
newpath is the gap left) - node:worker_threads: implement MessageChannel ports and receiveMessageOnPort #3157 (worker_threads MessageChannel ports)
- Compile
ink(React-based TUI framework) end-to-end viaperry.compilePackages#348 (ink end-to-end smoke test — where this surfaced)
Activity
fix(hir): route global new MessageChannel()/BroadcastChannel() to alw…
fix(runtime): #4873 — Object.setPrototypeOf spurious TypeError on eve…
Merge branch 'main' into fix/5837-numberformat
fix(runtime): #5837 — Intl.NumberFormat signDisplay/accounting, unit-…