Skip to content

Implement ReadableStream.from#32533

Open
robobun wants to merge 6 commits into
mainfrom
farm/74ef6f1c/readable-stream-from
Open

Implement ReadableStream.from#32533
robobun wants to merge 6 commits into
mainfrom
farm/74ef6f1c/readable-stream-from

Conversation

@robobun

@robobun robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

What

Adds the WHATWG ReadableStream.from(asyncIterable) static method, which Bun was missing on both the global ReadableStream and the one exported from node:stream/web (they are the same constructor in Bun).

Fixes #32529.
Fixes #3700 (original feature request).

Repro (before)

import { ReadableStream } from 'node:stream/web';

async function* gen() { yield 'a'; yield 'b'; yield 'c'; }

const stream = ReadableStream.from(gen());
for await (const chunk of stream) console.log(chunk);
TypeError: ReadableStream.from is not a function. (In 'ReadableStream.from(asyncIterableGenerator())', 'ReadableStream.from' is undefined)

After this change it prints a / b / c, matching Node.

Cause

The ReadableStream constructor (JSReadableStreamDOMConstructor) only installed length/name/prototype; no static from was ever defined. WebKit gates from behind a setting Bun does not enable, and Bun's builtins never provided it.

Fix

  • New from builtin in src/js/builtins/ReadableStream.ts implementing the ReadableStreamFromIterable algorithm:
    • reads Symbol.asyncIterator first, falling back to Symbol.iterator (a sync iterator is adapted per CreateAsyncFromSyncIterator, so its yielded values are awaited),
    • pulls lazily (highWaterMark: 0) so the iterator does not start until the stream is read,
    • forwards cancel(reason) to the iterator's return method,
    • throws ERR_ARG_NOT_ITERABLE (a TypeError) for non-iterable arguments, and propagates the usual TypeError for null/undefined.
  • Installed on the constructor in src/jsc/bindings/webcore/JSReadableStream.cpp with { writable, enumerable, configurable }, length 1, name "from", matching Node and WebIDL.

Because node:stream/web re-exports the global constructor, one install covers both entry points.

Verification

bun bd test test/js/web/streams/streams.test.js -t "ReadableStream.from" (17 tests): sync iterables, strings, async generators, null/undefined chunks, sync promise-value awaiting, async-over-sync precedence, laziness, cancel -> return, error propagation, and the ERR_ARG_NOT_ITERABLE / TypeError paths. The suite fails on the released binary and passes with this change.

Add the WHATWG `ReadableStream.from(asyncIterable)` static to the
constructor shared by the global `ReadableStream` and the one exported
from node:stream/web. It follows the ReadableStreamFromIterable
algorithm: reads Symbol.asyncIterator first and falls back to
Symbol.iterator (adapting a sync iterator per CreateAsyncFromSyncIterator,
awaiting its values), pulls lazily with highWaterMark 0, forwards cancel
to the iterator's return method, and throws ERR_ARG_NOT_ITERABLE for
non-iterable arguments, matching Node.
@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:22 AM PT - Jun 20th, 2026

@robobun, your commit fabbfb6 has 4 failures in Build #63640 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32533

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

bun-32533 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Support ReadableStream.from() #3700 - Original feature request to support ReadableStream.from() static method

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

```
Fixes #3700
```

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 98059845-be5e-4c06-a9b6-7022657d1105

📥 Commits

Reviewing files that changed from the base of the PR and between c672e31 and fabbfb6.

📒 Files selected for processing (3)
  • packages/bun-types/globals.d.ts
  • src/js/builtins/ReadableStream.ts
  • test/js/web/streams/streams.test.js

Walkthrough

Implements ReadableStream.from(iterable) as a JS builtin in ReadableStream.ts by selecting async or sync iteration with fallback logic, defining pull and cancel controller callbacks with sync-vs-async await adaptation, and returning a new stream. Installs the builtin on the ReadableStream constructor via C++ bindings and adds TypeScript type definitions. Tests cover all input types, iterator precedence, laziness, cancellation, error propagation, and argument validation.

Changes

ReadableStream.from static method

Layer / File(s) Summary
Iterator selection and pull/cancel logic
src/js/builtins/ReadableStream.ts
Implements from(this, iterable) by selecting Symbol.asyncIterator when present (validates callable), otherwise falling back to Symbol.iterator with sync-mode adaptation; throws $ERR_ARG_NOT_ITERABLE for null/undefined. Captures iterator.next once and defines pull(controller) that validates next() result is an iterator-record object, closes the stream on done, enqueues iterResult.value (awaiting it for sync adaptation), and awaits iterResult.value on done for sync iterators to surface rejections. Defines cancel(reason) that calls and validates iterator.return(reason) when present, validates the returned iterator-record object, and for sync adaptation awaits iterResult.value so rejection propagates.
Stream creation, constructor binding, and type signature
src/js/builtins/ReadableStream.ts, src/jsc/bindings/webcore/JSReadableStream.cpp, packages/bun-types/globals.d.ts
Returns a new ReadableStream configured with { highWaterMark: 0 } using the pull and cancel handlers. Installs ReadableStream.from as a static builtin function on the ReadableStream constructor via putDirectBuiltinFunction with readableStreamFromCodeGenerator. Defines TypeScript type signature accepting `AsyncIterable
Comprehensive test suite
test/js/web/streams/streams.test.js
Imports ReadableStream from node:stream/web as WebReadableStream and validates: function exposure on both global and node:stream/web constructors, constructor identity, and method properties. Tests stream creation from arrays, strings, async generators, undefined/null-yielding iterables, and sync iterables yielding promises. Validates Symbol.asyncIterator precedence when both symbols exist, lazy iteration start, and cancellation via iterator.return(reason) with reason propagation. Tests error propagation through for await with prior values retained, rejected promises in sync iterator done results surfacing as consumer errors, cancellation rejection on sync iterator return() rejection, async iterator closing without reading done.value getter, and argument validation asserting ERR_ARG_NOT_ITERABLE for non-iterables and null-prototype objects and ERR_INVALID_STATE for non-object returns from iterator methods.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "Implement ReadableStream.from" directly and concisely summarizes the main change—adding the ReadableStream.from() static method to Bun.
Description check ✅ Passed The description comprehensively covers both required sections: 'What' explains the implementation, objective, and fixes, while 'How did you verify' details the 17 test cases confirming correctness.
Linked Issues check ✅ Passed All code changes directly address the linked issues: #32529 (ReadableStream.from missing) and #3700 (feature request). Implementation includes the WHATWG algorithm with async/sync iterator handling, proper error handling, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing ReadableStream.from(): builtin implementation, constructor binding, type definitions, and comprehensive tests. No extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, linked. #3700 is the original ReadableStream.from() feature request and this PR implements exactly that (the WHATWG from static), so I've added Fixes #3700 to the description alongside the duplicate report #32529.

@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: 3

🤖 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/ReadableStream.ts`:
- Line 112: The static method `from` lacks explicit `this` type annotation
required for C++-bound builtin functions. Add `this: typeof ReadableStream` as a
typed parameter to the `from` function signature to match the pattern used in
the `initialize` instance method which correctly types `this` as the
ReadableStream type. This typing is necessary to enable proper direct method
binding in C++ as per the coding guidelines for builtin functions.
- Around line 145-149: In the ReadableStream iterator adaptation logic, you must
always await iterResult.value for sync iterators on all code paths to prevent
swallowing promise rejections from the value getter. Restructure the code to
extract the await statement outside of the conditional that checks
iterResult.done, ensuring that when sync is true, iterResult.value is awaited
regardless of whether done is true or false. Additionally, apply this same await
pattern to the cancel function mentioned in the comment to ensure promise
rejections are properly handled there as well.

In `@test/js/web/streams/streams.test.js`:
- Around line 535-620: Add two new test cases to the ReadableStream.from() test
suite to cover Promise rejection scenarios in the iterator value paths. Create a
test that verifies rejected promises are properly propagated when a sync
iterator's next() method returns { done: true, value: Promise.reject(...) }, and
another test that verifies rejection handling when a sync iterator's return()
method returns { value: Promise.reject(...) }. Both tests should verify that the
rejection is caught and propagated correctly during async iteration, ensuring
the implementation properly handles the await statements in the close path for
both successful and rejected promise outcomes.
🪄 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: 616da589-df72-497c-bc34-72930b93a97f

📥 Commits

Reviewing files that changed from the base of the PR and between 8841747 and 191fcc4.

📒 Files selected for processing (3)
  • src/js/builtins/ReadableStream.ts
  • src/jsc/bindings/webcore/JSReadableStream.cpp
  • test/js/web/streams/streams.test.js

Comment thread src/js/builtins/ReadableStream.ts
Comment thread src/js/builtins/ReadableStream.ts
Comment thread test/js/web/streams/streams.test.js
The CreateAsyncFromSyncIterator adaptation awaits the value of every
result, including a done result and the iterator's return() result. Await
it on those paths too so a sync iterator that hands back a rejected
promise surfaces the rejection (erroring the stream or rejecting cancel)
instead of leaving it as an unhandled rejection.
Comment thread src/js/builtins/ReadableStream.ts
Comment thread src/js/builtins/ReadableStream.ts Outdated
ReadableStreamFromIterable reads a native async iterator's value solely on
the not-done path; the earlier hoist read it unconditionally, invoking the
value getter of a done result. Keep the value read under the not-done
branch for async iterators while still awaiting it on the done branch for
sync iterators (CreateAsyncFromSyncIterator), so a rejected promise still
surfaces there.
Comment thread src/js/builtins/ReadableStream.ts
Comment thread src/js/builtins/ReadableStream.ts Outdated
Comment thread src/jsc/bindings/webcore/JSReadableStream.cpp
Comment thread src/js/builtins/ReadableStream.ts Outdated
Route the iterator-protocol validation failures through
$ERR_INVALID_STATE_TypeError so they carry code ERR_INVALID_STATE like
Node (iterator method, iterator.next(), and iterator.return() not
returning/fulfilling with an object). Guard the ERR_ARG_NOT_ITERABLE
message's String(iterable) with a try/catch so a null-prototype object
(or a throwing toString/Symbol.toPrimitive) still reports the code
instead of a raw 'Cannot convert ... to primitive value' TypeError.
Declare the from static in the no-DOM bun-types fallback so server-only
projects without lib.dom get the type.
@robobun robobun requested a review from alii as a code owner June 20, 2026 16:09
@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

This PR is ready for review: the implementation is complete, all review threads are resolved, and the tests pass locally (bun bd test test/js/web/streams/streams.test.js, 24 ReadableStream.from cases, plus the 12 bun-types cases).

CI build 63640 is red only because of a transient infrastructure failure during dependency setup:

error: failed to download bun-tracestrings@github:oven-sh/bun.report#912ca63: HTTP 5xx

That is a GitHub server error fetching a build dependency; the Windows and aarch64 test shards aborted before running any tests, so it is unrelated to this change. The previous run was likewise red only on unrelated macOS aarch64 infra (a puppeteer browser download, an amd64/arm64 Docker image mismatch for autobahn, and an R2 network timeout).

Could a maintainer re-run CI when convenient? The diff itself is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ReadableStream from node:stream/web does not implement method from Support ReadableStream.from()

1 participant