js/stream

js/stream.ts

fino:stream — channel-backed readers, writers, and byte I/O endpoints.

This public facade re-exports the stream primitives implemented by internal:stream so application code and internal modules share the same class identities. Reader and Writer are stable endpoint facades; their shared state owns delivery order, back-pressure, closure, and failure. Channel is a zero-capacity rendezvous, while UnboundedChannel is the explicit producer-ahead specialization. Transform values with async iterables and wrap the result with Reader.from().

FdReader and FdWriter borrow nonblocking POSIX descriptors. They retry short reads/writes and wait through EAGAIN with the runtime platform loop hooks (kqueue, epoll, or the active backend) before attempting more I/O. The caller-owned close callback is responsible for descriptor shutdown.

import { BufferedBytesReader, BytesReader } from 'fino:stream';

class EmptyBytes extends BytesReader {
  protected async doReadInto() {
    return { done: true, value: undefined };
  }
}

const reader = BufferedBytesReader.over(new EmptyBytes());
console.log(await reader.peek(1));

Classes

class BufferedBytesChannel {

A fixed-capacity byte channel backed by one reusable segment.

Writes wait while the segment is full. The segment capacity defaults to 64 KiB. read(n) returns a stable owned allocation; readInto(view) borrows caller storage until the read settles and avoids allocating its result.

import { BufferedBytesChannel } from 'fino:stream';
const channel = new BufferedBytesChannel(4096);
await channel.writer.write(new Uint8Array([1]));
console.log(await channel.reader.read());

Readonly Properties

readonly reader: BytesReader
readonly writer: BytesWriter

Constructors

constructor(capacity: number = 65536)

abstract class BufferedBytesReader extends BytesReader {

Byte reader with a chunk-list buffer and upstream pull coalescing.

The base doReadInto() implementation serves from buffered chunks and calls doPullInto() only when the buffer is empty. Structural reads can therefore scan cheaply while upstream backends pull larger chunks. peek(), scanBuffered(), and takeBuffered() expose the current buffer for parsers that need to inspect pipelined data without forcing another read.

import { BufferedBytesReader, BytesReader } from 'fino:stream';
class EmptyBytes extends BytesReader { async doReadInto() { return { done: true, value: undefined }; } }
const reader = BufferedBytesReader.over(new EmptyBytes());
console.log(await reader.peek(1));

Methods

protected abstract doPullInto( buffer: Uint8Array, options?: BytesReadOptions, ): Promise<ReadResult<number>>

Pull one raw chunk from the underlying resource.

Subclasses may return any positive chunk size in a value result and must return a done result on EOF. Empty chunks are ignored by the buffering layer and should be rare to avoid busy loops. options.signal carries the caller's cancellation request to implementations that wait on an external resource.

import { BufferedBytesReader } from 'fino:stream';
class EmptyBuffered extends BufferedBytesReader {
  async doPullInto() { return { done: true, value: undefined }; }
}
console.log(await new EmptyBuffered().read());
protected async doReadInto( buffer: Uint8Array, options?: BytesReadOptions, ): Promise<ReadResult<number>>

Serve a bounded read from the internal buffer.

The method pulls upstream only while the buffer is empty, skips empty pulls, and returns at most maxBytes. It returns null after upstream EOF and does not over-read from the buffered chunk list.

import { BufferedBytesReader } from 'fino:stream';
class OneChunk extends BufferedBytesReader {
  done = false;
  async doPullInto(buffer) { if (this.done) return { done: true, value: undefined }; this.done = true; buffer.set([1, 2]); return { done: false, value: 2 }; }
}
const result = await new OneChunk().read();
console.log(result.done ? 'eof' : result.value.byteLength);
peek(n: number): Promise<Uint8Array>

Return up to n buffered bytes without consuming them.

The method pulls upstream until at least n bytes are buffered or EOF is reached. It returns a copy of the first min(n, buffered) bytes, so callers can mutate the returned array without changing the internal buffer.

import { BufferedBytesReader } from 'fino:stream';
class OneChunk extends BufferedBytesReader {
  done = false;
  async doPullInto(buffer) {
    if (this.done) return { done: true, value: undefined };
    this.done = true;
    buffer.set([1, 2]);
    return { done: false, value: 2 };
  }
}
const reader = new OneChunk();
console.log((await reader.peek(1))[0]);
scanBuffered(delim: Uint8Array): number

Scan currently buffered bytes for a delimiter without pulling upstream.

Returns the offset one past the first delimiter match, or -1 if the delimiter is absent from the current buffer. Empty delimiters return -1. The method can match delimiters that cross chunk boundaries.

import { BufferedBytesReader } from 'fino:stream';
class Chunked extends BufferedBytesReader {
  chunks = [new Uint8Array([65]), new Uint8Array([10])];
  async doPullInto(buffer) { const chunk = this.chunks.shift(); if (!chunk) return { done: true, value: undefined }; buffer.set(chunk); return { done: false, value: chunk.length }; }
}
const reader = new Chunked();
await reader.peek(2);
console.log(reader.scanBuffered(new Uint8Array([10])));
takeBuffered(n: number): Uint8Array

Remove and return exactly n bytes from the current buffer.

This method never pulls upstream. It throws if fewer than n bytes are buffered. The returned bytes are copied so callers own the buffer.

import { BufferedBytesReader } from 'fino:stream';
class OneChunk extends BufferedBytesReader {
  done = false;
  async doPullInto(buffer) {
    if (this.done) return { done: true, value: undefined };
    this.done = true;
    buffer.set([1, 2]);
    return { done: false, value: 2 };
  }
}
const reader = new OneChunk();
await reader.peek(2);
console.log(reader.takeBuffered(1)[0]);
override readExactly(n: number, options?: BytesReadOptions): Promise<Uint8Array | null>

Read exactly n bytes without consuming on failure.

The method first accumulates bytes with peek(). If EOF arrives before n bytes are available, it returns null and leaves buffered bytes untouched. For n === 0, it returns an empty array.

import { BufferedBytesReader } from 'fino:stream';
class OneChunk extends BufferedBytesReader {
  done = false;
  async doPullInto(buffer) { if (this.done) return { done: true, value: undefined }; this.done = true; buffer[0] = 1; return { done: false, value: 1 }; }
}
const reader = new OneChunk();
console.log(await reader.readExactly(2));
console.log(reader.buffered);
override readUntil( delim: Uint8Array, max: number = 1 << 20, options?: BytesReadOptions, ): Promise<Uint8Array | null>

Read through a delimiter without consuming on failure.

The method scans buffered bytes first, pulls one chunk at a time as needed, and consumes only when a delimiter is found. EOF before a match returns null with buffered bytes preserved. Empty delimiters and scans beyond max throw.

import { BufferedBytesReader } from 'fino:stream';
class Lines extends BufferedBytesReader {
  chunks = [new TextEncoder().encode('a\\n')];
  async doPullInto(buffer) { const chunk = this.chunks.shift(); if (!chunk) return { done: true, value: undefined }; buffer.set(chunk); return { done: false, value: chunk.length }; }
}
const reader = new Lines();
console.log(new TextDecoder().decode(await reader.readUntil(new Uint8Array([10]))));

Static Methods

static over(source: BytesReader): BufferedBytesReader

Wrap an existing byte reader in a buffered reader.

The wrapper pulls from source.read() and closes the source when the buffered reader closes. This is useful for tests and for adding non- consuming peek() and readUntil() behavior to an unbuffered source.

import { BufferedBytesReader, BytesReader } from 'fino:stream';
class EmptyBytes extends BytesReader { async doReadInto() { return { done: true, value: undefined }; } }
const buffered = BufferedBytesReader.over(new EmptyBytes());
console.log(buffered.buffered);

Getters

get buffered(): number

Number of bytes currently buffered.

These bytes can be consumed by takeBuffered() without awaiting upstream I/O. The value does not include bytes that may still be available from the underlying resource.

import { BufferedBytesReader } from 'fino:stream';
class EmptyBuffered extends BufferedBytesReader { async doPullInto() { return { done: true, value: undefined }; } }
console.log(new EmptyBuffered().buffered);
override get bufferedBytes(): number
get eof(): boolean

Whether upstream EOF has been reached and all buffered bytes are drained.

The value is false before the first EOF-producing pull, even if no bytes are currently buffered. Use peek() or read() to discover EOF.

import { BufferedBytesReader } from 'fino:stream';
class EmptyBuffered extends BufferedBytesReader { async doPullInto() { return { done: true, value: undefined }; } }
const reader = new EmptyBuffered();
await reader.peek(1);
console.log(reader.eof);

abstract class BufferedBytesWriter extends BytesWriter {

Byte writer with a coalescing buffer.

Small writes accumulate in an internal buffer and are emitted by doFlush() when the buffer fills, when flush() is called, or before close() completes. Writes at least as large as the buffer bypass coalescing after pending bytes are flushed. writev() accumulates small vector batches through the same coalescing buffer without routing each vector through write().

import { BufferedBytesWriter, BytesWriter } from 'fino:stream';
class Sink extends BytesWriter { async doWrite(_buf) {} }
const writer = BufferedBytesWriter.over(new Sink());
await writer.write(new Uint8Array([1]));
await writer.flush();

Constructors

constructor(onClose: () => void | Promise<void> = () => {}, bufferSize: number = 65536)

Create a buffered byte writer.

bufferSize defaults to 64 KiB. A smaller buffer flushes more often; a larger buffer can reduce syscall frequency at the cost of memory. onClose is invoked after pending bytes are flushed.

import { BufferedBytesWriter } from 'fino:stream';
class Sink extends BufferedBytesWriter { async doFlush(_buf) {} }
const writer = new Sink(() => {}, 1024);
await writer.close();

Methods

protected abstract doFlush(buf: Uint8Array): Promise<void>

Flush a coalesced byte slice to the underlying resource.

Implementations must emit all bytes in buf or throw. Calls are serialized per writer, so an implementation that suspends on backpressure keeps the descriptor to itself until it returns.

import { BufferedBytesWriter } from 'fino:stream';
class Sink extends BufferedBytesWriter {
  async doFlush(buf) { console.log(buf.byteLength); }
}
await new Sink().write(new Uint8Array([1]));
protected doWrite(buf: Uint8Array): Promise<void>

Coalesce or immediately flush one byte buffer.

Buffers at least as large as the coalesce buffer bypass accumulation after pending bytes are flushed. Smaller buffers are copied into the internal buffer, flushing first if needed.

The base Writer invokes this hook while holding the per-instance FIFO, so callers cannot contend for buffer room or overlap flushes.

import { BufferedBytesWriter } from 'fino:stream';
class Sink extends BufferedBytesWriter { async doFlush(_buf) {} }
await new Sink().write(new Uint8Array([1, 2]));
writev(vecs: Uint8Array[], count: number = vecs.length): Promise<void>

Write multiple byte buffers through the coalescing buffer.

The complete vector batch occupies one writer operation. Small vectors are accumulated and flushed only when the buffer fills. A vector at least as large as the coalesce buffer flushes pending bytes first and then bypasses accumulation.

import { BufferedBytesWriter } from 'fino:stream';
class Sink extends BufferedBytesWriter { async doFlush(_buf) {} }
await new Sink().writev([new Uint8Array([1]), new Uint8Array([2])]);
protected _directAccumulate(buf: Uint8Array): boolean

Synchronously copy buf into the coalesce buffer.

Returns true if the bytes were accumulated; false if the buffer doesn't have enough room (caller must await flush() first, then retry). Only safe to call when buf.byteLength < this.#buf.byteLength.

import { BufferedBytesWriter } from 'fino:stream';
class Sink extends BufferedBytesWriter {
  async doFlush(_buf) {}
  tryAccumulate(buf) { return this._directAccumulate(buf); }
}
console.log(new Sink().tryAccumulate(new Uint8Array([1])));
override async flush(): Promise<void>

Flush-hook implementation that drains the coalesce buffer.

The public base flush() method invokes this after earlier writes. With no pending bytes it is a no-op. Errors from doFlush() reject the caller and the pending count has already been reset.

The pending bytes are taken as a copy: doFlush() can suspend on backpressure, and the coalesce buffer it was handed would otherwise be refilled from offset zero by the next write while those bytes were still on their way out.

import { BufferedBytesWriter } from 'fino:stream';
class Sink extends BufferedBytesWriter { async doFlush(_buf) {} }
const writer = new Sink();
await writer.write(new Uint8Array([1]));
await writer.flush();
protected _takePending(): Uint8Array | null

Return the buffered bytes (a copy) and reset the pending count. Used by subclasses that need to perform a synchronous flush (e.g. on process exit) without going through the async flush path.

import { BufferedBytesWriter } from 'fino:stream';
class Sink extends BufferedBytesWriter {
  async doFlush(_buf) {}
  take() { return this._takePending(); }
}
const writer = new Sink();
await writer.write(new Uint8Array([1]));
console.log(writer.take()?.byteLength);

Static Methods

static over(target: BytesWriter, bufferSize: number = 65536): BufferedBytesWriter

Wrap an existing byte writer with coalescing behavior.

The wrapper flushes by calling target.write(buf) and closes the target when the wrapper closes. bufferSize defaults to 64 KiB.

import { BufferedBytesWriter, BytesWriter } from 'fino:stream';
class Sink extends BytesWriter { async doWrite(_buf) {} }
const buffered = BufferedBytesWriter.over(new Sink(), 4096);
await buffered.close();

class BytesReader extends Reader<Uint8Array> {

Abstract byte-stream reader with structural read helpers.

Subclasses implement doReadInto(buffer, options) and fill caller-provided storage, returning the written byte count as a ReadResult or a done result on EOF. readExactly(), readUntil(), and readByte() are built on that hook and avoid over-fetching. If EOF interrupts an unbuffered structural read, partially consumed bytes are stashed and replayed on the next read operation. onConsume(bytes) is invoked only after bytes are delivered to the caller, which lets transports such as QUIC return receive credit when application code actually pulls buffered data.

import { BytesReader } from 'fino:stream';
class MemoryReader extends BytesReader {
  data = new Uint8Array([65, 10]);
  async doReadInto(buffer) {
    if (this.data.byteLength === 0) return { done: true, value: undefined };
    const n = Math.min(buffer.byteLength, this.data.byteLength);
    buffer.set(this.data.subarray(0, n));
    this.data = this.data.subarray(n);
    return { done: false, value: n };
  }
}
console.log(await new MemoryReader().readByte());

Constructors

constructor(stateOrClose: BytesReadableState | ReaderCloseCallback = () => {})

Methods

protected async doReadInto( buffer: Uint8Array, options?: BytesReadOptions, ): Promise<ReadResult<number>>

Read bytes from the underlying source.

Implementations fill at most buffer.byteLength bytes, may fill fewer, and return the number written in a value result or a done result on EOF. Backends should avoid returning zero when possible because structural helpers may need to retry.

import { BytesReader } from 'fino:stream';
class EmptyBytes extends BytesReader {
  async doReadInto(_buffer) { return { done: true, value: undefined }; }
}
console.log(await new EmptyBytes().read());
protected onConsume(_bytes: number): void

Called after bytes are delivered to the public reader caller.

The default implementation is a no-op. Protocol adapters can override this to report backpressure progress, for example by extending QUIC stream and connection flow-control credit. The hook is not called for bytes pulled internally and then stashed after an incomplete structural read.

read(options?: number | BytesReadOptions): Promise<ReadResult<Uint8Array>>

Read one byte chunk.

The default request size is 64 KiB, but subclasses may return fewer bytes. Any bytes stashed by an earlier partial structural read are returned before the underlying doReadInto() hook is called. The returned allocation is owned by the caller and remains stable across later reads. A result with done: true means EOF.

import { BytesReader } from 'fino:stream';
class EmptyBytes extends BytesReader {
  async doReadInto(_buffer) { return { done: true, value: undefined }; }
}
console.log(await new EmptyBytes().read());
readAtMost( maxBytes: number, options: Omit<BytesReadOptions, 'maxBytes'> = {}, ): Promise<ReadResult<Uint8Array>>

Read at most maxBytes bytes.

This is a named convenience around read({ maxBytes }) for protocols that need explicit bounded consumption.

readInto( buffer: ArrayBufferView, options: Omit<BytesReadOptions, 'maxBytes'> = {}, ): Promise<ReadResult<number>>

Read bytes directly into caller-provided view storage.

The reader borrows the exact view, including its offset and length, until the returned promise settles. The caller must not mutate or reuse that region while the read is pending; the reader does not retain it afterward. Returns the number of bytes written in a value result, or a done result on EOF, and never writes more than buffer.byteLength.

The view's byte offset and length are preserved, allowing slices of pooled or arena-allocated buffers to be filled without an intermediate copy.

readExactly(n: number, options?: BytesReadOptions): Promise<Uint8Array | null>

Read exactly n bytes.

Returns an empty array for n === 0. If EOF arrives before n bytes are available, returns null and stashes bytes already read so the next read operation can replay them. Buffered readers override this with atomic, non-consuming failure semantics.

import { BytesReader } from 'fino:stream';
class MemoryReader extends BytesReader {
  data = new Uint8Array([1, 2]);
  async doReadInto(buffer) {
    if (!this.data.byteLength) return { done: true, value: undefined };
    const n = Math.min(buffer.byteLength, this.data.byteLength);
    buffer.set(this.data.subarray(0, n));
    this.data = this.data.subarray(n);
    return { done: false, value: n };
  }
}
const reader = new MemoryReader();
console.log((await reader.readExactly(2))?.byteLength);
readByte(options?: BytesReadOptions): Promise<number | null>

Read a single byte.

The returned number is in the range 0 through 255. null indicates EOF. Stashed bytes from a previous partial structural read are consumed before the underlying source is queried.

import { BytesReader } from 'fino:stream';
class OneByte extends BytesReader {
  done = false;
  async doReadInto(buffer) {
    if (this.done) return { done: true, value: undefined };
    this.done = true;
    buffer[0] = 97;
    return { done: false, value: 1 };
  }
}
console.log(await new OneByte().readByte());
readUntil( delim: Uint8Array, max: number = 1 << 20, options?: BytesReadOptions, ): Promise<Uint8Array | null>

Read through the first delimiter occurrence.

The returned bytes include delim. An empty delimiter throws. EOF before the delimiter returns null; bytes scanned before EOF are stashed for the next read. Scanning more than max bytes without a delimiter throws.

import { BytesReader } from 'fino:stream';
class MemoryReader extends BytesReader {
  data = new TextEncoder().encode('ok\\nrest');
  async doReadInto(buffer) {
    if (!this.data.byteLength) return { done: true, value: undefined };
    const n = Math.min(buffer.byteLength, this.data.byteLength);
    buffer.set(this.data.subarray(0, n));
    this.data = this.data.subarray(n);
    return { done: false, value: n };
  }
}
const line = await new MemoryReader().readUntil(new Uint8Array([10]));
console.log(new TextDecoder().decode(line));

Getters

get bufferedBytes(): number

Bytes already held by this reader before another source pull is required.

For the base unbuffered reader this only includes bytes stashed after a partial structural read. Buffered readers include their chunk-list buffer.

class BytesChannel {

A zero-capacity byte rendezvous with standard byte endpoints.

The channel owns no byte buffer. A write waits for reader-provided storage, so producer progress follows consumer demand. read(n) allocates the offered region; readInto(view) lets the caller supply it.

import { BytesChannel } from 'fino:stream';
const channel = new BytesChannel();
const read = channel.reader.read(4);
await channel.writer.write(new Uint8Array([1, 2]));
console.log(await read);

Readonly Properties

readonly reader: BytesReader
readonly writer: BytesWriter

Constructors

constructor()

class BytesWriter extends Writer<ArrayBuffer | ArrayBufferView> {

Abstract byte-stream writer.

Subclasses implement doWrite(buf) to emit all bytes to the underlying resource. write() accepts ArrayBuffer and ArrayBufferView sources, rejects writes after close, and delegates to the hook. writev() defaults to sequential writes and can be overridden for scatter/gather implementations.

import { BytesWriter } from 'fino:stream';
class MemoryWriter extends BytesWriter {
  chunks = [];
  async doWrite(buf) { this.chunks.push(buf.slice()); }
}
const writer = new MemoryWriter();
await writer.write(new Uint8Array([1]));

Constructors

constructor(stateOrClose: BytesWritableState | WriterCloseCallback = () => {})

Methods

protected doWrite(buf: Uint8Array): Promise<void>

Emit all bytes in buf to the underlying resource.

Implementations must handle partial writes, backpressure, and sink errors internally. The base write() method has already converted input to a Uint8Array and checked the closed state.

import { BytesWriter } from 'fino:stream';
class MemoryWriter extends BytesWriter {
  async doWrite(buf) { console.log(buf.byteLength); }
}
await new MemoryWriter().write(new Uint8Array([1, 2]));
async write(data: ArrayBuffer | ArrayBufferView): Promise<void>

Normalize one byte source admitted by the base writer.

ArrayBuffer and ArrayBufferView inputs are wrapped in a Uint8Array preserving view byte offsets, then forwarded to doWrite() as one ordered writer operation.

import { BytesWriter } from 'fino:stream';
class MemoryWriter extends BytesWriter {
  bytes = 0;
  async doWrite(buf) { this.bytes += buf.byteLength; }
}
const writer = new MemoryWriter();
await writer.write(new ArrayBuffer(4));
console.log(writer.bytes);
writev(vecs: Uint8Array[], count: number = vecs.length): Promise<void>

Write multiple buffers in order.

The default implementation writes up to count vectors sequentially and skips missing or empty entries. Subclasses may override for vectorized system calls. Errors from any individual write abort the sequence.

import { BytesWriter } from 'fino:stream';
class MemoryWriter extends BytesWriter {
  bytes = 0;
  async doWrite(buf) { this.bytes += buf.byteLength; }
}
const writer = new MemoryWriter();
await writer.writev([new Uint8Array([1]), new Uint8Array([2])]);
console.log(writer.bytes);

class Channel<T> {

Zero-capacity in-memory Writer/Reader pair.

Each writer.write(value) remains pending until one reader.read() accepts that value. Overlapping reads and writes pair in FIFO order, providing back-pressure without retaining producer-ahead values. writer.close() signals EOF after admitted writes are consumed. Closing the reader rejects unread writes. writer.close(error) drains admitted values and then surfaces the terminal error to the reader.

Use UnboundedChannel only when producers must run ahead of consumers and unbounded memory growth is acceptable.

import { Channel } from 'fino:stream';
const ch = new Channel();
const consume = (async () => {
  for await (const value of ch.reader) console.log(value);
})();
await ch.writer.write(1);
await ch.writer.write(2);
await ch.writer.close();
await consume;

Readonly Properties

readonly writer: Writer<T>

Producer half of the channel.

write(value) waits for the reader to accept the value. close() signals EOF after admitted values. close(error) drains them and then surfaces a terminal error.

readonly reader: Reader<T>

Consumer half of the channel.

A standard Reader<T>: iterate it with for await, drive it with reader.read(), or feed another Writer with writer.pipe(reader).

Constructors

constructor()

Create a connected zero-capacity writer/reader pair.

The two halves share rendezvous state but no value buffer. Reads and writes pair in FIFO order.

class ChannelCancelledError extends Error {

Error reported to a producer when its paired reader terminates early.

Constructors

constructor((message = 'Channel reader closed before delivery completed'))

class Reader<T, Options = void> implements AsyncIterator<T> {

Pull endpoint for an asynchronous value source.

Reader delegates delivery, ordering, and closure to shared state and exposes the async iterator protocol. Reader.from() adapts an async iterable without adding transformation semantics to channels.

import { Reader } from 'fino:stream';
class OnceReader extends Reader {
  value = 'hello';
  async read() {
    const value = this.value;
    this.value = null;
    return value === null
      ? { done: true, value: undefined }
      : { done: false, value };
  }
}
const reader = new OnceReader();
for await (const value of reader) console.log(value);

Constructors

constructor(stateOrClose: ReadableState<T, Options> | ReaderCloseCallback = () => {})

Create a reader with an optional close callback.

The callback defaults to a no-op and is invoked at most once, the first time close() is awaited or when iteration reaches EOF. Callback errors reject the close operation.

import { Reader } from 'fino:stream';
const reader = new Reader(() => console.log('closed'));
await reader.close();

Static Methods

static from<T>(source: AsyncIterable<T>): Reader<T>

Wrap an async iterable in the standard pull-based reader facade.

Getters

get closed(): boolean

Whether the reader has been closed.

The flag flips before the close callback is awaited. It remains false while the reader is open, even if EOF has not yet been checked.

import { Reader } from 'fino:stream';
const reader = Reader.from((async function* () {})());
console.log(reader.closed);
await reader.close();
console.log(reader.closed);

Methods

read(options?: Options): Promise<ReadResult<T>>

Pull the next value from the state as an explicit iterator result.

import { Reader } from 'fino:stream';
const reader = Reader.from((async function* () {})());
console.log(await reader.read());
close(error?: Error): Promise<void>

Close the reader and run its close callback once.

Multiple calls are safe; only the first one invokes onClose. The method does not call read() and does not require EOF. Callback failures reject the returned promise. On channel-backed readers, an omitted error is clean early cancellation; providing an error propagates that error upstream.

import { Reader } from 'fino:stream';
const reader = Reader.from((async function* () {})());
await reader.close();
await reader.close();
async next(): Promise<IteratorResult<T>>

Advance the async iterator.

next() calls read(). When read() returns a done result, the reader is closed. Source errors or close callback errors reject the returned promise.

import { Reader } from 'fino:stream';
const reader = Reader.from((async function* () {})());
const result = await reader.next();
console.log(result.done);

class UnboundedBytesChannel {

An unbounded byte channel backed by capacity-sized segments.

Writes can run ahead by allocating another segment, so memory use grows with producer lead. Segment capacity defaults to 64 KiB; it controls allocation granularity rather than a total bound.

import { UnboundedBytesChannel } from 'fino:stream';
const channel = new UnboundedBytesChannel(4096);
await channel.writer.write(new Uint8Array([1]));
console.log(await channel.reader.read());

Readonly Properties

readonly reader: BytesReader
readonly writer: BytesWriter

Constructors

constructor(segmentCapacity: number = 65536)

class UnboundedChannel<T> {

Unbounded in-memory Writer/Reader pair.

Writes resolve after appending to an internal linked FIFO, without waiting for a reader. Reads remove values from the head in constant time. The queue has no capacity limit, so a producer can retain arbitrary memory if it outpaces its consumer. Writer close drains accepted values before EOF; reader close releases unread values.

import { UnboundedChannel } from 'fino:stream';

const channel = new UnboundedChannel<number>();
await channel.writer.write(1);
await channel.writer.write(2);
await channel.writer.close();
for await (const value of channel.reader) console.log(value);

Readonly Properties

readonly writer: Writer<T>

Writer that accepts values into the unbounded FIFO.

readonly reader: Reader<T>

Reader that removes accepted values in FIFO order.

Constructors

constructor()

Create an empty unbounded channel.

class Writer<T> {

Acceptance endpoint for an asynchronous value sink.

Writer delegates acceptance order, back-pressure, flushing, closure, and failure to shared state. The endpoint keeps that machinery out of the user API: callers use write(), flush(), and close().

import { Writer } from 'fino:stream';
class ArrayWriter extends Writer {
  values = [];
  async write(value) { this.values.push(value); }
}
const writer = new ArrayWriter();
await writer.write('hello');

Constructors

constructor(stateOrClose: WritableState<T> | WriterCloseCallback = () => {})

Create a writer with an optional close callback.

The callback defaults to a no-op and is invoked at most once. Callback failures reject close().

import { Writer } from 'fino:stream';
class NullWriter extends Writer { async write(_value) {} }
const writer = new NullWriter(() => console.log('closed'));
await writer.close();

Getters

get closed(): boolean

Whether the writer has been closed.

The flag is set as soon as close() begins, before admitted work drains and before the close callback is awaited. The base class rejects later writes.

import { Writer } from 'fino:stream';
class NullWriter extends Writer { async write(_value) {} }
const writer = new NullWriter();
await writer.close();
console.log(writer.closed);

Methods

write(value: T): Promise<void>

Write one value to the state.

import { Writer } from 'fino:stream';
class ArrayWriter extends Writer {
  values = [];
  async write(value) { this.values.push(value); }
}
await new ArrayWriter().write('x');
writeSync?(value: T): void

Write one value synchronously when the writer supports synchronous acceptance.

This method is an optional capability for hot paths that must not yield between producing bytes and updating native state. Implementations should either accept the value completely or throw. The base automatically prevents it from overtaking pending asynchronous work. Writer does not provide a fallback because calling async write() from a sync-only path would hide an ordering bug.

import { Writer } from 'fino:stream';
function writeNow(writer, value) {
  if (writer.writeSync === undefined) throw new Error('sync writes unavailable');
  writer.writeSync(value);
}
async pipe(source: AsyncIterable<T>): Promise<void>

Consume an async iterable and write each value in order.

The method awaits each write() before reading the next source value, preserving backpressure. It does not close the writer or the source.

import { Writer } from 'fino:stream';
class ArrayWriter extends Writer {
  values = [];
  async write(value) { this.values.push(value); }
}
const writer = new ArrayWriter();
await writer.pipe(['a', 'b']);
console.log(writer.values.length);
flush(): Promise<void>

Flush internally buffered data.

Subclass hook for flushing internally buffered data.

import { Writer } from 'fino:stream';
class NullWriter extends Writer { async write(_value) {} }
await new NullWriter().flush();
close(error?: Error): Promise<void>

Close the writer and run its close callback once.

Multiple calls return the same cleanup promise. New operations are rejected immediately; operations admitted earlier drain in FIFO order before the subclass close hook and cleanup callback run. Providing an error closes the stream after that drain and makes the reader reject at the terminal point.

import { Writer } from 'fino:stream';
class NullWriter extends Writer { async write(_value) {} }
const writer = new NullWriter();
await writer.close();
await writer.close();
closeSync?(): void

Close the writer synchronously when the writer supports synchronous close.

This optional capability is for callers that need close state to be visible immediately. Implementations should mark the writer closed before returning and perform only synchronous cleanup. Callers that can yield should continue to use close().

import { Writer } from 'fino:stream';
function closeNow(writer) {
  if (writer.closeSync === undefined) throw new Error('sync close unavailable');
  writer.closeSync();
}

Interfaces

interface BytesReadOptions {

Options for byte-reader pull operations.

maxBytes bounds the returned chunk size. signal lets backends cancel a pending source read without consuming future bytes for an abandoned caller. Every structural read method (read, readAtMost, readExactly, readByte, readUntil) accepts this object; a bare number is shorthand for maxBytes.

import { FdReader } from 'fino:stream';

const reader = new FdReader(0, () => {});
const controller = new AbortController();
setTimeout(() => controller.abort(new Error('slow input')), 1000);
const result = await reader.read({ maxBytes: 4096, signal: controller.signal });
console.log(result.done ? 'eof' : result.value.byteLength);

Properties

maxBytes?: number

Upper bound on the number of bytes a single read may return.

The reader is free to return fewer bytes, and never returns more. Omitting it lets each method choose its own default: 64 KiB for read, one byte for readByte, and the exact requested count for readExactly.

signal?: AbortSignal | null

Abort signal that cancels the read.

The signal is checked before each underlying source pull, so an already-aborted signal rejects before any bytes are touched and an abort that lands between chunks stops a multi-chunk structural read. The rejection carries the signal's reason. Passing null is equivalent to omitting it.

Types

type ReaderCloseCallback = (error?: Error) => void | Promise<void>

type ReadResult<T> = { done: false; value: T } | { done: true; value: undefined }

Result of a reader pull: either one delivered value or clean end-of-stream.

type WriterCloseCallback = (error?: Error) => void | Promise<void>

Cleanup callback invoked when a Writer closes.