UNPKG

it-queueless-pushable

Version:

A pushable queue that waits until a value is consumed before accepting another

169 lines (140 loc) 4.72 kB
/** * @packageDocumentation * * A pushable async generator that waits until the current value is consumed * before allowing a new value to be pushed. * * Useful for when you don't want to keep memory usage under control and/or * allow a downstream consumer to dictate how fast data flows through a pipe, * but you want to be able to apply a transform to that data. * * @example * * ```typescript * import { queuelessPushable } from 'it-queueless-pushable' * * const pushable = queuelessPushable<string>() * * // run asynchronously * Promise.resolve().then(async () => { * // push a value - the returned promise will not resolve until the value is * // read from the pushable * await pushable.push('hello') * }) * * // read a value * const result = await pushable.next() * console.info(result) // { done: false, value: 'hello' } * ``` */ import deferred, { type DeferredPromise } from 'p-defer' import { raceSignal, type RaceSignalOptions } from 'race-signal' import type { AbortOptions } from 'abort-error' export interface Pushable<T> extends AsyncGenerator<T, void, unknown> { /** * End the iterable after all values in the buffer (if any) have been yielded. If an * error is passed the buffer is cleared immediately and the next iteration will * throw the passed error */ end(err?: Error, options?: AbortOptions & RaceSignalOptions): Promise<void> /** * Push a value into the iterable. Values are yielded from the iterable in the order * they are pushed. Values not yet consumed from the iterable are buffered. */ push(value: T, options?: AbortOptions & RaceSignalOptions): Promise<void> } class QueuelessPushable <T> implements Pushable<T> { private readNext: DeferredPromise<void> private haveNext: DeferredPromise<void> private ended: boolean private nextResult: IteratorResult<T> | undefined private error?: Error constructor () { this.ended = false this.readNext = deferred() this.haveNext = deferred() } [Symbol.asyncIterator] (): AsyncGenerator<T, void, unknown> { return this } async next (): Promise<IteratorResult<T, void>> { if (this.nextResult == null) { // wait for the supplier to push a value await this.haveNext.promise } if (this.nextResult == null) { throw new Error('HaveNext promise resolved but nextResult was undefined') } const nextResult = this.nextResult this.nextResult = undefined // signal to the supplier that we read the value this.readNext.resolve() this.readNext = deferred() return nextResult } async throw (err?: Error): Promise<IteratorReturnResult<undefined>> { this.ended = true this.error = err if (err != null) { // this can cause unhandled promise rejections if nothing is awaiting the // next value so attach a dummy catch listener to the promise this.haveNext.promise.catch(() => {}) this.haveNext.reject(err) } const result: IteratorReturnResult<undefined> = { done: true, value: undefined } return result } async return (): Promise<IteratorResult<T>> { const result: IteratorReturnResult<undefined> = { done: true, value: undefined } this.ended = true this.nextResult = result // let the consumer know we have a new value this.haveNext.resolve() return result } async push (value: T, options?: AbortOptions & RaceSignalOptions): Promise<void> { await this._push(value, options) } async end (err?: Error, options?: AbortOptions & RaceSignalOptions): Promise<void> { if (err != null) { await this.throw(err) } else { // abortable return await this._push(undefined, options) } } private async _push (value?: T, options?: AbortOptions & RaceSignalOptions): Promise<void> { if (value != null && this.ended) { throw this.error ?? new Error('Cannot push value onto an ended pushable') } // wait for all values to be read while (this.nextResult != null) { await this.readNext.promise } if (value != null) { this.nextResult = { done: false, value } } else { this.ended = true this.nextResult = { done: true, value: undefined } } // let the consumer know we have a new value this.haveNext.resolve() this.haveNext = deferred() // wait for the consumer to have finished processing the value and requested // the next one or for the passed signal to abort the waiting await raceSignal( this.readNext.promise, options?.signal, options ) } } export function queuelessPushable <T> (): Pushable<T> { return new QueuelessPushable<T>() }