310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
/**
|
|
* @fileoverview This file contains the module that is required to enable specialised timers that have better control
|
|
* on a global level.
|
|
*
|
|
* @todo - the architecture of this sucks even if this "works".
|
|
* - the way to compute firing of start and end events suck
|
|
* - basically this needs a redo with more "engineering" put into it
|
|
*/
|
|
const /**
|
|
*
|
|
* @constant {String}
|
|
*/
|
|
FUNCTION = 'function',
|
|
|
|
/**
|
|
* The set of timer function names. We use this array to define common behaviour of all setters and clearer timer
|
|
* functions
|
|
*
|
|
* @constant {Array.<String>}
|
|
*/
|
|
timerFunctionNames = ['Timeout', 'Interval', 'Immediate', 'Event'],
|
|
|
|
/**
|
|
* This object defines a set of timer function names that are trigerred a number of times instead of a single time.
|
|
* Such timers, when placed in generic rules, needs special attention.
|
|
*
|
|
* @constant {Array.<Boolean>}
|
|
*/
|
|
multiFireTimerFunctions = {
|
|
Interval: true
|
|
},
|
|
|
|
/**
|
|
* This object defines a set of function timer names that do not fire based on any pre-set duration or interval.
|
|
* Such timers, when placed in generic rules, needs special attention.
|
|
*
|
|
* @constant {Array.<Boolean>}
|
|
*/
|
|
staticTimerFunctions = {
|
|
Event: true
|
|
},
|
|
|
|
/**
|
|
* A local copy of Slice function of Array
|
|
*
|
|
* @constant {Function}
|
|
*/
|
|
arrayProtoSlice = Array.prototype.slice,
|
|
|
|
/**
|
|
* This object holds the current global timers
|
|
*
|
|
* @extends Timers
|
|
*
|
|
* @note This is a very important piece of code from compatibility standpoint.
|
|
* The global timers need to be returned as a function that does not hold reference to the scope
|
|
* and does not retain references to scope. Aditionally, the invocation of the timer function is
|
|
* done without changing the scope to avoid Illegal Invocation errors.
|
|
*
|
|
* `timerFunctionNames` returns the suffixes of all timer operations that needs a
|
|
* a setter and clear method.
|
|
*/
|
|
defaultTimers = timerFunctionNames.reduce(function (timers, name) {
|
|
var
|
|
|
|
/**
|
|
* Check if global setter function is available
|
|
*
|
|
* @private
|
|
* @type {Boolean}
|
|
*/
|
|
// eslint-disable-next-line no-new-func
|
|
isGlobalSetterAvailable = (new Function(`return typeof set${name} === 'function'`))(),
|
|
|
|
/**
|
|
* Check if global clear function is available
|
|
*
|
|
* @private
|
|
* @type {Boolean}
|
|
*/
|
|
// eslint-disable-next-line no-new-func
|
|
isGlobalClearAvailable = (new Function(`return typeof clear${name} === 'function'`))();
|
|
|
|
if (isGlobalSetterAvailable) {
|
|
// eslint-disable-next-line no-new-func
|
|
timers[('set' + name)] = (new Function(`return function (fn, ms) { return set${name}(fn, ms); }`))();
|
|
}
|
|
|
|
if (isGlobalClearAvailable) {
|
|
// eslint-disable-next-line no-new-func
|
|
timers[('clear' + name)] = (new Function(`return function (id) { return clear${name}(id); }`))();
|
|
}
|
|
|
|
return timers;
|
|
}, {});
|
|
|
|
/**
|
|
* @constructor
|
|
*
|
|
* @param {Object} [delegations] -
|
|
* @param {Function} [onError] -
|
|
* @param {Function} [onAnyTimerStart] -
|
|
* @param {Function} [onAllTimerEnd] -
|
|
*/
|
|
function Timerz (delegations, onError, onAnyTimerStart, onAllTimerEnd) {
|
|
var /**
|
|
* Holds the present timers, either delegated or defaults
|
|
*
|
|
* @extends Timers
|
|
*/
|
|
timers = delegations || defaultTimers,
|
|
dummyContext = {},
|
|
|
|
total = 0, // accumulator to keep track of total timers
|
|
pending = 0, // counters to keep track of running timers
|
|
sealed = false, // flag that stops all new timer additions
|
|
wentAsync = false,
|
|
computeTimerEvents;
|
|
|
|
// do special handling to enable emulation of immediate timers in hosts that lacks them
|
|
if (typeof timers.setImmediate !== FUNCTION) {
|
|
timers.setImmediate = function (callback) {
|
|
return timers.setTimeout(callback, 0);
|
|
};
|
|
timers.clearImmediate = function (id) {
|
|
return timers.clearTimeout(id);
|
|
};
|
|
}
|
|
|
|
// write special handlers for event based timers if the delegations don't contain one
|
|
(typeof timers.setEvent !== FUNCTION) && (function () {
|
|
var events = {},
|
|
total = 0;
|
|
|
|
timers.setEvent = function (callback) {
|
|
var id = ++total;
|
|
|
|
events[id] = callback;
|
|
|
|
return id;
|
|
};
|
|
|
|
timers.clearEvent = function (id) {
|
|
var cb = events[id];
|
|
|
|
delete events[id];
|
|
(typeof cb === FUNCTION) && cb.apply(dummyContext, arrayProtoSlice.call(arguments, 1));
|
|
};
|
|
|
|
timers.clearAllEvents = function () {
|
|
Object.keys(events).forEach(function (prop) {
|
|
delete events[prop];
|
|
});
|
|
};
|
|
}());
|
|
|
|
// create a function that decides whether to fire appropriate callbacks
|
|
computeTimerEvents = function (increment, clearing) {
|
|
increment && (pending += increment);
|
|
|
|
if (pending === 0 && computeTimerEvents.started) {
|
|
!clearing && (typeof onAllTimerEnd === FUNCTION) && onAllTimerEnd();
|
|
computeTimerEvents.started = false;
|
|
|
|
return;
|
|
}
|
|
|
|
if (pending > 0 && !computeTimerEvents.started) {
|
|
!clearing && (typeof onAnyTimerStart === FUNCTION) && onAnyTimerStart();
|
|
computeTimerEvents.started = true;
|
|
}
|
|
};
|
|
|
|
// iterate through the timer variants and create common setter and clearer function behaviours for each of them.
|
|
timerFunctionNames.forEach(function (name) {
|
|
// create an accumulator for all timer references
|
|
var running = {};
|
|
|
|
// create the setter function for the timer
|
|
this[('set' + name)] = function (callback) {
|
|
// it is pointless to proceed with setter if there is no callback to execute
|
|
if (sealed || typeof callback !== FUNCTION) {
|
|
return;
|
|
}
|
|
|
|
var id = ++total, // get hold of the next timer id
|
|
args = arrayProtoSlice.call(arguments);
|
|
|
|
args[0] = function () {
|
|
wentAsync = true; // mark that we did go async once. this will ensure we do not pass erroneous events
|
|
|
|
// call the actual callback with a dummy context
|
|
try { callback.apply(dummyContext, staticTimerFunctions[name] ? arguments : null); }
|
|
catch (e) { onError && onError(e); }
|
|
|
|
// interval timers can only be cleared using clearXYZ function and hence we need not do anything
|
|
// except call the timer
|
|
if (staticTimerFunctions[name] || multiFireTimerFunctions[name]) {
|
|
// do not modify counter during interval type events
|
|
computeTimerEvents();
|
|
}
|
|
// when this is fired, the timer dies, so we decrement tracking counters and delete
|
|
// irq references
|
|
else {
|
|
computeTimerEvents(-1);
|
|
delete running[id];
|
|
}
|
|
};
|
|
|
|
// for static timers
|
|
staticTimerFunctions[name] && (wentAsync = true);
|
|
|
|
// call the underlying timer function and keep a track of its irq
|
|
running[id] = timers[('set' + name)].apply(this, args);
|
|
args = null; // precaution
|
|
|
|
// increment the counter and return the tracking ID to be used to pass on to clearXYZ function
|
|
computeTimerEvents(1);
|
|
|
|
return id;
|
|
};
|
|
|
|
// create the clear function of the timer
|
|
this[('clear' + name)] = function (id) {
|
|
var underLyingId = running[id],
|
|
args;
|
|
|
|
// it is pointless and erroenous to proceed in case it seems that it is no longer running
|
|
if (sealed || !underLyingId) {
|
|
return;
|
|
}
|
|
|
|
// prepare args to be forwarded to clear function
|
|
args = arrayProtoSlice.call(arguments);
|
|
args[0] = underLyingId;
|
|
delete running[id];
|
|
|
|
// fire the underlying clearing function
|
|
|
|
try { timers['clear' + name].apply(dummyContext, args); }
|
|
catch (e) { onError(e); }
|
|
|
|
// decrement counters and call the clearing timer function
|
|
computeTimerEvents(-1, !wentAsync);
|
|
|
|
args = underLyingId = null; // just a precaution
|
|
};
|
|
|
|
// create a sugar function to clear all running timers of this category
|
|
// @todo: decide how to handle clearing for underlying delegated timers, if they are instances of Timerz itself.
|
|
if (typeof timers[('clearAll' + name + 's')] === FUNCTION) {
|
|
// if native timers have a function to clear all timers, then use it
|
|
this[('clearAll' + name + 's')] = function () {
|
|
timers[('clearAll' + name + 's')]();
|
|
Object.keys(running).forEach(function () {
|
|
computeTimerEvents(-1, true);
|
|
});
|
|
};
|
|
}
|
|
else {
|
|
this[('clearAll' + name + 's')] = function () {
|
|
Object.keys(running).forEach(function (id) {
|
|
computeTimerEvents(-1, true);
|
|
// run clear functions except for static timers
|
|
timers['clear' + name](running[id]);
|
|
});
|
|
};
|
|
}
|
|
}.bind(this));
|
|
|
|
|
|
/**
|
|
* @memberof Timerz.prototype
|
|
* @returns {Number}
|
|
*/
|
|
this.queueLength = function () {
|
|
return pending;
|
|
};
|
|
|
|
/**
|
|
* @memberof Timerz.prototype
|
|
*/
|
|
this.clearAll = function () {
|
|
// call all internal timer clearAll function variants
|
|
timerFunctionNames.forEach(function (name) {
|
|
this[('clearAll' + name + 's')]();
|
|
}.bind(this));
|
|
};
|
|
|
|
/**
|
|
* @memberof Timerz.prototype
|
|
*/
|
|
this.seal = function () {
|
|
sealed = true;
|
|
};
|
|
|
|
this.error = function (err) {
|
|
return onError.call(dummyContext, err);
|
|
};
|
|
|
|
this.terminate = function () {
|
|
this.seal();
|
|
this.clearAll();
|
|
|
|
return onAllTimerEnd.apply(dummyContext, arguments);
|
|
};
|
|
}
|
|
|
|
module.exports = Timerz;
|