webstreams

js/globals/webstreams.ts

ReadableStream, WritableStream, TransformStream globals.

Registered as internal:globals/webstreams and re-exported onto globalThis by the global installer, so application code uses these classes without importing anything.

Architecture

Two internal representations per stream type allow an efficient fast path for I/O-backed streams while remaining spec-compliant for user-defined sources and sinks.

ReadableStream (WeakMap _rs): - kind: 'iterable' — backed by an async iterable (Reader, generator, channel). No controller, no queue; reads delegate directly to the underlying iterator. - kind: 'source' — backed by an UnderlyingSource with start/pull/cancel. Uses ReadableStreamDefaultController for default streams, or ReadableByteStreamController for type:'bytes' streams. Full HWM-driven pull algorithm, backpressure, BYOB support.

WritableStream (WeakMap _ws): - kind: 'writer' — backed by a Fino Writer (write/close). Writes delegate directly through; no controller overhead. - kind: 'sink' — backed by an UnderlyingSink with start/write/close/abort. Full serialized write queue, HWM-based backpressure, ready promise.

Short-circuit piping

ReadableStream.pipeTo(WritableStream) inspects internal kinds: iterable + writer → iterates source directly, writes via Writer — equivalent to Writer.pipe(asyncIterable), zero overhead otherwise → spec-compliant reader.read() / writer.write() pump

Structured clone and transfer

Streams are not structured-cloneable or transferable in this runtime. Passing a stream to structuredClone() throws, and transfer lists are limited to ArrayBuffer values. BYOB reads reject views whose backing buffer has become unusable through the runtime's best-effort transfer limitations.

Exports

ReadableStream, ReadableStreamDefaultReader, ReadableStreamDefaultController ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableByteStreamController WritableStream, WritableStreamDefaultWriter, WritableStreamDefaultController TransformStream, TransformStreamDefaultController CountQueuingStrategy, ByteLengthQueuingStrategy

Example

const stream = ReadableStream.from(['hello'])
  .pipeThrough(new TransformStream({
    transform(chunk, controller) {
      controller.enqueue(chunk.toUpperCase());
    },
  }));

for await (const chunk of stream) console.log(chunk);

WHATWG Streams specification: https://streams.spec.whatwg.org/

Classes

class CountQueuingStrategy {

WHATWG count queuing strategy with constant chunk size of 1.

highWaterMark is number-coerced and size() ignores the chunk value.

const strategy = new CountQueuingStrategy({ highWaterMark: 4 });
strategy.size({}); // 1

Properties

highWaterMark: number

Queue size threshold where backpressure begins.

new CountQueuingStrategy({ highWaterMark: 2 }).highWaterMark; // 2

Constructors

constructor(init: { highWaterMark: number; })

Create a count strategy.

const strategy = new CountQueuingStrategy({ highWaterMark: 1 });

Methods

size(): number

Return the size contribution for any chunk.

new CountQueuingStrategy({ highWaterMark: 1 }).size('x'); // 1

class ByteLengthQueuingStrategy {

WHATWG byte-length queuing strategy using chunk.byteLength as size.

const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 1024 });
strategy.size(new Uint8Array(8)); // 8

Properties

highWaterMark: number

Byte threshold where backpressure begins.

new ByteLengthQueuingStrategy({ highWaterMark: 16 }).highWaterMark; // 16

Constructors

constructor(init: { highWaterMark: number; })

Create a byte-length strategy.

const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 4096 });

Methods

size(chunk: ArrayBufferView): number

Return chunk.byteLength.

Passing a value without byteLength returns undefined at runtime through normal property access, so callers should pass ArrayBuffer views.

new ByteLengthQueuingStrategy({ highWaterMark: 1 }).size(new Uint8Array(3)); // 3

class ReadableStreamDefaultController {

Controller passed to non-byte ReadableStream underlying sources.

It lets a source enqueue chunks, close the stream, or error it.

const stream = new ReadableStream({
  start(controller) { controller.enqueue('x'); controller.close(); },
});

Getters

get desiredSize()

Desired queue size before backpressure applies.

Returns null when the stream is errored, 0 when closed, or highWaterMark minus queued size while readable.

new ReadableStream({ start(controller) { controller.desiredSize; } });

Methods

enqueue(chunk: any)

Enqueue a chunk into the readable stream.

Throws if close() has already been requested or the stream is not readable. Pending reads are fulfilled immediately before queueing.

const stream = new ReadableStream({ start(controller) { controller.enqueue('x'); } });
close()

Request stream closure after queued chunks are consumed.

Calling close() twice throws. If the queue is empty, the stream closes immediately.

new ReadableStream({ start(controller) { controller.close(); } });
error(reason: unknown)

Error the stream and reject pending reads.

new ReadableStream({ start(controller) { controller.error(new Error('stop')); } });

class ReadableByteStreamController {

Controller passed to byte ReadableStream underlying sources.

It supports Uint8Array enqueueing and BYOB request fulfillment.

const stream = new ReadableStream({
  type: 'bytes',
  pull(controller) { controller.enqueue(new Uint8Array([1])); },
});

Getters

get desiredSize()

Desired byte queue size before backpressure applies.

new ReadableStream({ type: 'bytes', start(controller) { controller.desiredSize; } });
get byobRequest()

Current BYOB request, or null when no BYOB read is pending.

The same request object is reused for the pending descriptor until respond() or respondWithNewView() resolves it.

new ReadableStream({ type: 'bytes', pull(controller) { controller.byobRequest; } });

Methods

enqueue(chunk: ArrayBuffer | ArrayBufferView)

Enqueue bytes into the stream.

Accepts ArrayBuffer or ArrayBufferView and normalizes to Uint8Array. Throws if the stream is closed, errored, or close() was requested.

new ReadableStream({ type: 'bytes', start(controller) { controller.enqueue(new Uint8Array([1])); } });
close()

Request byte stream closure after queued and pending BYOB bytes complete.

new ReadableStream({ type: 'bytes', start(controller) { controller.close(); } });
error(reason: unknown)

Error the byte stream and reject pending reads.

new ReadableStream({ type: 'bytes', start(controller) { controller.error('stop'); } });

class ReadableStreamBYOBRequest {

Active bring-your-own-buffer request for byte streams.

Underlying byte sources use this to report how many bytes were written into the caller-provided view.

new ReadableStream({ type: 'bytes', pull(controller) {
  const request = controller.byobRequest;
  if (request) request.respond(0);
} });

Getters

get view()

View supplied by the BYOB reader.

const view = request.view;

Methods

respond(bytesWritten: number)

Report how many bytes were written into view.

Resolves the pending BYOB read once the requested minimum has been filled.

request.respond(8);
respondWithNewView(view: ArrayBufferView)

Respond with a replacement ArrayBufferView.

Passing a non-view throws TypeError.

request.respondWithNewView(new Uint8Array([1, 2]));

class ReadableStream {

WHATWG ReadableStream implementation with source and iterable backends.

Source-backed streams use controllers and queues. Iterable-backed streams are created with ReadableStream.from() and read directly from the iterable.

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue('first');
    controller.enqueue('second');
    controller.close();
  },
});

for await (const chunk of stream) console.log(chunk); // "first", then "second"

Constructors

constructor(underlyingSource?: any, queuingStrategy?: QueuingStrategyLike)

Create a source-backed ReadableStream.

The underlying source may define start, pull, cancel, and type: "bytes". Queuing strategy defaults to highWaterMark 1 for default streams and 0 for byte streams.

const stream = new ReadableStream({
  pull(controller) { controller.enqueue('chunk'); controller.close(); },
});

Static Methods

static from(asyncIterable: AsyncIterable<any> | Iterable<any>)

Create a ReadableStream from an iterable or async iterable.

The iterable is consumed lazily. Non-iterable inputs throw TypeError.

const stream = ReadableStream.from([1, 2, 3]);

Getters

get locked()

Whether the stream is locked to a reader, pipe, or async iterator.

const stream = new ReadableStream();
stream.locked; // false

Methods

cancel(reason: unknown)

Cancel the stream with a reason.

Rejects when the stream is locked. Iterable streams call the iterator return() method when available; source streams call underlyingSource.cancel.

const stream = new ReadableStream();
await stream.cancel('done');
getReader(options?: { mode?: 'byob'; })

Acquire a reader and lock the stream.

mode: "byob" requires a byte stream and returns a ReadableStreamBYOBReader. Without mode, this returns a ReadableStreamDefaultReader.

const reader = new ReadableStream().getReader();
reader.releaseLock();
values(options?: { preventCancel?: boolean; })

Async iterator over stream chunks.

Iteration locks the stream until completion, error, or iterator return(). Calling return() cancels the stream unless preventCancel is true.

const iter = stream.values({ preventCancel: true });
await iter.return?.(); // releases the lock without canceling the source
async pipeTo(destination: WritableStream, options?: { preventClose?: boolean; preventAbort?: boolean; preventCancel?: boolean; signal?: AbortSignal | null; })

Pipe this readable stream into a writable stream.

Rejects if either stream is locked. preventClose, preventAbort, and preventCancel suppress the corresponding propagation steps. signal aborts the pipe and propagates according to those flags.

const source = ReadableStream.from(['a']);
const sink = new WritableStream({ write(chunk) { console.log(chunk); } });
await source.pipeTo(sink);
pipeThrough(transform: { readable: ReadableStream; writable: WritableStream; }, options?: { preventClose?: boolean; preventAbort?: boolean; preventCancel?: boolean; signal?: AbortSignal | null; })

Pipe through a transform and return its readable side.

Starts pipeTo(transform.writable) in the background. Throws if either side is invalid or locked.

const transform = new TransformStream();
const readable = ReadableStream.from(['a']).pipeThrough(transform);
tee()

Split this stream into two readable branches.

The original stream is locked while teeing. If both branches cancel, the original source is cancelled with both reasons.

const [a, b] = ReadableStream.from([1, 2]).tee();

class ReadableStreamDefaultReader {

Default reader for non-BYOB stream reads.

Acquiring a reader locks the stream until releaseLock(), cancel(), or stream completion.

const reader = ReadableStream.from(['x']).getReader();
await reader.read();

Constructors

constructor(stream: ReadableStream)

Create a default reader and lock the stream.

Throws if the argument is not a ReadableStream or is already locked.

const reader = new ReadableStreamDefaultReader(new ReadableStream());

Getters

get closed()

Promise resolved when the stream closes and rejected on error or release.

const reader = new ReadableStream().getReader();
reader.closed.catch(() => {});

Methods

read()

Read the next chunk.

Resolves to { done, value }. Rejects if the reader was released or the stream errors.

const result = await ReadableStream.from(['x']).getReader().read();
result.value; // "x"
cancel(reason: unknown)

Cancel the associated stream.

Rejects if the reader has been released.

const reader = new ReadableStream().getReader();
await reader.cancel('done');
releaseLock()

Release the reader lock.

Pending reads are rejected and the closed promise rejects because the reader was released before stream closure.

const reader = new ReadableStream().getReader();
reader.releaseLock();

class ReadableStreamBYOBReader {

BYOB reader for byte streams.

read(view) fills caller-provided buffers and can wait for a minimum byte count when options.min is supplied.

const stream = new ReadableStream({ type: 'bytes' });
const reader = stream.getReader({ mode: 'byob' });

Constructors

constructor(stream: ReadableStream)

Create a BYOB reader and lock a byte stream.

Throws if the stream is not a byte stream or is already locked.

const reader = new ReadableStream({ type: 'bytes' }).getReader({ mode: 'byob' });

Getters

get closed()

Promise resolved when the byte stream closes and rejected on error/release.

const reader = new ReadableStream({ type: 'bytes' }).getReader({ mode: 'byob' });
reader.closed.catch(() => {});

Methods

read(view: ArrayBufferView, options?: { min?: number; })

Read bytes into a supplied ArrayBufferView.

Rejects for released readers, non-views, zero-length views, or invalid min. Resolves to a Uint8Array view over the filled region.

const result = await reader.read(new Uint8Array(16), { min: 1 });
cancel(reason: unknown)

Cancel the associated byte stream.

await reader.cancel('done');
releaseLock()

Release the BYOB reader lock.

Pending BYOB reads are rejected.

reader.releaseLock();

class WritableStreamDefaultController {

Controller passed to WritableStream underlying sinks.

It exposes an abort signal and lets the sink error the stream.

const stream = new WritableStream({ start(controller) { controller.signal; } });

Getters

get signal()

AbortSignal that fires when the writable stream aborts.

new WritableStream({ start(controller) { controller.signal.aborted; } });
get abortReason()

Abort reason from the controller signal.

Undefined before the stream aborts.

new WritableStream({ start(controller) { controller.abortReason; } });

Methods

error(reason: unknown)

Error the writable stream.

Pending and future writes reject with the reason.

new WritableStream({ start(controller) { controller.error('stop'); } });

class WritableStream {

WHATWG WritableStream implementation with sink and Fino Writer backends.

Sink-backed streams serialize writes through an underlyingSink. Writer-backed streams delegate directly to a Fino Writer for low overhead.

const stream = new WritableStream({
  write(chunk) { console.log('writing', chunk); },
  close() { console.log('flushed'); },
});

const writer = stream.getWriter();
await writer.write('a');
await writer.write('b');
await writer.close();

Constructors

constructor(underlyingSink?: any, queuingStrategy?: QueuingStrategyLike)

Create a writable stream from an underlying sink or Fino Writer.

start(controller), write(chunk, controller), close(), and abort(reason) hooks are supported for sink objects.

const stream = new WritableStream({ write(chunk) { console.log(chunk); } });

Getters

get locked()

Whether the stream is locked to a writer or pipe operation.

new WritableStream().locked; // false

Methods

close()

Close the writable stream after queued writes finish.

Rejects when locked, already closing, or errored.

const stream = new WritableStream();
await stream.close();
abort(reason: unknown)

Abort the writable stream with a reason.

Rejects if the stream is locked. Calls underlyingSink.abort when provided.

const stream = new WritableStream();
await stream.abort('stop');
getWriter()

Acquire a default writer and lock the stream.

Throws when the stream is already locked.

const writer = new WritableStream().getWriter();
writer.releaseLock();

class WritableStreamDefaultWriter {

Default writer for WritableStream.

Writers expose backpressure through ready and desiredSize and release the stream lock with releaseLock().

const writer = new WritableStream().getWriter();
await writer.write('x');

Constructors

constructor(stream: WritableStream)

Create a writer and lock the stream.

Throws if the argument is not a WritableStream or is already locked.

const writer = new WritableStreamDefaultWriter(new WritableStream());

Getters

get closed()

Promise resolved when the stream closes and rejected on error/release.

const writer = new WritableStream().getWriter();
writer.closed.catch(() => {});
get desiredSize()

Remaining queue capacity before backpressure applies.

Returns null for errored streams and 0 for closed streams.

const writer = new WritableStream().getWriter();
writer.desiredSize;
get ready()

Promise that resolves when backpressure clears.

await writer.ready;

Methods

write(chunk: any)

Write a chunk to the stream.

Rejects if the writer is released or the stream is not writable.

const writer = new WritableStream().getWriter();
await writer.write('chunk');
close()

Close the stream through this writer.

Resolves writer.closed on success and rejects it on close failure.

await writer.close();
abort(reason: unknown)

Abort the stream through this writer.

await writer.abort('stop');
releaseLock()

Release the writer lock.

The writer's closed promise rejects because the writer no longer observes stream closure.

writer.releaseLock();

class TransformStreamDefaultController {

Controller passed to TransformStream transformer callbacks.

It can enqueue transformed chunks, terminate the readable side, or error the transform output.

const stream = new TransformStream({
  transform(chunk, controller) { controller.enqueue(chunk); },
});

Getters

get desiredSize()

Desired readable-side queue size.

Returns 1 before a readable state is attached.

new TransformStream({ transform(chunk, controller) { controller.desiredSize; } });

Methods

enqueue(chunk: any)

Enqueue a transformed chunk to the readable side.

new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk); } });
terminate()

Close the readable side immediately.

new TransformStream({ transform(_chunk, controller) { controller.terminate(); } });
error(reason: unknown)

Error the readable side with a reason.

new TransformStream({ transform(_chunk, controller) { controller.error('stop'); } });

class TransformStream {

WHATWG TransformStream composed of writable and readable sides.

Transformer callbacks can start, transform chunks, and flush before closing. Without a transform callback, chunks pass through unchanged.

const upper = new TransformStream({
  transform(chunk, controller) { controller.enqueue(String(chunk).toUpperCase()); },
});

const readable = ReadableStream.from(['a', 'b']).pipeThrough(upper);
for await (const chunk of readable) console.log(chunk); // "A", then "B"

Constructors

constructor( transformer?: any, writableStrategy?: QueuingStrategyLike, readableStrategy?: QueuingStrategyLike )

Create a transform stream.

transformer.start receives the controller, transformer.transform handles each written chunk, and transformer.flush runs before readable closure. Strategies apply to the writable and readable sides respectively.

const stream = new TransformStream({
  transform(chunk, controller) { controller.enqueue(chunk); },
});

Getters

get readable()

Readable side that emits transformed chunks.

const stream = new TransformStream();
stream.readable;
get writable()

Writable side that accepts input chunks.

const stream = new TransformStream();
stream.writable;