Iterable Web Streams Extensions

Draft,

This version:
https://iter-web-streams.proposal.wintertc.org/
Issue Tracking:
GitHub
Editor:
(Cloudflare)

This document is not yet an official WinterTC draft. It is an individual proposal intended to be submitted to ECMA TC55 (WinterTC) for consideration. It has not been adopted, endorsed, or reviewed by the committee. The content may change substantially before or during that process.

Introduction

The Streams Standard [STREAMS] defines ReadableStream, WritableStream, and TransformStream: class-based streams with chunk-at-a-time delivery. The Iterable Streams API [ITER-STREAMS] defines a bytes-only streaming model built directly on the ECMAScript iteration protocols: streams are AsyncIterable<Uint8Array[]>, iteration yields batches of chunks to amortize per-tick costs, and production goes through a Writer interface with an explicit byte budget and backpressure policies.

This specification bridges the two models from the web streams side. It defines:

// (a) Batched reads from any ReadableStream
const reader = rs.getReader({ mode: 'iter' });
for await (const chunks of reader) {
  // chunks is Uint8Array[]
}

// (b) Push-model construction; the controller IS an iter-streams Writer
const rs = new ReadableStream({
  type: 'iter',
  async start(writer) {
    if (!writer.writeSync(header)) await writer.write(header);
    await writer.end();
  }
});

// (c) Batch-native sink: write() receives Uint8Array[] batches
const ws = new WritableStream({
  type: 'iter',
  async write(chunks) {
    await socket.writev(chunks);
  }
});

Internally, the new controller and reader types are based on the iterable streams model, allowing implementations to take advantage of its performance characteristics. Because they are new controller and reader types, backwards compatibility with existing streams is preserved: every existing operation on an iter-type stream works through compatibility facades, and every existing stream gains batched reading without modification.

Goals

  1. Batched consumption for all streams: Reading a ReadableStream batch-at-a-time amortizes async iteration cost regardless of how the stream was created.

  2. Full iterable-streams citizenship: Objects defined by this specification conform structurally to the [ITER-STREAMS] interfaces and protocols. An iter reader is a ByteReadableStream; the iter readable controller and the iter writer implement Writer; the protocol symbols are honored. Web streams participate in the iterable streams ecosystem — Stream.pull(), Stream.pipeTo(), Stream.text(), transforms — without adapters.

  3. Backwards compatibility: An iter-type stream is a ReadableStream (or WritableStream). Default readers, default writers, tee(), piping, and every other existing operation continue to work.

  4. Pull-through piping: When both ends of a pipe are iter-type, the pipe is the iterable streams model: lazy, fused, and driven by the ultimate consumer.

  5. Bytes-only where it matters, generic where it helps: Iter-type streams are bytes-only, matching [ITER-STREAMS]. The iter reader is generic: on a value-oriented stream it still provides batching-as-amortization, without claiming iterable-streams integration.

Non-Goals

  1. Modifying the Iterable Streams API: [ITER-STREAMS] remains a separate specification. This specification consumes its interfaces and protocols; it defines nothing on the Stream namespace.

  2. Replacing default or byte streams: The default and "bytes" stream types are untouched. This specification adds a third type alongside them.

  3. New consumption utilities: Draining helpers (text(), bytes(), etc.) belong to [ITER-STREAMS]. This specification makes web streams work with them rather than duplicating them.

  4. BYOB integration: The "byob" reader mode and iter-type streams do not interact. getReader({ mode: "byob" }) on an iter-type stream throws, exactly as it does on a default stream.

Note: This specification is intended for standardization through ECMA TC55 (WinterTC), not through the WHATWG. Because Web IDL provides no mechanism for extending enumerations from outside a specification, the changes to the Streams Standard are expressed as amendments in § 15 Modifications to the Streams Standard. If the extensions prove successful, upstreaming some or all of them into the Streams Standard would be a desirable outcome; the design deliberately confines the amendments to well-isolated extension points (new enum values, reserved dictionary members, and type-dispatched algorithm steps) to keep that path open.

Design Rationale

The shape of the integration

Each surface adopts whichever model fits its direction:

Web streams side Model
Producing into a readable ReadableStreamIterController implements the [ITER-STREAMS] Writer interface
Consuming a readable ReadableStreamIterReader is a ByteReadableStream (AsyncIterable<Uint8Array[]>)
Producing into a writable WritableStreamIterWriter implements the [ITER-STREAMS] Writer interface
Consuming from a writable underlying sink with type: "iter" the Streams Standard sink model, batch-widened: write() receives Uint8Array[]

[ITER-STREAMS] states that Writer "is an interface, not a concrete class — any object implementing this interface can serve as a writer," and that its stream types are structural (AsyncIterable<Uint8Array[]>). The production surfaces defined here are those objects — the controller handed to an underlying source’s start() is a Writer, with no wrapping and no adapter layer. The sink side deliberately keeps the Streams Standard’s callback model: an iter writable stream is a default WritableStream in every respect except that its bookkeeping is the byte-budget model and its write() callback receives coalesced batches. The sink lifecycle — one write at a time, promise-gated, close() and abort() callbacks — is inherited, not reinvented.

Why the integration needs no changes to [ITER-STREAMS]

Three properties of [ITER-STREAMS] carry the integration:

  1. Stream.from() consults Symbol.for('Stream.toAsyncStreamable') before the iteration protocols. This specification installs that hook on ReadableStream.prototype (§ 10 The toAsyncStreamable hook), so every iterable-streams entry point — Stream.from(), Stream.pull(), Stream.text(), Stream.pipeTo() — automatically upgrades any ReadableStream to batched reads. Without the hook, web streams still work through the Symbol.asyncIterator fallback, but degrade to single-chunk batches.

  2. Stream.pipeTo() duck-types its destination on the write method (preferring writev). The controllers and writers defined here satisfy it structurally, so they are valid iterable-streams pipe destinations with batching, close, and error propagation intact.

  3. The Symbol.for('Stream.drainableProtocol') symbol makes Stream.ondrain() work with any object that carries it. The Writer-implementing objects defined here carry it.

Batch boundaries are not semantic

Batching exists to amortize iteration costs. It carries no meaning: any operation may split, merge, or regroup batches, provided the flattened chunk sequence and its ordering are preserved (§ 5.1 Batch boundaries are not semantic). This principle is what makes the compatibility facades and the fused pipe observationally coherent: unbatching for a default reader, re-coalescing at a sink, and end-to-end fusion all preserve the only thing that matters — the chunks and their order.

© 2026 Ecma International

Permission under Ecma’s copyright to copy, modify, prepare derivative works of, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the full text of this copyright notice on ALL copies of the work or portions thereof.

THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.

1. Scope

This proposal defines the Iterable Web Streams Extensions: an extension of the Streams Standard [STREAMS] that integrates it with the Iterable Streams API [ITER-STREAMS]. It specifies:

2. Conformance

A conforming implementation of this specification must also be a conforming implementation of the Streams Standard [STREAMS] as amended by § 15 Modifications to the Streams Standard, and must provide the [ITER-STREAMS] interfaces and protocol symbols to the extent this specification depends on them.

Note: An implementation need not expose the [ITER-STREAMS] Stream namespace to conform to this specification — the dependency is on the Writer interface contract, the structural stream types, the backpressure model, the transform contract, and the protocol symbols. In practice the two specifications are designed to be implemented together.

A conforming implementation shall also conform to [ECMASCRIPT] and [WEBIDL].

3. Normative references

The following documents are referred to in the text in such a way that some or all of their content constitutes requirements of this document.

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/
[ITER-STREAMS]
James M Snell. Iterable Streams API. Draft Proposal. URL: https://iter-streams.proposal.wintertc.org/
[STREAMS]
Adam Rice; et al. Streams Standard. Living Standard. URL: https://streams.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

4. Terms and definitions

For the purposes of this document, the terms and definitions given in [ECMASCRIPT], the Streams Standard [STREAMS], the Iterable Streams API [ITER-STREAMS], and the following apply.

4.1. Iterable Web Streams Extensions

the extensions to the Streams Standard defined by this specification

4.2. batch

an array of chunks delivered as a single iteration step or read result; for byte-oriented streams, a Uint8Array[] as defined by the [ITER-STREAMS] batched chunks concept

4.3. iter readable stream

a ReadableStream whose controller is a ReadableStreamIterController

4.4. iter writable stream

a WritableStream whose controller is a WritableStreamIterController

4.5. facade

the adaptation layer through which existing Streams Standard operations (default readers, default writers, and the algorithms built on them) interoperate with iter-type streams

4.6. fused pipe

the pull-through pipe established when both ends of a pipe operation are iter-type streams, in which source batches are delivered directly to the destination sink’s write() callback with no intermediate buffering

5. Core concepts

5.1. Batch boundaries are not semantic

A batch is a delivery grouping, not a message boundary. Operations defined by this specification — and implementations, wherever this specification grants latitude — may split, merge, or regroup batches freely. The normative invariant is the flattened chunk sequence: the sequence of individual chunks obtained by concatenating batches in order. All operations MUST preserve the flattened chunk sequence and its ordering; none are required to preserve batch boundaries.

Note: This matches [ITER-STREAMS], in which batch composition is implementation-chosen and transforms regroup freely. Applications MUST NOT attach meaning to how chunks are grouped into batches.

5.2. Bytes-only types, generic reader

Iter readable streams and iter writable streams are bytes-only: their chunks are Uint8Array objects, and their production surfaces accept Uint8Array or strings (which are UTF-8 encoded), following the [ITER-STREAMS] Writer contract. Chunks of any other type are rejected with a TypeError.

The ReadableStreamIterReader, by contrast, is generic: it may be acquired on any ReadableStream and performs no chunk coercion. On a byte-carrying stream its batches are Uint8Array[] and the reader is a conforming [ITER-STREAMS] ByteReadableStream. On a value-oriented stream, batches are arrays of whatever the stream carries — batching still amortizes iteration cost, but the reader does not claim integration with the [ITER-STREAMS] byte-oriented utilities.

5.3. Internal state: the slots buffer

An iter-type stream’s internal state is the [ITER-STREAMS] model, not the Streams Standard’s chunk queue: a slots buffer of batches, a byte budget, and a buffered byte count, governed by a backpressure policy. Each write or writev occupies one slot; a slot’s byte size is the sum of its chunks' byte lengths.

The byte budget is configured through the stream’s queuing strategy: highWaterMark is the budget in bytes. If no strategy is provided, the budget is an implementation-defined default of at least 16384 bytes, mirroring [ITER-STREAMS]. A provided value below the implementation’s minimum MAY be clamped to that minimum. A size function MUST NOT be provided for iter-type streams; if present, the constructor throws a RangeError. Byte accounting is intrinsic.

Buffered bytes are decremented as chunks leave the buffer. When a facade consumes a slot partially (chunk at a time), the buffered byte count decreases per chunk as each chunk is consumed, not when the slot empties. This keeps producer-observable backpressure signals (canWrite, the drainable protocol) consistent between batch-native and facade consumption.

Note: The [ITER-STREAMS] overshoot rule — a write is accepted whenever buffered bytes are below the budget, and then counts fully even if it overshoots — means buffered bytes can exceed the budget. Through the default-writer facade this surfaces as a negative desiredSize, which is exactly the Streams Standard’s existing behavior for an over-full queue. The two models align without adjustment.

5.4. The compatibility invariant

Every existing Streams Standard operation works on iter-type streams through the facades:

Stream default reader / writer byob reader iter reader / writer
default ReadableStream existing behavior TypeError (existing) adapt: drain queue into a batch
byte ReadableStream existing behavior existing behavior adapt: drain queue into a batch
iter readable stream adapt: unbatch (§ 8.4 The default reader facade) TypeError native
default WritableStream existing behavior adapt: Writer over default machinery (§ 12.3 On any other WritableStream: adapted)
iter writable stream adapt: writes into the slots buffer (§ 11.4 The default writer facade) native

tee(), piping between mixed types, async iteration of the stream itself (which remains chunk-at-a-time for compatibility), and integration points in other specifications that consume ReadableStream or WritableStream all operate through the default facades and continue to work unchanged.

5.5. Fusion

When both ends of a pipe are iter-type, the pipe is not a pump loop between two buffers. It is a fused pipe: source batches are delivered directly to the destination sink’s write() callback, bypassing the destination’s buffer, and each write()’s settlement gates the next pull from the source — through any composed transforms, back to the origin producer’s byte budget. Data moves only at the pace of the ultimate consumer. See § 14 Piping.

6. Web IDL definitions

6.1. IterWriteOptions

dictionary IterWriteOptions {
  AbortSignal signal;
};

The IterWriteOptions dictionary mirrors the [ITER-STREAMS] WriteOptions dictionary.

6.2. The ReadableStreamIterReader interface

[Exposed=*]
interface ReadableStreamIterReader {
  constructor(ReadableStream stream);

  Promise<ReadableStreamReadResult> read();
  undefined releaseLock();
};
ReadableStreamIterReader includes ReadableStreamGenericReader;

The ReadableStreamIterReader interface provides batched reads. It is acquired via getReader({ mode: "iter" }) on any ReadableStream. See § 7 The iter reader.

6.3. The ReadableStreamIterController interface

[Exposed=*]
interface ReadableStreamIterController {
  readonly attribute boolean? canWrite;

  Promise<undefined> write((Uint8Array or USVString) chunk,
                           optional IterWriteOptions options = {});
  Promise<undefined> writev(sequence<(Uint8Array or USVString)> chunks,
                            optional IterWriteOptions options = {});
  boolean writeSync((Uint8Array or USVString) chunk);
  boolean writevSync(sequence<(Uint8Array or USVString)> chunks);

  Promise<unsigned long long> end(optional IterWriteOptions options = {});
  long long endSync();

  undefined fail(optional any reason);
};

The ReadableStreamIterController interface is the controller for iter readable streams. It implements the [ITER-STREAMS] Writer interface; its member semantics are those of the Writer interface, bound to the stream’s slots buffer. See § 8 Iter readable streams.

6.4. The WritableStreamIterController interface

[Exposed=*]
interface WritableStreamIterController {
  readonly attribute AbortSignal signal;
  undefined error(optional any e);
};

The WritableStreamIterController interface is the controller for iter writable streams, passed to the underlying sink’s callbacks. Its surface mirrors the Streams Standard’s WritableStreamDefaultController: error() errors the stream, and signal is aborted when the stream is aborted, allowing a sink engaged in long-running work to observe cancellation. The difference from the default controller is internal: the stream’s bookkeeping is the slots buffer and byte budget rather than the size-algorithm queue. See § 11 Iter writable streams.

6.5. The WritableStreamIterWriter interface

[Exposed=*]
interface WritableStreamIterWriter {
  constructor(WritableStream stream);

  readonly attribute boolean? canWrite;
  readonly attribute Promise<undefined> closed;

  Promise<undefined> write((Uint8Array or USVString) chunk,
                           optional IterWriteOptions options = {});
  Promise<undefined> writev(sequence<(Uint8Array or USVString)> chunks,
                            optional IterWriteOptions options = {});
  boolean writeSync((Uint8Array or USVString) chunk);
  boolean writevSync(sequence<(Uint8Array or USVString)> chunks);

  Promise<unsigned long long> end(optional IterWriteOptions options = {});
  long long endSync();

  undefined fail(optional any reason);

  undefined releaseLock();
};

The WritableStreamIterWriter interface implements the [ITER-STREAMS] Writer interface over a WritableStream. It is acquired via getWriter({ mode: "iter" }). See § 12 Iter writers.

6.6. The IterTransformStream interface

[Exposed=*]
interface IterTransformStream {
  constructor(optional any transform,
              optional IterTransformStreamOptions options = {});

  readonly attribute ReadableStream readable;
  readonly attribute WritableStream writable;
};

dictionary IterTransformStreamOptions {
  unsigned long long budget;
  DOMString backpressure = "strict";
};

The IterTransformStream interface provides batch-native transforms. It conforms to the ReadableWritablePair shape and may be passed to pipeThrough(). The transform argument is typed any because the accepted values — a stateless transform function, a stateful transform object, or undefined — follow the [ITER-STREAMS] transform argument detection rules, which Web IDL cannot express as a single type. See § 13 Iter transform streams.

6.7. Options dictionaries

dictionary WritableStreamGetWriterOptions {
  DOMString mode;
  DOMString backpressure;
};

dictionary ReadableStreamFromOptions {
  DOMString type;
};

partial dictionary UnderlyingSource {
  DOMString backpressure = "strict";
};

partial dictionary UnderlyingSink {
  DOMString backpressure = "strict";
};

The backpressure members take [ITER-STREAMS] BackpressurePolicy values. They are consulted only when the corresponding type is "iter"; for other stream types they are ignored. For iter writable streams, only "strict" and "unbounded" are permitted; see § 11.3 Backpressure policy.

Note: BackpressurePolicy is defined by [ITER-STREAMS]. The members are declared as DOMString because the enumeration is defined in another specification; values are validated in prose.

6.8. Protocol conformance

Objects defined by this specification participate in the [ITER-STREAMS] protocol symbols:

7. The iter reader

7.1. Acquisition

A ReadableStreamIterReader is acquired by calling getReader({ mode: "iter" }) on any ReadableStream, regardless of the stream’s underlying source type, or by its constructor. Acquisition locks the stream, exactly as for other reader types: while the reader is active, locked is true and acquiring any other reader throws a TypeError.

Unlike mode: "byob", mode: "iter" does not constrain the stream’s controller type. The reader performs no chunk coercion; see § 5.2 Bytes-only types, generic reader.

7.2. Reading

The read() method reads the next batch. It returns a promise for a ReadableStreamReadResult:
  1. If the stream is errored, the promise rejects with the stored error.
  2. If chunks are available — in the slots buffer for an iter readable stream, or in the internal queue for other stream types — the promise resolves with { value, done: false }, where value is an array containing all currently available chunks, drained from the stream, in order. For an iter readable stream, the array MAY correspond to one or more whole or partial slots; boundaries are not preserved (§ 5.1 Batch boundaries are not semantic).
  3. Otherwise, if the stream is closed, the promise resolves with { value: undefined, done: true }.
  4. Otherwise, the read participates in the stream’s normal demand signaling (for pull-based sources, a pull is requested) and the promise resolves with a batch containing at least one chunk once data becomes available, or with { value: undefined, done: true } if the stream closes first.

Draining all available chunks per read is the batching amortization: one promise resolution delivers everything buffered. Implementations MAY cap the drained byte size of a single batch at an implementation-defined limit.

Reads decrement the buffered byte count and notify drain waiters per § 5.3 Internal state: the slots buffer, propagating demand to the producer.

7.3. Async iteration

The async iterator of a ReadableStreamIterReader is obtained via its Symbol.asyncIterator method. Its behavior:
  1. next() performs the same operation as read(), returning its result as the iterator result object.
  2. return(reason) cancels the stream with reason (as if by reader.cancel(reason)), releases the lock, and resolves with a done result.
  3. throw(reason) behaves as return(reason) and then rejects with reason.

A consumer breaking out of for await...of therefore cancels the source, matching the [ITER-STREAMS] convention that a consumer that stops iterating tears the pipeline down to the source.

When the stream’s chunks are Uint8Array objects, the reader is a conforming ByteReadableStream: it may be passed directly to Stream.pull(), Stream.text(), Stream.pipeTo(), and any other [ITER-STREAMS] consumer.

7.4. Reader generics

ReadableStreamIterReader includes the Streams Standard’s generic reader behaviors: the closed promise and cancel(reason) behave as for a default reader. releaseLock() releases the lock; pending read promises reject with a TypeError, matching default reader semantics.

8. Iter readable streams

8.1. Construction

An iter readable stream is constructed with type: "iter":

const rs = new ReadableStream({
  type: 'iter',
  async start(writer) {
    // writer is the ReadableStreamIterController — an iter-streams Writer
    if (!writer.writeSync(chunk)) await writer.write(chunk);
    const n = writer.endSync();
    if (n < 0) await writer.end();
  }
}, { highWaterMark: 65536 });

Constructor rules, applied when the underlying source’s type is "iter":

  1. start is required. If absent, throw a TypeError. The start callback is the only mechanism for acquiring the controller; a stream that can never be written to or ended is unusable by construction.

  2. If pull or autoAllocateChunkSize is present, throw a TypeError. The pull model is provided by ReadableStream.from() (§ 9 ReadableStream.from() in batch mode), not by the controller.

  3. cancel is permitted and is invoked per § 8.3 Cancellation.

  4. The queuing strategy provides the byte budget per § 5.3 Internal state: the slots buffer; a size function throws a RangeError.

  5. The underlying source’s backpressure member selects the [ITER-STREAMS] backpressure policy for the controller; the default is "strict". All four policies are permitted.

  6. start is invoked with the ReadableStreamIterController as its argument. If start throws or its returned promise rejects, the stream is errored with the thrown value, matching existing underlying-source semantics.

8.2. The controller is a Writer

The ReadableStreamIterController implements the [ITER-STREAMS] Writer interface, bound to the stream:

Chunks are accepted zero-copy: a written Uint8Array is not copied or detached, matching [ITER-STREAMS]. Producers MUST NOT mutate a chunk after writing it; see § 16 Security considerations.

8.3. Cancellation

When an iter readable stream is cancelled (via cancel(), a reader’s cancel, or iterator return):
  1. The slots buffer is discarded.
  2. The controller transitions to the errored state with the cancel reason: subsequent writes reject (or return false from sync variants), canWrite returns null, pending write promises reject, a pending end() promise rejects, and drain waiters are notified with the reason per the [ITER-STREAMS] fail() semantics.
  3. The underlying source’s cancel callback, if any, is invoked with the reason, per existing Streams Standard semantics.

This is the web-streams expression of the [ITER-STREAMS] rule that a consumer that stops iterating signals cancellation to the writer.

8.4. The default reader facade

Default readers (and every operation built on them: async iteration of the stream, tee(), mixed-type piping, and consumers in other specifications) interoperate with iter readable streams through the unbatching facade:

The flattened chunk sequence observed through the facade is identical to the sequence observed through batch reads.

9. ReadableStream.from() in batch mode

ReadableStream.from() is amended to accept an options argument: ReadableStream.from(source, { type: "iter" }).

When the type option is "iter", batch-mode from performs the following:
  1. Let iterable be source, interpreted as an async iterable if it has Symbol.asyncIterator, else as a sync iterable if it has Symbol.iterator; otherwise throw a TypeError.
  2. Return a new iter readable stream whose batch sequence is produced by lazily iterating iterable:
    1. Each value yielded by iterable that is an array is interpreted as a batch: its elements are the chunks. Each element must be a Uint8Array; otherwise the stream is errored with a TypeError.
    2. Each yielded value that is a Uint8Array is interpreted as a single-chunk batch.
    3. Any other yielded value errors the stream with a TypeError.
    4. Iteration is demand-driven: at most one iteration step of iterable is outstanding at a time, and steps are taken only to satisfy reads. Implementations MAY read ahead up to the byte budget.
    5. When iterable completes, the stream closes. If iteration throws, the stream errors with the thrown value.
    6. If the stream is cancelled, the iterator’s return() method is invoked, per existing ReadableStream.from() semantics.

When the type option is absent or undefined, ReadableStream.from() behaves exactly as currently specified (each yielded value is a single chunk of a default stream). No inspection-based batch detection is performed: an async iterable of Uint8Array[] passed without the option produces a default stream whose chunks are arrays, exactly as today.

Note: Batch-mode from() supplies the pull model for iter-type streams; type: "iter" construction with start supplies the push model. This mirrors the [ITER-STREAMS] division between Stream.from() and Stream.push(). Note the difference in element handling: Stream.from() normalizes forgivingly (strings encoded, buffers wrapped); batch-mode from() is strict, accepting only Uint8Array chunks. Applications wanting the forgiving behavior can compose the two specifications: ReadableStream.from(Stream.from(input), { type: "iter" }).

10. The toAsyncStreamable hook

ReadableStream.prototype gains a method keyed by Symbol.for('Stream.toAsyncStreamable'):

The ReadableStream toAsyncStreamable method, when invoked on a ReadableStream stream:
  1. If stream is locked, throw a TypeError.
  2. Return the result of getReader({ mode: "iter" }) on stream.

Because [ITER-STREAMS] Stream.from() consults Symbol.for('Stream.toAsyncStreamable') before the iteration protocols, this single method upgrades every iterable-streams entry point to batched reads for any ReadableStream:

// All of these consume the ReadableStream via batched reads:
await Stream.text(webReadableStream);
Stream.pull(webReadableStream, gzip);
await Stream.pipeTo(webReadableStream, writer);

Note: For value-oriented streams, the returned reader yields batches of non-byte values, which the [ITER-STREAMS] normalization rules will reject with a TypeError when consumed by its byte-oriented utilities — the same outcome as consuming such a stream through the Symbol.asyncIterator fallback.

11. Iter writable streams

11.1. Construction

An iter writable stream is constructed with type: "iter" in the underlying sink. The underlying sink’s type member is currently reserved by the Streams Standard (any value throws a RangeError); this specification defines its first value.

const ws = new WritableStream({
  type: 'iter',
  async write(chunks, controller) {
    // chunks is Uint8Array[] — every slot buffered at invocation time, coalesced
    await socket.writev(chunks);
  },
  close() {
    return socket.close();
  },
  abort(reason) {
    socket.destroy(reason);
  }
}, { highWaterMark: 65536 });

An iter writable stream is a default WritableStream with exactly two substitutions: its internal bookkeeping is the slots buffer and byte budget rather than the size-algorithm queue, and its write() callback receives a batch rather than a single chunk. The underlying sink contract — start, write, close, abort, all optional, with the Streams Standard’s invocation ordering and settlement gating — is otherwise inherited unchanged.

Constructor rules, applied when the underlying sink’s type is "iter":

  1. The queuing strategy provides the byte budget per § 5.3 Internal state: the slots buffer; a size function throws a RangeError.

  2. The underlying sink’s backpressure member selects the policy; only "strict" and "unbounded" are permitted — "drop-oldest" and "drop-newest" throw a RangeError. See § 11.3 Backpressure policy.

  3. Sink callbacks are invoked with the WritableStreamIterController as their controller argument.

11.2. Batch delivery

The sink’s write() callback is invoked with a batch: an array containing all chunks currently in the slots buffer — every available slot drained and concatenated in order. Boundaries are not preserved (§ 5.1 Batch boundaries are not semantic). Implementations MAY cap the byte size of a single delivered batch at an implementation-defined limit.

The Streams Standard’s sink sequencing is retained: one write() invocation at a time, with the returned promise gating the next. This gating is precisely where batching is regained: while a write() is pending, arriving chunks — whether written batch-at-a-time by an iter writer or chunk-at-a-time through the default writer facade — accumulate in the slots buffer, and the next invocation receives them all.

The remaining lifecycle is the Streams Standard’s, unchanged: close() is invoked after the end sentinel is reached and the final write() has settled, and its settlement fulfills the close request; abort(reason) is invoked per the standard abort sequencing; a rejected write() or a call to error() errors the stream.

During a fused pipe, write() is invoked with batches delivered directly from the pipe’s source rather than from the slots buffer; see § 14 Piping.

11.3. Backpressure policy

Iter writable streams support only the "strict" and "unbounded" policies. The dropping policies are excluded: data written to a WritableStream is committed toward a sink, and silently discarding committed data is not a meaningful sink behavior. (The dropping policies remain available where [ITER-STREAMS] defines them — push streams and broadcasts — and on iter readable streams, whose controller is a push-model writer.)

The policy is a property of the stream, configured at construction. It governs all producers: iter writers natively, and default writers through the facade.

11.4. The default writer facade

Default writers (and every operation built on them, including mixed-type piping) interoperate with iter writable streams through the facade:

12. Iter writers

12.1. Acquisition

A WritableStreamIterWriter is acquired by calling getWriter({ mode: "iter" }) on any WritableStream, or by its constructor. Acquisition locks the stream exactly as acquiring a default writer does.

The backpressure option is permitted only when the stream is not an iter writable stream (see § 12.3 On any other WritableStream: adapted); on an iter writable stream the policy belongs to the stream, and specifying the option throws a TypeError.

12.2. On an iter writable stream: native

On an iter writable stream, the writer’s operations bind directly to the stream’s slots buffer, budget, and policy — the Writer semantics of [ITER-STREAMS] with no adaptation. writev() batches occupy single slots and reach the sink’s write() callback whole (subject to coalescing with adjacent slots). end() is equivalent to closing the stream and resolves with the total bytes written through this writer. fail(reason) aborts the stream.

12.3. On any other WritableStream: adapted

On a default WritableStream, the writer adapts the Writer contract onto the default machinery:

Writer surface Default WritableStream mechanism
canWrite true if desiredSize > 0; false if ≤ 0; null if the stream is closed, closing, or errored
drainable protocol derived from the ready promise, wrapped to the true / false (closed) / reject (errored) contract
write(chunk) strings UTF-8 encoded; forwarded as a single write
writev(chunks) forwarded as sequential writes, in order; the returned promise settles when all have settled. Atomicity is best-effort: if a write fails mid-batch, the stream is already errored and no further chunks are written
writeSync() / writevSync() if desiredSize > 0, forward (per write/writev above) and return true; otherwise return false and write nothing — the try-fallback signal
end() close(); resolves with total bytes written through this writer
endSync() returns −1 unless the writer is already closing or closed (sink close is asynchronous); the try-fallback signal to await end()
fail(reason) abort(reason). Iter-level pending write promises reject synchronously with reason; the abort itself settles per Streams Standard semantics

The backpressure option (default "strict") selects the admission policy the adapter applies in front of the stream’s queue: "strict" tolerates one un-awaited pending write past exhaustion before rejecting with a RangeError; "unbounded" queues without limit. The dropping policies throw a TypeError, per § 11.3 Backpressure policy.

Note: This adaptation is what makes Stream.pipeTo(source, transforms, ws.getWriter({ mode: "iter" })) fully correct for any WritableStream. A default writer half-satisfies the [ITER-STREAMS] pipe destination duck-type today (it has write but not writev, end, or fail), silently losing batching, close propagation, and error propagation. The iter writer completes the contract.

13. Iter transform streams

13.1. The IterTransformStream class

Batch-native transforms are provided by IterTransformStream: a purpose-built class that conforms to the ReadableWritablePair shape — readable and writable attributes — rather than extending or modifying TransformStream. This follows the platform’s established pattern for transform-shaped classes (CompressionStream, TextDecoderStream, and their siblings), and it means pipeThrough() accepts an IterTransformStream structurally.

// Stateful: an object with a transform method —
// exactly an iter-streams stateful transform
const ts = new IterTransformStream({
  async *transform(source, { signal }) {
    // source is AsyncIterable<Uint8Array[] | null> — null is the flush signal
    for await (const chunks of source) {
      if (chunks === null) { yield finalize(); break; }
      yield process(chunks);
    }
  }
});

// Stateless: a bare function invoked once per batch —
// exactly an iter-streams stateless transform
const mapped = new IterTransformStream(
  (chunks) => chunks === null ? null : chunks.map(processChunk));

rs.pipeThrough(ts).pipeTo(ws);

Constructor rules:

  1. The transform argument is interpreted using the [ITER-STREAMS] transform argument detection rules, verbatim: a function is a stateless transform; an object with a function-valued transform property is a stateful transform; undefined yields the identity transform (batch pass-through). Any other value throws a TypeError.

  2. readable is an iter readable stream and writable is an iter writable stream — genuine stream instances, so the facades, iter readers and writers, and fused pipe dispatch all apply to them.

  3. The budget and backpressure options govern the writable leg’s slots buffer, with the same semantics and the same "strict"/"unbounded" restriction as § 11.3 Backpressure policy. The readable leg needs no buffer of its own: output flows on demand (§ 13.3 Laziness and composition).

13.2. The transform contract

The contract is the [ITER-STREAMS] transform contract, unchanged:

Note: These are not merely compatible shapes; they are the same values. The object or function given to IterTransformStream may be passed, unchanged, to Stream.pull() or any other [ITER-STREAMS] API that accepts transforms — the two specifications converge on one contract by construction.

Cancellation flows through the contract’s signal: cancelling readable or aborting writable aborts the AbortSignal delivered in options and tears the legs down per the usual stream semantics. There are no separate start, flush, or cancel callbacks: startup work belongs to the transform itself, flush is the null signal, and cancellation is the signal.

13.3. Laziness and composition

The transform executes pull-through: no transform code runs until the readable side is consumed. The writable side’s slots buffer is drained only as the transform’s source iterable is pulled, which happens only as the transform’s output is pulled. Backpressure spans the transform: a slow consumer of the readable side propagates, through the transform, to the writable side’s budget and its producers.

In a chain of IterTransformStream stages connected by fused pipes, this composes into a single lazily evaluated pipeline — the [ITER-STREAMS] compose-transform-pipeline model expressed through web streams plumbing. See § 14 Piping.

14. Piping

14.1. Type dispatch

The Streams Standard’s pipe-to algorithm gains a type dispatch as its first step:

pipeThrough() requires no changes: it is pipe-to plus returning the readable side, and each leg dispatches independently.

14.2. The fused pipe

A fused pipe from source rs to destination ws with options (preventClose, preventAbort, preventCancel, signal):
  1. Lock rs and ws for the duration of the pipe, exactly as the existing algorithm does. Internally, rs is consumed as batches (as if through an iter reader).
  2. Deliver rs’s batches directly to ws’s sink: each batch is passed to the sink’s write() callback, bypassing ws’s slots buffer entirely. Piped data does not enter ws’s buffer.
  3. Each write()’s settlement gates the next pull from rs: no batch is pulled while a write() is pending, and pulling drains rs’s slots buffer and notifies its producer’s drain waiters. Backpressure is coupled end to end with no intermediate elasticity.
  4. Batches may be split or merged in flight; the flattened chunk sequence is preserved (§ 5.1 Batch boundaries are not semantic).
  5. Shutdown propagation, byte-for-byte with the existing algorithm’s constraints:
    Event Default behavior With prevent* flag
    rs errors ws is aborted with the reason (the sink’s abort() callback is invoked); the pipe promise rejects preventAbort: ws is left writable; subsequent writers feed its slots buffer normally; the pipe promise rejects
    rs closes ws is closed as if by a writer’s close(): after the final piped write() settles, the sink’s close() callback is invoked; the pipe promise fulfills after close fulfillment preventClose: ws is left writable and open; the pipe promise fulfills
    ws errors (a write() rejects; error() is called) rs is cancelled with the reason; the pipe promise rejects preventCancel: rs is not cancelled; the pipe promise rejects
    signal aborted as in the existing algorithm: error both sides with the abort reason, subject to preventAbort and preventCancel; the pipe promise rejects with the reason
  6. On pipe completion for any reason that leaves ws writable, subsequently acquired writers feed ws’s slots buffer, whose batches flow to the same sink write() callback — the sink observes one continuous sequence of invocations across pipe and non-pipe phases.

Note: Nothing in the fused pipe is a mere optimization license; the laziness is normative. An implementation MUST NOT eagerly drain the source into destination-side buffering during a fused pipe — the absence of intermediate elasticity is observable to the source-side producer through its budget, and it is the point.

Note: A chain such as rs.pipeThrough(a).pipeThrough(b).pipeTo(ws), with every stage iter-type, collapses hop by hop: each fused pipe delivers upstream batches directly to the next stage, and each iter transform connects its legs lazily (§ 13.3 Laziness and composition). The result is a single consumer-driven pipeline from ws’s sink back to rs’s producer budget, semantically equivalent to a single [ITER-STREAMS] pull pipeline.

14.3. Mixed-type pipes

Mixed-type pipes use the existing algorithm through the facades. Two consequences worth noting:

15. Modifications to the Streams Standard

This section enumerates the amendments this specification makes to [STREAMS]. A runtime implementing both specifications behaves as if the Streams Standard were modified as follows:

  1. **ReadableStreamReaderMode**: add the enumeration value "iter".

  2. **getReader()**: when the options' mode is "iter", return a new ReadableStreamIterReader for the stream (§ 7.1 Acquisition). No controller-type restriction applies.

  3. **ReadableStreamType**: add the enumeration value "iter".

  4. **ReadableStream constructor**: when the underlying source’s type is "iter", set up a ReadableStreamIterController per § 8.1 Construction, including the start-required, no-pull, no-size rules and the backpressure member.

  5. **ReadableStream.from()**: accept a second argument, a ReadableStreamFromOptions; when its type is "iter", behave per § 9 ReadableStream.from() in batch mode.

  6. **ReadableStream.prototype**: add the Symbol.for('Stream.toAsyncStreamable') method defined in § 10 The toAsyncStreamable hook.

  7. **Underlying sink type**: replace the unconditional RangeError for a present type member with: if the value is "iter", set up a WritableStreamIterController per § 11.1 Construction; for any other non-undefined value, throw a RangeError as today.

  8. **getWriter()**: accept an optional WritableStreamGetWriterOptions argument; when mode is "iter", return a new WritableStreamIterWriter per § 12 Iter writers. getWriter() with no arguments is unchanged.

  9. Pipe-to: prepend the type dispatch of § 14.1 Type dispatch; when both ends are iter-type, the pipe is the fused pipe of § 14.2 The fused pipe.

  10. Default reader and writer interoperation with iter-type streams per § 8.4 The default reader facade and § 11.4 The default writer facade.

No other Streams Standard behavior is modified. In particular, async iteration of a ReadableStream itself remains chunk-at-a-time; tee() is unchanged (operating through the default facade on iter streams); the "bytes" type, BYOB machinery, and default streams are untouched; and TransformStream — including its reserved readableType and writableType transformer members — is not modified at all. Batch-native transforms are provided by the separate IterTransformStream class (§ 13 Iter transform streams), which conforms to the ReadableWritablePair shape rather than extending TransformStream.

16. Security considerations

17. Open issues

17.1. Budget floor

This specification mirrors the [ITER-STREAMS] 16384-byte budget floor by permitting implementations to clamp smaller highWaterMark values. Whether small explicit budgets should instead be honored (useful for testing) is an open question for both specifications.

17.2. Upstreaming

The amendments in § 15 Modifications to the Streams Standard are confined to enum values, reserved dictionary members, and a type dispatch in pipe-to, specifically to keep an upstreaming path into [STREAMS] open. Whether and when to propose that is a process question outside this document.

Index

Terms defined by this specification

Terms defined by reference

IDL Index

dictionary IterWriteOptions {
  AbortSignal signal;
};

[Exposed=*]
interface ReadableStreamIterReader {
  constructor(ReadableStream stream);

  Promise<ReadableStreamReadResult> read();
  undefined releaseLock();
};
ReadableStreamIterReader includes ReadableStreamGenericReader;

[Exposed=*]
interface ReadableStreamIterController {
  readonly attribute boolean? canWrite;

  Promise<undefined> write((Uint8Array or USVString) chunk,
                           optional IterWriteOptions options = {});
  Promise<undefined> writev(sequence<(Uint8Array or USVString)> chunks,
                            optional IterWriteOptions options = {});
  boolean writeSync((Uint8Array or USVString) chunk);
  boolean writevSync(sequence<(Uint8Array or USVString)> chunks);

  Promise<unsigned long long> end(optional IterWriteOptions options = {});
  long long endSync();

  undefined fail(optional any reason);
};

[Exposed=*]
interface WritableStreamIterController {
  readonly attribute AbortSignal signal;
  undefined error(optional any e);
};

[Exposed=*]
interface WritableStreamIterWriter {
  constructor(WritableStream stream);

  readonly attribute boolean? canWrite;
  readonly attribute Promise<undefined> closed;

  Promise<undefined> write((Uint8Array or USVString) chunk,
                           optional IterWriteOptions options = {});
  Promise<undefined> writev(sequence<(Uint8Array or USVString)> chunks,
                            optional IterWriteOptions options = {});
  boolean writeSync((Uint8Array or USVString) chunk);
  boolean writevSync(sequence<(Uint8Array or USVString)> chunks);

  Promise<unsigned long long> end(optional IterWriteOptions options = {});
  long long endSync();

  undefined fail(optional any reason);

  undefined releaseLock();
};

[Exposed=*]
interface IterTransformStream {
  constructor(optional any transform,
              optional IterTransformStreamOptions options = {});

  readonly attribute ReadableStream readable;
  readonly attribute WritableStream writable;
};

dictionary IterTransformStreamOptions {
  unsigned long long budget;
  DOMString backpressure = "strict";
};

dictionary WritableStreamGetWriterOptions {
  DOMString mode;
  DOMString backpressure;
};

dictionary ReadableStreamFromOptions {
  DOMString type;
};

partial dictionary UnderlyingSource {
  DOMString backpressure = "strict";
};

partial dictionary UnderlyingSink {
  DOMString backpressure = "strict";
};

Ecma International

Rue du Rhone 114

CH-1204 Geneva

Tel: +41 22 849 6000

Fax: +41 22 849 6001

Web: https://ecma-international.org/

© 2026 Ecma International

This draft document may be copied and furnished to others, and derivative works that comment on or otherwise explain it or assist in its implementation may be prepared, copied, published, and distributed, in whole or in part, without restriction of any kind, provided that the above copyright notice and this section are included on all such copies and derivative works. However, this document itself may not be modified in any way, including by removing the copyright notice or references to Ecma International, except as needed for the purpose of developing any document or deliverable produced by Ecma International.

This disclaimer is valid only prior to final version of this document. After approval all rights on the standard are reserved by Ecma International.

The limited permissions are granted through the standardization phase and will not be revoked by Ecma International or its successors or assigns during this time.

This document and the information contained herein is provided on an "AS IS" basis and ECMA INTERNATIONAL DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY OWNERSHIP RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.

Software License

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.