fix: two augmentation-compat regressions vs Node 25.8.1 - #556
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Important
The three fixes are sound and well-verified, but widening the source-maps gate to all of Node 26.x left two references describing the old 26.2.x-only behavior — including a user-facing docs page that now states the wrong versions.
Reviewed changes — three independent augmentation-compat fixes from the v0.5.0 Deno-corpus regression audit, plus a test-only process-group reap.
- Widen the source-maps gate to all of Node 26.x —
source_maps_safenow returnsnode_version.major() != 26; the doc comment and unit test are updated in-diff to reflect the 2026-07-23 sweep (26.0–26.5 all rethrow no-messageassert.ok(false)asTypeError; 24.17/25.9 clean). - Stop materializing undici at preload time in
polyfills.cjs— theFile/Blobbackfill and theMessageEvent.ports-freeze wrap now probe existence withininstead oftypeof, and theMessageEventwrap is additionally version-gated to skip on Node >= 22.3.0 (where the native undici-backedMessageEventalready freezes.ports). - Reap the child's process group on exit and timeout in the
node_compattest — the child is spawned as its own process-group leader (process_group(0)) and a newreap_groupSIGKILLs the negative pgid, so forked servers/workers/grandchildren aren't orphaned to PID 1 (the failure that pulled the compat corpus from CI).
⚠️ Docs still describe the old 26.2.x-only source-maps gate
The TypeScript runtime docs page still says source maps are withheld only on the "26.2.x patch band" and that "Every other version (18.19 through 26.1, and 26.3+) gets source maps." After this PR that's false — source maps are now withheld across all of 26.x, so 26.0 / 26.1 / 26.3 / 26.4 / 26.5 do NOT get remapped stack traces. A user on the recommended newest Node major will read behavior the binary no longer exhibits.
Technical details
# Docs describe the old 26.2.x-only source-maps gate
## Affected sites
- `site/content/docs/runtime/typescript.mdx:97` — "except the **26.2.x** patch band" and "Every other version (18.19 through 26.1, and 26.3+) gets source maps" now contradict `source_maps_safe` (`major() != 26`).
## Required outcome
- The page states that source maps are withheld across the entire Node 26.x line (not just 26.2.x), and does not claim 26.1/26.3+ get source maps.
## Open questions for the human (optional)
- Whether to keep the version specifics inline or generalize to "the Node 26 line" so the copy doesn't need re-editing when a fixed 26.x lands.ℹ️ Stale code comment in compute_inject_flags
The inline comment at the ALWAYS_INJECT loop still reads "withheld on Node 26.2.x (see source_maps_safe): there it turns a no-message AssertionError into a TypeError." It restates the pre-PR gate; the authoritative doc comment on source_maps_safe itself was updated, but this second copy wasn't.
Technical details
# Stale 26.2.x comment at the injection site
## Affected sites
- `crates/nub-core/src/node/flags.rs:161-162` — "withheld on Node 26.2.x" / "there it turns a no-message AssertionError into a TypeError"; not in the diff, so it wasn't updated alongside the gate.
## Required outcome
- The comment reflects the widened gate (all of 26.x), or defers to `source_maps_safe`'s now-authoritative doc comment rather than duplicating the band.Claude Opus | 𝕏
`globalThis.MessageEvent` and `globalThis.File` are lazy undici-backed globals.
The preload probed them with `typeof`, and that value read synchronously
materializes undici plus its whole http/http2/tls/crypto/zlib/worker closure —
process.moduleLoadList went to 227 entries where plain node has 110, on every
nub startup.
Probe with `in` instead, which sees the lazy property without firing its getter,
and version-gate the MessageEvent.ports freeze: Node freezes `.ports` natively
from 22.3.0, so the wrap is a no-op across the entire fast tier (floor 22.15).
Only the pre-22.3 compat tier still installs it, where the load is the accepted
cost of the correctness it buys.
The block directly above already documented this exact trap for Temporal
("Touching globalThis.Temporal here would defeat that laziness"); MessageEvent
was missed.
Measured: moduleLoadList 227 -> 114 (node: 110), undici absent at startup, and
`.ports` still frozen on delivery. Also removes the startup cost that made
several profiler-sensitive Node compat tests exceed their harness budget, and
the per-Worker accumulation behind a 5.5x RSS growth over 60 workers.
The harness killed only the leader on timeout. Node compat tests routinely fork servers, workers and `spawn(process.execPath)` grandchildren, which were then orphaned to PID 1 and kept running; across ~2,554 tests at up to 16-way parallelism they accumulated until the CI runner died. That is why the compat corpus job was removed from CI (2026-06-03) and never restored, leaving the suite with no automated signal. Spawn each test as its own process-group leader and SIGKILL the negative pgid, at both the timeout path and the clean-exit path — a leader can exit zero while leaving a detached server behind. `tests/cross-runtime/run.mjs` already solved this the same way; this ports it to the Rust harness. Verified against a leaking fixture: a non-detached child left running by the test is reaped by the group kill. A `detached: true` grandchild calls setsid and escapes any pgid reap by construction; that is inherent and out of scope.
516f422 to
97379de
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the PR narrowed from three fixes to two since the prior pullfrog review; the source-maps gate widening in flags.rs (and its typescript.mdx doc reference, which the prior review flagged) was dropped, so those findings no longer apply. The two remaining fixes are correct and self-contained.
- Stop materializing undici at preload time in
polyfills.cjs— theFile/Blobbackfill and theMessageEvent.ports-freeze wrap now probe existence withininstead of a value read, and theMessageEventwrap is additionally version-gated to skip on Node >= 22.3.0 (where the native undici-backedMessageEventalready freezes.ports), so undici is no longer forced at startup on the fast tier. - Reap the child's process group on exit and timeout in the
node_compattest — the child is spawned as its own process-group leader (process_group(0),#[cfg(unix)]) and a newreap_groupSIGKILLs the negative pgid, so forked servers/workers/grandchildren aren't orphaned to PID 1 (the failure that pulled the compat corpus from CI).
Verification notes for this pass:
- The
File/Blobin-probe still runs the backfill on the floor (where the globals are genuinely absent) and skips it on modern tiers without firing the lazy getter; the inner per-nametypeof … === "undefined"guard is correct on the floor. - The
MessageEventversion gate (major > 22 || (major === 22 && minor >= 3)) skips the wrap across the entire fast tier (floor 22.15), matching the confirmed 22.3.0 native-freeze point (nodejs/node#52370). - The process-group reap ordering is correct on both paths (clean-exit reaps the leader via
try_waitthen signals surviving members; timeout doesreap_group→child.kill()→child.wait()), andlibcis acfg(unix)dependency available to the integration test.
Claude Opus | 𝕏

Two verified fixes from the v0.5.0 Deno-corpus regression audit. Nine-cause breakdown:
wiki/research/nub-v0.5-augmentation-regressions.md.fix(runtime): probe lazyMessageEvent/Filewithin, nottypeof, so undici isn't loaded at startup (moduleLoadList227→114; Node 110).test(node-compat): reap the child's process group on timeout/exit (orphaned grandchildren killed the CI runner; job pulled 2026-06-03).