/**
 * @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 from 'p-defer';
import { raceSignal } from 'race-signal';
class QueuelessPushable {
    readNext;
    haveNext;
    ended;
    nextResult;
    error;
    constructor() {
        this.ended = false;
        this.readNext = deferred();
        this.haveNext = deferred();
    }
    [Symbol.asyncIterator]() {
        return this;
    }
    async next() {
        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) {
        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 = {
            done: true,
            value: undefined
        };
        return result;
    }
    async return() {
        const result = {
            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, options) {
        await this._push(value, options);
    }
    async end(err, options) {
        if (err != null) {
            await this.throw(err);
        }
        else {
            // abortable return
            await this._push(undefined, options);
        }
    }
    async _push(value, options) {
        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() {
    return new QueuelessPushable();
}
//# sourceMappingURL=index.js.map