Initial Commit

This commit is contained in:
2026-05-24 14:26:34 +02:00
commit c99568a59a
12 changed files with 2744 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
/**
* An async iterable queue that bridges push-based producers and pull-based consumers.
*
* Values are pushed from outside the iteration loop (for example, from an SSE
* read callback) and consumed with standard async iteration:
*
* ```ts
* const messages = new AsyncPushIterator<SSEvent>();
*
* // Producer (elsewhere)
* messages.push(event);
*
* // Consumer
* for await (const event of messages) {
* handle(event);
* }
* ```
*
* When a consumer is already waiting on {@link AsyncPushIterator.prototype.next},
* {@link push} delivers immediately. Otherwise values are buffered in FIFO order
* until consumed. Call {@link close} to signal end-of-stream; further
* {@link push} calls are ignored.
*
* Implements `Symbol.asyncDispose` so instances can be closed with `using` when
* the runtime supports explicit resource management.
*/
export class AsyncPushIterator<T> implements AsyncIterable<T> {
/** Values pushed before a consumer was waiting to read them. */
private queue: T[] = [];
/** Pending `next()` calls waiting for a pushed value or close. */
private resolvers: ((result: IteratorResult<T>) => void)[] = [];
/** When true, no more values are accepted and iteration eventually completes. */
public closed = false;
/**
* Enqueues a value for the consumer.
*
* If a consumer is blocked on `next()`, the value is delivered immediately and
* the queue is bypassed. After {@link close}, pushes are silently dropped.
*
* @param value - The next value to yield from the iterator.
*/
push(value: T): void {
if (this.closed) return;
if (this.resolvers.length > 0) {
// Someone is waiting for a value, resolve immediately
const resolve = this.resolvers.shift()!;
resolve({ value, done: false });
} else {
// No one waiting, buffer the value
this.queue.push(value);
}
}
/**
* Ends the stream.
*
* Marks the iterator closed so future {@link push} calls are ignored. Any
* consumer currently waiting on `next()` receives `{ done: true }`. Buffered
* values are still yielded before iteration completes.
*/
close(): void {
this.closed = true;
for (const resolve of this.resolvers) {
resolve({ value: undefined as T, done: true });
}
this.resolvers = [];
}
/**
* Returns an async iterator that reads from this instance's shared queue.
*
* Buffered values are returned first, then the iterator waits for pushes or
* for {@link close}. Intended for a single consumer per instance.
*/
[Symbol.asyncIterator](): AsyncIterator<T> {
return {
next: (): Promise<IteratorResult<T>> => {
// If we have buffered values, return immediately
if (this.queue.length > 0) {
return Promise.resolve({ value: this.queue.shift()!, done: false });
}
// If closed and no buffered values, we're done
if (this.closed) {
return Promise.resolve({ value: undefined as T, done: true });
}
// Wait for a value to be pushed
return new Promise((resolve) => {
this.resolvers.push(resolve);
});
},
};
}
/**
* Closes the iterator when used with explicit resource management (`using`).
*/
[Symbol.asyncDispose](): Promise<void> {
this.close();
return Promise.resolve();
}
}