Skip to content

v0.90: Node 10 parity pass 4 — npm install <local-tarball> end-to-end

Choose a tag to compare

@cellularmitosis cellularmitosis released this 10 May 22:13
· 35 commits to main since this release

v0.90 closes the cacache pump hang from v0.89 and lands the final
plumbing piece that unblocks npm install <local-tarball>
end-to-end. (TLS-to-Cloudflare-fronted endpoints for large responses
still need work — deferred to a later pass.)

Driven by docs/sessions/2026-05-10-session-4-node-10-parity-pass-4/notes.md.

Summary

npm install <local-tarball> now works end-to-end on PPC under
ionpower-node — first verified npm install on Tiger PPC since the
whole npm-6 bootstrap project began. The fix bag here is small (2
waves, +2 smokes), but each closes a real install-pipeline contract
gap.

Tests: 467 (v0.89) → 469 (v0.90, +2 new). G3 + G4 + G5 triad-builds
clean.

What's new (pass 4)

_Stream.prototype.pipe attaches 'end' BEFORE 'data'

The cacache pump hang in npm install was a race between cacache's
fast pump and pacote's slow rimraf+mkdirp chain inside tryExtract.
By the time pacote called tarStream.pipe(xtractor), the upstream
PassThrough had been written-to AND ended (s.ended=true,
endEmitted=false, buffer holds the chunk). The OLD pipe order was:

src.on('data', dataHandler);     // auto-resume drains buffer
                                 // and synchronously emits 'end'
                                 // to whatever 'end' listeners
                                 // exist NOW (eos's lingering one)
src.on('end',  endHandler);      // attached AFTER 'end' fired!
src.on('error', errorHandler);

eos's leftover 'end' listener (from cacache's pump destroyer setup)
caused listenerCount('end') > 0 to be true at drain time, so the
end-emission path picked the SYNC branch and fired 'end' to the
wrong subscriber. tar.x's writable side never got .end() called.
tar waited forever for input termination → "cb() never called".

Fix: reorder to attach 'end' (and 'error') BEFORE 'data'. The
inline drain now emits 'end' to pipe's actual end-handler, which
cascades dest.end() correctly. Same reorder applied to
fs.createReadStream's own pipe for symmetry. Smoke:
test/stream_resume_defer_smoke.js.

fs.fchown / fchownSync

With the pump unblocked, npm install reached tar.x's unpack and
immediately crashed:

fs.fchown is not a function
  @.../tar/lib/unpack.js:426

tar's unpacker calls fs.fchown(fd, uid, gid, cb) to restore
ownership on freshly extracted files (paired with fs.fchmod and
fs.futimes, both of which we already had). POSIX fchown(2) is
trivial to wire. Now in fs.fchown / fchownSync / fs.promises.fchown.
Smoke: test/fs_fchown_smoke.js.

End-to-end: npm install <local-tarball>

With pass-3 fixes plus the two waves above:

$ /opt/ionpower-node-0.90/bin/node \
    /Users/macuser/tmp/run-npm-trace.js install \
    /Users/macuser/tmp/mri-1.2.0.tgz --no-audit
+ mri@1.2.0
added 1 package from 1 contributor in <wall-time>s

The full pipeline — fetch / resolve / ideal-tree / cache-write /
extract / link / lifecycle — runs cleanly. The mri tarball's
contents land in node_modules/mri/ with intact mtime, permissions,
and ownership.

What's still broken

  • TLS to Cloudflare-fronted endpoints (LARGE responses). Smaller
    Cloudflare responses (e.g. cloudflare.com returns a 301 redirect
    with 167-byte body) work cleanly. Larger Cloudflare responses
    (e.g. www.cloudflare.com's ~100KB HTML) hang at TLS read after
    the initial handshake. New hypothesis from pass-4 probing: it's
    a multi-record reassembly issue specific to large bodies, not the
    ALPN absence hypothesis from pass-3's notes (cloudflare.com works
    fine without ALPN). Deferred to a future pass.

Compatibility

Additive only. pipe() listener order is a behavior change but
not an API change — code that depended on observing 'data' before
'end' was non-portable.

Same SpiderMonkey, same OpenSSL, same JIT. Existing v0.89 scripts
run unchanged.

Tarballs

  • ionpower-node-0.90-g3-ppc.tar.gz — G3 (PPC 750)
  • ionpower-node-0.90-g4-ppc.tar.gz — G4 (PPC 7450)
  • ionpower-node-0.90-g5-ppc.tar.gz — G5 (PPC 970)

Each pairs with the matching mozjs-45-ionpower-{g3,g4,g5}.tar.gz
from the v0.87 release
(SpiderMonkey hasn't changed since then; reuse those).