Summary
Follow-up to #234. Now that Blob has working .arrayBuffer() / .text() / .bytes() / .slice() (PR for #234), the only remaining gap on the WHATWG Blob surface is blob.stream(): ReadableStream<Uint8Array> — and that needs the broader Web Streams API stood up first.
Pre-existing state (verified via grep across crates/):
- Zero
ReadableStream / getReader / ReadableStreamDefaultReader implementations.
Response.body is not a stream — js_response_* FFIs in crates/perry-stdlib/src/fetch.rs only emit fully-buffered text / json / arrayBuffer / blob.
Request.body likewise only carries the buffered bytes.
Symbol.asyncIterator is recognized at HIR (crates/perry-hir/src/lower_decl.rs:77) but no protocol is wired for stream chunk iteration.
A scoped "Blob-only stream" stub would create inconsistency: users would discover blob.stream().getReader() works but response.body.getReader() fails the same way. Better to land Web Streams as a coherent surface.
Surface to land
Minimum viable for parity with Node 22 / browsers:
ReadableStream<Uint8Array> constructor + getReader() / cancel() / tee() / pipeTo(dest) / pipeThrough({ readable, writable })
ReadableStreamDefaultReader with read(): Promise<{ done, value }>, releaseLock(), closed promise
Symbol.asyncIterator protocol so for await (const chunk of stream) works
WritableStream + WritableStreamDefaultWriter (needed for pipeTo)
TransformStream (needed for pipeThrough)
Once those exist, the consumers fall in:
Why this needs its own issue
- ~500-800 LOC of runtime + codegen across multiple crates (
perry-runtime, perry-stdlib/fetch, perry-codegen, perry-hir).
- New GC scanners for the per-stream / per-reader queues (chunks held in the queue must be marked).
- Backpressure semantics (
ReadableStreamDefaultController.desiredSize, queueing strategies) need design work.
- Async-iteration protocol for streams is its own subsystem that doesn't exist yet —
Symbol.asyncIterator resolution + next() / return() shape.
Acceptance criteria
```ts
// blob.stream() round-trip
const blob = new Response("hello world").blob();
const reader = blob.stream().getReader();
const { value, done } = await reader.read(); // value: Uint8Array(b"hello world"), done: false
const r2 = await reader.read(); // r2.done: true
// for-await-of
let total = "";
for await (const chunk of (await new Response("abc").blob()).stream()) {
total += new TextDecoder().decode(chunk);
}
assertEq(total, "abc");
// response.body streaming (proves the API isn't blob-only)
const res = await fetch("https://example.com/big.bin\");
const reader = res.body.getReader();
let bytesSeen = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
bytesSeen += value.length;
}
```
Related
Summary
Follow-up to #234. Now that
Blobhas working.arrayBuffer()/.text()/.bytes()/.slice()(PR for #234), the only remaining gap on the WHATWG Blob surface isblob.stream(): ReadableStream<Uint8Array>— and that needs the broader Web Streams API stood up first.Pre-existing state (verified via grep across
crates/):ReadableStream/getReader/ReadableStreamDefaultReaderimplementations.Response.bodyis not a stream —js_response_*FFIs incrates/perry-stdlib/src/fetch.rsonly emit fully-bufferedtext/json/arrayBuffer/blob.Request.bodylikewise only carries the buffered bytes.Symbol.asyncIteratoris recognized at HIR (crates/perry-hir/src/lower_decl.rs:77) but no protocol is wired for stream chunk iteration.A scoped "Blob-only stream" stub would create inconsistency: users would discover
blob.stream().getReader()works butresponse.body.getReader()fails the same way. Better to land Web Streams as a coherent surface.Surface to land
Minimum viable for parity with Node 22 / browsers:
ReadableStream<Uint8Array>constructor +getReader()/cancel()/tee()/pipeTo(dest)/pipeThrough({ readable, writable })ReadableStreamDefaultReaderwithread(): Promise<{ done, value }>,releaseLock(),closedpromiseSymbol.asyncIteratorprotocol sofor await (const chunk of stream)worksWritableStream+WritableStreamDefaultWriter(needed forpipeTo)TransformStream(needed forpipeThrough)Once those exist, the consumers fall in:
blob.stream()— already declared in response.blob() drops body bytes — same shape as #227, needs Blob instance methods #234's docs/FFIs as deferred; wires to a ReadableStream that emits the body bytes as one chunk (or multiple if we add a chunk-size knob) thendone: true.response.body— switch from "always-buffered, never streamed" to a streaming pipe driven fromreqwest::Response::chunk().new Request(url, { body: stream })— accept a ReadableStream as a body source.new Response(stream)— accept a ReadableStream and serialize on read.Why this needs its own issue
perry-runtime,perry-stdlib/fetch,perry-codegen,perry-hir).ReadableStreamDefaultController.desiredSize, queueing strategies) need design work.Symbol.asyncIteratorresolution +next()/return()shape.Acceptance criteria
```ts
// blob.stream() round-trip
const blob = new Response("hello world").blob();
const reader = blob.stream().getReader();
const { value, done } = await reader.read(); // value: Uint8Array(b"hello world"), done: false
const r2 = await reader.read(); // r2.done: true
// for-await-of
let total = "";
for await (const chunk of (await new Response("abc").blob()).stream()) {
total += new TextDecoder().decode(chunk);
}
assertEq(total, "abc");
// response.body streaming (proves the API isn't blob-only)
const res = await fetch("https://example.com/big.bin\");
const reader = res.body.getReader();
let bytesSeen = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
bytesSeen += value.length;
}
```
Related
.stream())response.arrayBuffer()body bytes (the fix that made response.blob() drops body bytes — same shape as #227, needs Blob instance methods #234 surface its own gap)