UNPKG

async-channel

Version:

Send asynchronous values across concurrent lines of execution

468 lines (467 loc) 18.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IteratorChannel = exports.Channel = exports.BaseChannel = exports.UnsupportedOperationError = exports.ChannelClearedError = exports.ChannelClosedError = void 0; /** * Error used to signal that a channel has been closed. * This can be detected for graceful handling. */ class ChannelClosedError extends Error { } exports.ChannelClosedError = ChannelClosedError; /** * Error used to signal that a channel has been cleared. * This may be thrown to senders who are waiting on the channel. */ class ChannelClearedError extends Error { } exports.ChannelClearedError = ChannelClearedError; /** * Error used to indicate that an operation is not supported. * This is currently used to disallow some operations in iterator-based Channels. */ class UnsupportedOperationError extends Error { } exports.UnsupportedOperationError = UnsupportedOperationError; /** * A BaseChannel serves as a way to send asynchronous values across concurrent lines of execution. */ class BaseChannel { /** * Create a new Channel. * @param bufferCapacity The maximum number of items to buffer. * Defaults to 0; i.e. all push()/throw() calls will wait for a matching then() call. */ constructor(bufferCapacity = 0) { this.bufferCapacity = bufferCapacity; /** List of senders waiting for a receiver / buffer space */ this._senders = []; /** A list of receivers waiting for an item to be sent */ this._receivers = []; /** A list of buffered items in the channel */ this._buffer = []; /** true if the channel is closed and should no longer accept new items. */ this._closed = false; this._onClosePromise = new Promise((res) => (this._onClose = res)); } /** * Send a new value over the channel. * @param value The value to send, or a Promise resolving to a value. * @returns A Promise that resolves when the value has been successfully pushed. */ push(value) { return this._send(Promise.resolve(value)); } /** * Throw a new error in the channel. Note that errors are also buffered and subject to buffer capacity. * @param value The error to throw. * @returns A Promise that resolves when the error has been successfully thrown. */ throw(error) { return this._send(Promise.reject(error)); } /** * Close this channel. * @param clear Pass true to clear all buffered items / senders when closing the Channel. Defaults to false. */ close(clear = false) { if (this.closed) { throw new ChannelClosedError(); } this._closed = true; if (clear) { for (const sender of this._senders) { sender.reject(new ChannelClosedError()); } this._senders = []; this._buffer = []; } for (const receiver of this._receivers) { receiver.reject(new ChannelClosedError()); } this._receivers = []; this._onClose(); } /** * Clear the channel of all buffered items. * Also throws a `ChannelClearedError` to awaiting senders. * Does not close the Channel. */ clear() { for (const sender of this._senders) { sender.reject(new ChannelClearedError()); } this._senders = []; const res = this._buffer; this._buffer = []; return res; } /** * Wait for the next value (or error) on this channel. * @returns A Promise that resolves/rejects when the next value (or error) on this channel is emitted. */ get() { if (this.bufferSize > 0) { const res = this._buffer.shift(); if (this._senders.length > 0 && this.bufferSize < this.bufferCapacity) { const sender = this._senders.shift(); this._buffer.push(sender.item); sender.resolve(); } return res; } if (this._senders.length > 0) { const sender = this._senders.shift(); sender.resolve(); return sender.item; } if (this.closed) { return Promise.reject(new ChannelClosedError()); } return new Promise((resolve, reject) => { this._receivers.push({ resolve, reject }); }); } /** * Wait for the next value (or error) on this channel and process it. * Shorthand for `chan.get().then(...)`. */ then(onvalue, onerror) { return this.get().then(onvalue, onerror); } /** * The number of items currently buffered. */ get bufferSize() { return this._buffer.length; } /** * True if this channel is closed and no longer accepts new values. */ get closed() { return this._closed; } /** * A Promise that will resolve when this Channel is closed. */ get onClose() { return this._onClosePromise; } /** * Returns true if this channel is closed and contains no buffered items or waiting senders. */ get done() { return this.closed && this.bufferSize === 0 && this._senders.length === 0; } /** * Enables async iteration over the channel. * The iterator will stop and throw on the first error encountered. */ [Symbol.asyncIterator]() { return __asyncGenerator(this, arguments, function* _a() { try { while (!this.done) { yield yield __await(yield __await(this)); } } catch (e) { if (!(e instanceof ChannelClosedError)) { throw e; } } }); } /** * Throws the given error to all waiting receivers. * Useful if you want to interrupt all waiting routines immediately. */ interrupt(error) { for (const receiver of this._receivers) { receiver.reject(error); } this._receivers = []; } /** * Send the given Item. Returns a Promise that resolves when sent. */ _send(item) { item.catch(() => { // Prevent Node.js from complaining about unhandled rejections }); if (this.closed) { return Promise.reject(new ChannelClosedError()); } if (this._receivers.length > 0) { const receiver = this._receivers.shift(); receiver.resolve(item); return Promise.resolve(); } if (this.bufferSize < this.bufferCapacity) { this._buffer.push(item); return Promise.resolve(); } return new Promise((resolve, reject) => { this._senders.push({ item, resolve, reject }); }); } } exports.BaseChannel = BaseChannel; /** * A Channel extends BaseChannel and provides additional functionality. * This includes performing concurrent processing, serving iterators, limiting, etc. */ class Channel extends BaseChannel { /** * Creates a new Channel from a given source. * @param values An Array-like or iterable object containing values to be processed. */ static from(source) { if ('length' in source) { return new IteratorChannel(Array.from(source)); } return new IteratorChannel(source); } /** * Creates a new Channel for the given values. * A new Channel will be created with these values. * @param values A list of values to be processed. These may be Promises, in which case they will be flattened. */ static of(...values) { const chan = new Channel(values.length); for (const value of values) { chan.push(value); } chan.close(); return chan; } /** * Returns a new Channel that reads up to `n` items from this Channel * @param n The number of items to read from this Channel */ take(n) { return new IteratorChannel(this, n); } /** * Applies a transformation function, applying the transformation to this Channel until it is empty and * @param func The transformation function. * This function may read from the given input channel and write to the given output channel as desired. * Because this function should at minimum read from the input channel, and possibly write to the output channel, it should return a Promise in order for concurrency limits to be obeyed. * @param concurrency The number of "coroutines" to spawn to perform this operation. Must be positive and finite. Defaults to 1. * @param bufferCapacity The buffer size of the output channel. Defaults to 0. */ transform(func, concurrency, bufferCapacity) { const output = new Channel(bufferCapacity); this._consume((chan) => __awaiter(this, void 0, void 0, function* () { try { yield func(chan, output); } catch (e) { if (!(e instanceof ChannelClosedError)) output.throw(e); } }), concurrency).then(() => output.close()); return output; } /** * Applies the given 1-to-1 mapping function to this Channel and returns a new Channel with the mapped values. * @param onvalue A function that maps values from this Channel. * To map to an error, either throw or return a rejecting Promise. * May return a Promise or a plain value. If omitted, values will be propagated as-is. * @param onerror A function that maps errors from this Channel to *values*. * To map to an error, either throw or return a rejecting Promise. * May return a Promise or a plain value. If omitted, errors will be propagated as-is. * @param concurrency The number of "coroutines" to spawn to perform this operation. Must be positive and finite. Defaults to 1. * @param bufferCapacity The buffer size of the output channel. Defaults to 0. */ map(onvalue, onerror, concurrency, bufferCapacity) { return this.transform((input, output) => input .then(onvalue, onerror && ((error) => { if (error instanceof ChannelClosedError) { throw error; } return onerror(error); })) .then((value) => output.push(value), (error) => { if (!(error instanceof ChannelClosedError)) { return output.throw(error); } }), concurrency, bufferCapacity); } /** * Applies the given filter function to the values from this Channel and returns a new Channel with only the filtered values. * @param onvalue A function that takes a value from this Channel and returns a boolean of whether to include the value in the resulting Channel. * May return a Promise or a plain value. Defaults to passing all values. * @param onerror A function that takes an error from this Channel and returns a boolean of whether to include the error in the resulting Channel. * May return a Promise or a plain value. Defaults to passing all values. * @param concurrency The number of "coroutines" to spawn to perform this operation. Must be positive and finite. Defaults to 1. * @param bufferCapacity The buffer size of the output channel. Defaults to 0. */ filter(onvalue, onerror, concurrency, bufferCapacity) { return this.transform((input, output) => { return input.then((value) => __awaiter(this, void 0, void 0, function* () { if (!onvalue || (yield onvalue(value))) { yield output.push(value); } }), (err) => __awaiter(this, void 0, void 0, function* () { if (!(err instanceof ChannelClosedError) && (!onerror || (yield onerror(err)))) { yield output.throw(err); } })); }, concurrency, bufferCapacity); } /** * Consumes each value from this Channel, applying the given function on each. Errors on the Channel or in the function will cause the returned Promise to reject. * @param onvalue A function to invoke with each value from this Channel. * @param onerror A function to invoke with each error from this Channel. * @param concurrency The number of "coroutines" to spawn to perform this operation. Must be positive and finite. Defaults to 1. * @returns A Promise that resolves when all values have been consumed, or rejects when an error is received from the Channel. */ forEach(onvalue, onerror, concurrency) { // if one error is unhandled, all coroutines should stop processing. let didThrow = false; let thrownError; return this._consume((chan) => __awaiter(this, void 0, void 0, function* () { if (didThrow) { throw thrownError; } yield chan .then(onvalue, (e) => { if (e instanceof ChannelClosedError) { return; } if (!didThrow && onerror) { return onerror(e); } throw e; }) .catch((e) => { if (!didThrow) { didThrow = true; thrownError = e; chan.interrupt(e); } throw e; }); }), concurrency); } /** * Consumes the values in this Channel and inserts them into an Array. * Returns a Promise that resolves to that Array if no errors were emitted. */ toArray() { return __awaiter(this, void 0, void 0, function* () { const result = []; yield this.forEach((value) => result.push(value)); return result; }); } /** * General function for applying a consumer function with multiple "coroutines" until the Channel is done. * Also handles errors by stopping all routines. */ _consume(consumer, concurrency = 1) { if (concurrency <= 0 || !isFinite(concurrency)) { throw new RangeError('Value for concurrency must be positive and finite'); } const promises = []; for (let i = 0; i < concurrency; i++) { promises.push((() => __awaiter(this, void 0, void 0, function* () { while (!this.done) { yield consumer(this); } }))()); } return Promise.all(promises).then(); } } exports.Channel = Channel; /** * An IteratorChannel automatically emits values from an (async-)iterable source. * It uses a pull-based mechanism for fetching the values -- i.e. iteration is not started until the first get() call is made. */ class IteratorChannel extends Channel { /** * Create a new IteratorChannel. * @param source the iterable source to take elements from. * @param limit An optional maximum number of items to take from the source before closing this Channel. */ constructor(source, limit = Infinity) { super(0); this.limit = limit; this._iterating = false; if (Symbol.asyncIterator in source) { this._iterator = source[Symbol.asyncIterator](); } else { this._iterator = source[Symbol.iterator](); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars push(value) { throw new UnsupportedOperationError('Cannot push to an iterator-based Channel'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars throw(error) { throw new UnsupportedOperationError('Cannot push to an iterator-based Channel'); } clear() { throw new UnsupportedOperationError('Cannot clear an iterator-based Channel'); } get() { if (this.limit <= 0) { this.close(); } else { this.limit--; } const res = super.get(); this._iterate(); return res; } _iterate() { return __awaiter(this, void 0, void 0, function* () { if (!this.closed && this._iterator && this._receivers.length > 0 && !this._iterating) { this._iterating = true; try { const it = yield this._iterator.next(); this._iterating = false; if (it.done) { this.close(); } else { this._send(Promise.resolve(it.value)); if (this._senders.length === 0) { this._iterate(); } } } catch (e) { this._iterating = false; this._send(Promise.reject(e)); } } }); } } exports.IteratorChannel = IteratorChannel;