UNPKG

p-queue

Version:

Promise queue with concurrency control

95 lines (94 loc) 3.59 kB
import lowerBound from './lower-bound.js'; const compactionThreshold = 100; export default class PriorityQueue { #queue = []; // The queue is stored as a sorted array, but dequeued items are left before `#head` until compaction. Only items from `#head` onward are live, which keeps repeated dequeues amortized O(1). #head = 0; enqueue(run, options) { const { priority = 0, id, } = options ?? {}; const { size } = this; const element = { priority, id, run, }; if (size === 0) { // When the queue is logically empty, discard any consumed prefix before accepting new work. this.#queue.length = 0; this.#head = 0; this.#queue.push(element); return; } if (this.#queue.at(-1).priority >= priority) { // Same-priority and lower-priority items belong after the current tail. Appending preserves FIFO order for equal priorities. this.#queue.push(element); return; } // Binary insertion must run on the live sorted range only. this.#compact(); const index = lowerBound(this.#queue, element, (a, b) => b.priority - a.priority); this.#queue.splice(index, 0, element); } setPriority(id, priority) { // A dequeued item with the same id is no longer part of the queue. const index = this.#queue.findIndex((element, index) => index >= this.#head && element.id === id); if (index === -1) { throw new ReferenceError(`No promise function with the id "${id}" exists in the queue.`); } const [item] = this.#queue.splice(index, 1); this.enqueue(item.run, { priority, id }); } remove(idOrRun) { const index = this.#queue.findIndex((element, index) => { // The consumed prefix may still contain references that should not be removable. if (index < this.#head) { return false; } if (typeof idOrRun === 'string') { return element.id === idOrRun; } return element.run === idOrRun; }); if (index !== -1) { this.#queue.splice(index, 1); } } dequeue() { if (this.#head === this.#queue.length) { return undefined; } const item = this.#queue[this.#head]; this.#head++; if (this.#head === this.#queue.length) { // Fully drained queues are reset immediately so the next enqueue starts from a clean array. this.#queue.length = 0; this.#head = 0; } else if (this.#head > compactionThreshold && this.#head > this.#queue.length / 2) { // Keep repeated dequeues cheap, but stop the consumed prefix from growing without bound. this.#compact(); } return item?.run; } filter(options) { const result = []; for (let index = this.#head; index < this.#queue.length; index++) { const element = this.#queue[index]; if (element.priority === options.priority) { result.push(element.run); } } return result; } get size() { return this.#queue.length - this.#head; } #compact() { // Compaction restores the invariant that the whole array is the live sorted range. if (this.#head === 0) { return; } this.#queue.splice(0, this.#head); this.#head = 0; } }