qyu
Version:
A general-purpose asynchronous job queue for Node.js
212 lines • 9.41 kB
JavaScript
"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