You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Compiled $MessagePort.Start() unconditionally Refs the event loop → started main-thread MessageChannel port hangs the process (dual-mode divergence) #1254
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.
The emitted $MessagePort has no _crossThread/bridge-aware ref gating equivalent — it always Refs on start.
Impact
Any compiled program that attaches a 'message'/'messageerror' listener to a MessageChannel port and relies on natural loop quiescence to exit will hang unless it explicitly close()s the port.
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.
Summary
The compiled
$MessagePort.Start()unconditionally takes a keep-aliveRefon the$EventLoop, so a started, unclosed main-threadMessageChannelport keeps the process alive forever. The interpreter'sSharpTSMessagePort.Start()only Refs the loop for a cross-thread (worker-transferred) port, so a plain in-processMessageChannelport 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
got:hi, then the process exits.--compile): printsgot:hi, then hangs forever (the started port holds a loop Ref that is never released).Root cause
RuntimeEmitter.MessageChannel.cs→EmitMessagePortStartemitsthis.Ref()unconditionally; the Ref is only released byClose().Runtime/Types/SharpTSMessagePort.cs→Start()only Refs when_crossThread(see theif (_crossThread && !_loopRefed ...)guard, added for Compiled Worker: transferList / cross-boundary MessagePort transfer is ignored #406). A plainMessageChannelport is not cross-thread, so it never Refs.The emitted
$MessagePorthas no_crossThread/bridge-aware ref gating equivalent — it always Refs on start.Impact
'message'/'messageerror'listener to aMessageChannelport and relies on natural loop quiescence to exit will hang unless it explicitlyclose()s the port.MessageChanneltests mask this by closing both ports inside their listeners (e.g.MessageChannel_PortOnMessage_ReceivesPostedValue), and worker_threads: compiled main-thread MessageChannel messageerror + synchronous receiveMessageOnPort #1077's newmessageerrortest had to follow the same convention.Suggested fix direction
Mirror the interpreter: don't keep the event loop alive for an idle, in-process
$MessagePortwith 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_onEnqueueports) — those paths must keep their existing keep-alive behavior.