Problem statement:
We have a number of functions in our web application that are getting called too frequently. We'd like you to create a function that, given a function and a time interval, returns a new function that we can call as often as we like, but ensures that the original function is never called more than once per interval.
Example usage:
limitedDoSomething = rateLimit(doSomething, 500);
Now calls to
limitedDoSomething
will simply calldoSomething
, but never more than once every 500 milliseconds.
Test Cases:
function logHello(){ // just a sample function
console.log('Hello!');
}
limitedDoSomething = rateLimit(logHello, 500);
limitedDoSomething(); // should log "Hello!"
window.setTimeout(function(){
limitedDoSomething(); // should not log "Hello!"
}, 400);
window.setTimeout(function(){
limitedDoSomething(); // should log "Hello!"
}, 500);
My solution:
function rateLimit(func, limit){
var lastInvokedTimestamp;
return function(){
if(typeof lastInvokedTimestamp === 'undefined' || Date.now() - lastInvokedTimestamp >= limit){
lastInvokedTimestamp = Date.now();
console.log('go ahead!');
return func();
} else {
console.log('too early!');
return;
}
};
}
// run test cases
function logHello(){ // just a sample function
console.log('Hello!');
}
limitedDoSomething = rateLimit(logHello, 500);
limitedDoSomething(); // should log "Hello!"
window.setTimeout(function(){
limitedDoSomething(); // should not log "Hello!"
}, 400);
window.setTimeout(function(){
limitedDoSomething(); // should log "Hello!"
}, 500);
I would love to know if there is a way to improve my solution.
1 Answer 1
This is called a debounce function, and you are doing it ok.
Here are some small improvements I can think of. Nothing big:
- Initialise
lastInvokedTimestamp
, so that it'll never beundefined
. - Cache current time.
- Track the threshold of next call instead, so that it becomes one addition per interval instead of one subtraction per entrance.
- Pass arguments into the debounced function.
- Simplify test cases's
setTimeout
.
function rateLimit ( func, interval ) {
var nextInvokeTimestamp = 0;
return function(){
var now = Date.now();
if ( now < nextInvokeTimestamp ) {
return console.log('too early!');
}
nextInvokeTimestamp = now + interval;
console.log('go ahead!');
return func.apply( this, arguments );
};
}
// run test cases
function logHello( message ){
console.log( message || 'Hello!' );
}
limitedDoSomething = rateLimit( logHello, 500 );
limitedDoSomething( "Welcome!" ); // should log "Welcome!"
setTimeout( limitedDoSomething, 400 ); // should not log "Hello!"
setTimeout( limitedDoSomething, 500 ); // should log "Hello!"
You may notice that David Walsh is doing it quite differently.
- Using
setTimeout
defers the actual call to a later event loop, instead of current event, to let things settle down a bit. - Because of the delay (which can be as low as 0, i.e. 4ms), he can call with the latest arguments in the interval, for example the latest mouse move. A bigger interval lets you catch more calls, with a slower respond.
It is not better or worse; it's just a different speed bump for different needs. If your requirement is simply "reduce multiple calls into one", you are doing fine.