Files
skills/docs/foundation/flow/streams/src/sinks/to-async-iterable/to-async-iterable.md
2026-05-29 08:20:56 +01:00

3.4 KiB

toAsyncIterable

(no submodules)

Purpose

toAsyncIterable adapts a ReadableStream<T> into a standard AsyncIterable<T>, letting consumers use ergonomic for await ... of loops to drain a stream rather than driving the WHATWG reader API by hand. While modern Node and recent browser engines expose ReadableStream[Symbol.asyncIterator] natively, the streams package ships its own implementation so that:

  • behaviour is identical across runtimes regardless of native support;
  • abort semantics are pinned — aborts always surface as StreamAbortedError;
  • early-break cleanup is guaranteed (the underlying reader is cancelled, the lock is released, and abort listeners are removed) so upstream producers can promptly free resources.

Features

  • for await ... of support for any ReadableStream<T>.
  • Optional AbortSignal input — abort throws StreamAbortedError.
  • Cancels the underlying reader on early-break (via the iterator's return() hook).
  • Idempotent cleanup — releasing an already-released lock is swallowed.
  • Generic over T; works equally for Uint8Array byte streams and arbitrary object streams.

Usage: Basic Example

import { toAsyncIterable } from "@suaveplan/streams/sinks/to-async-iterable/to-async-iterable";

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

Usage: Advanced Example

import { toAsyncIterable } from "@suaveplan/streams/sinks/to-async-iterable/to-async-iterable";
import { StreamAbortedError } from "@suaveplan/streams/errors/stream-error";

async function consumeWithDeadline<T>(
    stream: ReadableStream<T>,
    deadlineMs: number,
    onItem: (item: T) => Promise<void>
): Promise<void> {
    const ac = new AbortController();
    const timer = setTimeout(() => ac.abort(new Error("deadline")), deadlineMs);
    try {
        for await (const item of toAsyncIterable(stream, ac.signal)) {
            await onItem(item);
        }
    } catch (err) {
        if (err instanceof StreamAbortedError) {
            console.warn("hit deadline before stream completed");
            return;
        }
        throw err;
    } finally {
        clearTimeout(timer);
    }
}

API Reference

toAsyncIterable<T>(stream, signal?): AsyncIterable<T>

  • stream: ReadableStream<T> — source stream to adapt.
  • signal?: AbortSignal — optional cancellation token. Aborting causes the active or next next() call to reject with StreamAbortedError and cancels the underlying reader.
  • Returns — an AsyncIterable<T> that yields chunks until the stream closes or aborts.
  • Throws — StreamAbortedError on abort; otherwise propagates upstream stream errors verbatim.

Implementation Notes

  • The iterator implements return() so break/throw inside the for await loop both trigger reader cancel + lock release. Without this, upstream producers would keep filling buffers for chunks no consumer will ever read.
  • Both pre-iteration abort and mid-read() abort paths produce StreamAbortedError. The mid-read path uses the same aborted flag plus a defensive re-check after read() resolves to handle the race where the signal fires after the read promise was already in flight.
  • Abort listeners are registered with { once: true } and explicitly removed in cleanup — long-lived signals shared across many iterators do not accumulate listeners.