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
js_stdlib_has_active_handles() in crates/perry-stdlib/src/common/async_bridge.rs decides whether the event loop stays alive. It is a hand-maintained chain of #[cfg(feature = "...-pump")] blocks, each of which hard-references one ext crate's has_active / inflight / has_pending symbol (net, tls, http-client, http-server, ws, zlib, fastify). The reference is a direct extern "C" call, defined only in the ext crate, which the compile driver links per-program.
The runtime already avoids this pattern one layer up. ensure_pump_registered() registers stdlib's aggregate with the runtime through a vtable:
So the runtime holds a function pointer and never names stdlib's internals. The stdlib -> ext seam does not use the same registration; it hard-wires every ext under a compile feature. That asymmetry is the root of a recurring bug class.
Why it is a problem
The gate and the ext archive must be kept in lockstep by hand. When they drift, one of two failures follows, and both have already shipped:
Undefined symbols at link. If a build enables external-http-client-pump in stdlib's full feature but the ext-http archive is not on the link line, every full-stdlib link fails with undefined js_ext_http_* / js_http_*. This is what 0.5.1220 shipped: fs/promises, http, fetch, and ws all fail to link, because any async program pulls the gate. Fixed in 0.5.1239 (fix(stdlib): drop external-http-client-pump from full — restore the ext-http link lockstep #5983) by moving the pump back out of full so it activates only when the program imports node:http and ext-http is linked. That restores the lockstep; it does not remove the need for one.
Loop exits early. The external-net-pump block carries the @perryts/mysql crashes inside perry run with 'js_box_get: invalid box pointer 0x0' #536 comment: a TS-source driver (@perryts/mysql via perry-ext-net) had its await new Promise(r => sock.on('connect', r)) exit early, because the bundled-net gate fired while the external pump's handles were invisible to it. Same shape, different pump.
Two separate pumps, the same "the gate references the wrong or absent source" failure. The design generates it.
A related asymmetry: ws and fetch have no-op fallbacks in perry-runtime/src/stdlib_stubs.rs, so a build without their real bodies degrades to "never pending" and links. The http-client keepalive symbols have no such stub, so http has no graceful-degradation path at all. It depends entirely on the lockstep being correct.
Proposed direction
Apply the registration pattern that already exists at the runtime -> stdlib seam to the stdlib -> ext seam. Each ext pump registers its own keepalive contributor when it initializes:
// in each ext crate's initjs_register_keepalive_contributor(js_ext_http_client_inflight);
js_stdlib_has_active_handles() then iterates the registered contributors and references no ext by name. Consequences:
A pump contributes to keepalive only when it is linked and initialized, so a program that never touches http never references http. This is what a user reported wanting for their own stack.
A build cannot fail to link on a pump symbol whose archive is absent, because nothing hard-references it.
A third-party or out-of-tree stack can register its own contributor with no change to stdlib.
The #[cfg(feature = "...-pump")] ladder in the gate collapses to one loop.
This removes both failure modes above at the source, rather than keeping the feature/archive lockstep correct by hand.
Version
Observed at HEAD (v0.5.1220-2468-g8c7932497, reported as 0.5.1520) and at v0.5.1220. The #5983 packaging fix is in HEAD; the design described here is unchanged by it.
Summary
js_stdlib_has_active_handles()incrates/perry-stdlib/src/common/async_bridge.rsdecides whether the event loop stays alive. It is a hand-maintained chain of#[cfg(feature = "...-pump")]blocks, each of which hard-references one ext crate'shas_active/inflight/has_pendingsymbol (net, tls, http-client, http-server, ws, zlib, fastify). The reference is a directextern "C"call, defined only in the ext crate, which the compile driver links per-program.The runtime already avoids this pattern one layer up.
ensure_pump_registered()registers stdlib's aggregate with the runtime through a vtable:So the runtime holds a function pointer and never names stdlib's internals. The stdlib -> ext seam does not use the same registration; it hard-wires every ext under a compile feature. That asymmetry is the root of a recurring bug class.
Why it is a problem
The gate and the ext archive must be kept in lockstep by hand. When they drift, one of two failures follows, and both have already shipped:
Undefined symbols at link. If a build enables
external-http-client-pumpin stdlib'sfullfeature but the ext-http archive is not on the link line, every full-stdlib link fails with undefinedjs_ext_http_*/js_http_*. This is what0.5.1220shipped:fs/promises,http,fetch, andwsall fail to link, because any async program pulls the gate. Fixed in0.5.1239(fix(stdlib): drop external-http-client-pump from full — restore the ext-http link lockstep #5983) by moving the pump back out offullso it activates only when the program importsnode:httpand ext-http is linked. That restores the lockstep; it does not remove the need for one.Loop exits early. The
external-net-pumpblock carries the @perryts/mysql crashes inside perry run with 'js_box_get: invalid box pointer 0x0' #536 comment: a TS-source driver (@perryts/mysqlviaperry-ext-net) had itsawait new Promise(r => sock.on('connect', r))exit early, because the bundled-net gate fired while the external pump's handles were invisible to it. Same shape, different pump.Two separate pumps, the same "the gate references the wrong or absent source" failure. The design generates it.
A related asymmetry:
wsandfetchhave no-op fallbacks inperry-runtime/src/stdlib_stubs.rs, so a build without their real bodies degrades to "never pending" and links. The http-client keepalive symbols have no such stub, so http has no graceful-degradation path at all. It depends entirely on the lockstep being correct.Proposed direction
Apply the registration pattern that already exists at the runtime -> stdlib seam to the stdlib -> ext seam. Each ext pump registers its own keepalive contributor when it initializes:
js_stdlib_has_active_handles()then iterates the registered contributors and references no ext by name. Consequences:#[cfg(feature = "...-pump")]ladder in the gate collapses to one loop.This removes both failure modes above at the source, rather than keeping the feature/archive lockstep correct by hand.
Version
Observed at
HEAD(v0.5.1220-2468-g8c7932497, reported as0.5.1520) and atv0.5.1220. The #5983 packaging fix is in HEAD; the design described here is unchanged by it.