Skip to content
LogoLogo

@portable/streams

Functions

Streams.abortable

Wraps a transformer so that aborting signal errors its TransformStream with signal.reason. Erroring the stream makes pipeThrough cancel the source. The listener is removed when the stream ends, is cancelled, or fails because the transformer threw. Without a signal, the transformer is returned unchanged.

function abortable<input, output>(
  transformer: abortable.Transformer<input, output>,
  options: abortable.Options = {},
): abortable.Transformer<input, output>;
OptionTypeDefaultDescription
signalAbortSignalErrors the stream with signal.reason and cancels its source when aborted.
import * as Streams from '@portable/streams';
 
class UpperCaseStream extends TransformStream<string, string> {
  constructor(options: { signal?: AbortSignal | undefined } = {}) {
    super(
      Streams.abortable(
        {
          transform: (chunk, controller) =>
            controller.enqueue(chunk.toUpperCase()),
        },
        options,
      ),
    );
  }
}
 
text.pipeThrough(
  new UpperCaseStream({ signal: AbortSignal.timeout(5000) }),
);

Streams.toArray

Reads a stream or iterable to the end and returns its chunks in an array.

async function toArray<value>(
  source: Source<value>,
  options: toArray.Options = {},
): Promise<value[]>;
OptionTypeDefaultDescription
signalAbortSignalRejects with signal.reason and cancels the source when aborted.
import * as Streams from '@portable/streams';
import * as Lines from '@portable/lines';
 
const lines = await Streams.toArray(
  response.body!.pipeThrough(new Lines.DecoderStream()),
);

Streams.toBlob

Reads a stream or iterable to the end and returns its bytes in a Blob. String chunks are encoded as UTF-8. Unlike new Response(stream).blob(), it takes a maxBytes limit and a signal, and reads async iterables.

async function toBlob(
  source: Source<Uint8Array | string>,
  options: toBlob.Options = {},
): Promise<Blob>;
OptionTypeDefaultDescription
maxBytesnumberInfinityFail with MaxBytesError and cancel the source once it yields more bytes than this.
signalAbortSignalRejects with signal.reason and cancels the source when aborted.
typestring''The Blob's type, such as 'image/png'.
import * as Streams from '@portable/streams';
 
const response = await fetch('https://example.com/image.png');
const blob = await Streams.toBlob(response.body!, {
  maxBytes: 10_000_000,
  type: 'image/png',
});

Streams.toBytes

Reads a stream or iterable to the end and returns its bytes in one Uint8Array. String chunks are encoded as UTF-8.

async function toBytes(
  source: Source<Uint8Array | string>,
  options: toBytes.Options = {},
): Promise<Uint8Array<ArrayBuffer>>;
OptionTypeDefaultDescription
maxBytesnumberInfinityFail with MaxBytesError and cancel the source once it yields more bytes than this.
signalAbortSignalRejects with signal.reason and cancels the source when aborted.
import * as Streams from '@portable/streams';
 
const response = await fetch('https://example.com/image.png');
const bytes = await Streams.toBytes(response.body!, {
  maxBytes: 10_000_000,
});

Streams.toJson

Reads a stream or iterable to the end and parses it as JSON.

async function toJson(
  source: Source<Uint8Array | string>,
  options: toJson.Options = {},
): Promise<unknown>;
OptionTypeDefaultDescription
encodingstring'utf-8'Text encoding of byte chunks, as a TextDecoder label.
maxBytesnumberInfinityFail with MaxBytesError and cancel the source once it yields more bytes than this. String chunks count as their UTF-8 length.
signalAbortSignalRejects with signal.reason and cancels the source when aborted.
import * as Streams from '@portable/streams';
 
const body = await Streams.toJson(request.body!, { maxBytes: 100_000 });

Streams.toText

Reads a stream or iterable to the end and decodes it as text. String chunks are kept as they are, and byte chunks are decoded with TextDecoder, so a character split across chunks decodes correctly.

async function toText(
  source: Source<Uint8Array | string>,
  options: toText.Options = {},
): Promise<string>;
OptionTypeDefaultDescription
encodingstring'utf-8'Text encoding of byte chunks, as a TextDecoder label.
maxBytesnumberInfinityFail with MaxBytesError and cancel the source once it yields more bytes than this. String chunks count as their UTF-8 length.
signalAbortSignalRejects with signal.reason and cancels the source when aborted.
import * as Streams from '@portable/streams';
 
const response = await fetch('https://example.com/');
const html = await Streams.toText(response.body!, {
  maxBytes: 1_000_000,
});

Streams.concat

Joins sources end to end: every chunk of the first source, then every chunk of the next. A source is read only once the one before it has ended. An error in any source errors the output and cancels the sources not yet read, and cancelling the output cancels them all.

function concat<chunk>(
  sources: readonly Source<chunk>[],
  options: concat.Options = {},
): ReadableStream<chunk>;
OptionTypeDefaultDescription
signalAbortSignalErrors the output with signal.reason and cancels every source when aborted.
import * as Streams from '@portable/streams';
 
const body = Streams.concat([[header], file.stream(), [footer]]);

Streams.limitBytes

A TransformStream that passes chunks through unchanged and fails with MaxBytesError once more than maxBytes bytes have passed. The chunk that crosses the limit is not passed on, and the source is cancelled. String chunks count as their UTF-8 length.

function limitBytes<chunk extends Uint8Array | string>(
  maxBytes: number,
  options: limitBytes.Options = {},
): TransformStream<chunk, chunk>;
OptionTypeDefaultDescription
signalAbortSignalErrors the stream with signal.reason and cancels its source when aborted.
import * as Streams from '@portable/streams';
 
await request
  .body!.pipeThrough(Streams.limitBytes(1_000_000))
  .pipeTo(upload);

Streams.merge

Interleaves sources: reads all of them at once and passes each chunk on as it arrives. The output ends when every source has ended. An error in any source errors the output and cancels the others, and cancelling the output cancels them all.

function merge<chunk>(
  sources: readonly Source<chunk>[],
  options: merge.Options = {},
): ReadableStream<chunk>;
OptionTypeDefaultDescription
signalAbortSignalErrors the output with signal.reason and cancels every source when aborted.
import * as Streams from '@portable/streams';
 
const events = Streams.merge([serverA.body!, serverB.body!], {
  signal,
});

Streams.fromIterable

Creates a ReadableStream from an iterable or async iterable, such as an array or a generator. The stream pulls one value each time its reader asks for one, so a slow reader slows the generator down. Cancelling the stream calls the iterator's return(), which runs the generator's finally blocks. Uses ReadableStream.from where the runtime has it.

function fromIterable<chunk>(
  iterable: Iterable<chunk | PromiseLike<chunk>> | AsyncIterable<chunk>,
): ReadableStream<chunk>;
import * as Streams from '@portable/streams';
 
async function* countdown() {
  for (let i = 3; i > 0; i--) {
    yield `${i}\n`;
  }
}
 
return new Response(
  Streams.fromIterable(countdown()).pipeThrough(
    new TextEncoderStream(),
  ),
);

Streams.toAsyncIterable

Returns an async iterator over a ReadableStream, for for await loops in runtimes whose streams aren't async iterable yet. Leaving the loop early with break, return or a throw cancels the stream, or with preventCancel: true only releases its lock. Uses the stream's own values() where the runtime has it.

function toAsyncIterable<chunk>(
  stream: ReadableStream<chunk>,
  options: toAsyncIterable.Options = {},
): AsyncIterableIterator<chunk>;
OptionTypeDefaultDescription
preventCancelbooleanfalseRelease the stream's lock instead of cancelling it when the loop exits early.
import * as Streams from '@portable/streams';
 
for await (const chunk of Streams.toAsyncIterable(response.body!)) {
  if (done(chunk)) {
    break; // cancels the response body
  }
}

Streams.transform

Builds a transform from an async generator function. The function receives the input chunks as an async iterable and yields output chunks, so state that spans chunks is a local variable. Use the result with pipeThrough.

Output is pulled, so the generator runs only as fast as the reader reads. An error thrown by the generator errors the output and cancels the input. Cancelling the output ends the input iterable and calls the generator's return(), so its finally blocks run.

function transform<input, output>(
  fn: (source: AsyncIterable<input>) => AsyncIterable<output>,
  options: transform.Options = {},
): ReadableWritablePair<output, input>;
OptionTypeDefaultDescription
signalAbortSignalErrors the output with signal.reason, cancels the input and ends the generator when aborted.
import * as Streams from '@portable/streams';
 
const sum = Streams.transform(async function* (
  source: AsyncIterable<number>,
) {
  let total = 0;
  for await (const n of source) {
    yield (total += n);
  }
});
 
numbers.pipeThrough(sum);

Streams.batch

A TransformStream that groups chunks into arrays of size. The last array holds whatever is left and may be shorter.

function batch<chunk>(
  size: number,
  options: batch.Options = {},
): TransformStream<chunk, chunk[]>;
OptionTypeDefaultDescription
signalAbortSignalErrors the stream with signal.reason and cancels its source when aborted.
import * as Streams from '@portable/streams';
 
await rows
  .pipeThrough(Streams.batch(500))
  .pipeTo(new WritableStream({ write: insertMany }));

Streams.filter

A TransformStream that passes on only the chunks for which fn(chunk, index) returns true. fn may return a promise.

function filter<chunk, state extends chunk>(
  fn: (chunk: chunk, index: number) => chunk is state,
  options?: filter.Options,
): TransformStream<chunk, state>;
function filter<chunk>(
  fn: (chunk: chunk, index: number) => boolean | PromiseLike<boolean>,
  options?: filter.Options,
): TransformStream<chunk, chunk>;
OptionTypeDefaultDescription
signalAbortSignalErrors the stream with signal.reason and cancels its source when aborted.
import * as Streams from '@portable/streams';
 
lines.pipeThrough(Streams.filter((line) => line !== ''));

Streams.map

A TransformStream that replaces each chunk with fn(chunk, index). If fn returns a promise, the result is passed on once it resolves, and chunks stay in order.

function map<input, output>(
  fn: (chunk: input, index: number) => output | PromiseLike<output>,
  options: map.Options = {},
): TransformStream<input, output>;
OptionTypeDefaultDescription
signalAbortSignalErrors the stream with signal.reason and cancels its source when aborted.
import * as Streams from '@portable/streams';
import * as Lines from '@portable/lines';
 
const records = response
  .body!.pipeThrough(new Lines.DecoderStream())
  .pipeThrough(Streams.map((line) => JSON.parse(line)));

Streams.tap

A TransformStream that calls fn(chunk, index) for each chunk and passes the chunk on unchanged. If fn returns a promise, the chunk waits for it.

function tap<chunk>(
  fn: (chunk: chunk, index: number) => void | PromiseLike<void>,
  options: tap.Options = {},
): TransformStream<chunk, chunk>;
OptionTypeDefaultDescription
signalAbortSignalErrors the stream with signal.reason and cancels its source when aborted.
import * as Streams from '@portable/streams';
 
let received = 0;
response.body!.pipeThrough(
  Streams.tap((chunk) => {
    received += chunk.byteLength;
  }),
);

Streams.tee

Splits a source into count streams that each receive every chunk. The source is read only as fast as the slowest branch allows: once a branch holds highWaterMark unread chunks or bytes, the source isn't read until that branch reads. Cancelling a branch removes it, and the source is cancelled only when every branch is cancelled, with the last branch's reason. An error in the source errors every branch. Aborting signal errors every branch with its reason and cancels the source.

Branches receive the same chunk objects, as with ReadableStream.prototype.tee(), so a branch must copy a chunk (chunk.slice()) before changing it.

function tee<chunk>(
  source: Source<chunk>,
  count: number,
  options: tee.Options = {},
): ReadableStream<chunk>[];
OptionTypeDefaultDescription
highWaterMarknumber16 chunks, or 65536 bytesHow much the slowest branch may hold unread before the source stops being read, in units. The fastest branch can be this far ahead of the slowest.
unit"chunks" | "bytes"'chunks'What highWaterMark counts: 'chunks', or 'bytes' for Uint8Array and string chunks, with strings counted as their UTF-8 length. With 'bytes', any other chunk errors every branch with a TypeError.
signalAbortSignalAborting errors every branch with signal.reason and cancels the source.
import * as Streams from '@portable/streams';
 
const [forExif, forStorage, forResize] = Streams.tee(request.body!, 3, {
  highWaterMark: 1_000_000,
  unit: 'bytes',
});

Errors

Streams.MaxBytesError

Thrown when a stream has more bytes than maxBytes allows.

class MaxBytesError extends Errors.MaxBytesError {
  constructor(options: Errors.MaxBytesError.Options);
  readonly kind: string;
}
import * as Streams from '@portable/streams';
 
const response = await fetch('https://example.com/upload');
try {
  await Streams.toBytes(response.body!, { maxBytes: 1_000_000 });
} catch (error) {
  if (error instanceof Streams.MaxBytesError) {
    console.log(`larger than ${error.maxBytes} bytes`);
  }
}

Types

Streams.Source

Anything the collectors and combinators read from: a ReadableStream, an async iterable or an iterable.

type Source<chunk> =
  ReadableStream<chunk> | AsyncIterable<chunk> | Iterable<chunk>;