Skip to content

Instantly share code, notes, and snippets.

@codeasaurus
Last active December 30, 2015 00:57
Show Gist options
  • Save codeasaurus/c034df72bf220a82b878 to your computer and use it in GitHub Desktop.
Save codeasaurus/c034df72bf220a82b878 to your computer and use it in GitHub Desktop.
Utility Wrapper for managing the unruly javascript interval timer

Synopsis

Anyone that's ever tried to wrangle the native javascript interval timer has thought, "Why does this have to be so ugly and unmanageable?" MorphInterval provides a simple but flexible solution to managing javascript interval timers.

Code Example

This example shows a simple interval that calls the doSomething() function once every 60 seconds

var thisObj = this;
this.refreshInterval = new window.MorphInterval(
{
    minInterval: 60,
    maxInterval: 60,
    curve: 'flat',
    callback: function()
    {
        thisObj.doSomething.call(thisObj);
    }
}).start();

Killing the interval is easy, just call the kill() method:

this.refreshInterval.kill();

With a constant interval, you might wonder why this is any better than just using the native implementation. But what if you want the interval to morph over time. For example, let's say you need to poll an api for updates (news feed, for example), but those updates come in irregular bursts, and you don't want to keep pestering the server when there hasn't been any activity for a while. For this you would want to have an exponential growth type of interval, where you start out checking every 5 seconds but check less frequently as time passes and you don't get a response. And then, when you do get a response, you will probably be getting another one soon, so you want to reset and start checking more frequently. As long as responses keep coming, you want to display them responsively. This is just one use case. So let's take a look at how you would implement this:

var thisObj = this;
this.refreshInterval = new window.MorphInterval(
{
    minInterval: 5,
    maxInterval: 60,
    curve: 'exponentialGrowth',
    callback: function()
    {
        thisObj.doSomething.call(thisObj);
    }
}).start();

This is going to instantiate a morphing interval that starts out calling the doSomething() method every 5 seconds, but the interval exponentially increases until it flattens out at its max of 60 seconds. If desired you could also provide a rate of change as a percentage. The default is 5% (0.05). If you wanted to reset the interval, you would simply do something like this:

if (condition)
{
    this.refreshInterval.reset();
}

Motivation

I wrote this utility class a few years ago to reduce the frustration I was feeling trying to manage the native javascript intervals, which I found to be inflexible and too easy for lazy programmers to mismanage, often resulting in excessively multiplying intervals that didn't get terminated. This implementation forces OOP practices, adds useful functionality, and makes it easy to manage.

Contributors

License

GPL

/**
* Instantiates a highly flexible interval object that provides consistent and
* clean utilities to manage complex interval timer needs.
*
* By default, this creates a standard interval that refreshes every minute.
*
* @param args (object)
* minInterval (int) : (default: 15) the minimum refresh interval. If curve not set, this will be ignored
* maxInterval (int) : (default: 60) the max refresh interval. If curve not set, this will be used.
* rateOfChange (float): (default: 0.05) a fractional percentage used in some functions.
* callback (function) : (default: fn) function to execute when timer resets. By default, this does nothing.
* NOTE: caller must define the scope of the callback fn, typically by wrapping it in an anonymous fn and
* calling it with call() or apply() and passing in the scope, e.g.: callback: function() {thisObj.myCallbackFn.call(thisObj)}
* curve (string) : (default: 'flat') any of the defined curveModels:
* "flat" : a simple interval with a constant time,
* "exponentialGrowth" : the interval increases exponentially by the rateOfChange, starting with the minInterval and increasing
* to maxInterval. Once maxInterval is reached, it flattens out.
* "exponentialDecay" : interval decreases exponentially by rateOfChange, starting with maxInterval and decreasing to
* minInterval, where it then flattens out.
* "sGrowth" : interval increases on an s-curve by the rateOfChange, starting at the minInterval and flattening
* out at the maxInterval.
* "linearGrowth" : interval increases linearly by the rateOfChange, starting at minInterval, and increasing to maxInterval.
* "linearDecay" : interval decreases linearly by the rateOfChange, starting at maxInterval, and decreasing to minInterval
*/
function MorphInterval(args)
{
assert(arguments.length <= 1);
assert(arguments.length === 1 && isset(args) ? is_strict_object(args) : false, "if passed in, args must be an object");
this.minInterval = args.minInterval || 15;
this.maxInterval = args.maxInterval || 60;
assert(this.minInterval > 0);
assert(this.maxInterval >= this.minInterval);
var thisInterval = this;
this.curveModels = {
flat : function(base) { return base },
exponentialGrowth : function(base) { return base * Math.pow((1 + thisInterval.rateOfChange), ++thisInterval.count) },
exponentialDecay : function(base) { return base * Math.pow((1 - thisInterval.rateOfChange), ++thisInterval.count) },
sGrowth : function(base) { return thisInterval.maxInterval / (1 + ((thisInterval.maxInterval - base) / base) * Math.pow(Math.E, (-thisInterval.rateOfChange * ++thisInterval.count))) },
linearGrowth : function(base) { return base + thisInterval.rateOfChange },
linearDecay : function(base) { return base - thisInterval.rateOfChange }
}
this.callback = args.hasOwnProperty('callback') && $.isFunction(args.callback) ? args.callback : function(){assert(false, "callback invalid, interval has no effect.")};
this.getNewTime = isset(args.curve) ? this.curveModels[args.curve] : this.curveModels.flat;
this.id;
this.initialInterval = (isset(args.curve) && args.curve.search(/growth/i) !== -1) ? this.minInterval : this.maxInterval; // if growth, min, else max
this.rateOfChange = args.hasOwnProperty('rateOfChange') && is_number(args.rateOfChange) ? args.rateOfChange : 0.05; // 5%
this.count; // how many times interval has grown or decayed
}
window.MorphInterval = MorphInterval;
/**
* wrapper for the reset fn (more intuitive). Should only be called once at instantiation.
*
* @return (MorphInterval) returns the instance of MorphInterval. This is simply for
* convenience in chaining. For example:
*
* var myInterval = new MorphInterval({parameters:values}).start();
*
* vs.
*
* var myInterval = new MorphInterval({parameters:values});
* myInterval.start();
*/
MorphInterval.prototype.start = function()
{
var thisInterval = this;
thisInterval.reset();
return thisInterval;
}
/**
* kills the interval.
*
* After killed, call myMorphIntervalInstance.reset() with no
* parameters if need to restart.
*/
MorphInterval.prototype.kill = function()
{
var thisInterval = this;
thisInterval.id = window.clearInterval(thisInterval.id);
}
/**
* recursive function that sets up the interval.
*
* IMPORTANT: Should never be explicitly passed the lastTime parameter. This is
* done in the 2nd+ iteration(s).
*/
MorphInterval.prototype.reset = function(lastTime) // newInterval will be the value of rateOfChange when called recursively
{
assert(arguments.length <= 1);
var thisInterval = this;
// must kill the existing interval before making any change to it.
thisInterval.kill();
// decay: action == -1, growth: action == 1
// will only be set on the second and greater iterations
var newTime;
if (is_number(lastTime))
{
//thisInterval.count++;
//newTime = lastTime * Math.pow((1 - thisInterval.rateOfChange), ++thisInterval.count); // exponential decay
newTime = thisInterval.getNewTime(lastTime);
// if this is a growth model, we want to make sure that the new time is not larger than the max
if (thisInterval.initialInterval === thisInterval.minInterval) // growth
{
newTime = (newTime > thisInterval.maxInterval) ? thisInterval.maxInterval : newTime;
}
else
{
newTime = (newTime < thisInterval.minInterval) ? thisInterval.minInterval : newTime;
}
}
else
{
// if lastTime not given, is is the first call, so reset the timer at the beginning,
// which in this case is the max. Also reset the global counter
newTime = thisInterval.initialInterval;
thisInterval.count = 0;
}
var normalizedTime = Math.floor(Math.round(newTime * 1000000)/1000); // converts time to microtime by rounding to the nearest thousanth
//console.log('morphInterval time', newTime, normalizedTime, thisInterval.count);
thisInterval.id = setInterval(function(){ thisInterval.reset(newTime); thisInterval.callback(); }, normalizedTime);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment