/** * Copyright (c) 2025 xwra * SPDX-License-Identifier: AGPL-3.0-or-later */ export class ChunkedStream implements AsyncIterable { private readonly chunks: T[] = []; private readonly resolvers: ((result: IteratorResult) => void)[] = []; private readonly rejectors: ((error: Error) => void)[] = []; private _error: Error | null = null; private _closed = false; get closed(): boolean { return this._closed; } write(chunk: T) { if (this._closed) throw new Error("Cannot write to closed stream"); const resolver = this.resolvers.shift(); if (resolver) { this.rejectors.shift(); resolver({ value: chunk, done: false }); } else { this.chunks.push(chunk); } } close(): void { this._closed = true; while (this.resolvers.length) { this.rejectors.shift(); this.resolvers.shift()!({ value: undefined! as any, done: true }); } } error(err: Error): void { if (this._closed) return; this._error = err; this._closed = true; while (this.rejectors.length) { this.rejectors.shift()!(err); this.resolvers.shift(); } } async next(): Promise> { if (this._error) { throw this._error; } if (this.chunks.length) { return { value: this.chunks.shift()!, done: false }; } if (this._closed) return { value: undefined as any, done: true }; return new Promise((resolve, reject) => { this.resolvers.push(resolve); this.rejectors.push(reject); }); } [Symbol.asyncIterator](): AsyncIterableIterator { return this; } } export const mapStream = ( fn: (chunk: T, index: number) => U | Promise, ) => async function* (source: AsyncIterable): AsyncIterable { let index = 0; for await (const chunk of source) yield await fn(chunk, index++); }; export const filterStream = ( pred: (chunk: T, index: number) => boolean | Promise, ) => async function* (source: AsyncIterable): AsyncIterable { let index = 0; for await (const chunk of source) { if (await pred(chunk, index++)) yield chunk; } }; export const takeStream = (count: number) => async function* (source: AsyncIterable): AsyncIterable { let taken = 0; for await (const chunk of source) { if (taken++ >= count) return; yield chunk; } }; export const skipStream = (count: number) => async function* (source: AsyncIterable): AsyncIterable { let index = 0; for await (const chunk of source) { if (index++ >= count) yield chunk; } }; export const batchStream = (size: number) => async function* (source: AsyncIterable): AsyncIterable { let batch: T[] = []; for await (const chunk of source) { batch.push(chunk); if (batch.length >= size) { yield batch; batch = []; } } if (batch.length) yield batch; }; export const tapStream = ( fn: (chunk: T, index: number) => void | Promise, ) => async function* (source: AsyncIterable): AsyncIterable { let index = 0; for await (const chunk of source) { yield chunk; await fn(chunk, index++); } }; export const catchStream = ( handler: (error: Error) => void | Promise, ) => async function* (source: AsyncIterable): AsyncIterable { try { for await (const chunk of source) yield chunk; } catch (err) { await handler(err instanceof Error ? err : new Error(String(err))); } }; export const pipe = (...fns: Array<(src: AsyncIterable) => AsyncIterable>) => (source: AsyncIterable) => fns.reduce((acc, fn) => fn(acc), source);