Dani
Dani

Reputation: 63

Can I throttle a function without dropping function calls in JS?

I need a function to have certain amount of time between one function call and another, very similar to what a throttle would do, but I need all the function calls to be attended.

Let me explain:

Let's imagine we have a throttle of 10 seconds on a function foo, and an event that calls foobar. The event is fired 3 times, one every 5 seconds. Only the first one and the last one would be attended while the one in the middle would be ignored.

Normal throttle:

[event]--5s--[event]--5s--[event]
[foo]---------10s---------[foo]

What I need is that the event is always listened, but 10 seconds after the previous one.

[event]--5s--[event]--5s--[event]
[foo]----------10s--------[foo]---------10s--------[foo]

Any ideas? I thought about throttle and also debouncer but none of them would fit what I need to do.

Upvotes: 2

Views: 1027

Answers (3)

a cat
a cat

Reputation: 854

I solved it like this. It's basically a normal throttle, but it keeps track when the function is called while it's in cooldown and keeps executing it on schedule as long as that's happening.

function throttle(expensiveFunction, delay = 1000) {
    let cooldown = false;
    let calledDuringCooldown = false;
    
    return function () {
        if (cooldown) {
            // Function call suppressed, make note
            calledDuringCooldown = true;
        } else {
            // Function is run and enters cooldown
            cooldown = true;
            expensiveFunction();
            
            // Attempt to remove cooldown after the delay
            setTimeout(function nextTick() {
                if (calledDuringCooldown) {
                    calledDuringCooldown = false;

                    // Call the function again and stay in cooldown
                    expensiveFunction();
                    setTimeout(nextTick, delay);
                } else {
                    // Function was not called again,
                    // so remove the cooldown
                    cooldown = false;
                }
            }, delay);
        }
    };
}


// Example usage
function timid() {
    console.log(performance.now(), "I don't want to be called very often...");
}

const throttled = throttle(timid, 2000);
// Should be called only twice, with a 2s delay
throttled();
throttled();
throttled();

Upvotes: 1

Dani
Dani

Reputation: 63

In case anyone's interested, I used Alex Wayne's answer and added a bit of logic to have a better control on the interval as he mentioned.

const queue = []
var intervalExecution

function rateLimit(fn) {
  queue.push(fn)
  if (queue.length && !intervalExecution) {
    intervalExecution = setInterval(() => {
      const nextFn = queue.shift()
      if (nextFn) {
        nextFn()
      } else {
        clearInterval(intervalExecution)
        intervalExecution = undefined
      }
    }, 1000)
  }
}

rateLimit(() => console.log('test'))
rateLimit(() => console.log('test'))
rateLimit(() => console.log('test'))
rateLimit(() => console.log('test'))

Upvotes: 1

Alex Wayne
Alex Wayne

Reputation: 187262

This is pretty simple to do. Just create an array of function objects, and pull the oldest one off every 10 seconds and execute it.

    const queue = []
    
    function rateLimit(fn) {
      queue.push(fn)
    }
    
    setInterval(() => {
      const nextFn = queue.shift() // remove the first function, and return it.
      if (nextFn) nextFn() // execute the first function if it exists.
    }, 1000) // Your interval in milliseconds. Set to 1s for testing.
    
    
    rateLimit(() => console.log('A'))
    rateLimit(() => console.log('B'))
    rateLimit(() => console.log('C'))

You could add some logic to clear the interval when the queue is empty, and start it up again fresh when a new item comes in so that the first call always happens immediately if it's not waiting. But I leave that an exercise for you.

Upvotes: 2

Related Questions