UNPKG

@shutterstock/p-map-iterable

Version:

Set of classes used for async prefetching with backpressure (IterableMapper) and async flushing with backpressure (IterableQueueMapper, IterableQueueMapperSimple)

193 lines 7.83 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BlockingQueue = void 0; const queue_1 = require("./queue"); /** * `enqueue` blocks when the queue is full, until an item is read with `dequeue`, or done * `dequeue` blocks when the queue is empty, until an item is added with `enqueue`, or done * `done` must be called when no more items will be added * `dequeue` returns `undefined` there are no more items and processing is complete * * @category Low-Level */ class BlockingQueue { /** * Create a new `BlockingQueue` * * @param options BlockingQueue options */ constructor(options = {}) { // NOTE: unreadQueue has to be a proper FIFO queue with O(1) removal from the front // as we may have O(m) unread items, where m may approach n, and Array.shift() // would be O(m) (approaching O(n)) in that case as the array gets large. this._unreadQueue = new queue_1.Queue(); // NOTE: Yes, it is possible to call next() multiple times in parallel (and we // have a test that demonstrates that), so we may have multiple readers waiting // and thus need a queue not just a single item. // The items in the queue are 'resolve' functions that we call when an item // is ready. this._readersWaiting = new queue_1.Queue(); // NOTE: Yes, it is possible to call add() multiple times in parallel (and we // have a test that demonstrates that), so we may have multiple writers waiting // and thus need a queue not just a single item. // The items in the queue are 'resolve' functions that we call when an item // is ready. this._writersWaiting = new queue_1.Queue(); this._doneAdding = false; const { maxUnread = 8 } = options; this._options = { maxUnread }; // Avoid undefined errors on options if (this._options.maxUnread === undefined) { throw new TypeError('`maxUnread` must be set'); } // Validate maxUnread option if (!((Number.isSafeInteger(this._options.maxUnread) || this._options.maxUnread === Number.POSITIVE_INFINITY) && this._options.maxUnread >= 0)) { throw new TypeError(`Expected \`maxUnread\` to be an integer from 0 and up or \`Infinity\`, got \`${maxUnread}\` (${typeof maxUnread})`); } } get length() { return this._unreadQueue.length; } /** * Indicate that no more items will be enqueued. * * This releases all readers blocked on `enqueue` */ done() { // Just mark that we're done adding this._doneAdding = true; // If there are writers waiting then then they will release the waiting readers // The reader that sees the writer count hit zero will clean up if (this._writersWaiting.length !== 0) { return; } // If there are no writers waiting then we release all the readers this.releaseAllReaders(); } /** * Add an item to the queue, wait if the queue is full. * * @param item Element to add */ async enqueue(item) { if (this._doneAdding) { throw new Error('`enqueue` called after `done` called'); } // We always add to the internal queue, we just wait to return if the queue is full this._unreadQueue.enqueue(item); if (this._readersWaiting.length > 0) { // Release a reader if one is waiting for an item // Remove the first waiting reader Promise from the queue const reader = this._readersWaiting.dequeue(); if (reader === undefined) { throw new TypeError('reader queue returned undefined'); } // Resolve the Promise for a waiting reader reader(undefined); } if (this._unreadQueue.length > this._options.maxUnread) { // We wait if the queue is at max length await this.waitForRead(); } } /** * Gets an element when one is ready, waits if none are ready. * * @returns Element or undefined if queue is empty and `done` has been called */ async dequeue() { // Bail out and release all waiters if there are no more items coming const done = this.areWeDone(); if (done) { return undefined; } // Check if queue has an item let item; // Get the item if one is ready if (this._unreadQueue.length !== 0 && this._readersWaiting.length === 0) { // If there are items and no waiters, pop one and return it item = this._unreadQueue.dequeue(); } else { // If there are no items and/or there are already waiters, we have to wait for one to be ready await this.waitForWrite(); item = this._unreadQueue.dequeue(); } if (this._writersWaiting.length > 0) { // Release a writer if one is waiting // Remove the first waiting reader Promise from the queue const writer = this._writersWaiting.dequeue(); if (writer === undefined) { throw new TypeError('writer queue returned undefined'); } // Resolve the Promise for a waiting writer writer(undefined); } return item; } releaseAllReaders() { // Release all the waiting readers since we're done let waiter; while ((waiter = this._readersWaiting.dequeue()) !== undefined) { waiter(undefined); } } areWeDone() { if (this._doneAdding) { // No more calls to `enqueue` will be made if (this._writersWaiting.length === 0) { // No `enqueue` calls are waiting to add an item if (this._unreadQueue.length === 0) { // There are no unread items left this.releaseAllReaders(); return true; } } } return false; } // /** // * Waits for an item to be ready in the queue // * @returns Item from the ready queue // */ // private async readWhenReady(): Promise<Element | undefined> { // // If there are waiters, create a waiter promise and add to end of list // // Note: the background reader removes the promise from the readers waiting queue // const itemReady = new Promise((resolve) => { // this._readersWaiting.enqueue(resolve); // }); // // Wait for our item to be ready // await itemReady; // const item = this._unreadQueue.dequeue(); // // Pull the item from the front // return item; // } /** * Waits for an item to be ready in the queue */ async waitForWrite() { // If there are waiters, create a waiter promise and add to end of list // Note: the background reader removes the promise from the readers waiting queue const itemReady = new Promise((resolve) => { this._readersWaiting.enqueue(resolve); }); // Wait for our item to be ready await itemReady; } /** * Waits to be able to add an item to the queue when full */ async waitForRead() { // If there are waiters, create a waiter promise and add to end of list // Note: the background reader removes the promise from the readers waiting queue const writeReady = new Promise((resolve) => { this._writersWaiting.enqueue(resolve); }); // Wait for our turn to add to the queue await writeReady; } } exports.BlockingQueue = BlockingQueue; //# sourceMappingURL=blocking-queue.js.map