stream-to-async-iterator
Version:
ES async interator wrapper for node streams
245 lines (194 loc) • 5.96 kB
JavaScript
"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