UNPKG

qyu

Version:

A general-purpose asynchronous job queue for Node.js

212 lines 9.41 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.QyuBase = void 0; const qyu_error_js_1 = __importDefault(require("./qyu-error.js")); const promiseWithResolvers_js_1 = require("./utils/promiseWithResolvers.js"); const omitNilProps_js_1 = require("./utils/omitNilProps.js"); class QyuBase { constructor(opts = {}) { this.jobQueue = []; this.jobChannels = []; this.isAtMaxConcurrency = false; this.isRunningJobChannels = false; this.isPaused = false; this.whenEmptyDeferred = (0, promiseWithResolvers_js_1.promiseWithResolvers)(); this.whenEmptyDeferred.resolve(); this.whenFreeDeferred = (0, promiseWithResolvers_js_1.promiseWithResolvers)(); this.whenFreeDeferred.resolve(); this.opts = Object.assign({ concurrency: 1, capacity: Infinity, rampUpTime: 0 }, (0, omitNilProps_js_1.omitNilProps)(opts)); } set(newOpts) { const oldOpts = this.opts; this.opts = Object.assign(Object.assign({}, this.opts), (0, omitNilProps_js_1.omitNilProps)(newOpts)); if (oldOpts.concurrency && newOpts.concurrency && newOpts.concurrency > oldOpts.concurrency) { this.runJobChannels(); } if (newOpts.capacity) { while (this.jobQueue.length > newOpts.capacity) { this.jobQueue .pop() .deferred.reject(new qyu_error_js_1.default('ERR_CAPACITY_FULL', "Can't queue job, queue is at max capacity")); } } } add(input) { return __awaiter(this, void 0, void 0, function* () { const normalizeInput = (input) => typeof input !== 'function' ? input : { fn: input, timeout: undefined, priority: undefined, signal: undefined, }; const result = !Array.isArray(input) ? yield this.enqueue(normalizeInput(input)) : yield Promise.all(input.map(normalizeInput).map(job => this.enqueue(job))); return result; }); } pause() { return __awaiter(this, void 0, void 0, function* () { if (this.isPaused) { return; } this.isPaused = true; if (!this.jobQueue.length && !this.jobChannels.length) { this.whenEmptyDeferred = (0, promiseWithResolvers_js_1.promiseWithResolvers)(); } // TODO: return a promise that will resolve when current jobs that were already running will finish. Perhaps: return this.whenEmpty(); yield Promise.all(this.jobChannels); }); } resume() { if (!this.isPaused) { return; } this.isPaused = false; if (!this.jobQueue.length && !this.jobChannels.length) { this.whenEmptyDeferred.resolve(); } this.runJobChannels(); } empty() { for (const job of this.jobQueue.splice(0)) { job.deferred.reject(new qyu_error_js_1.default('ERR_JOB_DEQUEUED', 'Job was dequeued out of the queue')); guardUnhandledPromiseRejections(job); } return Promise.all(this.jobChannels); } whenEmpty() { return this.whenEmptyDeferred.promise; } whenFree() { return this.whenFreeDeferred.promise; } enqueue(params) { const { fn, timeout, priority, signal } = params; const job = { fn, deferred: (0, promiseWithResolvers_js_1.promiseWithResolvers)(), timeoutId: undefined, opts: { timeout: timeout !== null && timeout !== void 0 ? timeout : 0, priority: priority !== null && priority !== void 0 ? priority : 0, }, }; if (this.jobQueue.length === this.opts.capacity) { job.deferred.reject(new qyu_error_js_1.default('ERR_CAPACITY_FULL', "Can't queue job, queue is at max capacity")); guardUnhandledPromiseRejections(job); return job.deferred.promise; } if (typeof job.opts.timeout === 'number' && job.opts.timeout > 0) { job.timeoutId = setTimeout(() => { this.dequeue(job.deferred.promise); job.deferred.reject(new qyu_error_js_1.default('ERR_JOB_TIMEOUT', 'Job cancelled due to timeout')); guardUnhandledPromiseRejections(job); // TODO: Does it *really* make sense to swallow this unhandled rejection? }, job.opts.timeout); } if (signal) { if (signal.aborted) { job.deferred.reject(signal.reason); return job.deferred.promise; } signal.addEventListener('abort', () => { this.dequeue(job.deferred.promise); job.deferred.reject(signal.reason); }); } let i = 0; while (i < this.jobQueue.length && job.opts.priority <= this.jobQueue[i].opts.priority) { ++i; } this.jobQueue.splice(i, 0, job); this.runJobChannels(); return job.deferred.promise; } // TODO: Modify this function to return something more appropriate for publich exposure rather than the internal job structure, and add it a suitable return type annotation dequeue(promise) { for (let i = 0; i < this.jobQueue.length; ++i) { if (this.jobQueue[i].deferred.promise === promise) { const splice = this.jobQueue.splice(i, 1); return splice[0]; } } return false; } runJobChannel() { return __awaiter(this, void 0, void 0, function* () { let job; while (!this.isPaused && this.jobChannels.length <= this.opts.concurrency && (job = this.jobQueue.shift())) { if (job.timeoutId) { clearTimeout(job.timeoutId); } try { const result = yield job.fn(); job.deferred.resolve(result); } catch (err) { job.deferred.reject(err); guardUnhandledPromiseRejections(job); } } }); } runJobChannels() { return __awaiter(this, void 0, void 0, function* () { if (this.isRunningJobChannels) { return; } this.isRunningJobChannels = true; while (this.jobQueue.length && !this.isPaused && this.jobChannels.length < this.opts.concurrency) { (() => __awaiter(this, void 0, void 0, function* () { // TODO: Add additional condition here: "&& !this.jobQueue.length" for when pause() is engaged while there are still jobs in the jobQueue if (!this.jobChannels.length) { this.whenEmptyDeferred = (0, promiseWithResolvers_js_1.promiseWithResolvers)(); } if (this.jobChannels.length === this.opts.concurrency - 1) { this.whenFreeDeferred = (0, promiseWithResolvers_js_1.promiseWithResolvers)(); } const promise = this.runJobChannel(); this.jobChannels.push(promise); yield promise; this.jobChannels.splice(this.jobChannels.indexOf(promise), 1); if (this.jobChannels.length === this.opts.concurrency - 1) { this.whenFreeDeferred.resolve(); } // TODO: Add additional condition here: "&& !this.jobQueue.length" for when pause() is engaged while there are still jobs in the jobQueue if (!this.jobChannels.length && !this.isPaused) { this.whenEmptyDeferred.resolve(); } }))(); if (this.opts.rampUpTime && this.jobChannels.length) { yield new Promise(resolve => setTimeout(resolve, this.opts.rampUpTime)); } } this.isRunningJobChannels = false; }); } } exports.QyuBase = QyuBase; const noop = (val) => val; // To avoid "Unhandled promise rejections": const guardUnhandledPromiseRejections = (jobObject) => { return jobObject.deferred.promise.catch(noop); }; //# sourceMappingURL=QyuBase.js.map