Graph database Analysis of the Steam Network
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

984 lines
25 KiB

/**
* 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);