/** * conrad.js is a tiny JavaScript jobs scheduler, * * Version: 0.1.0 * Sources: http://github.com/jacomyal/conrad.js * Doc: http://github.com/jacomyal/conrad.js#readme * * License: * -------- * Copyright © 2013 Alexis Jacomy, Sciences-Po médialab * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * The Software is provided "as is", without warranty of any kind, express or * implied, including but not limited to the warranties of merchantability, * fitness for a particular purpose and noninfringement. In no event shall the * authors or copyright holders be liable for any claim, damages or other * liability, whether in an action of contract, tort or otherwise, arising * from, out of or in connection with the software or the use or other dealings * in the Software. */ (function(global) { 'use strict'; // Check that conrad.js has not been loaded yet: if (global.conrad) throw new Error('conrad already exists'); /** * PRIVATE VARIABLES: * ****************** */ /** * A flag indicating whether conrad is running or not. * * @type {Number} */ var _lastFrameTime; /** * A flag indicating whether conrad is running or not. * * @type {Boolean} */ var _isRunning = false; /** * The hash of registered jobs. Each job must at least have a unique ID * under the key "id" and a function under the key "job". This hash * contains each running job and each waiting job. * * @type {Object} */ var _jobs = {}; /** * The hash of currently running jobs. * * @type {Object} */ var _runningJobs = {}; /** * The array of currently running jobs, sorted by priority. * * @type {Array} */ var _sortedByPriorityJobs = []; /** * The array of currently waiting jobs. * * @type {Object} */ var _waitingJobs = {}; /** * The array of finished jobs. They are stored in an array, since two jobs * with the same "id" can happen at two different times. * * @type {Array} */ var _doneJobs = []; /** * A dirty flag to keep conrad from starting: Indeed, when addJob() is called * with several jobs, conrad must be started only at the end. This flag keeps * me from duplicating the code that effectively adds a job. * * @type {Boolean} */ var _noStart = false; /** * An hash containing some global settings about how conrad.js should * behave. * * @type {Object} */ var _parameters = { frameDuration: 20, history: true }; /** * This object contains every handlers bound to conrad events. It does not * requirea any DOM implementation, since the events are all JavaScript. * * @type {Object} */ var _handlers = Object.create(null); /** * PRIVATE FUNCTIONS: * ****************** */ /** * Will execute the handler everytime that the indicated event (or the * indicated events) will be triggered. * * @param {string|array|object} events The name of the event (or the events * separated by spaces). * @param {function(Object)} handler The handler to bind. * @return {Object} Returns conrad. */ function _bind(events, handler) { var i, i_end, event, eArray; if (!arguments.length) return; else if ( arguments.length === 1 && Object(arguments[0]) === arguments[0] ) for (events in arguments[0]) _bind(events, arguments[0][events]); else if (arguments.length > 1) { eArray = Array.isArray(events) ? events : events.split(/ /); for (i = 0, i_end = eArray.length; i !== i_end; i += 1) { event = eArray[i]; if (!_handlers[event]) _handlers[event] = []; // Using an object instead of directly the handler will make possible // later to add flags _handlers[event].push({ handler: handler }); } } } /** * Removes the handler from a specified event (or specified events). * * @param {?string} events The name of the event (or the events * separated by spaces). If undefined, * then all handlers are removed. * @param {?function(Object)} handler The handler to unbind. If undefined, * each handler bound to the event or the * events will be removed. * @return {Object} Returns conrad. */ function _unbind(events, handler) { var i, i_end, j, j_end, a, event, eArray = Array.isArray(events) ? events : events.split(/ /); if (!arguments.length) _handlers = Object.create(null); else if (handler) { for (i = 0, i_end = eArray.length; i !== i_end; i += 1) { event = eArray[i]; if (_handlers[event]) { a = []; for (j = 0, j_end = _handlers[event].length; j !== j_end; j += 1) if (_handlers[event][j].handler !== handler) a.push(_handlers[event][j]); _handlers[event] = a; } if (_handlers[event] && _handlers[event].length === 0) delete _handlers[event]; } } else for (i = 0, i_end = eArray.length; i !== i_end; i += 1) delete _handlers[eArray[i]]; } /** * Executes each handler bound to the event. * * @param {string} events The name of the event (or the events separated * by spaces). * @param {?Object} data The content of the event (optional). * @return {Object} Returns conrad. */ function _dispatch(events, data) { var i, j, i_end, j_end, event, eventName, eArray = Array.isArray(events) ? events : events.split(/ /); data = data === undefined ? {} : data; for (i = 0, i_end = eArray.length; i !== i_end; i += 1) { eventName = eArray[i]; if (_handlers[eventName]) { event = { type: eventName, data: data || {} }; for (j = 0, j_end = _handlers[eventName].length; j !== j_end; j += 1) try { _handlers[eventName][j].handler(event); } catch (e) {} } } } /** * Executes the most prioritary job once, and deals with filling the stats * (done, time, averageTime, currentTime, etc...). * * @return {?Object} Returns the job object if it has to be killed, null else. */ function _executeFirstJob() { var i, l, test, kill, pushed = false, time = __dateNow(), job = _sortedByPriorityJobs.shift(); // Execute the job and look at the result: test = job.job(); // Deal with stats: time = __dateNow() - time; job.done++; job.time += time; job.currentTime += time; job.weightTime = job.currentTime / (job.weight || 1); job.averageTime = job.time / job.done; // Check if the job has to be killed: kill = job.count ? (job.count <= job.done) : !test; // Reset priorities: if (!kill) { for (i = 0, l = _sortedByPriorityJobs.length; i < l; i++) if (_sortedByPriorityJobs[i].weightTime > job.weightTime) { _sortedByPriorityJobs.splice(i, 0, job); pushed = true; break; } if (!pushed) _sortedByPriorityJobs.push(job); } return kill ? job : null; } /** * Activates a job, by adding it to the _runningJobs object and the * _sortedByPriorityJobs array. It also initializes its currentTime value. * * @param {Object} job The job to activate. */ function _activateJob(job) { var l = _sortedByPriorityJobs.length; // Add the job to the running jobs: _runningJobs[job.id] = job; job.status = 'running'; // Add the job to the priorities: if (l) { job.weightTime = _sortedByPriorityJobs[l - 1].weightTime; job.currentTime = job.weightTime * (job.weight || 1); } // Initialize the job and dispatch: job.startTime = __dateNow(); _dispatch('jobStarted', __clone(job)); _sortedByPriorityJobs.push(job); } /** * The main loop of conrad.js: * . It executes job such that they all occupate the same processing time. * . It stops jobs that do not need to be executed anymore. * . It triggers callbacks when it is relevant. * . It starts waiting jobs when they need to be started. * . It injects frames to keep a constant frapes per second ratio. * . It stops itself when there are no more jobs to execute. */ function _loop() { var k, o, l, job, time, deadJob; // Deal with the newly added jobs (the _jobs object): for (k in _jobs) { job = _jobs[k]; if (job.after) _waitingJobs[k] = job; else _activateJob(job); delete _jobs[k]; } // Set the _isRunning flag to false if there are no running job: _isRunning = !!_sortedByPriorityJobs.length; // Deal with the running jobs (the _runningJobs object): while ( _sortedByPriorityJobs.length && __dateNow() - _lastFrameTime < _parameters.frameDuration ) { deadJob = _executeFirstJob(); // Deal with the case where the job has ended: if (deadJob) { _killJob(deadJob.id); // Check for waiting jobs: for (k in _waitingJobs) if (_waitingJobs[k].after === deadJob.id) { _activateJob(_waitingJobs[k]); delete _waitingJobs[k]; } } } // Check if conrad still has jobs to deal with, and kill it if not: if (_isRunning) { // Update the _lastFrameTime: _lastFrameTime = __dateNow(); _dispatch('enterFrame'); setTimeout(_loop, 0); } else _dispatch('stop'); } /** * Adds one or more jobs, and starts the loop if no job was running before. A * job is at least a unique string "id" and a function, and there are some * parameters that you can specify for each job to modify the way conrad will * execute it. If a job is added with the "id" of another job that is waiting * or still running, an error will be thrown. * * When a job is added, it is referenced in the _jobs object, by its id. * Then, if it has to be executed right now, it will be also referenced in * the _runningJobs object. If it has to wait, then it will be added into the * _waitingJobs object, until it can start. * * Keep reading this documentation to see how to call this method. * * @return {Object} Returns conrad. * * Adding one job: * *************** * Basically, a job is defined by its string id and a function (the job). It * is also possible to add some parameters: * * > conrad.addJob('myJobId', myJobFunction); * > conrad.addJob('myJobId', { * > job: myJobFunction, * > someParameter: someValue * > }); * > conrad.addJob({ * > id: 'myJobId', * > job: myJobFunction, * > someParameter: someValue * > }); * * Adding several jobs: * ******************** * When adding several jobs at the same time, it is possible to specify * parameters for each one individually or for all: * * > conrad.addJob([ * > { * > id: 'myJobId1', * > job: myJobFunction1, * > someParameter1: someValue1 * > }, * > { * > id: 'myJobId2', * > job: myJobFunction2, * > someParameter2: someValue2 * > } * > ], { * > someCommonParameter: someCommonValue * > }); * > conrad.addJob({ * > myJobId1: {, * > job: myJobFunction1, * > someParameter1: someValue1 * > }, * > myJobId2: {, * > job: myJobFunction2, * > someParameter2: someValue2 * > } * > }, { * > someCommonParameter: someCommonValue * > }); * > conrad.addJob({ * > myJobId1: myJobFunction1, * > myJobId2: myJobFunction2 * > }, { * > someCommonParameter: someCommonValue * > }); * * Recognized parameters: * ********************** * Here is the exhaustive list of every accepted parameters: * * {?Function} end A callback to execute when the job is ended. It is * not executed if the job is killed instead of ended * "naturally". * {?Integer} count The number of time the job has to be executed. * {?Number} weight If specified, the job will be executed as it was * added "weight" times. * {?String} after The id of another job (eventually not added yet). * If specified, this job will start only when the * specified "after" job is ended. */ function _addJob(v1, v2) { var i, l, o; // Array of jobs: if (Array.isArray(v1)) { // Keep conrad to start until the last job is added: _noStart = true; for (i = 0, l = v1.length; i < l; i++) _addJob(v1[i].id, __extend(v1[i], v2)); _noStart = false; if (!_isRunning) { // Update the _lastFrameTime: _lastFrameTime = __dateNow(); _dispatch('start'); _loop(); } } else if (typeof v1 === 'object') { // One job (object): if (typeof v1.id === 'string') _addJob(v1.id, v1); // Hash of jobs: else { // Keep conrad to start until the last job is added: _noStart = true; for (i in v1) if (typeof v1[i] === 'function') _addJob(i, __extend({ job: v1[i] }, v2)); else _addJob(i, __extend(v1[i], v2)); _noStart = false; if (!_isRunning) { // Update the _lastFrameTime: _lastFrameTime = __dateNow(); _dispatch('start'); _loop(); } } // One job (string, *): } else if (typeof v1 === 'string') { if (_hasJob(v1)) throw new Error( '[conrad.addJob] Job with id "' + v1 + '" already exists.' ); // One job (string, function): if (typeof v2 === 'function') { o = { id: v1, done: 0, time: 0, status: 'waiting', currentTime: 0, averageTime: 0, weightTime: 0, job: v2 }; // One job (string, object): } else if (typeof v2 === 'object') { o = __extend( { id: v1, done: 0, time: 0, status: 'waiting', currentTime: 0, averageTime: 0, weightTime: 0 }, v2 ); // If none of those cases, throw an error: } else throw new Error('[conrad.addJob] Wrong arguments.'); // Effectively add the job: _jobs[v1] = o; _dispatch('jobAdded', __clone(o)); // Check if the loop has to be started: if (!_isRunning && !_noStart) { // Update the _lastFrameTime: _lastFrameTime = __dateNow(); _dispatch('start'); _loop(); } // If none of those cases, throw an error: } else throw new Error('[conrad.addJob] Wrong arguments.'); return this; } /** * Kills one or more jobs, indicated by their ids. It is only possible to * kill running jobs or waiting jobs. If you try to kill a job that does not * exist or that is already killed, a warning will be thrown. * * @param {Array|String} v1 A string job id or an array of job ids. * @return {Object} Returns conrad. */ function _killJob(v1) { var i, l, k, a, job, found = false; // Array of job ids: if (Array.isArray(v1)) for (i = 0, l = v1.length; i < l; i++) _killJob(v1[i]); // One job's id: else if (typeof v1 === 'string') { a = [_runningJobs, _waitingJobs, _jobs]; // Remove the job from the hashes: for (i = 0, l = a.length; i < l; i++) if (v1 in a[i]) { job = a[i][v1]; if (_parameters.history) { job.status = 'done'; _doneJobs.push(job); } _dispatch('jobEnded', __clone(job)); delete a[i][v1]; if (typeof job.end === 'function') job.end(); found = true; } // Remove the priorities array: a = _sortedByPriorityJobs; for (i = 0, l = a.length; i < l; i++) if (a[i].id === v1) { a.splice(i, 1); break; } if (!found) throw new Error('[conrad.killJob] Job "' + v1 + '" not found.'); // If none of those cases, throw an error: } else throw new Error('[conrad.killJob] Wrong arguments.'); return this; } /** * Kills every running, waiting, and just added jobs. * * @return {Object} Returns conrad. */ function _killAll() { var k, jobs = __extend(_jobs, _runningJobs, _waitingJobs); // Take every jobs and push them into the _doneJobs object: if (_parameters.history) for (k in jobs) { jobs[k].status = 'done'; _doneJobs.push(jobs[k]); if (typeof jobs[k].end === 'function') jobs[k].end(); } // Reinitialize the different jobs lists: _jobs = {}; _waitingJobs = {}; _runningJobs = {}; _sortedByPriorityJobs = []; // In case some jobs are added right after the kill: _isRunning = false; return this; } /** * Returns true if a job with the specified id is currently running or * waiting, and false else. * * @param {String} id The id of the job. * @return {?Object} Returns the job object if it exists. */ function _hasJob(id) { var job = _jobs[id] || _runningJobs[id] || _waitingJobs[id]; return job ? __extend(job) : null; } /** * This method will set the setting specified by "v1" to the value specified * by "v2" if both are given, and else return the current value of the * settings "v1". * * @param {String} v1 The name of the property. * @param {?*} v2 Eventually, a value to set to the specified * property. * @return {Object|*} Returns the specified settings value if "v2" is not * given, and conrad else. */ function _settings(v1, v2) { var o; if (typeof a1 === 'string' && arguments.length === 1) return _parameters[a1]; else { o = (typeof a1 === 'object' && arguments.length === 1) ? a1 || {} : {}; if (typeof a1 === 'string') o[a1] = a2; for (var k in o) if (o[k] !== undefined) _parameters[k] = o[k]; else delete _parameters[k]; return this; } } /** * Returns true if conrad is currently running, and false else. * * @return {Boolean} Returns _isRunning. */ function _getIsRunning() { return _isRunning; } /** * Unreference every job that is stored in the _doneJobs object. It will * not be possible anymore to get stats about these jobs, but it will release * the memory. * * @return {Object} Returns conrad. */ function _clearHistory() { _doneJobs = []; return this; } /** * Returns a snapshot of every data about jobs that wait to be started, are * currently running or are done. * * It is possible to get only running, waiting or done jobs by giving * "running", "waiting" or "done" as fist argument. * * It is also possible to get every job with a specified id by giving it as * first argument. Also, using a RegExp instead of an id will return every * jobs whose ids match the RegExp. And these two last use cases work as well * by giving before "running", "waiting" or "done". * * @return {Array} The array of the matching jobs. * * Some call examples: * ******************* * > conrad.getStats('running') * > conrad.getStats('waiting') * > conrad.getStats('done') * > conrad.getStats('myJob') * > conrad.getStats(/test/) * > conrad.getStats('running', 'myRunningJob') * > conrad.getStats('running', /test/) */ function _getStats(v1, v2) { var a, k, i, l, stats, pattern, isPatternString; if (!arguments.length) { stats = []; for (k in _jobs) stats.push(_jobs[k]); for (k in _waitingJobs) stats.push(_waitingJobs[k]); for (k in _runningJobs) stats.push(_runningJobs[k]); stats = stats.concat(_doneJobs); } if (typeof v1 === 'string') switch (v1) { case 'waiting': stats = __objectValues(_waitingJobs); break; case 'running': stats = __objectValues(_runningJobs); break; case 'done': stats = _doneJobs; break; default: pattern = v1; } if (v1 instanceof RegExp) pattern = v1; if (!pattern && (typeof v2 === 'string' || v2 instanceof RegExp)) pattern = v2; // Filter jobs if a pattern is given: if (pattern) { isPatternString = typeof pattern === 'string'; if (stats instanceof Array) { a = stats; } else if (typeof stats === 'object') { a = []; for (k in stats) a = a.concat(stats[k]); } else { a = []; for (k in _jobs) a.push(_jobs[k]); for (k in _waitingJobs) a.push(_waitingJobs[k]); for (k in _runningJobs) a.push(_runningJobs[k]); a = a.concat(_doneJobs); } stats = []; for (i = 0, l = a.length; i < l; i++) if (isPatternString ? a[i].id === pattern : a[i].id.match(pattern)) stats.push(a[i]); } return __clone(stats); } /** * TOOLS FUNCTIONS: * **************** */ /** * This function takes any number of objects as arguments, copies from each * of these objects each pair key/value into a new object, and finally * returns this object. * * The arguments are parsed from the last one to the first one, such that * when two objects have keys in common, the "earliest" object wins. * * Example: * ******** * > var o1 = { * > a: 1, * > b: 2, * > c: '3' * > }, * > o2 = { * > c: '4', * > d: [ 5 ] * > }; * > __extend(o1, o2); * > // Returns: { * > // a: 1, * > // b: 2, * > // c: '3', * > // d: [ 5 ] * > // }; * * @param {Object+} Any number of objects. * @return {Object} The merged object. */ function __extend() { var i, k, res = {}, l = arguments.length; for (i = l - 1; i >= 0; i--) for (k in arguments[i]) res[k] = arguments[i][k]; return res; } /** * This function simply clones an object. This object must contain only * objects, arrays and immutable values. Since it is not public, it does not * deal with cyclic references, DOM elements and instantiated objects - so * use it carefully. * * @param {Object} The object to clone. * @return {Object} The clone. */ function __clone(item) { var result, i, k, l; if (!item) return item; if (Array.isArray(item)) { result = []; for (i = 0, l = item.length; i < l; i++) result.push(__clone(item[i])); } else if (typeof item === 'object') { result = {}; for (i in item) result[i] = __clone(item[i]); } else result = item; return result; } /** * Returns an array containing the values of an object. * * @param {Object} The object. * @return {Array} The array of values. */ function __objectValues(o) { var k, a = []; for (k in o) a.push(o[k]); return a; } /** * A short "Date.now()" polyfill. * * @return {Number} The current time (in ms). */ function __dateNow() { return Date.now ? Date.now() : new Date().getTime(); } /** * Polyfill for the Array.isArray function: */ if (!Array.isArray) Array.isArray = function(v) { return Object.prototype.toString.call(v) === '[object Array]'; }; /** * EXPORT PUBLIC API: * ****************** */ var conrad = { hasJob: _hasJob, addJob: _addJob, killJob: _killJob, killAll: _killAll, settings: _settings, getStats: _getStats, isRunning: _getIsRunning, clearHistory: _clearHistory, // Events management: bind: _bind, unbind: _unbind, // Version: version: '0.1.0' }; if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) exports = module.exports = conrad; exports.conrad = conrad; } global.conrad = conrad; })(this);