// The MIT License (MIT)
const NOT_READABLE = Symbol('not readable');
const READABLE = Symbol('readable');
const ENDED = Symbol('ended');
const ERRORED = Symbol('errored');
const STATES = {
    notReadable: NOT_READABLE,
    readable: READABLE,
    ended: ENDED,
    errored: ERRORED,
};
/**
 * Wraps a stream into an object that can be used as an async iterator.
 *
 * This will keep a stream in a paused state, and will only read from the stream on each
 * iteration. A size can be supplied to set an explicit call to `stream.read([size])` in
 * the options for each iteration.
 */
export default class StreamToAsyncIterator {
    /** The underlying readable stream */
    _stream;
    /** Contains stream's error when stream has error'ed out */
    _error;
    /** The current state of the iterator (not readable, readable, ended, errored) */
    _state = STATES.notReadable;
    _size;
    /** The rejections of promises to call when stream errors out */
    _rejections = new Set();
    get closed() {
        return this._state === STATES.ended;
    }
    constructor(stream, { size } = {}) {
        this._stream = stream;
        this._size = size;
        const bindMethods = ['_handleStreamEnd', '_handleStreamError'];
        for (const method of bindMethods) {
            Object.defineProperty(this, method, {
                configurable: true,
                writable: true,
                value: this[method].bind(this),
            });
        }
        stream.once('error', this._handleStreamError);
        stream.once('end', this._handleStreamEnd);
    }
    [Symbol.asyncIterator]() {
        return this;
    }
    /**
       * Returns the next iteration of data. Rejects if the stream errored out.
       */
    async next() {
        switch (this._state) {
            case STATES.notReadable: {
                let untilReadable;
                let untilEnd;
                try {
                    untilReadable = this._untilReadable();
                    untilEnd = this._untilEnd();
                    await Promise.race([
                        untilReadable.promise,
                        untilEnd.promise,
                    ]);
                }
                finally {
                    // need to clean up any hanging event listeners
                    if (untilReadable != null) {
                        untilReadable.close();
                    }
                    if (untilEnd != null) {
                        untilEnd.close();
                    }
                }
                return this.next();
            }
            case STATES.ended: {
                this.close();
                return { done: true, value: undefined };
            }
            case STATES.errored: {
                this.close();
                throw this._error;
            }
            case STATES.readable: {
                // stream.read returns null if not readable or when stream has ended
                // todo: Could add a way to ensure data-type/shape of reads to make this type safe
                const data = this._size
                    ? this._stream.read(this._size)
                    : this._stream.read();
                if (data !== null) {
                    return { done: false, value: data };
                }
                else {
                    // we're no longer readable, need to find out what state we're in
                    this._state = STATES.notReadable;
                    // need to let event loop run to fill stream buffer
                    await new Promise(setImmediate);
                    return this.next();
                }
            }
        }
    }
    /**
       * Waits until the stream is readable. Rejects if the stream errored out.
       * @returns Promise when stream is readable
       */
    _untilReadable() {
        // let is used here instead of const because the exact reference is
        // required to remove it, this is why it is not a curried function that
        // accepts resolve & reject as parameters.
        let handleReadable = undefined;
        const promise = new Promise((resolve, reject) => {
            handleReadable = () => {
                this._state = STATES.readable;
                this._rejections.delete(reject);
                resolve();
            };
            this._stream.once('readable', handleReadable);
            this._rejections.add(reject);
        });
        const cleanup = () => {
            if (handleReadable != null) {
                this._stream.removeListener('readable', handleReadable);
            }
        };
        return { close: cleanup, promise };
    }
    /**
       * Waits until the stream is ended. Rejects if the stream errored out.
       * @returns Promise when stream is finished
       */
    _untilEnd() {
        let handleEnd = undefined;
        const promise = new Promise((resolve, reject) => {
            handleEnd = () => {
                this._state = STATES.ended;
                this._rejections.delete(reject);
                resolve();
            };
            this._stream.once('end', handleEnd);
            this._rejections.add(reject);
        });
        const cleanup = () => {
            if (handleEnd != null) {
                this._stream.removeListener('end', handleEnd);
            }
        };
        return { close: cleanup, promise };
    }
    return() {
        this._state = STATES.ended;
        return this.next();
    }
    throw(err) {
        this._error = err;
        this._state = STATES.errored;
        return this.next();
    }
    /**
       * Destroy the stream
       * @param err An optional error to pass to the stream for an error event
       */
    close(err) {
        this._stream.removeListener('end', this._handleStreamEnd);
        this._stream.removeListener('error', this._handleStreamError);
        this._state = STATES.ended;
        this._stream.destroy(err);
    }
    _handleStreamError(err) {
        this._error = err;
        this._state = STATES.errored;
        for (const reject of this._rejections) {
            reject(err);
        }
    }
    _handleStreamEnd() {
        this._state = STATES.ended;
    }
}
//# sourceMappingURL=stream-to-async-iterator.js.map