parallel-universe
Version:
The set of async flow control structures and promise utils.
113 lines (110 loc) • 3.91 kB
JavaScript
import { AbortablePromise } from './AbortablePromise.mjs';
/**
* Asynchronous queue decouples value producers and value consumers.
*
* @template T The value stored in the queue.
*/
class AsyncQueue {
constructor() {
/**
* The elements stored in the queue.
*/
this._elements = [];
/**
* The promise that resolves after the most recent take was acknowledged.
*/
this._promise = Promise.resolve();
}
/**
* Returns the number of values stored in this queue.
*/
get size() {
return this._elements.length;
}
/**
* Appends a new value to the end of the queue.
*
* @param value The value to append.
*/
append(value) {
this._elements.push(value);
if (this._resolveTake !== undefined) {
this._resolveTake();
}
return this;
}
/**
* Returns a promise that is fulfilled with a value when it is available.
*
* Values are taken in the same order they were appended. Taken values are removed from the queue.
*
* @returns The promise that is fulfilled with a value that was added to the queue. Aborting the returned promise
* after the value was taken is a no-op.
*/
take() {
return new AbortablePromise((resolve, reject, signal) => {
this.takeAck()
.withSignal(signal)
.then(([value, ack]) => {
ack(!signal.aborted);
resolve(value);
}, reject);
});
}
/**
* Returns a promise that is fulfilled with a value and an acknowledgement callback.
*
* The promise is fulfilled when a value is available. Consequent consumers are blocked until the acknowledgement
* callback is invoked. Invoking acknowledgement callback multiple times is a no-op.
*
* **Note:** Be sure to always call an acknowledgement callback. Otherwise, consequent consumers would never be
* fulfilled.
*
* @returns A tuple that contains a value available in the queue, and a callback that acknowledges that the value was
* processed and should be removed from the queue. Aborting the returned promise after a consumer received an
* acknowledgement callback is a no-op.
*/
takeAck() {
return new AbortablePromise((resolveTake, _rejectTake, signal) => {
this._promise = this._promise.then(() => {
if (signal.aborted) {
return;
}
return new Promise(resolveAck => {
let isAcked = false;
const ack = (isTaken) => {
if (isAcked) {
return;
}
if (isTaken) {
this._elements.shift();
}
resolveAck();
isAcked = true;
};
if (this._elements.length !== 0) {
resolveTake([this._elements[0], ack]);
return;
}
const abortTake = () => {
this._resolveTake = undefined;
resolveAck();
};
signal.addEventListener('abort', abortTake);
this._resolveTake = () => {
signal.removeEventListener('abort', abortTake);
this._resolveTake = undefined;
resolveTake([this._elements[0], ack]);
};
});
});
});
}
/**
* Iterates over elements that are available in the queue.
*/
[Symbol.iterator]() {
return this._elements[Symbol.iterator]();
}
}
export { AsyncQueue };