Skip to content

Compiled $MessagePort.Start() unconditionally Refs the event loop → started main-thread MessageChannel port hangs the process (dual-mode divergence) #1254

Description

@nickna

Summary

The compiled $MessagePort.Start() unconditionally takes a keep-alive Ref on the $EventLoop, so a started, unclosed main-thread MessageChannel port keeps the process alive forever. The interpreter's SharpTSMessagePort.Start() only Refs the loop for a cross-thread (worker-transferred) port, so a plain in-process MessageChannel port does not keep the loop alive. This is a compiled-vs-interpreter divergence: the same program hangs when compiled but exits under the interpreter.

Surfaced while implementing #1077 (PR #1253).

Repro

import { MessageChannel } from "worker_threads";
const { port1, port2 } = new MessageChannel();
port2.on("message", (v: any) => console.log("got:" + v)); // implicitly starts port2
port1.postMessage("hi");
// no close() anywhere
  • Interpreted: prints got:hi, then the process exits.
  • Compiled (--compile): prints got:hi, then hangs forever (the started port holds a loop Ref that is never released).

Root cause

  • Compiled: RuntimeEmitter.MessageChannel.csEmitMessagePortStart emits this.Ref() unconditionally; the Ref is only released by Close().
  • Interpreter: Runtime/Types/SharpTSMessagePort.csStart() only Refs when _crossThread (see the if (_crossThread && !_loopRefed ...) guard, added for Compiled Worker: transferList / cross-boundary MessagePort transfer is ignored #406). A plain MessageChannel port is not cross-thread, so it never Refs.

The emitted $MessagePort has no _crossThread/bridge-aware ref gating equivalent — it always Refs on start.

Impact

Suggested fix direction

Mirror the interpreter: don't keep the event loop alive for an idle, in-process $MessagePort with a drained queue. This likely means introducing a cross-thread/bridge distinction into the emitted $MessagePort (as the interpreter has via _crossThread) so only worker-transferred ports Ref the loop. Interacts with #406 (transferred-port liveness) and #465 (bridge-driven _onEnqueue ports) — those paths must keep their existing keep-alive behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions