ai
Version:
AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.
116 lines (103 loc) • 3.98 kB
text/typescript
/**
* A type that combines AsyncIterable and ReadableStream.
* This allows a ReadableStream to be consumed using for-await-of syntax.
*/
export type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>;
/**
* Wraps a ReadableStream and returns an object that is both a ReadableStream and an AsyncIterable.
* This enables consumption of the stream using for-await-of, with proper resource cleanup on early exit or error.
*
* @template T The type of the stream's chunks.
* @param source The source ReadableStream to wrap.
* @returns An AsyncIterableStream that can be used as both a ReadableStream and an AsyncIterable.
*/
export function createAsyncIterableStream<T>(
source: ReadableStream<T>,
): AsyncIterableStream<T> {
// Pipe through a TransformStream to ensure a fresh, unlocked stream.
return asAsyncIterableStream(source.pipeThrough(new TransformStream<T, T>()));
}
/**
* Attaches the async iterator protocol to an existing ReadableStream in place,
* turning it into an AsyncIterableStream without piping through an additional
* TransformStream.
*
* Use this when the stream is already known to be fresh and exclusively owned
* (e.g. the readable side of a TransformStream created for this consumer).
* Adding an extra `pipeThrough` in that situation creates a chain of two
* transforms fed by an active upstream pipe, which can surface a spurious
* unhandled `undefined` rejection when the consumer cancels early (observed on
* Node.js 26). {@link createAsyncIterableStream} wraps this after adding a
* fresh transform for callers that may pass shared or locked streams.
*
* @template T The type of the stream's chunks.
* @param stream The ReadableStream to augment. It must be fresh and unlocked.
* @returns The same stream, augmented with the async iterator protocol.
*/
export function asAsyncIterableStream<T>(
stream: ReadableStream<T>,
): AsyncIterableStream<T> {
/**
* Implements the async iterator protocol for the stream.
* Ensures proper cleanup (cancelling and releasing the reader) on completion, early exit, or error.
*/
(stream as AsyncIterableStream<T>)[Symbol.asyncIterator] = function (
this: ReadableStream<T>,
): AsyncIterator<T> {
const reader = this.getReader();
let finished = false;
/**
* Cleans up the reader by cancelling and releasing the lock.
*/
async function cleanup(cancelStream: boolean) {
if (finished) return;
finished = true;
try {
if (cancelStream) {
await reader.cancel?.();
}
} finally {
try {
reader.releaseLock();
} catch {}
}
}
return {
/**
* Reads the next chunk from the stream.
* @returns A promise resolving to the next IteratorResult.
*/
async next(): Promise<IteratorResult<T>> {
if (finished) {
return { done: true, value: undefined };
}
const { done, value } = await reader.read();
if (done) {
await cleanup(true);
return { done: true, value: undefined };
}
return { done: false, value };
},
/**
* May be called on early exit (e.g., break from for-await) or after completion.
* Ensures the stream is cancelled and resources are released.
* @returns A promise resolving to a completed IteratorResult.
*/
async return(): Promise<IteratorResult<T>> {
await cleanup(true);
return { done: true, value: undefined };
},
/**
* Called on early exit with error.
* Ensures the stream is cancelled and resources are released, then rethrows the error.
* @param err The error to throw.
* @returns A promise that rejects with the provided error.
*/
async throw(err: unknown): Promise<IteratorResult<T>> {
await cleanup(true);
throw err;
},
};
};
return stream as AsyncIterableStream<T>;
}