Skip to content

node:stream: sync the stream suite to Node v26.3.0 and fix the gaps it surfaces — stream/iter + zlib/iter behind --experimental-stream-iter, read()-one-buffer semantics, compose/pipeline/web-adapter/http/fs/net parity (247/251 upstream tests, +63 vendored)#31826

Open
cirospaciari wants to merge 28 commits into
mainfrom
claude/streams-node26-sync

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Jun 4, 2026

Copy link
Copy Markdown
Member

Brings the node:stream suite to full parity with Node v26.3.0 by vendoring every upstream stream test, implementing the experimental stream/iter / zlib/iter API behind --experimental-stream-iter, and fixing the runtime gaps the tests expose.

parallel sequential total
stream 245/249 (98.4%) 2/2 247/251 (98.4%)

All 251 upstream stream test files are now vendored (63 added, 64 resynced to v26.3.0 content); before this PR 124 were byte-identical to upstream and 188 existed at all.

New feature: stream/iter + zlib/iter (experimental)

Node v26's iterable-streams API, ported (~7.5k lines: push/pull/from/share/broadcast/transform/consumers/classic/duplex/ringbuffer + entry modules), gated like Node:

  • Resolution-level gating on --experimental-stream-iter: without the flag, node:stream/iter reports No such built-in module and bare stream/iter falls through to filesystem resolution, byte-matching Node's errors.
  • Readable.prototype[Symbol.for("Stream.toAsyncStreamable")] interop with Node's lazy flag check (ERR_STREAM_ITER_MISSING_FLAG).
  • The transform stage rides bun's native zlib/brotli/zstd bindings.
  • 40/42 upstream test-stream-iter-* files pass (the 2 disabled tests also required the eval-entry exception fix below).

Behavior changes

  • read() returns one buffered chunk at a time instead of concatenating the whole buffer (stream: readable read one buffer at a time nodejs/node#60441): the howMuchToRead fast path for buffers.
  • read(n) with a decoder no longer over-returns: fromList compared n against the buffer-array length instead of the chunk length.
  • resume()/pause() are no-ops on destroyed streams.
  • Readable.prototype.compose moved off the operators registry onto the prototype returning the composed Duplex directly (Node v26 layout); the registry wrapper hid the writable side.
  • Pipeline error precedence: an internal AbortError no longer wins over the real failure.
  • Readable.from destroy chaining: _destroy chains the instance's original _destroy (e.g. duplexify's abort) before closing the iterator, and duplexify's fromAsyncGen.destroy unblocks a generator parked on its source — errors now propagate through nested .compose() chains instead of hanging.
  • Duplex.from({ readable, writable }): a readable-side error destroys the writable side (was destroying the readable itself).
  • write(string, "buffer") throws ERR_UNKNOWN_ENCODING.
  • Web adapters: Duplex.toWeb/Readable.toWeb support byte streams (readableType: 'bytes' + the deprecated type alias with DEP0201); ReadableStream.cancel() settles pending BYOB reads with {done: true}; readable completion no longer waits for a half-open writable side, so the web stream closes when the readable side ends.
  • http client: response EOF is pushed synchronously with the final chunk (one 'readable' for data+EOF, like Node's parser); ClientRequest.destroy(err) emits 'error' on the request and destroys an incomplete response with ECONNRESET (Node's socketCloseListener semantics).
  • http server: the IncomingMessage.socket getter no longer resurrects a FakeSocket after the stream destroyer detached it — req.socket = null now signals "connection must outlive the request" like Node, so res.end() after destroy(req) still delivers the body; detachSocket tolerates the detached state.
  • fs.ReadStream: the read callback bails out when the stream was destroyed mid-read (kIsPerformingIO/kIoDone, Node-verbatim), so close() between reads yields ERR_STREAM_PREMATURE_CLOSE instead of a clean end, and _destroy waits for in-flight IO before closing the fd.
  • net.Socket({ fd }) adopts pipe/character-device/file/socketpair fds with synchronous write(2)-backed writes — Node's effective stdio-socket semantics (data survives an immediate process.exit()). Network-socket fd adoption still needs the handle layer.
  • async_hooks: createHook init events now fire for TickObject resources — one per process.nextTick() call, with a single array-length check on the disabled hot path. Init-only hooks no longer emit the "not implemented" warning. stdio writers are born constructed (no construct TickObjects; Node's stdio streams never have _construct).
  • --expose-internals: vendored node tests can require internal/* modules, served from bun:internal-for-testing via a require interceptor installed by the test harness; new internal/async_context_frame and internal/async_hooks shims back the finished() ALS tests.
  • CJS eval entries report uncaught errors: the 3-arg JSC::evaluate overload silently discards exceptions into an unused out-param, so bun -e 'require("./missing")' exited 0 with no output; it now prints the resolution error and exits nonzero (also un-hangs --print for the same case).
  • Error-code parity: brotli decoder codes match Node (ERR__ERROR_FORMAT_PADDING_2 style — "ERR_" + enum suffix with its leading underscore); ERR_CRYPTO_UNKNOWN_CIPHER message is Unknown cipher; Unknown encoding: values format like Node's %s (objects inspect to {}); new codes ERR_OPERATION_FAILED, ERR_STREAM_ITER_MISSING_FLAG, ERR_INVALID_ARG_VALUE RangeError variant. src/jsc/ErrorCode.rs had drifted from the generated C++ table (191 stale ordinals — native code threw adjacent error codes, e.g. ERR_INVALID_HANDLE_TYPE for ERR_INVALID_HTTP_TOKEN); it is renumbered from ErrorCode+List.h.

Known limitations / follow-ups

  • test-stream-wrap{,-drain,-encoding} need internal/js_stream_socket, i.e. a net.Socket({ handle }) libuv-handle compatibility layer — the same subsystem TLS-over-arbitrary-duplex wants; left as the follow-up.
  • test-stream-pipeline needs per-socket-read http client body delivery: the native fetch path coalesces response-body segments between JS pulls (one merged buffer instead of Node's one 'data' per TCP read). Pre-existing, proven on the unmodified baseline; a fix wants an opt-in per-segment mode on the native response stream to avoid regressing fetch throughput.
  • AsyncContextFrame.current() returns undefined (bun tracks async context natively and never materializes frame objects); enabled is honestly true. Tests coupling enabled to a non-null current() would not pass.
  • async_hooks init is emitted for TickObject only; promise/timer/native resources and before/after/destroy remain unimplemented (and still warn).
  • 2 vendored files (test-stream-readable-default-encoding-error, held-back pair) run pre-v26 content because their upstream versions require internals not in the exposed map.

Testing

Validated locally with the debug build:

  • Full vendored test-stream sweep (CI-parity mode): 247/251 files pass; the 4 failures are the documented native gaps above, each verified pre-existing on an unmodified baseline build.
  • Blast-radius A/B against the baseline binary: full vendored test-http sweep (488 files — the only deltas were the ErrorCode-ordinal regressions, fixed in this PR), vendored zlib/crypto tests, and the bun-native console / process / fs / net / child_process / async_hooks / web-streams suites — identical or better everywhere.
  • Eval-entry matrix: -e happy path, missing-require error+exit-1, stdin scripts, --print.

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator
Updated 9:37 AM PT - Jun 10th, 2026

@robobun, your commit 699fc81 has 1 failures in Build #61758 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31826

That installs a local version of the PR into your bun-31826 executable, so you can run:

bun-31826 --bun

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds experimental stream/iter modules, web/classic adapters, pull/push/share/broadcast primitives, compression transforms, runtime gating for node:stream/iter, async_hooks–nextTick init calls, stream/http/net adjustments, error-code/binding updates, process exception hardening, and extensive test coverage.

Changes

Experimental Streams Iteration Stack

Layer / File(s) Summary
Core stream/iter modules and types
src/js/internal/streams/iter/*, src/js/internal/streams/*, src/js/internal/*
Implements push, pull, from, fromSync, broadcast, share, duplex, ringbuffer, consumers, transforms, utilities and protocol symbols used by stream/iter.
Adapters & interop
src/js/internal/streams/iter/classic.ts, src/js/internal/webstreams_adapters.ts, src/js/internal/streams/duplex.ts, src/js/internal/streams/readable.ts, src/js/builtins/ReadableStreamInternals.ts
Classic Readable/Writable adapters, webstreams BYOB support and adapters, Duplex.toWeb options, Readable to async-iterator protocol and compose interop.
Transforms (zlib/brotli/zstd)
src/js/internal/streams/iter/transform.ts, src/js/node/zlib.iter.ts
Native-backed async/sync compression/decompression transforms with factories exported to node:zlib/iter.
Runtime & gating
src/js/node/stream.iter.ts, src/resolve_builtins/*, src/runtime/*, src/runtime/cli/*
New entrypoints node:stream/iter and node:zlib/iter, hardcoded-module mappings, CLI --experimental-stream-iter flag and runtime gating.
nextTick / async_hooks wiring
src/js/internal/async_hooks.ts, src/js/internal/async_hooks_tick.ts, src/js/builtins/ProcessObjectInternals.ts, src/js/internal/async_context_frame.ts, src/js/node/async_hooks.ts
Stable tick-init hook bridge, per-nextTick init-hook invocation allocating async ids, AsyncContextFrame export for tests, and per-hook enable/disable registration logic.
Core stream/http/net fixes
src/js/internal/fs/streams.ts, src/js/internal/http.ts, src/js/node/_http_incoming.ts, src/js/node/_http_client.ts, src/js/node/_http_server.ts, src/js/node/net.ts
ReadStream IO coordination and shutdown gating, synchronous EOF emission option, IncomingMessage destroy refinements, ClientRequest destroy behavior, detachSocket guard, and Socket fd-adoption for sync-write fast paths.
Error codes & bindings
src/jsc/ErrorCode.rs, src/jsc/bindings/ErrorCode.cpp, src/jsc/bindings/ErrorCode.ts
Regenerated ErrorCode table with new/renumbered codes (e.g., ERR_OPERATION_FAILED, ERR_STREAM_ITER_MISSING_FLAG, RangeError variant) and updated error message formatting paths.
Process exception-scope hardening
src/jsc/bindings/BunProcess.cpp, src/jsc/bindings/JSCommonJSModule.cpp
Wraps lazy property builders and eval-entry-point evaluation in top exception scopes to avoid leaking exceptions during property reification or module evaluation.
Tests & harness updates
test/js/node/test/parallel/*, test/js/node/stream/*, test/expectations.txt, test/js/bun/*
Extensive new/updated tests for stream/iter APIs, transforms, interop, backpressure/abort semantics, and expectation adjustments; adds require interceptor for --expose-internals testing.

Suggested reviewers:

  • Jarred-Sumner
  • alii
  • RiskyMH

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Upgrade reported Node.js version to 26.3.0 #31818 - Also syncs node:stream to Node v26.3.0, implementing the same behavioral changes (read() one-chunk semantics, compose() on prototype, Duplex.from destroy fix, BYOB Readable.toWeb/DEP0201) across the same stream internal files
  2. fs: reject async iterator with ERR_STREAM_PREMATURE_CLOSE when destroy() is called during iteration #30920 - Fixes the same fs.ReadStream destroy-mid-read bug in src/js/internal/fs/streams.ts (async iterator should reject with ERR_STREAM_PREMATURE_CLOSE when destroy() is called during iteration)

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/js/internal/streams/operators.ts:397-400 — Nit: removing compose from streamReturningOperators leaves function compose(stream, options) (line 21) and its staticCompose require (line 6) unreferenced — Readable.prototype.compose now lives in readable.ts with its own lazy require. The isWritable/isNodeStream/addAbortSignalNoValidate imports (lines 7-8) are also only used by that function, so all of these can be deleted.

    Extended reasoning...

    What

    This PR moves Readable.prototype.compose off the operators registry and onto the prototype directly in readable.ts (so it returns the composed Duplex rather than wrapping it in a Readable). The diff to operators.ts removes compose from the streamReturningOperators export object — but it leaves the function compose(stream, options) definition at line 21 in place, along with the staticCompose require at line 6 that only this function uses.

    Step-by-step

    1. Before this PR, operators.ts defined function compose and exported it via streamReturningOperators.compose; stream.ts then iterated that object to install Readable.prototype.compose as a registry-wrapped operator.
    2. This PR adds Readable.prototype.compose = function compose(stream, options) { … } directly in src/js/internal/streams/readable.ts:1289, with its own composeImpl ??= require("internal/streams/compose") lazy load.
    3. The diff hunk at operators.ts:397-400 removes compose, from streamReturningOperators so the registry no longer installs a competing prototype method.
    4. After that removal, grepping operators.ts shows compose referenced only at line 21 (the definition) and line 33 (staticCompose(this, stream) inside its own body). Nothing else in the file — and nothing outside it, since the function was never exported by name — references it.
    5. The staticCompose import at line 6 is used solely at line 33 inside compose. Likewise, addAbortSignalNoValidate (line 7) is used only at line 37, and isWritable / isNodeStream (line 8) only at line 29 — all inside the now-dead function body.

    Why nothing prevents it

    compose is a top-level function declaration with no other call sites, so it simply sits in the module as dead code. Bun's builtin bundler may or may not tree-shake the function and its CommonJS-style require("internal/streams/compose") import; either way there is no observable runtime behavior change.

    Impact

    None at runtime — this is purely a maintenance/cleanliness issue. The dead function is ~20 lines plus four unused import bindings across three require calls. It's mildly confusing to leave a compose here when the real implementation has moved to readable.ts, but it cannot cause incorrect behavior.

    Fix

    Delete function compose(stream, options) { … } (lines 21-41) and the now-unused imports: const staticCompose = require("internal/streams/compose"); (line 6), const { addAbortSignalNoValidate } = require("internal/streams/add-abort-signal"); (line 7), and const { isWritable, isNodeStream } = require("internal/streams/utils"); (line 8).

Comment thread src/jsc/ErrorCode.rs Outdated
Comment thread src/js/internal/streams/iter/classic.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/internal/streams/iter/broadcast.ts`:
- Around line 146-147: Replace truthy checks that treat falsy cancellation
reasons as absent by explicitly checking for undefined: change occurrences of
"if (self.#error)" to "if (self.#error !== undefined)" (and similarly for the
abort-idempotency flag, e.g., replace "if (this.#aborted)" with "if
(this.#aborted !== undefined)"). Update all spots in broadcast.ts that currently
use truthy checks on private fields (notably the self.#error checks and the
abort/idempotency check) so cancel(0), cancel(""), or cancel(false) correctly
trigger rejection instead of returning kDone.
- Line 528: The end(options) method in broadcast.ts has an unused parameter
causing eslint no-unused-vars; rename the parameter to _options (e.g.,
end(_options)) or prefix it with an underscore to silence the linter, or remove
the parameter entirely if the method signature isn't required by an interface;
update any internal references to that parameter accordingly and keep the method
body unchanged.

In `@src/js/internal/streams/iter/classic.ts`:
- Around line 511-517: The sync stub methods writeSync and writevSync are
triggering eslint(no-unused-vars); rename their parameters to unused-prefixed
names (e.g., _chunk for writeSync and _chunks for writevSync) or otherwise
prefix them with an underscore so the linter recognizes them as intentionally
unused, leaving the bodies returning false unchanged; update the function
signatures for writeSync(chunk) and writevSync(chunks) to use the new names
(_chunk, _chunks) to resolve the lint errors.

In `@src/js/internal/streams/iter/duplex.ts`:
- Around line 112-123: The abort listener currently stays registered if both
endpoints close before abort; keep a reference to the listener (abortBoth) as
you already have, attach it with signal.addEventListener("abort", abortBoth, {
__proto__: null, once: true }), and ensure you call
signal.removeEventListener("abort", abortBoth) when both channels are closed
(e.g., in the completion/close handlers for the aWriter and bWriter endpoints)
so the listener and its captured channel state can be released; also call
removeEventListener inside abortBoth after failing both writers to guarantee
cleanup when abort actually fires.

In `@src/js/internal/streams/iter/from.ts`:
- Around line 289-310: The async-iterable branch can emit arbitrarily large
batches (via isUint8ArrayBatch and the accumulated `batch` from
normalizeAsyncValue), bypassing FROM_BATCH_SIZE; modify the logic in the
isAsyncIterable path so that pre-batched arrays from isUint8ArrayBatch are split
into chunks of at most FROM_BATCH_SIZE before yielding, and when collecting
normalized chunks from normalizeAsyncValue push into a temporary buffer and
yield slices of size FROM_BATCH_SIZE as they fill (yield any final partial slice
at the end). Keep the existing fast path for single Uint8Array (isUint8Array
yielding [value]) but ensure all emitted batches never exceed FROM_BATCH_SIZE.
Use the existing symbols isAsyncIterable, isUint8ArrayBatch, isUint8Array,
normalizeAsyncValue and the FROM_BATCH_SIZE constant to locate and implement the
changes.

In `@src/js/internal/streams/iter/pull.ts`:
- Around line 870-875: The code calls writer.writev(batch, opts).then(...)
assuming a Promise; change this to handle non-Promise returns by normalizing the
result (e.g., assign result = writer.writev(batch, opts) and use
Promise.resolve(result).then(...) or check for a thenable before calling .then)
so that if writev returns undefined or a non-Promise the byte-counting loop
still runs without throwing; update the same pattern if writer.write(...) is
used elsewhere in this pull flow to ensure consistent handling.

In `@src/js/internal/streams/iter/push.ts`:
- Line 609: The method end(options) declares an unused parameter causing lint
failures; either remove the parameter or rename it to _options (or /* options
*/) in the end function signature to mark it as intentionally unused. Locate the
end function declaration (symbol: end) and update its signature to drop the
options argument or underscore it, and run tests/lint to confirm the warning is
resolved.

In `@src/js/internal/streams/iter/transform.ts`:
- Around line 586-610: The sync transform loop currently only finalizes when
receiving a null batch and can finalize multiple times; add a boolean guard
(e.g., finalized) alongside the existing async pattern so finalization (calling
processSyncInput(..., finishFlag) and draining via drainBatch()) happens exactly
once when the source ends or when a null flush marker is seen; update the for-of
loop over source to check/trigger finalization on end-of-loop (before leaving
the try) and ensure the finally block also uses the finalized guard to avoid
duplicate finalize calls—modify references in this function to processSyncInput,
drainBatch, pending, pendingBytes, BATCH_HWM, finishFlag and processFlag
accordingly.

In `@src/js/internal/webstreams_adapters.ts`:
- Around line 441-445: Guard against null/undefined `options` before
dereferencing `options.type`: first check that `options` is an object (e.g. if
(options != null && options.type !== undefined) { validateOneOf(options.type,
"options.type", ["bytes", undefined]); } const isBYOB = options != null &&
options.type === "bytes";) Update the code paths that use `options.type`,
referencing the existing `validateOneOf` and `isBYOB` identifiers so property
access is always preceded by the null check.

In `@src/js/node/net.ts`:
- Around line 744-757: The adoption path for FIFO/character-device/file
descriptors can leave the stream readable forever if options.readable === true
but there is no backing reader (_handle), so update the fd-adoption branch
(where stats.isFIFO(), stats.isCharacterDevice(), stats.isFile(), and
stats.isSocket()) to detect absence of a reader (e.g. this._handle or
equivalent) and explicitly close the readable side in that case by calling
this.push(null) and this.read(0); ensure this logic runs regardless of
options.readable value when no _handle exists so the stream does not hang
(adjust the condition around kSyncWriteFd/_write/_writev assignment and the
push(null)/read(0) calls to handle the "readable but no _handle" case).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9deac0d9-283a-4f76-a5f4-25d4408e8ad6

📥 Commits

Reviewing files that changed from the base of the PR and between 977f054 and 48689ca.

📒 Files selected for processing (171)
  • src/js/builtins/ProcessObjectInternals.ts
  • src/js/builtins/ReadableStreamInternals.ts
  • src/js/internal-for-testing.ts
  • src/js/internal/async_context_frame.ts
  • src/js/internal/async_hooks.ts
  • src/js/internal/async_hooks_tick.ts
  • src/js/internal/fs/streams.ts
  • src/js/internal/http.ts
  • src/js/internal/streams/duplex.ts
  • src/js/internal/streams/duplexify.ts
  • src/js/internal/streams/from.ts
  • src/js/internal/streams/iter/broadcast.ts
  • src/js/internal/streams/iter/classic.ts
  • src/js/internal/streams/iter/consumers.ts
  • src/js/internal/streams/iter/duplex.ts
  • src/js/internal/streams/iter/from.ts
  • src/js/internal/streams/iter/pull.ts
  • src/js/internal/streams/iter/push.ts
  • src/js/internal/streams/iter/ringbuffer.ts
  • src/js/internal/streams/iter/share.ts
  • src/js/internal/streams/iter/transform.ts
  • src/js/internal/streams/iter/types.ts
  • src/js/internal/streams/iter/utils.ts
  • src/js/internal/streams/operators.ts
  • src/js/internal/streams/pipeline.ts
  • src/js/internal/streams/readable.ts
  • src/js/internal/streams/writable.ts
  • src/js/internal/webstreams_adapters.ts
  • src/js/node/_http_client.ts
  • src/js/node/_http_incoming.ts
  • src/js/node/_http_server.ts
  • src/js/node/async_hooks.ts
  • src/js/node/net.ts
  • src/js/node/stream.iter.ts
  • src/js/node/zlib.iter.ts
  • src/jsc/ErrorCode.rs
  • src/jsc/bindings/ErrorCode.cpp
  • src/jsc/bindings/ErrorCode.ts
  • src/jsc/bindings/JSCommonJSModule.cpp
  • src/resolve_builtins/HardcodedModule.rs
  • src/resolve_builtins/lib.rs
  • src/runtime/jsc_hooks.rs
  • src/runtime/node/zlib/NativeBrotli.rs
  • test/js/node/stream/node-stream-uint8array.test.ts
  • test/js/node/test/common/index.js
  • test/js/node/test/parallel/test-crypto-cipheriv-decipheriv.js
  • test/js/node/test/parallel/test-stream-add-abort-signal.js
  • test/js/node/test/parallel/test-stream-big-push.js
  • test/js/node/test/parallel/test-stream-compose.js
  • test/js/node/test/parallel/test-stream-construct.js
  • test/js/node/test/parallel/test-stream-consumers.js
  • test/js/node/test/parallel/test-stream-destroy.js
  • test/js/node/test/parallel/test-stream-drop-take.js
  • test/js/node/test/parallel/test-stream-duplex-destroy.js
  • test/js/node/test/parallel/test-stream-duplex-from.js
  • test/js/node/test/parallel/test-stream-duplex-readable-writable.js
  • test/js/node/test/parallel/test-stream-duplex-writable-finished.js
  • test/js/node/test/parallel/test-stream-duplex.js
  • test/js/node/test/parallel/test-stream-duplexpair.js
  • test/js/node/test/parallel/test-stream-end-paused.js
  • test/js/node/test/parallel/test-stream-filter.js
  • test/js/node/test/parallel/test-stream-finished-async-local-storage.js
  • test/js/node/test/parallel/test-stream-finished-bindAsyncResource-path.js
  • test/js/node/test/parallel/test-stream-finished-default-path.js
  • test/js/node/test/parallel/test-stream-forEach.js
  • test/js/node/test/parallel/test-stream-iter-broadcast-backpressure.js
  • test/js/node/test/parallel/test-stream-iter-broadcast-basic.js
  • test/js/node/test/parallel/test-stream-iter-broadcast-coverage.js
  • test/js/node/test/parallel/test-stream-iter-broadcast-from.js
  • test/js/node/test/parallel/test-stream-iter-consumers-bytes.js
  • test/js/node/test/parallel/test-stream-iter-consumers-merge.js
  • test/js/node/test/parallel/test-stream-iter-consumers-tap.js
  • test/js/node/test/parallel/test-stream-iter-consumers-text.js
  • test/js/node/test/parallel/test-stream-iter-cross-realm.js
  • test/js/node/test/parallel/test-stream-iter-disabled.js
  • test/js/node/test/parallel/test-stream-iter-duplex.js
  • test/js/node/test/parallel/test-stream-iter-from-async.js
  • test/js/node/test/parallel/test-stream-iter-from-coverage.js
  • test/js/node/test/parallel/test-stream-iter-from-sync.js
  • test/js/node/test/parallel/test-stream-iter-from-writable-cache-options.js
  • test/js/node/test/parallel/test-stream-iter-namespace.js
  • test/js/node/test/parallel/test-stream-iter-pipeto-edge.js
  • test/js/node/test/parallel/test-stream-iter-pipeto-signal.js
  • test/js/node/test/parallel/test-stream-iter-pipeto-writev.js
  • test/js/node/test/parallel/test-stream-iter-pipeto.js
  • test/js/node/test/parallel/test-stream-iter-pull-async.js
  • test/js/node/test/parallel/test-stream-iter-pull-sync.js
  • test/js/node/test/parallel/test-stream-iter-push-backpressure.js
  • test/js/node/test/parallel/test-stream-iter-push-basic.js
  • test/js/node/test/parallel/test-stream-iter-push-writer.js
  • test/js/node/test/parallel/test-stream-iter-readable-interop-disabled.js
  • test/js/node/test/parallel/test-stream-iter-readable-interop.js
  • test/js/node/test/parallel/test-stream-iter-share-async.js
  • test/js/node/test/parallel/test-stream-iter-share-coverage.js
  • test/js/node/test/parallel/test-stream-iter-share-from.js
  • test/js/node/test/parallel/test-stream-iter-share-sync.js
  • test/js/node/test/parallel/test-stream-iter-sharedarraybuffer.js
  • test/js/node/test/parallel/test-stream-iter-to-readable.js
  • test/js/node/test/parallel/test-stream-iter-transform-compat.js
  • test/js/node/test/parallel/test-stream-iter-transform-coverage.js
  • test/js/node/test/parallel/test-stream-iter-transform-errors.js
  • test/js/node/test/parallel/test-stream-iter-transform-output.js
  • test/js/node/test/parallel/test-stream-iter-transform-roundtrip.js
  • test/js/node/test/parallel/test-stream-iter-transform-sync.js
  • test/js/node/test/parallel/test-stream-iter-validation.js
  • test/js/node/test/parallel/test-stream-iter-writable-from.js
  • test/js/node/test/parallel/test-stream-iter-writable-interop.js
  • test/js/node/test/parallel/test-stream-map.js
  • test/js/node/test/parallel/test-stream-pipe-await-drain.js
  • test/js/node/test/parallel/test-stream-pipe-flow.js
  • test/js/node/test/parallel/test-stream-pipe-objectmode-to-non-objectmode.js
  • test/js/node/test/parallel/test-stream-pipeline-http2.js
  • test/js/node/test/parallel/test-stream-pipeline-listeners.js
  • test/js/node/test/parallel/test-stream-pipeline.js
  • test/js/node/test/parallel/test-stream-push-strings.js
  • test/js/node/test/parallel/test-stream-readable-aborted.js
  • test/js/node/test/parallel/test-stream-readable-async-iterators.js
  • test/js/node/test/parallel/test-stream-readable-compose.js
  • test/js/node/test/parallel/test-stream-readable-didRead.js
  • test/js/node/test/parallel/test-stream-readable-dispose.js
  • test/js/node/test/parallel/test-stream-readable-emittedReadable.js
  • test/js/node/test/parallel/test-stream-readable-ended.js
  • test/js/node/test/parallel/test-stream-readable-event.js
  • test/js/node/test/parallel/test-stream-readable-hwm-0-no-flow-data.js
  • test/js/node/test/parallel/test-stream-readable-infinite-read.js
  • test/js/node/test/parallel/test-stream-readable-needReadable.js
  • test/js/node/test/parallel/test-stream-readable-readable-one.js
  • test/js/node/test/parallel/test-stream-readable-strategy-option.js
  • test/js/node/test/parallel/test-stream-readable-to-web-byob.js
  • test/js/node/test/parallel/test-stream-readable-to-web-termination-byob.js
  • test/js/node/test/parallel/test-stream-readable-to-web-termination.js
  • test/js/node/test/parallel/test-stream-readable-to-web.mjs
  • test/js/node/test/parallel/test-stream-reduce.js
  • test/js/node/test/parallel/test-stream-toArray.js
  • test/js/node/test/parallel/test-stream-transform-destroy.js
  • test/js/node/test/parallel/test-stream-transform-final.js
  • test/js/node/test/parallel/test-stream-transform-flush-data.js
  • test/js/node/test/parallel/test-stream-typedarray.js
  • test/js/node/test/parallel/test-stream-uint8array.js
  • test/js/node/test/parallel/test-stream-unpipe-event.js
  • test/js/node/test/parallel/test-stream-unshift-empty-chunk.js
  • test/js/node/test/parallel/test-stream-unshift-read-race.js
  • test/js/node/test/parallel/test-stream-wrap-drain.js
  • test/js/node/test/parallel/test-stream-wrap-encoding.js
  • test/js/node/test/parallel/test-stream-wrap.js
  • test/js/node/test/parallel/test-stream-writable-change-default-encoding.js
  • test/js/node/test/parallel/test-stream-writable-decoded-encoding.js
  • test/js/node/test/parallel/test-stream-writable-destroy.js
  • test/js/node/test/parallel/test-stream-writable-ended-state.js
  • test/js/node/test/parallel/test-stream-writable-finished-state.js
  • test/js/node/test/parallel/test-stream-writable-finished.js
  • test/js/node/test/parallel/test-stream-writable-null.js
  • test/js/node/test/parallel/test-stream-writable-samecb-singletick.js
  • test/js/node/test/parallel/test-stream-writable-write-error.js
  • test/js/node/test/parallel/test-stream-writev.js
  • test/js/node/test/parallel/test-stream2-base64-single-char-read-end.js
  • test/js/node/test/parallel/test-stream2-basic.js
  • test/js/node/test/parallel/test-stream2-compatibility.js
  • test/js/node/test/parallel/test-stream2-httpclient-response-end.js
  • test/js/node/test/parallel/test-stream2-objects.js
  • test/js/node/test/parallel/test-stream2-push.js
  • test/js/node/test/parallel/test-stream2-read-correct-num-bytes-in-utf8.js
  • test/js/node/test/parallel/test-stream2-readable-wrap.js
  • test/js/node/test/parallel/test-stream2-transform.js
  • test/js/node/test/parallel/test-stream2-writable.js
  • test/js/node/test/parallel/test-stream3-cork-end.js
  • test/js/node/test/parallel/test-stream3-cork-uncork.js
  • test/js/node/test/parallel/test-stream3-pause-then-read.js
  • test/js/node/test/parallel/test-stream3-pipeline-async-iterator.js
  • test/js/node/test/parallel/test-zlib-flush-write-sync-interleaved.js
  • test/js/node/test/sequential/test-stream2-stderr-sync.js
💤 Files with no reviewable changes (1)
  • src/js/internal/streams/operators.ts

Comment thread src/js/internal/streams/iter/broadcast.ts
Comment thread src/js/internal/streams/iter/broadcast.ts Outdated
Comment thread src/js/internal/streams/iter/classic.ts Outdated
Comment thread src/js/internal/streams/iter/duplex.ts
Comment thread src/js/internal/streams/iter/from.ts
Comment thread src/js/internal/streams/iter/pull.ts
Comment thread src/js/internal/streams/iter/push.ts Outdated
Comment thread src/js/internal/streams/iter/transform.ts
Comment thread src/js/internal/webstreams_adapters.ts
Comment thread src/js/node/net.ts Outdated
@cirospaciari

Copy link
Copy Markdown
Member Author

Review triage (claude[bot] + coderabbit rounds):

Fixed:

  • ErrorCode.rs: MODULE_NOT_FOUND ordinal (155 -> 268), COUNT (312 -> 315), CODE_STR regenerated wholesale from ErrorCode+Data.h.
  • webstreams_adapters.ts: added node's validateObject(options) in newReadableStreamFromStreamReadable (explicit null now throws ERR_INVALID_ARG_TYPE instead of a raw property-access error).

Already fixed before the review snapshot (stale findings):

  • the _options/_chunk/_chunks lint renames in broadcast/push/classic and the dead compose + imports in operators.ts landed in 7f48cb2.

Faithful-port rebuttals — these match node v26.3.0 source line-for-line, so changing them would diverge from upstream (and from the vendored upstream tests):

  • broadcast.ts truthy #error checks: node broadcast.js:182,199 uses the identical truthy check.
  • classic.ts _destroy calling cb(): node classic.js:885.
  • duplex.ts abort-listener lifetime, from.ts async-branch batching, pull.ts writev().then(...), transform.ts batch === null finalization: all structurally identical to node's iter/*.js.

🤖 PR feedback addressed by Claude Code.

Comment thread src/resolve_builtins/HardcodedModule.rs
Comment thread src/jsc/bindings/ErrorCode.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/internal/streams/readable.ts (1)

1269-1275: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Use the CLI-parsed gate here instead of process.execArgv.

process.execArgv is userland-mutable, so this feature gate can be flipped after startup: pushing --experimental-stream-iter enables the API without the CLI flag, and removing it makes this method throw even though resolver gating still uses the write-once flag from src/resolve_builtins/HardcodedModule.rs. Please plumb the same immutable startup bit into this builtin so both resolution and Readable[Symbol.for("Stream.toAsyncStreamable")]() stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/internal/streams/readable.ts` around lines 1269 - 1275, The code
checks process.execArgv for "--experimental-stream-iter" which is user-mutable;
replace that check in Readable.prototype[toAsyncStreamable] (the block
referencing createBatchedAsyncIterator, normalizeBatch and kValidatedSource)
with the immutable CLI-parsed startup bit used by module resolution; locate the
existing startup flag exported into the JS runtime (the same flag the
resolver/hardcoded gating logic reads) and use that boolean here to decide
whether to require("internal/streams/iter/classic") and to throw
$ERR_STREAM_ITER_MISSING_FLAG() when unset, so resolution and
Readable[Symbol.for("Stream.toAsyncStreamable")] use the same write-once gate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/js/internal/streams/readable.ts`:
- Around line 1269-1275: The code checks process.execArgv for
"--experimental-stream-iter" which is user-mutable; replace that check in
Readable.prototype[toAsyncStreamable] (the block referencing
createBatchedAsyncIterator, normalizeBatch and kValidatedSource) with the
immutable CLI-parsed startup bit used by module resolution; locate the existing
startup flag exported into the JS runtime (the same flag the resolver/hardcoded
gating logic reads) and use that boolean here to decide whether to
require("internal/streams/iter/classic") and to throw
$ERR_STREAM_ITER_MISSING_FLAG() when unset, so resolution and
Readable[Symbol.for("Stream.toAsyncStreamable")] use the same write-once gate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5448f35-9cc3-43d3-b389-8c70e887ee7b

📥 Commits

Reviewing files that changed from the base of the PR and between 48689ca and a6ff02c.

📒 Files selected for processing (14)
  • src/js/internal/streams/iter/broadcast.ts
  • src/js/internal/streams/iter/classic.ts
  • src/js/internal/streams/iter/push.ts
  • src/js/internal/streams/operators.ts
  • src/js/internal/streams/readable.ts
  • src/js/internal/webstreams_adapters.ts
  • src/jsc/ErrorCode.rs
  • src/jsc/bindings/ErrorCode.cpp
  • src/resolve_builtins/HardcodedModule.rs
  • src/resolve_builtins/lib.rs
  • src/runtime/cli/Arguments.rs
  • test/expectations.txt
  • test/js/bun/crypto/cipheriv-decipheriv.test.ts
  • test/js/node/crypto/crypto.test.ts
💤 Files with no reviewable changes (1)
  • src/js/internal/streams/operators.ts

Comment thread src/js/node/_http_incoming.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/cli/Arguments.rs`:
- Around line 1280-1283: The flag handling for
args.flag(b"--experimental-stream-iter") is only in the runtime-command branch
so bun build cannot enable resolver behavior; move the
bun_resolve_builtins::set_stream_iter_enabled(true) and
Bun__streamIterEnabled.store(...) out of the runtime-only block and into the
common/BUILD_PARAMS path (expose the flag in the build table as well) so that
when BUILD_PARAMS is constructed the resolver gate is reachable for both build
and runtime flows; update any parameter parsing that currently writes only to
RUNTIME_PARAMS_ to also populate the build params so the resolver sees the flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6e2c2bf0-ba6d-4442-80fa-49f5b67a715d

📥 Commits

Reviewing files that changed from the base of the PR and between a6ff02c and ef6a715.

📒 Files selected for processing (5)
  • src/js/internal/streams/readable.ts
  • src/js/node/_http_incoming.ts
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/BunProcess.h
  • src/runtime/cli/Arguments.rs

Comment thread src/runtime/cli/Arguments.rs
Comment thread src/js/builtins/ProcessObjectInternals.ts
Comment thread src/js/internal/webstreams_adapters.ts
Comment thread src/js/internal/streams/from.ts
Comment thread src/js/node/net.ts Outdated
Comment thread src/js/node/net.ts Outdated
Comment thread src/js/internal/streams/iter/classic.ts
Comment thread src/js/node/net.ts Outdated
Comment thread test/js/node/stream/node-stream.test.js Outdated
@cirospaciari

Copy link
Copy Markdown
Member Author

Finding from the macOS CI failures (fs.watch.test.ts on all three darwin testers):

The fs.watch(dir) on macOS does not leak the resolved FSEvents path test has been a false-positive pass everywhere: its child throws on leak detection from a CJS -e script, and the silent-exit-0 eval bug this PR fixes (JSC::evaluate discarding the exception into an unused out-param) swallowed that throw - empty stderr, exit 0, all three assertions vacuously satisfied.

With the exception fix in this PR the throw finally surfaces, and the child reports ~70 MB RSS growth over 5000 watch cycles (~14 KB per fs.watch). Instrumented verification: an unmodified baseline binary's child prints the same ~70 MB growth and still exits 0 (swallowed); this branch's child correctly errors. The leak is therefore pre-existing on main and path-length-independent - distinct from (and much larger than) the resolved_path dupeZ issue (#29854) the test was written to guard.

Marked expected-fail on darwin in test/expectations.txt with the full story; the PathWatcher leak deserves its own investigation/PR.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/io/posix_event_loop.rs:1024-1031 — Commit 5f0b651 ("test: expect fs.watch leak test failure on macOS") silently reverts merged PR #31821 in src/io/posix_event_loop.rs — deleting deregistration_already_gone() and its three POSIX call sites, restoring the file byte-for-byte to its pre-#31821 state. This is unrelated to the node:stream sync, isn't mentioned in the commit message or PR description, and re-introduces the regression #31821 fixed (ENOENT/EBADF on deregistration early-returns before clearing registration flags, leaving the poll re-issuing doomed deletes). Please drop the posix_event_loop.rs hunk from this PR.

    Extended reasoning...

    What the bug is

    Commit 5f0b651 in this PR is titled "test: expect fs.watch leak test failure on macOS" and the PR description discusses only the node:stream sync. But git show 5f0b651a --stat shows it touches two files: test/expectations.txt (the described change) and src/io/posix_event_loop.rs (+23/−39, never mentioned anywhere).

    The posix_event_loop.rs hunk is a byte-for-byte revert of merged PR #31821 ("Treat unregistering an already-removed poll registration as success", commit 29f2c7e, merged by Dylan Conway 2026-06-04). Verified via git diff 29f2c7e6^..29f2c7e6 -- src/io/posix_event_loop.rs being the exact inverse of this PR's hunk for that file: the file blob goes from 6a82cf50 (post-#31821) back to 630461f4 (pre-#31821). PR #31821 was merged ~5h before this PR was opened, consistent with a rebase-conflict mis-resolution where the branch's older copy of the file was kept.

    Step-by-step proof

    The diff for src/io/posix_event_loop.rs in this PR:

    1. Deletes the deregistration_already_gone() helper (the function whose doc comment explained "the registration flags must still be cleared, which an error return would skip, leaving the poll claiming to be registered and re-issuing doomed deletes on later teardown calls").
    2. Removes its call site in the Linux epoll CTL_DEL branch (lines 1024-1029): the match sys::get_errno(ctl) that treated deregistration_already_gone(e) as success is replaced with the old if let Some(errno) = errno_sys(ctl, …) { return errno; }.
    3. Removes its call site in the macOS kevent64 EV_DELETE branch (lines 1124-1132): the loop that skipped EV_ERROR results matching deregistration_already_gone is replaced with the old unconditional return kevent_change_error(...).
    4. Removes its call site in the FreeBSD kevent EV_DELETE branch (lines 1171-1176): same pattern.
    5. Restores the pre-#31821 doc comment on kevent_change_error at lines 58-61 ("the deinit path tolerates that race by discarding the returned error") — this is not new documentation; it's the old comment that #31821 deleted, reappearing because the file went back to its prior state.

    This is the entirety of #31821, undone.

    Why existing code doesn't prevent it

    There is no test asserting the tolerance behavior (per #31821's own commit message, "there's no JS-observable surface" — callers discard the error). The file change is buried in a 180-file PR and the commit message describes only the test-expectation half. Per CLAUDE.md: "Before deleting odd-looking code … git-blame why it was written; it is usually load-bearing."

    Functional impact

    Per #31821's commit message: when an fd is closed while still registered (or a pty master close marks the slave's knotes EV_EOF|EV_ONESHOT and the kernel auto-deletes them — "happens on every terminal window/tab close while a tty is polled"), the deregistration syscall returns ENOENT/EBADF. With the tolerance removed, unregister_with_fd_impl now early-returns at lines 1027-1029 / 1127-1132 / 1174-1176 before reaching the flag-clearing block at lines 1179-1184.

    The deinit path itself is fine (deinit_possibly_defer at line 408 does let _ = self.unregister(...) then self.flags = FlagsSet::empty() at line 412, clearing flags regardless). But the non-deinit callersPipeReader::pause() at PipeReader.rs:308 and dns.rs:4972 — call unregister() directly and discard the result without a subsequent flag clear. So after a failed unregister the poll's is_registered() keeps returning true, and a later deinit() re-issues another doomed EV_DELETE — exactly the double-delete #31821 instrumented ("observed via instrumentation: the same poll failing the delete twice during one pty teardown"). libuv ignores these same errnos for the same reason.

    The runtime impact is mild (callers discard the error; no crash/hang). But silently reverting another contributor's merged fix via an unrelated commit in a 180-file PR is exactly what review must catch — it would otherwise re-land as a regression with no audit trail.

    How to fix

    Drop the src/io/posix_event_loop.rs hunk from commit 5f0b651 (e.g. git checkout origin/main -- src/io/posix_event_loop.rs and amend). The test-expectation half of that commit is fine and should stay.

Comment thread src/js/builtins/ProcessObjectInternals.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/js/node/_http_server.ts (1)

1552-1559: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fall back to this.socket when the request already detached its alias.

ServerResponse.end() passes req.socket here. After IncomingMessage._destroy() sets that alias to null, this guard skips clearing the live response socket’s _httpMessage / "close" listener and only nulls this.socket, leaving a stale back-reference on the keep-alive socket.

🐛 Proposed fix
 ServerResponse.prototype.detachSocket = function (socket) {
+  socket ??= this.socket;
   // socket can be null when the stream destroyer detached the request's
   // socket (req.socket = null) before the response finished.
   if (socket && socket._httpMessage === this) {
     if (socket[kCloseCallback]) socket[kCloseCallback] = undefined;
     socket.removeListener("close", onServerResponseClose);
     socket._httpMessage = null;
   }

   this.socket = null;
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/node/_http_server.ts` around lines 1552 - 1559,
ServerResponse.prototype.detachSocket currently only operates on the passed-in
socket, which can be null after IncomingMessage._destroy(), leaving a stale
back-reference on the real keep-alive socket; change the logic in detachSocket
to fall back to the live socket (use socket || this.socket) and operate on that
alternativeSocket: check alternativeSocket._httpMessage === this, clear
alternativeSocket[kCloseCallback], removeListener("close",
onServerResponseClose) from alternativeSocket, set
alternativeSocket._httpMessage = null, and ensure this.socket is also nulled so
no stale references remain (refer to ServerResponse.prototype.detachSocket,
kCloseCallback, onServerResponseClose, _httpMessage, and ServerResponse.end).
src/js/internal/webstreams_adapters.ts (1)

576-597: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate mapped readable type before fast paths.

readableOptions.type is only validated when the readable branch calls newReadableStreamFromStreamReadable(). The destroyed/non-readable branches bypass that and pass the value directly into new ReadableStream(...), which can produce inconsistent error behavior for invalid values.

Suggested fix
 function newReadableWritablePairFromDuplex(duplex, options = kEmptyObject) {
@@
   const readableOptions = { type: options.readableType };
   if (options.readableType == null && options.type != null) {
     // 'options.type' is a deprecated alias for 'options.readableType'.
     emitDEP0201();
     readableOptions.type = options.type;
   }
+  if (readableOptions.type !== undefined) {
+    const typeArgName =
+      options.readableType == null && options.type != null
+        ? "options.type"
+        : "options.readableType";
+    validateOneOf(readableOptions.type, typeArgName, ["bytes", undefined]);
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/internal/webstreams_adapters.ts` around lines 576 - 597, The code
constructs ReadableStream with readableOptions.type in the destroyed and
non-readable fast paths without validating it, causing inconsistent errors;
before any fast-path creation (inside isDestroyed and the non-readable branch
where new ReadableStream is used), run the same validation/conversion logic used
by newReadableStreamFromStreamReadable on readableOptions.type (extract or call
the validator used there) and use the validated value when constructing new
ReadableStream so invalid types throw the same error as in
newReadableStreamFromStreamReadable; update references around readableOptions,
isDestroyed(duplex), isReadable(duplex), and newReadableStreamFromStreamReadable
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/builtins/ProcessObjectInternals.ts`:
- Around line 380-387: Iterating tickInitHooks directly can skip callbacks if
hooks mutate the array during iteration; fix by taking a snapshot (e.g., const
hooks = tickInitHooks.slice()) after obtaining asyncId (from
asyncHooksTick.newAsyncId()) and then iterate over that snapshot, calling each
hook with (asyncId, "TickObject", 0, tock) so mutations to tickInitHooks inside
callbacks do not affect the current dispatch.

In `@src/js/builtins/ReadableStreamInternals.ts`:
- Around line 1609-1617: The BYOB read-into queue draining logic currently only
runs in cancel() causing BYOB readers to hang on EOF; move the readIntoRequests
drain into the shared readableStreamClose() path (use
$getByIdDirectPrivate/$isReadableStreamBYOBReader to detect a BYOB reader and
$getByIdDirectPrivate to read its "readIntoRequests", then $putByIdDirectPrivate
to replace it with a new FIFO and $fulfillPromise pending requests with {value:
undefined, done: true}) and update cancel() to call/reuse readableStreamClose()
so both close and cancel share the same drain behavior for readIntoRequests.

In `@src/js/internal/streams/iter/consumers.ts`:
- Around line 379-437: The ready.shift() draining is O(n^2); replace the queue
with a head-pointer ring: keep the existing ready array but add a numeric head
(e.g., readyHead = 0) and change pushes to ready.push(...) as-is, then drain by
reading items with const item = ready[readyHead++]; treat FIFO emptiness as
readyHead < ready.length (and update the outer loop condition to use readyHead
vs ready.length); when readyHead grows large (or readyHead === ready.length)
reset the buffer by doing ready = ready.slice(readyHead); readyHead = 0 to avoid
unbounded memory; update both the synchronous drain loop (where ready.shift() is
used) and any code paths that check ready.length (e.g., the outer while
condition and waitResolve logic) to use readyHead/ready.length semantics; keep
onSettled, iterator.next().then, and the error-push logic unchanged aside from
the new empty-checks.

In `@src/js/internal/streams/iter/push.ts`:
- Around line 213-237: The method `#createPendingWrite` currently pushes the entry
into `#pendingWrites` before validating/using the provided signal; change the flow
so the entry is only enqueued after any signal work succeeds: if signal is
falsy, push immediately; if signal is provided, prepare onAbort and the wrapped
resolve/reject, attempt to addEventListener inside a try/catch, and only push
the entry into `#pendingWrites` after addEventListener succeeds (on failure,
throw/reject without mutating `#pendingWrites`); ensure you still remove the
listener in the wrapped entry.resolve/entry.reject and remove the entry from the
queue in onAbort as before.

In `@src/js/node/_http_incoming.ts`:
- Around line 413-421: The connection getter must be kept in sync with the
socket getter: stop using the `??=` pattern on the same backing field
`this[fakeSocketSymbol]` (which resurrects a FakeSocket when the field is
intentionally set to null) and instead match the socket getter's logic (only
create a new FakeSocket when `this[fakeSocketSymbol] === undefined`, otherwise
return the current value including null); update the `connection` accessor (and
any code that sets `stream.socket`) to reference `fakeSocketSymbol`,
`FakeSocket`, and the same undefined-only lazy-init behavior so `socket` and
`connection` remain alias-consistent.

In `@src/js/node/async_hooks.ts`:
- Around line 382-403: The current disable() removes the shared function
reference stored in enabledInit which can remove another hook using the same
init callback; instead create and store a per-instance wrapper/token when
registering init (e.g. assign a unique wrapper value alongside enabledInit
inside the installation path where enabledInit = init and
tickInitHooks.push(init)), push that wrapper/token into
require("internal/async_hooks_tick").tickInitHooks, and in disable() remove that
specific wrapper/token (use the stored wrapper variable rather than enabledInit)
and clear the wrapper variable; adjust any references that read enabledInit to
use the instance wrapper token so other hooks with the same raw init function
aren’t affected.

In `@src/js/node/net.ts`:
- Around line 1368-1397: The synchronous fd writers fdSyncWrite and fdSyncWritev
perform writes but never update the socket's byte counters; update
this[kBytesWritten] and this._bytesDispatched with the number of bytes returned
from each fs.writeSync call so socket.bytesWritten and _bytesDispatched reflect
the writes. In both fdSyncWrite and fdSyncWritev, capture the return value from
fs.writeSync for each write chunk/iteration, add that value to
this[kBytesWritten] and this._bytesDispatched (accumulating across the inner
loop and across v-write entries), then proceed as before with offset and
callback handling.
- Around line 1157-1170: When releasing the adopted fd (this[kSyncWriteFd]) also
restore the instance's sync-write function overrides so future reconnects don't
keep using the stale path: after clearing this[kSyncWriteFd] in the Socket
shutdown/cleanup code, restore or remove the fdSyncWrite and fdSyncWritev
overrides (the properties set earlier around lines where fdSyncWrite /
fdSyncWritev were installed) back to their default/original implementations (or
delete them from the instance) so subsequent Socket.connect() calls use the
fresh handle's normal write path instead of the stale sync-fd handlers.

In `@test/js/node/stream/node-stream.test.js`:
- Around line 582-589: The test spawns a subprocess into variable proc
(Bun.spawn) and only drains proc.stdout, risking a deadlock because stderr is
piped but never read; update the Promise.all call that awaits proc.stdout.text()
and proc.exited to also include proc.stderr.text() so both stdout and stderr are
consumed concurrently (i.e., use Promise.all([proc.stdout.text(),
proc.stderr.text(), proc.exited])) and adjust the destructuring accordingly to
capture and ignore or assert stderr as needed.

In `@test/js/node/test/parallel/test-stream-iter-broadcast-basic.js`:
- Around line 221-230: The test testCancelWithFalsyReason is asserting that
bc.cancel(0) should reject (expects result 0) but upstream semantics treat falsy
cancel reasons as normal completion; update the test to expect successful
completion instead of rejection: call text(consumer) without the .catch(...) (or
change the promise handling) and assert that the consumer completes normally
(e.g., resolved value rather than 0). Adjust assertions in
testCancelWithFalsyReason to reflect completion behavior for broadcast().cancel
when given a falsy reason.

In `@test/js/node/test/parallel/test-stream-iter-from-sync.js`:
- Around line 195-207: The test claims zero-copy for DataView inputs but only
checks content; either enforce aliasing or drop the claim. In
testFromSyncDataView, if zero-copy is required, add assertions that the returned
Uint8Array (from fromSync(view), e.g. batches[0][0]) shares the same underlying
ArrayBuffer and offsets as the original DataView (compare .buffer === buf and
.byteOffset/.byteLength with view.byteOffset/view.byteLength); otherwise remove
or reword the comment about zero-copy and leave only the content assertion.

In `@test/js/node/test/parallel/test-stream-iter-share-from.js`:
- Around line 103-125: The testShareBlockBackpressure test currently starts both
consumers together so the slow consumer never lags; modify it to force the fast
consumer to run ahead before the slow one is allowed to pull. Specifically, in
testShareBlockBackpressure (the shared = share(...), fast = shared.pull(), slow
= shared.pull() block) advance the fast reader (call fast.next() repeatedly or
await text(fast) partially) to consume enough items to exceed highWaterMark
(e.g., consume 2–3 items) and only then start/await the slow reader (or gate
slow.next() with a deferred promise) so the source must stall under
backpressure:'block' and the test actually verifies blocking behavior. Ensure
final assertions still check both readers receive all items.

In `@test/js/node/test/parallel/test-stream-iter-writable-from.js`:
- Line 270: Replace the non-deterministic sleep with an event-based wait: in the
tests testDestroyDelegatesToFail, testDestroyWithoutError, testDestroyWithError,
and testDestroyWithoutFail, remove the await new Promise(resolve =>
setTimeout(resolve, 10)) and instead await the stream close event (for example
using once(writable, 'close') from the 'events' module or stream.finished) so
the test proceeds only after writable emits 'close' following destroy().

---

Outside diff comments:
In `@src/js/internal/webstreams_adapters.ts`:
- Around line 576-597: The code constructs ReadableStream with
readableOptions.type in the destroyed and non-readable fast paths without
validating it, causing inconsistent errors; before any fast-path creation
(inside isDestroyed and the non-readable branch where new ReadableStream is
used), run the same validation/conversion logic used by
newReadableStreamFromStreamReadable on readableOptions.type (extract or call the
validator used there) and use the validated value when constructing new
ReadableStream so invalid types throw the same error as in
newReadableStreamFromStreamReadable; update references around readableOptions,
isDestroyed(duplex), isReadable(duplex), and newReadableStreamFromStreamReadable
accordingly.

In `@src/js/node/_http_server.ts`:
- Around line 1552-1559: ServerResponse.prototype.detachSocket currently only
operates on the passed-in socket, which can be null after
IncomingMessage._destroy(), leaving a stale back-reference on the real
keep-alive socket; change the logic in detachSocket to fall back to the live
socket (use socket || this.socket) and operate on that alternativeSocket: check
alternativeSocket._httpMessage === this, clear
alternativeSocket[kCloseCallback], removeListener("close",
onServerResponseClose) from alternativeSocket, set
alternativeSocket._httpMessage = null, and ensure this.socket is also nulled so
no stale references remain (refer to ServerResponse.prototype.detachSocket,
kCloseCallback, onServerResponseClose, _httpMessage, and ServerResponse.end).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 12ad007f-2b22-408e-b158-d95e9ecc5945

📥 Commits

Reviewing files that changed from the base of the PR and between ef6a715 and 0edd836.

📒 Files selected for processing (179)
  • src/js/builtins/ProcessObjectInternals.ts
  • src/js/builtins/ReadableStreamInternals.ts
  • src/js/internal-for-testing.ts
  • src/js/internal/async_context_frame.ts
  • src/js/internal/async_hooks.ts
  • src/js/internal/async_hooks_tick.ts
  • src/js/internal/fs/streams.ts
  • src/js/internal/http.ts
  • src/js/internal/streams/duplex.ts
  • src/js/internal/streams/duplexify.ts
  • src/js/internal/streams/from.ts
  • src/js/internal/streams/iter/broadcast.ts
  • src/js/internal/streams/iter/classic.ts
  • src/js/internal/streams/iter/consumers.ts
  • src/js/internal/streams/iter/duplex.ts
  • src/js/internal/streams/iter/from.ts
  • src/js/internal/streams/iter/pull.ts
  • src/js/internal/streams/iter/push.ts
  • src/js/internal/streams/iter/ringbuffer.ts
  • src/js/internal/streams/iter/share.ts
  • src/js/internal/streams/iter/transform.ts
  • src/js/internal/streams/iter/types.ts
  • src/js/internal/streams/iter/utils.ts
  • src/js/internal/streams/operators.ts
  • src/js/internal/streams/pipeline.ts
  • src/js/internal/streams/readable.ts
  • src/js/internal/streams/writable.ts
  • src/js/internal/webstreams_adapters.ts
  • src/js/node/_http_client.ts
  • src/js/node/_http_incoming.ts
  • src/js/node/_http_server.ts
  • src/js/node/async_hooks.ts
  • src/js/node/net.ts
  • src/js/node/stream.iter.ts
  • src/js/node/zlib.iter.ts
  • src/jsc/ErrorCode.rs
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/ErrorCode.cpp
  • src/jsc/bindings/ErrorCode.ts
  • src/jsc/bindings/JSCommonJSModule.cpp
  • src/resolve_builtins/HardcodedModule.rs
  • src/resolve_builtins/lib.rs
  • src/runtime/cli/Arguments.rs
  • src/runtime/jsc_hooks.rs
  • src/runtime/node/zlib/NativeBrotli.rs
  • test/expectations.txt
  • test/js/bun/crypto/cipheriv-decipheriv.test.ts
  • test/js/node/crypto/crypto.test.ts
  • test/js/node/net/node-net.test.ts
  • test/js/node/stream/node-stream-uint8array.test.ts
  • test/js/node/stream/node-stream.test.js
  • test/js/node/test/common/index.js
  • test/js/node/test/parallel/test-crypto-cipheriv-decipheriv.js
  • test/js/node/test/parallel/test-stream-add-abort-signal.js
  • test/js/node/test/parallel/test-stream-big-push.js
  • test/js/node/test/parallel/test-stream-compose.js
  • test/js/node/test/parallel/test-stream-construct.js
  • test/js/node/test/parallel/test-stream-consumers.js
  • test/js/node/test/parallel/test-stream-destroy.js
  • test/js/node/test/parallel/test-stream-drop-take.js
  • test/js/node/test/parallel/test-stream-duplex-destroy.js
  • test/js/node/test/parallel/test-stream-duplex-from.js
  • test/js/node/test/parallel/test-stream-duplex-readable-writable.js
  • test/js/node/test/parallel/test-stream-duplex-writable-finished.js
  • test/js/node/test/parallel/test-stream-duplex.js
  • test/js/node/test/parallel/test-stream-duplexpair.js
  • test/js/node/test/parallel/test-stream-end-paused.js
  • test/js/node/test/parallel/test-stream-filter.js
  • test/js/node/test/parallel/test-stream-finished-async-local-storage.js
  • test/js/node/test/parallel/test-stream-finished-bindAsyncResource-path.js
  • test/js/node/test/parallel/test-stream-finished-default-path.js
  • test/js/node/test/parallel/test-stream-forEach.js
  • test/js/node/test/parallel/test-stream-iter-broadcast-backpressure.js
  • test/js/node/test/parallel/test-stream-iter-broadcast-basic.js
  • test/js/node/test/parallel/test-stream-iter-broadcast-coverage.js
  • test/js/node/test/parallel/test-stream-iter-broadcast-from.js
  • test/js/node/test/parallel/test-stream-iter-consumers-bytes.js
  • test/js/node/test/parallel/test-stream-iter-consumers-merge.js
  • test/js/node/test/parallel/test-stream-iter-consumers-tap.js
  • test/js/node/test/parallel/test-stream-iter-consumers-text.js
  • test/js/node/test/parallel/test-stream-iter-cross-realm.js
  • test/js/node/test/parallel/test-stream-iter-disabled.js
  • test/js/node/test/parallel/test-stream-iter-duplex.js
  • test/js/node/test/parallel/test-stream-iter-from-async.js
  • test/js/node/test/parallel/test-stream-iter-from-coverage.js
  • test/js/node/test/parallel/test-stream-iter-from-sync.js
  • test/js/node/test/parallel/test-stream-iter-from-writable-cache-options.js
  • test/js/node/test/parallel/test-stream-iter-namespace.js
  • test/js/node/test/parallel/test-stream-iter-pipeto-edge.js
  • test/js/node/test/parallel/test-stream-iter-pipeto-signal.js
  • test/js/node/test/parallel/test-stream-iter-pipeto-writev.js
  • test/js/node/test/parallel/test-stream-iter-pipeto.js
  • test/js/node/test/parallel/test-stream-iter-pull-async.js
  • test/js/node/test/parallel/test-stream-iter-pull-sync.js
  • test/js/node/test/parallel/test-stream-iter-push-backpressure.js
  • test/js/node/test/parallel/test-stream-iter-push-basic.js
  • test/js/node/test/parallel/test-stream-iter-push-writer.js
  • test/js/node/test/parallel/test-stream-iter-readable-interop-disabled.js
  • test/js/node/test/parallel/test-stream-iter-readable-interop.js
  • test/js/node/test/parallel/test-stream-iter-share-async.js
  • test/js/node/test/parallel/test-stream-iter-share-coverage.js
  • test/js/node/test/parallel/test-stream-iter-share-from.js
  • test/js/node/test/parallel/test-stream-iter-share-sync.js
  • test/js/node/test/parallel/test-stream-iter-sharedarraybuffer.js
  • test/js/node/test/parallel/test-stream-iter-to-readable.js
  • test/js/node/test/parallel/test-stream-iter-transform-compat.js
  • test/js/node/test/parallel/test-stream-iter-transform-coverage.js
  • test/js/node/test/parallel/test-stream-iter-transform-errors.js
  • test/js/node/test/parallel/test-stream-iter-transform-output.js
  • test/js/node/test/parallel/test-stream-iter-transform-roundtrip.js
  • test/js/node/test/parallel/test-stream-iter-transform-sync.js
  • test/js/node/test/parallel/test-stream-iter-validation.js
  • test/js/node/test/parallel/test-stream-iter-writable-from.js
  • test/js/node/test/parallel/test-stream-iter-writable-interop.js
  • test/js/node/test/parallel/test-stream-map.js
  • test/js/node/test/parallel/test-stream-pipe-await-drain.js
  • test/js/node/test/parallel/test-stream-pipe-flow.js
  • test/js/node/test/parallel/test-stream-pipe-objectmode-to-non-objectmode.js
  • test/js/node/test/parallel/test-stream-pipeline-http2.js
  • test/js/node/test/parallel/test-stream-pipeline-listeners.js
  • test/js/node/test/parallel/test-stream-pipeline.js
  • test/js/node/test/parallel/test-stream-push-strings.js
  • test/js/node/test/parallel/test-stream-readable-aborted.js
  • test/js/node/test/parallel/test-stream-readable-async-iterators.js
  • test/js/node/test/parallel/test-stream-readable-compose.js
  • test/js/node/test/parallel/test-stream-readable-didRead.js
  • test/js/node/test/parallel/test-stream-readable-dispose.js
  • test/js/node/test/parallel/test-stream-readable-emittedReadable.js
  • test/js/node/test/parallel/test-stream-readable-ended.js
  • test/js/node/test/parallel/test-stream-readable-event.js
  • test/js/node/test/parallel/test-stream-readable-hwm-0-no-flow-data.js
  • test/js/node/test/parallel/test-stream-readable-infinite-read.js
  • test/js/node/test/parallel/test-stream-readable-needReadable.js
  • test/js/node/test/parallel/test-stream-readable-readable-one.js
  • test/js/node/test/parallel/test-stream-readable-strategy-option.js
  • test/js/node/test/parallel/test-stream-readable-to-web-byob.js
  • test/js/node/test/parallel/test-stream-readable-to-web-termination-byob.js
  • test/js/node/test/parallel/test-stream-readable-to-web-termination.js
  • test/js/node/test/parallel/test-stream-readable-to-web.mjs
  • test/js/node/test/parallel/test-stream-reduce.js
  • test/js/node/test/parallel/test-stream-toArray.js
  • test/js/node/test/parallel/test-stream-transform-destroy.js
  • test/js/node/test/parallel/test-stream-transform-final.js
  • test/js/node/test/parallel/test-stream-transform-flush-data.js
  • test/js/node/test/parallel/test-stream-typedarray.js
  • test/js/node/test/parallel/test-stream-uint8array.js
  • test/js/node/test/parallel/test-stream-unpipe-event.js
  • test/js/node/test/parallel/test-stream-unshift-empty-chunk.js
  • test/js/node/test/parallel/test-stream-unshift-read-race.js
  • test/js/node/test/parallel/test-stream-wrap-drain.js
  • test/js/node/test/parallel/test-stream-wrap-encoding.js
  • test/js/node/test/parallel/test-stream-wrap.js
  • test/js/node/test/parallel/test-stream-writable-change-default-encoding.js
  • test/js/node/test/parallel/test-stream-writable-decoded-encoding.js
  • test/js/node/test/parallel/test-stream-writable-destroy.js
  • test/js/node/test/parallel/test-stream-writable-ended-state.js
  • test/js/node/test/parallel/test-stream-writable-finished-state.js
  • test/js/node/test/parallel/test-stream-writable-finished.js
  • test/js/node/test/parallel/test-stream-writable-null.js
  • test/js/node/test/parallel/test-stream-writable-samecb-singletick.js
  • test/js/node/test/parallel/test-stream-writable-write-error.js
  • test/js/node/test/parallel/test-stream-writev.js
  • test/js/node/test/parallel/test-stream2-base64-single-char-read-end.js
  • test/js/node/test/parallel/test-stream2-basic.js
  • test/js/node/test/parallel/test-stream2-compatibility.js
  • test/js/node/test/parallel/test-stream2-httpclient-response-end.js
  • test/js/node/test/parallel/test-stream2-objects.js
  • test/js/node/test/parallel/test-stream2-push.js
  • test/js/node/test/parallel/test-stream2-read-correct-num-bytes-in-utf8.js
  • test/js/node/test/parallel/test-stream2-readable-wrap.js
  • test/js/node/test/parallel/test-stream2-transform.js
  • test/js/node/test/parallel/test-stream2-writable.js
  • test/js/node/test/parallel/test-stream3-cork-end.js
  • test/js/node/test/parallel/test-stream3-cork-uncork.js
  • test/js/node/test/parallel/test-stream3-pause-then-read.js
  • test/js/node/test/parallel/test-stream3-pipeline-async-iterator.js
  • test/js/node/test/parallel/test-zlib-flush-write-sync-interleaved.js
  • test/js/node/test/sequential/test-stream2-stderr-sync.js
  • test/napi/node-napi-tests/test/node-api/test_uv_threadpool_size/do.test.ts
💤 Files with no reviewable changes (8)
  • test/js/node/test/parallel/test-stream-pipe-flow.js
  • test/js/node/test/parallel/test-stream-pipe-await-drain.js
  • test/js/node/test/parallel/test-stream-pipe-objectmode-to-non-objectmode.js
  • test/js/node/test/parallel/test-stream-map.js
  • test/js/node/test/parallel/test-stream-pipeline-listeners.js
  • src/js/internal/streams/operators.ts
  • test/js/node/test/parallel/test-stream-pipeline-http2.js
  • test/js/node/test/parallel/test-stream-iter-writable-interop.js

Comment thread src/js/builtins/ProcessObjectInternals.ts
Comment thread src/js/builtins/ReadableStreamInternals.ts
Comment thread src/js/internal/streams/iter/consumers.ts
Comment thread src/js/internal/streams/iter/push.ts
Comment thread src/js/node/_http_incoming.ts
Comment thread test/js/node/stream/node-stream.test.js
Comment thread test/js/node/test/parallel/test-stream-iter-broadcast-basic.js
Comment thread test/js/node/test/parallel/test-stream-iter-from-sync.js
Comment thread test/js/node/test/parallel/test-stream-iter-share-from.js
Comment thread test/js/node/test/parallel/test-stream-iter-writable-from.js
Comment thread src/js/node/_http_incoming.ts
Comment thread src/js/builtins/ProcessObjectInternals.ts Outdated
@cirospaciari cirospaciari force-pushed the claude/streams-node26-sync branch from 22307d5 to 2305b6e Compare June 5, 2026 20:32
Comment thread src/js/builtins/ReadableStreamInternals.ts Outdated
Comment thread src/js/node/_http_incoming.ts
Comment thread src/resolve_builtins/HardcodedModule.rs
Comment thread src/jsc/bindings/JSCommonJSModule.cpp
Comment thread src/js/node/_http_client.ts Outdated
Comment thread src/jsc/bindings/BunProcess.cpp
@cirospaciari cirospaciari force-pushed the claude/streams-node26-sync branch from f6fd167 to 22fd91f Compare June 6, 2026 01:33
Comment thread src/js/node/_http_client.ts
Comment thread test/js/node/http/node-http.test.ts Outdated
Comment thread src/js/node/_http_client.ts
Comment thread test/js/node/net/node-net.test.ts Outdated
cirospaciari and others added 27 commits June 10, 2026 00:12
Sync the vendored test/js/node/test stream tests (61 files, plus the
zlib/crypto transform-stream consumers) to their node v26.3.0 versions
and fix the behavior gaps they expose:

- readable: read() returns one buffered chunk at a time instead of
  concatenating the whole buffer (nodejs/node#60441). Port the
  "fast path for buffers" branch of howMuchToRead.
- writable: write(string, 'buffer') now throws ERR_UNKNOWN_ENCODING.
- duplexify: an error on the readable side of a { readable, writable }
  pair now destroys the writable side (was destroying the readable
  side itself - port typo).
- Duplex.toWeb: forward options; support readableType: 'bytes' and the
  deprecated type alias (emits DEP0201).
- Readable.toWeb: support BYOB byte streams (options.type = 'bytes');
  readable completion no longer waits for a half-open writable side of
  a duplex, so the web stream closes when the readable side ends.
- ErrorCode: format "Unknown encoding:" values like node's %s (inspect
  objects -> "{}", not "[object Object]"), and drop the cipher name
  from ERR_CRYPTO_UNKNOWN_CIPHER to match node's exact-match-asserted
  "Unknown cipher" message.

test/js/node/stream/node-stream-uint8array.test.ts asserted the old
read() concatenation behavior; updated to match node v26.3.0.

Verified: all 193 vendored stream tests pass; full node-compat sweep
(2455 entries) shows no regressions vs the unmodified binary; bun-native
stream/web-stream test files pass.
…3.0 stream tests

Brings the vendored stream suite to all 251 node v26.3.0 stream test
files (249 parallel + 2 sequential); 238 pass.

New feature: node's experimental iterable-streams API behind
--experimental-stream-iter (also BUN_EXPERIMENTAL_STREAM_ITER=1):
- node:stream/iter and node:zlib/iter entry modules plus the 13
  internal/streams/iter/* modules ported from node v26.3.0 (~7.5k
  lines: push/pull/from/share/broadcast/transform/consumers/classic/
  duplex/ringbuffer/types/utils).
- Resolution gated in the module loader (HardcodedModule + alias gate):
  without the flag, node:stream/iter reports "No such built-in module"
  and bare stream/iter falls through to filesystem resolution, matching
  node. Readable.prototype[Symbol.for("Stream.toAsyncStreamable")]
  interop with lazy flag check (ERR_STREAM_ITER_MISSING_FLAG).
- New error codes: ERR_OPERATION_FAILED, ERR_STREAM_ITER_MISSING_FLAG,
  ERR_INVALID_ARG_VALUE RangeError variant. src/jsc/ErrorCode.rs had
  drifted from the generated C++ list; renumbered all ordinals from
  ErrorCode+List.h (a stale mirror made native code throw adjacent
  error codes, e.g. ERR_INVALID_HANDLE_TYPE for ERR_INVALID_HTTP_TOKEN).

Behavior fixes exposed by the newly vendored tests:
- readable: fromList decoder slow path compared n against the buffer
  array length instead of the chunk length, over-returning on read(n)
  with an encoding set.
- readable: resume()/pause() are no-ops on destroyed streams.
- readable: compose() moved off the operators registry onto
  Readable.prototype.compose returning the Duplex directly (node
  v26.3.0 layout); registry wrapper hid the writable side.
- pipeline: an AbortError no longer wins over the real failure
  (node's error precedence clause).
- from: _destroy now chains the instance's original _destroy (e.g.
  duplexify's abort) before closing the iterator, so generators parked
  on a pending source settle; errors aggregate via aggregateTwoErrors.
- duplexify: fromAsyncGen destroy unblocks a generator awaiting the
  next write/final signal (port of node's resolver flush).
- writable: write(string, "buffer") throws ERR_UNKNOWN_ENCODING.
- web adapters: ReadableStream cancel settles pending BYOB reads with
  done:true; Readable.toWeb completion only waits for the readable
  side of a half-open duplex.
- http: client response EOF is pushed synchronously with the final
  chunk (single 'readable' for data+EOF, like node's parser);
  ClientRequest.destroy(err) emits 'error' on the request and destroys
  an incomplete response with ECONNRESET (node's socketCloseListener
  semantics); IncomingMessage.socket getter no longer resurrects a
  FakeSocket after the stream destroyer detached it (null signals the
  connection must outlive the request); ServerResponse.detachSocket
  tolerates that null.
- zlib: brotli decoder error codes match node ("ERR_" + enum suffix
  with its leading underscore, e.g. ERR__ERROR_FORMAT_PADDING_2).
- crypto: ERR_CRYPTO_UNKNOWN_CIPHER message is "Unknown cipher"
  (node dropped the cipher name).

13 vendored files still fail, all for runtime gaps outside the stream
JS layer: 7 need node-private internals (internal/js_stream_socket,
internal/test/binding, internal/async_context_frame, --expose-internals
userland requires), 2 need async_hooks init events / synchronous fd
adoption in net.Socket, 2 need per-segment http client body delivery
(native fetch path coalesces), and the 2 stream-iter disabled tests
need the CLI to report uncaught require failures under -e (currently
exits 0 silently; pre-existing).
…failures

- --expose-internals: vendored tests can require node-internal modules.
  bun:internal-for-testing gains an exposedInternals map (static requires);
  the bun-patched test common/index.js installs a require interceptor for
  internal/* when a test declares the flag. New internal shims:
  internal/async_context_frame (enabled=true, current() undefined - bun
  tracks context natively and never materializes frames) and
  internal/async_hooks (enabledHooksExist backed by real enable/disable
  tracking).
- async_hooks: createHook init events now fire for TickObject resources.
  process.nextTick emits one init per call when an init hook is enabled
  (array-length check on the hot path, nothing when disabled). Hooks with
  only init no longer warn; before/after/destroy still do.
- stdio: process.stdout/stderr writers are born constructed (instance
  _construct removed on the already-open-fd fast path), so they no longer
  allocate construct TickObjects - node's stdio (net.Socket/tty) never has
  a _construct. Without this, lazily creating stdout inside an init-hooked
  window miscounts tick objects.
- fs: ReadStream._read marks kIsPerformingIO and bails out of the read
  callback when the stream was destroyed mid-read (node parity), so
  close() between reads yields ERR_STREAM_PREMATURE_CLOSE instead of a
  clean end; _destroy waits for in-flight IO (kIoDone) before closing the
  fd.
- net: Socket({fd}) adopts pipe/character-device/file/socketpair fds with
  synchronous write(2)-based _write, matching node's effective stdio
  semantics (writes survive an immediate process.exit()). Network-socket
  fd adoption (handle layer) remains unsupported.
- cjs eval entry: use the out-param JSC::evaluate overload and rethrow.
  The 3-arg convenience overload catches the exception into a discarded
  NakedPtr, so an uncaught require() failure in `bun -e` (CJS-transpiled)
  exited 0 with no output; now it reports and exits nonzero.

Vendored stream suite: 247/251 node v26.3.0 files pass. Remaining 4:
test-stream-wrap{,-drain,-encoding} need a net.Socket({handle}) libuv
compat layer (internal/js_stream_socket); test-stream-pipeline needs
per-socket-read http client body delivery (native fetch path coalesces).
…ator, _-prefix unused params

- clippy disallows std::env::var_os in bun_resolve_builtins (and bun_core
  is not a dependency there); stream/iter is now gated on the CLI flag
  only, exactly like node. The readable interop check drops the env
  clause to stay consistent with the resolver.
- operators.ts: remove the dead compose() and its now-unused imports
  (Readable.prototype.compose replaced the registry entry).
- iter ports: underscore-prefix interface-compliance params that are
  intentionally unused.
…DE_STR table

The renumbering pass missed three things the review caught:
- MODULE_NOT_FOUND (the bare code, distinct from ERR_MODULE_NOT_FOUND) was
  mapped to the ERR_-prefixed entry's ordinal (155) instead of its own
  (268).
- COUNT stayed at 312; it is 315 with the three new codes.
- The CODE_STR table was not re-aligned; it is now regenerated wholesale
  from the generated ErrorCode+Data.h ordering, so code_str() is correct
  for every ordinal including the three insertions.
Node validates the options object before reading options.type
(lib/internal/webstreams/adapters.js); the port skipped it, so an
explicit null produced a raw property-access TypeError instead of
ERR_INVALID_ARG_TYPE.
Review caught that the resolver gate scanned raw argv, so the flag was
honored even when it appeared after the script name, where node treats
it as a script argument (and process.execArgv correctly excludes it).
The flag is now a declared CLI param; parsing sets an AtomicBool in
bun_resolve_builtins, and the resolver/alias gates read that instead of
scanning argv. `bun script.js --experimental-stream-iter` no longer
enables the module.
…xpectations for known stream gaps

The two bun-native crypto tests asserted the pre-v26 read()-concatenates
behavior (single cipher.read() returning the whole ciphertext). Node
v26.3.0 fails the same pattern (verified: 48 of 64 bytes on first read,
then ERR_OSSL_BAD_DECRYPT). Both now drain the stream; with that, the
cipher event order is byte-identical to node v26.3.0 - including the
ordering the old test marked as a TODO bug.

expectations.txt gains the four vendored stream tests blocked on
missing native subsystems (http per-segment delivery; the
net.Socket({handle}) libuv compat layer), so CI tracks them as known
rather than new failures. test-stream-pipeline is SKIP because its
failure mode is a timeout.
…rWithCode

The RangeError variant had no case in the switch, so
$ERR_INVALID_ARG_VALUE_RangeError("options.encoding", value) fell through
to the default branch and produced an error whose message was just the
property name. Route it through the same formatter as ERR_INVALID_ARG_VALUE,
parameterized on the error code so the RangeError structure is used.
A bare { fd } is the connect({ fd }) path: child_process extra stdio
creates sockets that attach a native duplex handle in
Socket.prototype.connect. Adopting in the constructor ended their readable
side with push(null) and replaced the native write path, which broke
puppeteer's CDP pipe (fds 3/4) and, on Windows, threw EBADF from
fstatSync on uv pseudo-fds. Node's own stdio-style fd sockets always pass
explicit flags (e.g. new Socket({ fd: 2, readable: false, writable: true })),
so keying adoption on writable: true keeps the stdio semantics while
restoring the native path for bare fds.

Also address review findings on the adoption path:
- close the adopted fd in _destroy (fds 0-2 exempt, matching
  SyncWriteStream's autoClose gating); node closes the wrapping libuv
  handle in the equivalent path
- rethrow fstat failures as ERR_INVALID_FD_TYPE('UNKNOWN'), matching
  node's createHandle -> guessHandleType behavior
reifyAllStaticProperties (hit by the node:process ESM module generator)
performs no exception check between PropertyCallback invocations, so any
builder that leaves a throw-scope obligation aborts debug/ASAN builds
under BUN_JSC_validateExceptionChecks once the next builder declares a
scope. Convert the throw-capable builders (config, _preload_modules /
moduleLoadList stub, env, mainModule, nextTick, channel) to the
DECLARE_TOP_EXCEPTION_SCOPE + clear pattern already used by the stdio
builders. Exposed by the new test-stream-readable-to-web.mjs, which is
the first vendored .mjs test to import node:process.

Also account for two tests that only passed while bun -e swallowed
child-process exceptions (fixed earlier in this branch):
- test-inspector-enabled.js needs process.binding('inspector'); mark it
  as a known failure in test/expectations.txt
- test_uv_threadpool_size/node-options.js runs the same addon as test.js
  (unsupported uv_sleep) and its env fixture was never vendored; mark it
  todo alongside test.js
Per the spec (whatwg/streams #1103), ReadableStreamClose performs each
pending readIntoRequest's close steps with undefined, resolving them as
{ value: undefined, done: true }. We only did this on the cancel path, so
a BYOB read pending at ordinary EOF (controller.close()) hung forever.
Move the drain into readableStreamClose and let cancel reuse it.
Readable.prototype[toAsyncStreamable] gated on
process.execArgv.includes("--experimental-stream-iter"), which user code
can mutate. Expose the resolver's write-once AtomicBool through a tiny
$cpp accessor (createStreamIterEnabledFlag) and consult that instead.
- _http_incoming: get connection() used ??= on the shared backing field,
  so reading req.connection from an 'aborted' listener resurrected a
  FakeSocket after the stream destroyer assigned stream.socket = null and
  defeated the socket-null teardown gate. Make it a strict alias of the
  socket getter, like node.
- async_hooks: register a per-instance wrapper in tickInitHooks so two
  hooks sharing the same init function stay independently removable
  (removal is by identity and could otherwise reorder the survivor).
- nextTick init dispatch: snapshot the hook list (mutations from inside a
  hook must not affect the in-flight emit, matching node's tmp_array
  staging) and treat a throwing init hook as fatal like node's fatalError:
  print (shielded from a user-replaced console) and exit 1 instead of
  surfacing the throw to the process.nextTick caller.
- drop the always-true stats !== undefined guard (the fstat try/catch
  rethrows unconditionally)
- restore prototype _write/_writev when _destroy releases an adopted fd
  so a later connect() on the reused socket writes through the new handle
- account synchronous fd writes in kBytesWritten so bytesWritten and
  _bytesDispatched reflect them
…atch leak

- node-stream.test.js: the spawned child pipes stderr (it emits the
  stream/iter ExperimentalWarning there); drain it concurrently and
  assert the warning, per the subprocess-pipe convention
- expectations.txt: fs.watch leak test false-passed everywhere while the
  eval-entry bug swallowed the child's thrown error; the underlying
  darwin PathWatcher leak is pre-existing and tracked separately
The runner uppercases machine modifiers (DARWIN, X64, ...) and compares
them case-sensitively against expectations.txt tags, so the lowercase
[ darwin ] entry never matched any lane and the known PathWatcher leak
failure kept surfacing on darwin testers. Matches the existing
[ WINDOWS-AARCH64 ] / [ ASAN ] casing.
…spec

Moving the drain into readableStreamClose was wrong: the WHATWG spec's
ReadableStreamClose only settles default-reader read requests, while the
drain-with-undefined step belongs to ReadableStreamCancel (step 6). With
the drain in close, the standard byte-source EOF pattern -
controller.close() then byobRequest.respond(0) - found readIntoRequests
already empty and the caller's transferred buffer was never returned
(node/browsers hand back a zero-length view of it). Restore the cancel-only
drain and replace the regression test with two spec-shaped ones:
close()+respond(0) returns the zero-length view over the caller's buffer,
and cancel() resolves the pending read with undefined.
Node defines IncomingMessage.prototype.connection with both accessors;
getter-only meant req.connection = x threw in strict mode instead of
writing through to req.socket.
ModuleLoader__isBuiltin (behind require.resolve.paths and
Module._resolveLookupPaths) scans the alias tables directly and never saw
the --experimental-stream-iter gate that Alias::get applies, so without
the flag the introspection APIs reported stream/iter as a builtin (null)
while require() correctly failed to resolve it - node returns the lookup
paths array there. Factor the gate into stream_iter_alias_gated() and
consult it from both readers; test asserts paths-array without the flag
and null with it.
ClientRequest.destroy(err) scheduled 'error' on a nextTick, but the abort
chain it triggers (onAbort -> socketCloseListener) emits 'close'
synchronously inside the same destroy() call, reversing node's order where
'close' is the terminal event - listeners cleaned up on 'close' missed the
error entirely. Emit the error first (re-entry is a no-op via the
destroyed guard); test asserts the [error, close] order.
- JSCommonJSModule: the CJS function-wrapper evaluate still used the
  3-arg JSC::evaluate overload that swallows exceptions (making the
  following RETURN_IF_EXCEPTION dead); use the out-param overload and
  rethrow like the eval-entry path above it, so a wrapper parse failure
  surfaces the real error instead of the 'function wrapper' TypeError
- BunProcess: the six lazy property builders converted to
  TopExceptionScope now also report the cleared exception via
  reportUncaughtExceptionAtEventLoop, matching the established
  constructStdin/constructStdioWriteStream pattern, so the original
  failure is diagnosable instead of silently yielding undefined
The synchronous 'error' emit added for event ordering threw straight out
of destroy() when no listener was attached (standard EventEmitter
behavior), skipping the abort/teardown chain and leaking the in-flight
fetch - destroyed was already set, so a retry no-oped. Emit synchronously
only when a listener exists (preserving the error-then-close order);
otherwise defer to a nextTick so the teardown always runs, a listener
attached later in the same tick still catches, and an unhandled error
crashes asynchronously - matching node, where destroy() never throws.

Also drop the stray un-unref'd 5s timeout from the ordering test (Bun's
per-test timeout covers the hang case) and add a no-listener regression
test.
…throwing error handler

The listenerCount gate only covered the no-listener half of the
'destroy() never throws' invariant: a present 'error' handler that itself
throws still unwound out of the synchronous emit before the
abort/socket-destroy chain ran, leaking the in-flight fetch with destroyed
already set. Catch the listener's throw and re-throw it on a fresh tick,
matching node where a throwing socketErrorListener becomes an async
uncaught exception after socket.destroy(err) already ran. Subprocess test
asserts destroy() returns, the teardown ran, and the handler bug surfaces
as an async uncaught exception.
… import

Matches the file's existing ESM import style and the test convention of
reserving require/dynamic import for tests that exercise them.
@cirospaciari cirospaciari force-pushed the claude/streams-node26-sync branch from dd7d9dd to 74a5aff Compare June 10, 2026 00:19
@cirospaciari cirospaciari force-pushed the claude/streams-node26-sync branch from 2789aa3 to 699fc81 Compare June 10, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants