2
\$\begingroup\$

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 call doSomething, 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);

Equivalent jsBin

I would love to know if there is a way to improve my solution.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

This is called a debounce function, and you are doing it ok.

Here are some small improvements I can think of. Nothing big:

  1. Initialise lastInvokedTimestamp, so that it'll never be undefined.
  2. Cache current time.
  3. Track the threshold of next call instead, so that it becomes one addition per interval instead of one subtraction per entrance.
  4. Pass arguments into the debounced function.
  5. 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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.