| 24 | */ |
| 25 | |
| 26 | export class AsyncEventQueue<T> implements AsyncIterable<T> { |
| 27 | private buf: T[] = []; |
| 28 | private waiters: Array<{ |
| 29 | resolve: (r: IteratorResult<T>) => void; |
| 30 | reject: (e: Error) => void; |
| 31 | }> = []; |
| 32 | private closed = false; |
| 33 | private err: Error | null = null; |
| 34 | /** Monotonic count of events accepted by push(). */ |
| 35 | pushedCount = 0; |
| 36 | /** Monotonic count of events the consumer has fully processed. */ |
| 37 | consumedCount = 0; |
| 38 | /** Set when the consumer abandoned the stream; progress waiters must not block on it. */ |
| 39 | consumerDetached = false; |
| 40 | private progressWaiters: Array<() => void> = []; |
| 41 | |
| 42 | push(item: T): void { |
| 43 | if (!this.canEnqueue()) return; |
| 44 | this.enqueue(item); |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * Enqueue one item and resolve only after the consumer has fully processed |
| 49 | * that exact sequence. Unlike `push()`, an enqueue rejected by queue state is |
| 50 | * observable by the producer. |
| 51 | */ |
| 52 | pushAndWaitUntilConsumed(item: T): Promise<void> { |
| 53 | if (this.err) return Promise.reject(this.err); |
| 54 | if (this.consumerDetached) return Promise.reject(consumerDetachedError()); |
| 55 | if (this.closed) return Promise.reject(queueClosedError()); |
| 56 | const sequence = this.enqueue(item); |
| 57 | return this.waitUntilConsumed(sequence); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Wait through the producer boundary captured at call time. Items enqueued |
| 62 | * later do not extend the wait. |
| 63 | */ |
| 64 | waitUntilConsumedThroughCurrent(): Promise<void> { |
| 65 | return this.waitUntilConsumed(this.pushedCount); |
| 66 | } |
| 67 | |
| 68 | private canEnqueue(): boolean { |
| 69 | return !this.closed && !this.err && !this.consumerDetached; |
| 70 | } |
| 71 | |
| 72 | private enqueue(item: T): number { |
| 73 | const sequence = ++this.pushedCount; |
| 74 | const w = this.waiters.shift(); |
| 75 | if (w) { |
| 76 | w.resolve({ value: item, done: false }); |
| 77 | } else { |
| 78 | this.buf.push(item); |
| 79 | } |
| 80 | this.wake(); |
| 81 | return sequence; |
| 82 | } |
| 83 |
nothing calls this directly
no outgoing calls
no test coverage detected