@buttercup/channel-queue
Version:
A queue management library with channels
269 lines (268 loc) • 8.09 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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Channel = void 0;
const eventemitter3_1 = require("eventemitter3");
const layerr_1 = require("layerr");
const Task_1 = require("./Task");
const types_1 = require("./types");
/**
* Compare two tasks for sorting
* @param {Task} taskA A task
* @param {Task} taskB Another task
* @returns {Number} The sorting operation regarding the provided tasks
* @private
* @static
* @memberof Channel
*/
function compareTasks(taskA, taskB) {
const { type: typeA, created: createdA } = taskA;
const { type: typeB, created: createdB } = taskB;
// Sort by priority:
if (typeA === types_1.TaskPriority.High && typeB !== types_1.TaskPriority.High) {
// A is high priority, and B isn't
return -1;
}
else if (typeB === types_1.TaskPriority.High && typeA !== types_1.TaskPriority.High) {
// B is high priority, and A isn't
return 1;
}
else if (typeB === types_1.TaskPriority.Tail && typeA !== types_1.TaskPriority.Tail) {
// B is a tail-task, and A isn't
return -1;
}
else if (typeA === types_1.TaskPriority.Tail && typeB !== types_1.TaskPriority.Tail) {
// A is a tail-task, and B isn't
return 1;
}
// Sort by created time:
if (createdA < createdB) {
// A is older
return -1;
}
else if (createdB < createdA) {
// B is older
return 1;
}
// Equal priority
return 0;
}
/**
* Channel class (queue)
* @augments EventEmitter
*/
class Channel extends eventemitter3_1.EventEmitter {
/**
* Constructor for a Channel
* @param {String} name The name of the channel
* @memberof Channel
*/
constructor(name) {
super();
this._taskErrors = [];
this._tasksThrow = true;
if (typeof name !== "string" || name.length <= 0) {
throw new Error("Failed creating Channel: Invalid or empty name");
}
this._name = name;
this._tasks = [];
this._running = false;
this._autostart = true;
}
/**
* Whether the execution should start automatically or not
* Defaults to true
* @type {Boolean}
* @memberof Channel
*/
get autostart() {
return this._autostart;
}
/**
* Whether the queue is empty or not
* @type {Boolean}
* @readonly
* @memberof Channel
*/
get isEmpty() {
return !this.isRunning && this.tasks.length === 0;
}
/**
* Whether the queue is currently running or not
* @type {Boolean}
* @readonly
* @memberof Channel
*/
get isRunning() {
return this._running;
}
/**
* The name of the channel
* @type {String}
* @readonly
* @memberof Channel
*/
get name() {
return this._name;
}
/**
* Array of tasks (in queue)
* @type {Array.<Task>}
* @readonly
* @memberof Channel
*/
get tasks() {
return this._tasks;
}
/**
* Whether or not tasks throw errors
* @type {Boolean}
* @memberof Channel
*/
get tasksThrow() {
return this._tasksThrow;
}
set autostart(auto) {
this._autostart = !!auto;
}
set isRunning(isRunning) {
this._running = isRunning;
}
set tasksThrow(tasksThrow) {
this._tasksThrow = tasksThrow;
}
/**
* Remove all pending tasks from the channel
* @param {String=} priorityType Optional priority type to clear
* only tasks with a certain priority value
* @memberof Channel
*/
clear(priorityType) {
if (!priorityType) {
this.tasks.splice(0, Infinity);
return;
}
for (let i = this.tasks.length - 1; i >= 0; i -= 1) {
if (this.tasks[i].type === priorityType) {
this.tasks.splice(i, 1);
}
}
}
/**
* Enqueues a function
* @param {Function|Promise} item The item to place into the queue
* @param {TaskPriority=} type The task priority to use
* @param {String=} stack The stack name
* @param {Number=} timeout Optional millisecond time-limt
* @returns {Promise} A promise that eventually resolves with the result from the
* enqueued function or promise
* @memberof Channel
*/
enqueue(item, type = types_1.TaskPriority.Normal, stack, timeout) {
if (stack) {
const stackItems = this.getStackedItems(stack);
if (stackItems.length > 0) {
return stackItems[stackItems.length - 1].queuedPromise;
}
}
const task = new Task_1.Task(item, type, stack);
if (typeof timeout === "number" && timeout >= 0) {
task.timeLimit = timeout;
}
this.tasks.push(task);
this.sort();
if (this.autostart) {
this.start();
}
return task.queuedPromise;
}
/**
* Get all task items for a stack name
* @param {String} stack The stack name
* @returns {Array.<Task>} An array of task instances
* @memberof Channel
*/
getStackedItems(stack) {
return this.tasks.filter(task => task.stack && task.stack === stack);
}
/**
* Get the next queued Task instance
* This modifies the task queue by removing the task
* @returns {Task|undefined} A task instance if there are any in queue
* @memberof Channel
*/
retrieveNextItem() {
return this.tasks.shift();
}
/**
* Sort the tasks
* @memberof Channel
*/
sort() {
this.tasks.sort(compareTasks);
}
/**
* Start processing the queue
* Will automatically return early if queue has already started
* @fires Channel#started
* @fires Channel#stopped
* @returns {Boolean} Returns true if started, false if already started
* @memberof Channel
*/
start() {
if (this.isRunning) {
return false;
}
this.emit("started");
this.isRunning = true;
setTimeout(() => this._runNextItem(), 0);
return true;
}
/**
* Wait for the queue to become empty
* @returns {Promise}
*/
waitForEmpty(opts = {}) {
return __awaiter(this, void 0, void 0, function* () {
const { throwForFailures = false } = opts;
yield new Promise(resolve => {
if (this.isEmpty)
return resolve();
this.once("stopped", () => {
resolve();
});
});
if (throwForFailures && this._taskErrors.length > 0) {
throw new layerr_1.Layerr(this._taskErrors[0], "Enqueued task failed");
}
});
}
_runNextItem() {
const item = this.retrieveNextItem();
if (!item) {
this.isRunning = false;
this.emit("stopped");
}
else {
item.execute(this._tasksThrow)
.then(() => {
if (item.error) {
this._taskErrors.push(item.error);
}
})
.then(() => this._runNextItem())
.catch(err => {
console.error(err);
});
}
}
}
exports.Channel = Channel;