taskon
Version:
A simple JavaScript/TypeScript tasks queue that supports dynamic concurrency control
238 lines • 9.01 kB
JavaScript
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TaskQueueBase = void 0;
const core_1 = __importDefault(require("./core"));
const utils_1 = require("./utils");
const PROMISE_QUEUE_CAPACITY = 1;
/**
* Base task queue with abstract methods implementation and base public methods.
*/
class TaskQueueBase extends core_1.default {
constructor(_a = {}) {
var { memorizeTasks = false, stopOnError = true, defaultIncrementalTaskId = true, taskPrioritizationMode = 'head' } = _a, rest = __rest(_a, ["memorizeTasks", "stopOnError", "defaultIncrementalTaskId", "taskPrioritizationMode"]);
super(rest);
/** @internal */
this.retrying = false;
/** @internal */
this.stopped = false;
/** @internal */
this.promiseQueueCapacity = PROMISE_QUEUE_CAPACITY;
/** @internal */
this.tasksWaitingQueue = [];
/** @internal */
this.prioritizedTasksWaitingQueue = [];
/** @internal */
this.failedRetryableTaskQueue = [];
/** @internal */
this.taskLookup = {};
this.memorizeTasks = memorizeTasks;
this.stopOnError = stopOnError;
this.defaultIncrementalTaskId = defaultIncrementalTaskId;
this.taskPrioritizationMode = taskPrioritizationMode;
}
/** @internal */
_getAvailablePromiseQueue() {
var _a;
// Find any queue whose load is under capacity
return ((_a = this.promiseQueues.find((queue) => queue.length < this.promiseQueueCapacity)) !== null && _a !== void 0 ? _a : null);
}
/** @internal */
_pushTaskToWaitingQueue(task) {
if (task.priority === 'normal') {
this.tasksWaitingQueue.push(task);
this._log({
level: 'info',
taskId: task.taskId,
}, `Pushed task ${task.taskId} to waiting queue`);
}
else {
this.prioritizedTasksWaitingQueue.push(task);
this._log({
level: 'info',
taskId: task.taskId,
}, `Pushed task ${task.taskId} to prioritized waiting queue`);
}
}
/** @internal */
_getNextTask(previousTaskHasRun) {
let queue;
// If the queue instance is retrying the failed retryable tasks, then it
// should first consider the failed retryable task queue
if (this.retrying && this.failedRetryableTaskQueue.length) {
queue = this.failedRetryableTaskQueue;
}
// Otherwise (not retrying or retrying while the failed retryable task
// queue is empty), then first consider the prioritized task waiting queue
else if (this.prioritizedTasksWaitingQueue.length) {
queue = this.prioritizedTasksWaitingQueue;
}
// Otherwise (neither failed retryable task queue nor prioritized task
// waiting queue is available), then consider the normal task waiting queue
else {
queue = this.tasksWaitingQueue;
}
// If the queue instance is retrying while there is no failed retryable
// task queue, mark the queue as not retrying
if (this.retrying && !this.failedRetryableTaskQueue.length) {
this.retrying = false;
}
switch (this.taskPrioritizationMode) {
case 'head': {
return queue.shift();
}
case 'head-with-truncation': {
this.failedRetryableTaskQueue = [];
this.prioritizedTasksWaitingQueue = [];
this.tasksWaitingQueue = [];
// If the previous task has run, then clear the corresponding waiting
// queue and return null
if (previousTaskHasRun) {
return null;
}
// Otherwise, pick the first task from the waiting queue and clear the
// corresponding waiting queue
else {
return queue.shift();
}
}
case 'tail': {
return queue.pop();
}
case 'tail-with-truncation': {
this.failedRetryableTaskQueue = [];
this.prioritizedTasksWaitingQueue = [];
this.tasksWaitingQueue = [];
// If the previous task has run, then clear the corresponding waiting
// queue and return null
if (previousTaskHasRun) {
return null;
}
// Otherwise, pick the last task from the waiting queue and clear the
// corresponding waiting queue
else {
return queue.pop();
}
}
}
}
/** @internal */
_shouldStop(task) {
// If the current task has error and the queue should stop on error, then
// stop the queue and push the failed task to the failed retryable task
// queue
if ((task === null || task === void 0 ? void 0 : task.error) && this.stopOnError) {
this.failedRetryableTaskQueue.push(task);
this._log({
level: 'info',
taskId: task.taskId,
}, `Stopped queue due to the error ${task.error} from the task \
${task.taskId}`);
return true;
}
// If the task queue should stop, then stop the queue
if (this.stopped) {
this._log({
level: 'info',
taskId: task === null || task === void 0 ? void 0 : task.taskId,
}, `Stopped queue as it should stop`);
return true;
}
return false;
}
/** @internal */
_createTask(callback, taskId, onStatusUpdate, priority = 'normal') {
const finalTaskId = taskId !== null && taskId !== void 0 ? taskId : (0, utils_1.getTaskId)(this.defaultIncrementalTaskId);
const task = {
taskId: finalTaskId,
callback,
createdAt: new Date().getTime(),
status: 'idle',
onStatusUpdate,
priority,
};
if (this.memorizeTasks) {
this.taskLookup[finalTaskId] = task;
}
return task;
}
/**
* Start the queue execution.
*/
start() {
this._log({
level: 'info',
}, 'Start the queue execution');
this.stopped = false;
// Wait for appending tasks to waiting queue
Promise.resolve().then(() => {
// Enumerate through all promise queues and add tasks to the queue if the
// queue is not full
this.promiseQueues.forEach((queue) => {
if (queue.length < this.promiseQueueCapacity) {
const task = this._getNextTask(false);
if (task) {
this._addTask(task);
}
}
});
});
}
/**
* Stop the queue execution.
* Please note, the current ongoing task will not be stopped immediately.
*/
stop() {
this._log({
level: 'info',
}, 'Stop the queue execution');
this.stopped = true;
}
/**
* Retry running the queue with failed tasks.
* Please note, this method will be effective only when marking "stopOnError"
* as "true" for the queue.
*/
retry() {
this._log({
level: 'info',
}, 'Retry the queue execution');
this.retrying = true;
this.stopped = false;
// Wait for appending tasks to waiting queue
Promise.resolve().then(() => {
// Enumerate through all promise queues and add tasks to the queue if the
// queue is not full
this.promiseQueues.forEach((queue) => {
if (queue.length < this.promiseQueueCapacity) {
const task = this._getNextTask(false);
if (task) {
this._addTask(task);
}
}
});
});
}
/**
* Check if the queue is manually stopped.
*/
isManuallyStopped() {
return this.stopped;
}
}
exports.TaskQueueBase = TaskQueueBase;
exports.default = TaskQueueBase;
//# sourceMappingURL=base.js.map