async-channel
Version:
Send asynchronous values across concurrent lines of execution
468 lines (467 loc) • 18.9 kB
JavaScript
"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;