/** * Scheduler. * * @author Alain Pitiot * @version 3.0.8 * @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com}) * @license Distributed under the terms of the MIT License */ /** *
A scheduler helps run the main loop by managing scheduled functions, * called tasks, after each frame is displayed.
* ** Tasks are either another [Scheduler]{@link module:util.Scheduler}, or a * JavaScript functions returning one of the following codes: *
It is possible to create sub-schedulers, e.g. to handle loops. * Sub-schedulers are added to a parent scheduler as a normal * task would be by calling [scheduler.add(subScheduler)]{@link module:util.Scheduler#add}.
* *Conditional branching is also available: * [scheduler.addConditionalBranches]{@link module:util.Scheduler#addConditional}
* * * @name module:util.Scheduler * @class * @param {PsychoJS} psychoJS - the PsychoJS instance * */ export class Scheduler { constructor(psychoJS) { this._psychoJS = psychoJS; this._taskList = []; this._currentTask = undefined; this._argsList = []; this._currentArgs = undefined; this._stopAtNextUpdate = false; this._stopAtNextTask = false; this._status = Scheduler.Status.STOPPED; } /** * Get the status of the scheduler. * * @name module:util.Scheduler#status * @public * @returns {module:util.Scheduler#Status} the status of the scheduler */ get status() { return this._status; } /** * Task to be run by the scheduler. * * @callback module:util.Scheduler~Task * @param {*} [args] optional arguments */ /** * Schedule a new task. * * @name module:util.Scheduler#add * @public * @param {module:util.Scheduler~Task | module:util.Scheduler} task - the task to be scheduled * @param {...*} args - arguments for that task */ add(task, ...args) { this._taskList.push(task); this._argsList.push(args); } /** * Condition evaluated when the task is run. * * @callback module:util.Scheduler~Condition * @return {boolean} */ /** * Schedule a series of task or another, based on a condition. * *Note: the tasks are [sub-schedulers]{@link module:util.Scheduler}.
* * @name module:util.Scheduler#addConditional * @public * @param {module:util.Scheduler~Condition} condition - the condition * @param {module:util.Scheduler} thenScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is satisfied * @param {module:util.Scheduler} elseScheduler - the [Scheduler]{@link module:util.Scheduler} to be run if the condition is not satisfied */ addConditional(condition, thenScheduler, elseScheduler) { const self = this; let task = function () { if (condition()) self.add(thenScheduler); else self.add(elseScheduler); return Scheduler.Event.NEXT; }; this.add(task); } /** * Start this scheduler. * *Note: tasks are run after each animation frame.
* * @name module:util.Scheduler#start * @public */ start() { const self = this; let update = () => { // stop the animation if need be: if (self._stopAtNextUpdate) { self._status = Scheduler.Status.STOPPED; return; } // self._psychoJS.window._writeLogOnFlip(); // run the next scheduled tasks until a scene render is requested: const state = self._runNextTasks(); if (state === Scheduler.Event.QUIT) { self._status = Scheduler.Status.STOPPED; return; } // render the scene in the window: self._psychoJS.window.render(); // request a new frame: requestAnimationFrame(update); }; // start the animation: requestAnimationFrame(update); } /** * Stop this scheduler. * * @name module:util.Scheduler#stop * @public */ stop() { this._status = Scheduler.Status.STOPPED; this._stopAtNextTask = true; this._stopAtNextUpdate = true; } /** * Run the next scheduled tasks, in sequence, until a rendering of the scene is requested. * * @name module:util.Scheduler#_runNextTasks * @private * @return {module:util.Scheduler#Event} the state of the scheduler after the last task ran */ _runNextTasks() { this._status = Scheduler.Status.RUNNING; let state = Scheduler.Event.NEXT; while (state === Scheduler.Event.NEXT) { // check if we need to quit: if (this._stopAtNextTask) return Scheduler.Event.QUIT; // if there is no current task, we look for the next one in the list or quit if there is none: if (typeof this._currentTask == 'undefined') { // a task is available in the taskList: if (this._taskList.length > 0) { this._currentTask = this._taskList.shift(); this._currentArgs = this._argsList.shift(); } // the taskList is empty: we quit else { this._currentTask = undefined; this._currentArgs = undefined; return Scheduler.Event.QUIT; } } else { // we are repeating a task } // if the current task is a scheduler, we run its tasks until a rendering of the scene is required: if (this._currentTask instanceof Scheduler) { state = this._currentTask._runNextTasks(); if (state === Scheduler.Event.QUIT) { // if the experiment has not ended, we move onto the next task: if (!this._psychoJS.experiment.experimentEnded) state = Scheduler.Event.NEXT; } } // if the current task is a function, we run it: else if (this._currentTask instanceof Function) { state = this._currentTask(...this._currentArgs); } // we should not be here... else { console.log(this._currentTask); throw { origin: 'Scheduler._runNextTasks', context: 'when running the scheduler\'s tasks', error: 'the next' + ' task has unknown type (neither a Scheduler nor a Function)' }; } // if the current task's return status is FLIP_REPEAT, we will re-run it, otherwise // we move onto the next task: if (state !== Scheduler.Event.FLIP_REPEAT) { this._currentTask = undefined; this._currentArgs = undefined; } } return state; } } /** * Events. * * @name module:util.Scheduler#Event * @enum {Symbol} * @readonly * @public */ Scheduler.Event = { /** * Move onto the next task *without* rendering the scene first. */ NEXT: Symbol.for('NEXT'), /** * Render the scene and repeat the task. */ FLIP_REPEAT: Symbol.for('FLIP_REPEAT'), /** * Render the scene and move onto the next task. */ FLIP_NEXT: Symbol.for('FLIP_NEXT'), /** * Quit the scheduler. */ QUIT: Symbol.for('QUIT') }; /** * Status. * * @name module:util.Scheduler#Status * @enum {Symbol} * @readonly * @public */ Scheduler.Status = { /** * The Scheduler is running. */ RUNNING: Symbol.for('RUNNING'), /** * The Scheduler is stopped. */ STOPPED: Symbol.for('STOPPED') };