Skip to content

Instantly share code, notes, and snippets.

@jeffreytgilbert
Last active August 29, 2015 14:00
Show Gist options
  • Save jeffreytgilbert/11202429 to your computer and use it in GitHub Desktop.
Save jeffreytgilbert/11202429 to your computer and use it in GitHub Desktop.
CommonJS module for managing a timeline of timers with an optional execution time limit. The timers can all be paused and resumed independent of the execution timer. This was tested to within 1/100th of a second accuracy in Chrome, but uses Date.now() which is not compatible with some older browsers (IE 9 for instance). Easy to replace with new …
'use strict';
// ****** BEGIN SOLUTION TIMER ******* //
module.exports = (function () {
// all times will be measured in seconds by default
return function (_arrayOfTimes, _scriptTimeLimit, _timerCallback, _timeLimitCallback, _useMilliseconds) { // _useSeconds - add this later to add the ability to measure in milliseconds
var _startTime = 0, // the time this script started
_initialPlayTime = 0, // the time the playhead initially started
_endTime = 0, // the time that this script ended
_isRunning = false, // indicator of if the timer playhead is recording or if it is cleared
_lastResumedStreakTime = 0, // save point for tracking streaks
_lastResumedTime = 0, // save point for cumulative time (start point)
_lastPausedTime = 0, // save point for cumulative time (end point)
_cumulativeTimeTracked = 0, // do not read from this directly. it is only accurate/updated when it is called from the method
_longestStreak = 0, // long time spent waiting for a timer to fire
_baseIncrement = 1, // if using seconds (default) this will be 1000, otherwise this will be 1
_clocks = [], // array of setTimeout instances that will go off eventually if left playing
_scriptTimeoutClock,// the setTimeout instance for the script timelimit, serving as the master clock
// internal object for keeping track of which timers have hit. Has length property to mimic an array, but is not an array.
_finishedTimers = {
times: {},
findByIndex: function (index) {
// console.log(_finishedTimers.times);
// console.log(_finishedTimers.times[index]);
if (!!_finishedTimers.times[index]) { // this should be either the time or null, so force it to be true/false
// console.log('Found the timer in this check for index ' + index);
return _finishedTimers.times[index];
} else {
// console.log('Did not find the timer in this check for index ' + index);
return -1;
}
},
addTimer: function (index) {
_finishedTimers.times[index] = _arrayOfTimes[index];
_finishedTimers.length += 1;
},
resetTimers: function () {
_finishedTimers.times = {};
_finishedTimers.length = 0;
},
length: 0
},
_self = this;
var now = function (){
return new Date().getTime();
};
if (typeof _arrayOfTimes !== 'object' || !_arrayOfTimes.length) {
_arrayOfTimes = [];
}
if (_scriptTimeLimit <= 0) {
_scriptTimeLimit = Infinity;
}
if (typeof _timerCallback !== 'function') {
_timerCallback = function (self, timeHit, timesLeft, cumulativeTimeTracked, cumulativeRunTime, longestStreak, scriptTimeRemaining) {
console.log('Time hit: ' + timeHit);
console.log('Timers left: ' + timesLeft);
console.log('Cumulative time tracked ' + cumulativeTimeTracked);
console.log('Cumulative run time: ' + cumulativeRunTime);
console.log('Longest timing streak: ' + longestStreak);
console.log('Remainder before script times out: ' + scriptTimeRemaining);
console.log(self);
};
}
if (typeof _timeLimitCallback !== 'function') {
_timeLimitCallback = function (self, timesLeft, cumulativeTimeTracked, cumulativeRunTime, longestStreak) {
console.log('Script timeout hit!');
console.log('Timers left: ' + timesLeft);
console.log('Cumulative time tracked ' + cumulativeTimeTracked);
console.log('Cumulative run time: ' + cumulativeRunTime);
console.log('Longest timing streak: ' + longestStreak);
console.log(self);
};
}
if (_useMilliseconds) {
_baseIncrement = 1;
} else {
_baseIncrement = 1000;
}
(function () {
var x = 0;
while (x < _arrayOfTimes.length) {
if (_scriptTimeLimit !== Infinity && _arrayOfTimes[x] > _scriptTimeLimit) {
console.log('!Warning! Some timers which have been declared will never fire because the script will timeout first.');
}
x++;
}
})();
var saveEndScriptTime = function () {
_endTime = now();
};
var startScriptTimeoutClock = function () {
if (_scriptTimeLimit !== Infinity && _scriptTimeLimit > 0) {
_scriptTimeoutClock = setTimeout(function () {
clearScriptTimeout(_scriptTimeoutClock);
clearTimeouts();
saveEndScriptTime();
_isRunning = false;
_timeLimitCallback(
_self,
_self.timersLeft(),
_self.cumulativeTimeTracked() / _self.getBaseIncrement(),
_self.cumulativeRunTime() / _self.getBaseIncrement(),
_self.longestStreak() / _self.getBaseIncrement()
);
return _self;
// don't reset everything here because we may want some of the info after the script timeout has fired
}, (_scriptTimeLimit * _baseIncrement));
}
};
var clearScriptTimeout = function () {
if (_scriptTimeLimit !== Infinity && _scriptTimeLimit > 0) {
clearTimeout(_scriptTimeoutClock);
}
};
var timeoutWrapper = function (index) {
return function () {
_finishedTimers.addTimer(index);
var timersLeft = _self.timersLeft();
if (timersLeft < 1) {
clearTimeouts();
clearScriptTimeout();
saveEndScriptTime();
_isRunning = false;
// } else {
// saveCumulativeTimeTracked();
}
var _timeTrackedSoFar = _self.cumulativeTimeTracked();
console.log('The timer just went off for ' + _arrayOfTimes[index] + ' with ' + _timeTrackedSoFar + ' spent so far.');
_timerCallback(
_self,
_arrayOfTimes[index], // time hit
timersLeft,
_timeTrackedSoFar / _self.getBaseIncrement(),
_self.cumulativeRunTime() / _self.getBaseIncrement(),
_self.longestStreak() / _self.getBaseIncrement(),
(_self.timeRemaining() !== Infinity ? _self.timeRemaining() / _self.getBaseIncrement(): Infinity)
);
return _self;
};
};
var setTimer = function (index) {
var newTime = (_arrayOfTimes[index] * _baseIncrement) - _cumulativeTimeTracked;
console.log('New time is being set for ' + newTime + ' milliseconds');
_clocks[index] = setTimeout(timeoutWrapper(index), newTime); // if you use the getter here it will run the whole update algo X number of times, which is slow and potentially buggy.
};
var setTimeouts = function () {
console.log('Setting all the timeouts');
if (_startTime) {
_lastResumedStreakTime = now();
}
// initially the playhead was not moving, just the run time was.
if (!_initialPlayTime) {
_initialPlayTime = now();
}
saveCumulativeTimeTracked();
var x = 0, t = 0;
while (x < _arrayOfTimes.length) {
t = _finishedTimers.findByIndex(x);
if (t === -1) {
console.log('Timer number ' + x + '(' + t + ') is being set.');
setTimer(x);
}
x++;
}
};
var clearTimeouts = function () {
_lastPausedTime = now();
saveCumulativeTimeTracked();
var x = 0, t = 0;
while (x < _clocks.length) {
t = _finishedTimers.findByIndex(x);
if (t === -1) {
console.log('Timer number ' + x + '(' + t + ') is still active and was running, so it is being removed.');
clearTimeout(_clocks[x]);
}
x++;
}
};
this.timeRemaining = function () {
if (_scriptTimeLimit !== Infinity) {
return (_scriptTimeLimit * _baseIncrement) - this.cumulativeRunTime(); // the difference between then and now minus the bigger limit number
} else {
return Infinity;
}
};
// The time when the clock started (read only)
this.startTime = function () {
return _startTime;
};
// The time when the clock stopped running (read only)
this.endTime = function () {
return _endTime;
};
// The cumulative time limit this script will run for before quiting. Set to 0 or Infinity for no limit (read only)
this.scriptTimeLimit = function () {
return _scriptTimeLimit;
};
var saveCumulativeTimeTracked = function () {
var rightNow = now();
// console.log('****************************************************');
// console.log('(::) _startTime: ' + _startTime);
// console.log('(::) _endTime: ' + _endTime);
// console.log('**------------------------------------------------**');
// console.log('(::) _lastResumedTime: ' + _lastResumedTime);
// console.log('(::) _lastPausedTime: ' + _lastPausedTime);
if (_initialPlayTime && _initialPlayTime > _lastPausedTime) { _lastPausedTime = _initialPlayTime; }
if (_endTime !== 0 && _endTime < _lastPausedTime) { _lastPausedTime = _endTime; }
if (_initialPlayTime && _initialPlayTime > _lastResumedTime) { _lastResumedTime = _initialPlayTime; }
// console.log('**------------------------------------------------**');
// console.log('(::) _lastResumedTime: ' + _lastResumedTime);
// console.log('(::) _lastPausedTime: ' + _lastPausedTime);
// console.log('**------------------------------------------------**');
var difference;
if (_lastPausedTime > _lastResumedTime) {
difference = _lastPausedTime - _lastResumedTime;
_lastPausedTime = _lastResumedTime;
// console.log('(+++) Adding the difference: ' + difference);
_cumulativeTimeTracked += difference;
} else {
if (_isRunning) {
// console.log('The timer is still running, so we\'re using NOW as the last pause, then we\'ll move the playhead accordingly.');
difference = rightNow - _lastResumedTime;
_lastPausedTime = rightNow;
_lastResumedTime = rightNow;
// console.log('(+++) Adding the difference: ' + difference);
_cumulativeTimeTracked += difference;
// } else {
// console.log('Not updating the cumulative time because last resumed time is bigger than the last pause time and the thread isnt running.');
// difference = _lastPausedTime - _lastResumedTime;
// console.log('(---) Ignoring the difference: ' + difference);
}
}
// console.log('(::) _cumulativeTimeTracked: ' + _cumulativeTimeTracked);
// console.log('****************************************************');
};
_self.useMilliseconds = function () {
return !!_useMilliseconds;
};
_self.baseIncrement = function () {
return _baseIncrement;
};
// Total time in view according to this timer
_self.cumulativeTimeTracked = function () {
saveCumulativeTimeTracked();
return _cumulativeTimeTracked;
};
_self.cumulativeRunTime = function () {
return now() - _startTime;
};
// Longest in view streak
_self.longestStreak = function (val) {
if (val) {
if (val > _longestStreak) { _longestStreak = val; }
} else {
if (_isRunning) {
var currentStreak = now() - _lastResumedStreakTime;
if (currentStreak > _longestStreak) { _longestStreak = currentStreak; }
return _longestStreak;
} else {
return _longestStreak;
}
}
};
// get the number of timers yet to run
_self.timersLeft = function () {
return _arrayOfTimes.length - _finishedTimers.length;
};
_self.startTimers = function (andStartScriptTimer) {
if (!_startTime && !_isRunning) {
_startTime = now();
if (andStartScriptTimer) {
_initialPlayTime = _startTime;
setTimeouts();
_isRunning = true;
}
startScriptTimeoutClock();
}
};
_self.resumeTimers = function () {
console.log('Trying to resume timers');
if (!_isRunning) {
console.log('None were running, so we are ok to do our magic');
_lastResumedTime = now();
setTimeouts();
_isRunning = true;
console.log('All the timers should be resumed');
}
};
_self.pauseTimers = function () {
if (_isRunning) {
_self.longestStreak();
clearTimeouts();
_isRunning = false;
}
};
_self.fullReset = function () {
clearTimeouts();
clearScriptTimeout();
_isRunning = false;
_startTime = 0;
_endTime = 0;
_lastResumedStreakTime = 0;
_lastResumedTime = 0;
_lastPausedTime = 0;
_cumulativeTimeTracked = 0;
_longestStreak = 0;
_baseIncrement = 1;
_finishedTimers.resetTimers();
};
_self.isRunning = function () {
return _isRunning;
};
_self.skipToIndex = function (index, andStartTimers){
var targetTimestamp = _startTime + (_arrayOfTimes[index] * _baseIncrement);
_self.movePlayheadToTime(targetTimestamp, andStartTimers);
};
_self.skipToNow = function (andStartTimers){
_self.movePlayheadToTime(now(), andStartTimers);
};
_self.movePlayheadToTime = function (targetTimestamp, andStartTimers) {
console.log('Pausing the timers so we can move the playhead with known streak times.');
// sure, we're pausing, but we have to catch the last known good time before skipping forward and restarting
_self.pauseTimers();
andStartTimers = !!andStartTimers;
console.log('We ' + (andStartTimers ? 'WILL' : 'WONT') + ' be starting timers after the playhead moves.');
// now what happens when we start back up?
// normally this sets the new time:
// var newTime = (_arrayOfTimes[index] * _baseIncrement) - _cumulativeTimeTracked;
// so we need to first know how much time has passed since the start of the script
_cumulativeTimeTracked = targetTimestamp - _startTime;
console.log('calculating the time tracked as: ' + _cumulativeTimeTracked);
var index = 0, targetTime = 0;
while (index < _arrayOfTimes.length) {
targetTime = (_arrayOfTimes[index] * _baseIncrement);
console.log('The target time is: ' + targetTime);
if (targetTime <= _cumulativeTimeTracked && _finishedTimers.findByIndex(index) === -1) {
console.log('It has not expired, and it is at or beyond the time where it should be, so we will attempt to fire it now.');
timeoutWrapper(index)(); // if this should have fired by now (with the fast forward) fire the timeout instantly as it would be normally
} else if (targetTime > _cumulativeTimeTracked && andStartTimers) { // && _finishedTimers.findByIndex(index) !== -1
console.log('It has not expired, so we will start the timer for it.');
setTimer(index); // else if this has yet to fire because it is in the future, set another timeout
}
index++;
}
};
_self.getLongestStreak = function () {
return _self.getUseMilliseconds() ? _self.longestStreak() : (_self.longestStreak() / 1000).toFixed(3);
};
_self.play = function (andStartScriptTimer) {
console.log('Intercepted a request to play/resume the in view timer. It ' + (andStartScriptTimer?'should':'should NOT') + 'also start the script timer.');
if (_self.startTime()) {
console.log('It has been detected that times have started before, so we are going to resume.');
_self.resumeTimers();
} else {
console.log('It has been detected that times have NOT started before, so we are going to initialize them.');
_self.startTimers(andStartScriptTimer);
}
};
// Aliases for public easy public access
_self.pause = _self.pauseTimers;
_self.kill = _self.fullReset;
_self.getScriptTimeLimit = _self.scriptTimeLimit;
_self.getTimersLeft = _self.timersLeft;
_self.getUseMilliseconds = _self.useMilliseconds;
_self.getBaseIncrement = _self.baseIncrement;
_self.getEndTime = _self.endTime;
_self.getStartTime = _self.startTime;
_self.isPlaying = _self.isRunning;
};
})();
// ****** END SOLUTION TIMER ******* //
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment