UNPKG

async-scheduler

Version:

![CI](https://github.com/kremi151/async-scheduler/workflows/CI/badge.svg) ![NPM](https://img.shields.io/npm/v/async-scheduler?color=green)

284 lines (283 loc) 11 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 }); 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;