UNPKG

@naturalcycles/js-lib

Version:

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

113 lines (112 loc) 3.97 kB
import { ErrorMode } from '../error/errorMode.js'; import { createCommonLoggerAtLevel, } from '../log/commonLogger.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 = { errorMode: ErrorMode.THROW_IMMEDIATELY, ...cfg, }; this.logger = createCommonLoggerAtLevel(cfg.logger, cfg.logLevel); this.resolveOnStart = this.cfg.resolveOn === 'start'; } cfg; resolveOnStart; logger; inFlight = 0; queue = []; onIdleListeners = []; /** * 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, logger } = this; const fn = fn_; fn.defer ||= pDefer(); if (this.inFlight < concurrency) { // There is room for more jobs. Can start immediately this.inFlight++; logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`); if (resolveOnStart) fn.defer.resolve(); runSafe(fn) .then(result => { if (!resolveOnStart) fn.defer.resolve(result); }) .catch((err) => { if (resolveOnStart) { logger.error(err); return; } if (this.cfg.errorMode === ErrorMode.SUPPRESS) { logger.error(err); fn.defer.resolve(); // resolve with `void` } else { // Should be handled on the outside, otherwise it'll cause UnhandledRejection // Not logging, because it's re-thrown upstream fn.defer.reject(err); } }) .finally(() => { this.inFlight--; logger.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) { logger.debug('onIdle'); this.onIdleListeners.forEach(defer => defer.resolve()); this.onIdleListeners.length = 0; // empty the array } } }); } else { this.queue.push(fn); logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`); } return await fn.defer; } 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; } } // Here we intentionally want it not async, as we don't want it to throw // oxlint-disable-next-line typescript/promise-function-async function runSafe(fn) { try { // Here we are intentionally not awaiting return fn(); } catch (err) { // Handle synchronous throws - ensure inFlight is decremented return Promise.reject(err); } }