UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

103 lines (102 loc) 3.51 kB
import { ErrorMode } from '../error/errorMode.js'; import { pDefer } from './pDefer.js'; /** * Inspired by: https://github.com/sindresorhus/p-queue * * Allows to push "jobs" to the queue and control its concurrency. * Jobs are "promise-returning functions". * * API is @experimental */ export class PQueue { constructor(cfg) { this.cfg = { // concurrency: Number.MAX_SAFE_INTEGER, errorMode: ErrorMode.THROW_IMMEDIATELY, logger: console, debug: false, resolveOn: 'finish', ...cfg, }; if (!cfg.debug) { this.debug = () => { }; } } cfg; debug(...args) { this.cfg.logger.log(...args); } inFlight = 0; queue = []; onIdleListeners = []; get queueSize() { return this.queue.length; } /** * Returns a Promise that resolves when the queue is Idle (next time, since the call). * Resolves immediately in case the queue is Idle. * Idle means 0 queue and 0 inFlight. */ async onIdle() { if (this.queue.length === 0 && this.inFlight === 0) return; const listener = pDefer(); this.onIdleListeners.push(listener); return await listener; } /** * Push PromiseReturningFunction to the Queue. * Returns a Promise that resolves (or rejects) with the return value from the Promise. */ async push(fn_) { const { concurrency } = this.cfg; const resolveOnStart = this.cfg.resolveOn === 'start'; const fn = fn_; fn.defer ||= pDefer(); if (this.inFlight < concurrency) { // There is room for more jobs. Can start immediately this.inFlight++; this.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`); if (resolveOnStart) fn.defer.resolve(); fn() .then(result => { if (!resolveOnStart) fn.defer.resolve(result); }) .catch((err) => { this.cfg.logger.error(err); if (resolveOnStart) return; if (this.cfg.errorMode === ErrorMode.SUPPRESS) { fn.defer.resolve(); // resolve with `void` } else { // Should be handled on the outside, otherwise it'll cause UnhandledRejection fn.defer.reject(err); } }) .finally(() => { this.inFlight--; this.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`); // check if there's room to start next job if (this.queue.length && this.inFlight <= concurrency) { const nextFn = this.queue.shift(); void this.push(nextFn); } else { if (this.inFlight === 0) { this.debug('onIdle'); this.onIdleListeners.forEach(defer => defer.resolve()); this.onIdleListeners.length = 0; // empty the array } } }); } else { this.queue.push(fn); this.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`); } return await fn.defer; } }