UNPKG

rx-player

Version:
329 lines (328 loc) 13.4 kB
import log from "../../../log"; import arrayFindIndex from "../../../utils/array_find_index"; import createCancellablePromise from "../../../utils/create_cancellable_promise"; import TaskCanceller, { CancellationError } from "../../../utils/task_canceller"; /** * Utilitary class which allows to perform multiple tasks at once each with an * associated priority. * * This class will then schedule the given tasks in the right order based on the * priorities. * * @class TaskPrioritizer */ export default class TaskPrioritizer { /** * @param {Options} prioritizerOptions */ constructor({ prioritySteps }) { this._minPendingPriority = null; this._waitingQueue = []; this._pendingTasks = []; this._prioritySteps = prioritySteps; if (this._prioritySteps.high >= this._prioritySteps.low) { throw new Error("TP: the max high level priority should be given a lower" + "priority number than the min low priority."); } } /** * Create a priorized Promise from a base task. * * This task will immediately have its priority compared to all the * already-running ones created from this class. * * Only if this number is inferior or equal to the priority of the * minimum priority number of all currently-running tasks will it be * immediately started. * In the opposite case, we will wait for higher-priority tasks to * finish before starting it. * * Note that while this task is waiting for its turn, it is possible * to update its property through the updatePriority method, by providing * the task again and its new priority number. * * @param {Function} taskFn * @param {number} priority * @param {Object} callbacks * @param {Object} cancelSignal * @returns {Promise} */ create(taskFn, priority, callbacks, cancelSignal) { let newTask; return createCancellablePromise(cancelSignal, (resolve, reject) => { /** Function allowing to start the underlying Promise. */ const trigger = () => { if (newTask.hasEnded) { return; } const finishTask = () => { unlinkInterrupter(); this._endTask(newTask); }; const onResolve = (value) => { callbacks.beforeEnded(); finishTask(); resolve(value); }; const onReject = (err) => { finishTask(); reject(err); }; const interrupter = new TaskCanceller(); const unlinkInterrupter = interrupter.linkToSignal(cancelSignal); newTask.interrupter = interrupter; interrupter.signal.register(() => { newTask.interrupter = null; if (!cancelSignal.isCancelled()) { callbacks.beforeInterrupted(); } }); this._minPendingPriority = this._minPendingPriority === null ? newTask.priority : Math.min(this._minPendingPriority, newTask.priority); this._pendingTasks.push(newTask); newTask .taskFn(interrupter.signal) .then(onResolve) .catch((err) => { if (!cancelSignal.isCancelled() && interrupter.isUsed() && err instanceof CancellationError) { return; } onReject(err); }); }; newTask = { hasEnded: false, priority, trigger, taskFn, interrupter: null, }; if (!this._canBeStartedNow(newTask)) { this._waitingQueue.push(newTask); } else { // We can start the task right away newTask.trigger(); if (this._isRunningHighPriorityTasks()) { // Note: we want to begin interrupting low-priority tasks just // after starting the current one because the interrupting // logic can call external code. // This would mean re-entrancy, itself meaning that some weird // half-state could be reached unless we're very careful. // To be sure no harm is done, we put that code at the last // possible position. this._interruptCancellableTasks(); } } return () => this._endTask(newTask); }); } _endTask(task) { task.hasEnded = true; const waitingQueueIndex = _findTaskIndex(task.taskFn, this._waitingQueue); if (waitingQueueIndex >= 0) { // If it was still waiting for its turn this._waitingQueue.splice(waitingQueueIndex, 1); } else { // remove it from pending queue if in it const pendingTasksIndex = _findTaskIndex(task.taskFn, this._pendingTasks); if (pendingTasksIndex < 0) { return; } this._pendingTasks.splice(pendingTasksIndex, 1); if (this._pendingTasks.length > 0) { if (this._minPendingPriority === task.priority) { this._minPendingPriority = Math.min(...this._pendingTasks.map((t) => t.priority)); } } else { this._minPendingPriority = null; } this._loopThroughWaitingQueue(); } } /** * Update the priority of a promise given to the TaskPrioritizer. * @param {Object} promise * @param {number} priority */ updatePriority(promise, priority) { const waitingQueueIndex = _findTaskIndex(promise, this._waitingQueue); if (waitingQueueIndex >= 0) { // If it was still waiting for its turn const waitingQueueElt = this._waitingQueue[waitingQueueIndex]; if (waitingQueueElt.priority === priority) { return; } waitingQueueElt.priority = priority; if (!this._canBeStartedNow(waitingQueueElt)) { return; } this._findAndRunWaitingQueueTask(waitingQueueIndex); if (this._isRunningHighPriorityTasks()) { // Re-check to cancel every "cancellable" pending task // // Note: We start the task before interrupting cancellable tasks on // purpose. // Because both `_findAndRunWaitingQueueTask` and // `_interruptCancellableTasks` can emit events and thus call external // code, we could retrieve ourselves in a very weird state at this point // // By starting the task first, we ensure that this is manageable: // `_getMinPendingPriority()` has already been updated to the right value at // the time we reached external code, the priority of the current // Task has just been updated, and `_interruptCancellableTasks` // will ensure that we're basing ourselves on the last `priority` value // each time. // Doing it in the reverse order is an order of magnitude more difficult // to write and to reason about. this._interruptCancellableTasks(); } return; } const pendingTasksIndex = _findTaskIndex(promise, this._pendingTasks); if (pendingTasksIndex < 0) { log.warn("TP: request to update the priority of a non-existent task"); return; } const task = this._pendingTasks[pendingTasksIndex]; if (task.priority === priority) { return; } const prevPriority = task.priority; task.priority = priority; if (this._minPendingPriority === null || priority < this._minPendingPriority) { this._minPendingPriority = priority; } else if (this._minPendingPriority === prevPriority) { // was highest priority if (this._pendingTasks.length === 1) { this._minPendingPriority = priority; } else { this._minPendingPriority = Math.min(...this._pendingTasks.map((t) => t.priority)); } this._loopThroughWaitingQueue(); } if (this._isRunningHighPriorityTasks()) { // Always interrupt cancellable tasks after all other side-effects, to // avoid re-entrancy issues this._interruptCancellableTasks(); } } /** * Browse the current waiting queue and start all task in it that needs to be * started: start the ones with the lowest priority value below * `_minPendingPriority`. * * Private properties, such as `_minPendingPriority` are updated accordingly * while this method is called. */ _loopThroughWaitingQueue() { const minWaitingPriority = this._waitingQueue.reduce((acc, elt) => { return acc === null || acc > elt.priority ? elt.priority : acc; }, null); if (minWaitingPriority === null || (this._minPendingPriority !== null && this._minPendingPriority < minWaitingPriority)) { return; } for (let i = 0; i < this._waitingQueue.length; i++) { const priorityToCheck = this._minPendingPriority === null ? minWaitingPriority : Math.min(this._minPendingPriority, minWaitingPriority); const elt = this._waitingQueue[i]; if (elt.priority <= priorityToCheck) { this._findAndRunWaitingQueueTask(i); i--; // previous operation should have removed that element from the // the waiting queue } } } /** * Interrupt and move back to the waiting queue all pending tasks that are * low priority (having a higher priority number than * `this._prioritySteps.low`). */ _interruptCancellableTasks() { for (const pendingObj of this._pendingTasks) { if (pendingObj.priority >= this._prioritySteps.low) { this._interruptPendingTask(pendingObj); // The previous call could have a lot of potential side-effects. // It is safer to re-start the function to not miss any pending // task that needs to be cancelled. return this._interruptCancellableTasks(); } } } /** * Start task which is at the given index in the waiting queue. * The task will be removed from the waiting queue in the process. * @param {number} index */ _findAndRunWaitingQueueTask(index) { if (index >= this._waitingQueue.length || index < 0) { log.warn("TP : Tried to start a non existing task"); return false; } const task = this._waitingQueue.splice(index, 1)[0]; task.trigger(); return true; } /** * Move back pending task to the waiting queue and interrupt it. * @param {object} task */ _interruptPendingTask(task) { var _a; const pendingTasksIndex = _findTaskIndex(task.taskFn, this._pendingTasks); if (pendingTasksIndex < 0) { log.warn("TP: Interrupting a non-existent pending task. Aborting..."); return; } // Stop task and put it back in the waiting queue this._pendingTasks.splice(pendingTasksIndex, 1); this._waitingQueue.push(task); if (this._pendingTasks.length === 0) { this._minPendingPriority = null; } else if (this._minPendingPriority === task.priority) { this._minPendingPriority = Math.min(...this._pendingTasks.map((t) => t.priority)); } (_a = task.interrupter) === null || _a === void 0 ? void 0 : _a.cancel(); // Interrupt at last step because it calls external code } /** * Return `true` if the given task can be started immediately based on its * priority. * @param {Object} task * @returns {boolean} */ _canBeStartedNow(task) { return this._minPendingPriority === null || task.priority <= this._minPendingPriority; } /** * Returns `true` if any running task is considered "high priority". * returns `false` otherwise. * @returns {boolean} */ _isRunningHighPriorityTasks() { return (this._minPendingPriority !== null && this._minPendingPriority <= this._prioritySteps.high); } } /** * Simple utils function allowing to find a given task function in the given * `queue`. * * Returns `-1` if `taskFn` is not found. * @param {Function} taskFn * @param {Array.<Object>} queue * @returns {number} */ function _findTaskIndex(taskFn, queue) { return arrayFindIndex(queue, (elt) => elt.taskFn === taskFn); }