UNPKG

stream-to-async-iterator

Version:
245 lines (194 loc) 5.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; 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. */ class StreamToAsyncIterator { /** The underlying readable stream */ /** Contains stream's error when stream has error'ed out */ /** The current state of the iterator (not readable, readable, ended, errored) */ _state = STATES.notReadable; /** 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) }); } // eslint-disable-next-line @typescript-eslint/unbound-method stream.once("error", this._handleStreamError); // eslint-disable-next-line @typescript-eslint/unbound-method 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 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 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) { // eslint-disable-next-line @typescript-eslint/unbound-method this._stream.removeListener("end", this._handleStreamEnd); // eslint-disable-next-line @typescript-eslint/unbound-method 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; } } exports.default = StreamToAsyncIterator; //# sourceMappingURL=stream-to-async-iterator.js.map