UNPKG

rx-player

Version:
382 lines (381 loc) 16.1 kB
"use strict"; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; Object.defineProperty(exports, "__esModule", { value: true }); var log_1 = require("../../../log"); var array_find_index_1 = require("../../../utils/array_find_index"); var create_cancellable_promise_1 = require("../../../utils/create_cancellable_promise"); var task_canceller_1 = require("../../../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 */ var TaskPrioritizer = /** @class */ (function () { /** * @param {Options} prioritizerOptions */ function TaskPrioritizer(_a) { var prioritySteps = _a.prioritySteps; this._minPendingPriority = null; this._waitingQueue = []; this._pendingTasks = []; this._prioritySteps = prioritySteps; if (this._prioritySteps.high >= this._prioritySteps.low) { throw new Error("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} */ TaskPrioritizer.prototype.create = function (taskFn, priority, callbacks, cancelSignal) { var _this = this; var newTask; return (0, create_cancellable_promise_1.default)(cancelSignal, function (resolve, reject) { /** Function allowing to start the underlying Promise. */ var trigger = function () { if (newTask.hasEnded) { return; } var finishTask = function () { unlinkInterrupter(); _this._endTask(newTask); }; var onResolve = function (value) { callbacks.beforeEnded(); finishTask(); resolve(value); }; var onReject = function (err) { finishTask(); reject(err); }; var interrupter = new task_canceller_1.default(); var unlinkInterrupter = interrupter.linkToSignal(cancelSignal); newTask.interrupter = interrupter; interrupter.signal.register(function () { 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(function (err) { if (!cancelSignal.isCancelled() && interrupter.isUsed() && err instanceof task_canceller_1.CancellationError) { return; } onReject(err); }); }; newTask = { hasEnded: false, priority: priority, trigger: trigger, taskFn: 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 function () { return _this._endTask(newTask); }; }); }; TaskPrioritizer.prototype._endTask = function (task) { task.hasEnded = true; var 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 var 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.apply(Math, __spreadArray([], __read(this._pendingTasks.map(function (t) { return t.priority; })), false)); } } else { this._minPendingPriority = null; } this._loopThroughWaitingQueue(); } }; /** * Update the priority of a promise given to the TaskPrioritizer. * @param {Object} promise * @param {number} priority */ TaskPrioritizer.prototype.updatePriority = function (promise, priority) { var waitingQueueIndex = _findTaskIndex(promise, this._waitingQueue); if (waitingQueueIndex >= 0) { // If it was still waiting for its turn var 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; } var pendingTasksIndex = _findTaskIndex(promise, this._pendingTasks); if (pendingTasksIndex < 0) { log_1.default.warn("SF", "request to update the priority of a non-existent task"); return; } var task = this._pendingTasks[pendingTasksIndex]; if (task.priority === priority) { return; } var 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.apply(Math, __spreadArray([], __read(this._pendingTasks.map(function (t) { return t.priority; })), false)); } 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. */ TaskPrioritizer.prototype._loopThroughWaitingQueue = function () { var minWaitingPriority = this._waitingQueue.reduce(function (acc, elt) { return acc === null || acc > elt.priority ? elt.priority : acc; }, null); if (minWaitingPriority === null || (this._minPendingPriority !== null && this._minPendingPriority < minWaitingPriority)) { return; } for (var i = 0; i < this._waitingQueue.length; i++) { var priorityToCheck = this._minPendingPriority === null ? minWaitingPriority : Math.min(this._minPendingPriority, minWaitingPriority); var 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`). */ TaskPrioritizer.prototype._interruptCancellableTasks = function () { var e_1, _a; try { for (var _b = __values(this._pendingTasks), _c = _b.next(); !_c.done; _c = _b.next()) { var pendingObj = _c.value; 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(); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } }; /** * 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 */ TaskPrioritizer.prototype._findAndRunWaitingQueueTask = function (index) { if (index >= this._waitingQueue.length || index < 0) { log_1.default.warn("SF", "Tried to start a non existing task"); return false; } var 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 */ TaskPrioritizer.prototype._interruptPendingTask = function (task) { var _a; var pendingTasksIndex = _findTaskIndex(task.taskFn, this._pendingTasks); if (pendingTasksIndex < 0) { log_1.default.warn("SF", "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.apply(Math, __spreadArray([], __read(this._pendingTasks.map(function (t) { return t.priority; })), false)); } (_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} */ TaskPrioritizer.prototype._canBeStartedNow = function (task) { return this._minPendingPriority === null || task.priority <= this._minPendingPriority; }; /** * Returns `true` if any running task is considered "high priority". * returns `false` otherwise. * @returns {boolean} */ TaskPrioritizer.prototype._isRunningHighPriorityTasks = function () { return (this._minPendingPriority !== null && this._minPendingPriority <= this._prioritySteps.high); }; return TaskPrioritizer; }()); exports.default = TaskPrioritizer; /** * 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 (0, array_find_index_1.default)(queue, function (elt) { return elt.taskFn === taskFn; }); }