async-scheduler
Version:
 
284 lines (283 loc) • 11 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 });
const SchedulableTask_1 = require("./SchedulableTask");
const SchedulerError_1 = __importDefault(require("./SchedulerError"));
const SchedulerMutexStrategy_1 = require("./SchedulerMutexStrategy");
const Builder_1 = __importDefault(require("./Builder"));
var ExecutionState;
(function (ExecutionState) {
ExecutionState[ExecutionState["PENDING"] = 0] = "PENDING";
ExecutionState[ExecutionState["EXECUTING"] = 1] = "EXECUTING";
ExecutionState[ExecutionState["TERMINATED"] = 2] = "TERMINATED";
})(ExecutionState || (ExecutionState = {}));
function rejectTask(task, error) {
for (let listener of task.listeners) {
listener.reject(error);
}
}
class Scheduler {
constructor(maxConcurrentTasks, options = {}) {
this._queue = [];
this._isExecuting = false;
this._idleListeners = [];
this._maxConcurrentTasks = maxConcurrentTasks;
this._samePriorityMutex = !!options.samePriorityMutex;
this._mutexStrategy = options.mutexStrategy || SchedulerMutexStrategy_1.mutexEquality;
if (options.disableLogging) {
this._errorLog = () => { };
}
else {
this._errorLog = console.error;
}
}
enqueue(task) {
return new Promise((resolve, reject) => {
if (typeof task === 'function') {
task = {
priority: 0,
execute: task,
};
}
const mutexResult = this._checkMutexes(task, resolve, reject);
if (mutexResult.task) {
this._addTask(mutexResult.task);
}
else if (mutexResult.canceled) {
reject(this.createCanceledError());
}
});
}
prepare(task) {
return new Builder_1.default(task, this);
}
_addTask(task) {
this._queue.push(task);
this._applyPriorities();
if (!this._isExecuting) {
this._isExecuting = true;
// Queue will be executed on next tick
setTimeout(this._executeNextTasks.bind(this));
}
}
get executingTasks() {
return this._queue.reduce((count, task) => (task.state === ExecutionState.EXECUTING) ? count + 1 : count, 0);
}
createCanceledError() {
return new SchedulerError_1.default(50, "Task has been canceled in favor of another task");
}
_findFirstPendingTask() {
return this._queue.find((task) => task.state === ExecutionState.PENDING);
}
_isIdle() {
return this._queue.length === 0 || !this._queue.find((task) => task.state !== ExecutionState.TERMINATED);
}
_removeTaskAt(index) {
this._queue.splice(index, 1);
}
_executeTask(task) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (task.task.onPreExecute) {
task.task.onPreExecute();
}
return yield task.task.execute();
}
catch (error) {
throw error;
}
finally {
task.state = ExecutionState.TERMINATED;
let index = this._queue.indexOf(task);
this._removeTaskAt(index);
this._executeNextTasks();
}
});
}
_executeNextTasks() {
let executing = this.executingTasks;
if (executing >= this._maxConcurrentTasks) {
return;
}
let launchable = this._maxConcurrentTasks - executing;
for (let i = 0; i < launchable; i++) {
const task = this._findFirstPendingTask();
if (!task) {
if (this._isIdle()) {
this._switchToIdle();
}
return;
}
task.state = ExecutionState.EXECUTING;
this._executeTask(task)
.then((result) => {
for (const { resolve } of task.listeners) {
try {
resolve(result);
}
catch (e) {
this._errorLog('An error occurred while resolving listener', e);
}
}
})
.catch((error) => {
for (const { reject } of task.listeners) {
try {
reject(error);
}
catch (e) {
this._errorLog('An error occurred while rejecting listener', e);
}
}
});
}
}
_switchToIdle() {
this._isExecuting = false;
const idleListeners = this._idleListeners;
this._idleListeners = [];
for (const { resolve } of idleListeners) {
try {
resolve();
}
catch (e) { }
}
}
waitForIdle() {
if (!this._isExecuting) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
this._idleListeners.push({ resolve, reject });
});
}
_applyPriorities() {
this._queue.sort((a, b) => b.task.priority - a.task.priority);
}
_checkMutexes(newTask, resolve, reject) {
for (let i = 0; i < this._queue.length; i++) {
let taskA = this._queue[i];
if (taskA.state === ExecutionState.TERMINATED) {
// Terminated tasks will be ignored
continue;
}
if (this._samePriorityMutex && taskA.task.priority != newTask.priority) {
// Skip check if not the same priority
continue;
}
if (!taskA.task.mutex || !newTask.mutex || !this._mutexStrategy(taskA.task.mutex, newTask.mutex)) {
// If mutexes do not collide, skip check
continue;
}
let strategyA;
let strategyB;
if (taskA.task.onTaskCollision) {
strategyA = taskA.task.onTaskCollision(newTask);
if (strategyA === SchedulableTask_1.TaskCollisionStrategy.KEEP_OTHER && taskA.state !== ExecutionState.EXECUTING) {
this._removeTaskAt(i--);
rejectTask(taskA, this.createCanceledError());
continue;
}
else if (strategyA === SchedulableTask_1.TaskCollisionStrategy.KEEP_THIS) {
return { canceled: true };
}
else if (strategyA === SchedulableTask_1.TaskCollisionStrategy.RESOLVE_OTHER) {
this._removeTaskAt(i--);
return {
canceled: false,
task: {
task: newTask,
state: ExecutionState.PENDING,
listeners: [
{
resolve: resolve,
reject: reject
},
...taskA.listeners
]
}
};
}
else if (strategyA === SchedulableTask_1.TaskCollisionStrategy.RESOLVE_THIS) {
taskA.listeners = [
...taskA.listeners,
{
resolve: resolve,
reject: reject
}
];
return { canceled: false };
}
}
if (newTask.onTaskCollision) {
strategyB = newTask.onTaskCollision(taskA.task);
if (strategyB === SchedulableTask_1.TaskCollisionStrategy.KEEP_OTHER) {
return { canceled: true };
}
else if (strategyB === SchedulableTask_1.TaskCollisionStrategy.KEEP_THIS) {
this._removeTaskAt(i--);
rejectTask(taskA, this.createCanceledError());
continue;
}
else if (strategyB === SchedulableTask_1.TaskCollisionStrategy.RESOLVE_OTHER) {
taskA.listeners = [
...taskA.listeners,
{
resolve: resolve,
reject: reject
}
];
return { canceled: false };
}
else if (strategyB === SchedulableTask_1.TaskCollisionStrategy.RESOLVE_THIS) {
this._removeTaskAt(i--);
return {
canceled: false,
task: {
task: newTask,
state: ExecutionState.PENDING,
listeners: [
{
resolve: resolve,
reject: reject
},
...taskA.listeners
]
}
};
}
}
if (strategyA === SchedulableTask_1.TaskCollisionStrategy.KEEP_BOTH && strategyB === SchedulableTask_1.TaskCollisionStrategy.KEEP_BOTH) {
// Both tasks chose to ignore the collision, so both tasks will be kept
continue;
}
// Apply default action by keeping the already existing task and rejecting the new one
return { canceled: true };
}
return {
canceled: false,
task: {
task: newTask,
state: ExecutionState.PENDING,
listeners: [
{
resolve: resolve,
reject: reject
}
]
}
};
}
}
exports.default = Scheduler;