Restrict the number of times a function can be executed concurrently.

Pablo Garcia
2 min readMar 10, 2022

--

When working with browser APIs, you might have noticed that asynchronous listeners can be consecutively executed (multiple times in a row) even if the first asynchronous callback hasn’t finished executing.

This is painfully obvious if you work with themousemoveevent but it might also not be so obvious if you work with other APIs such as the Intersection Observer API which if you scroll fast enough (and the threshold is less than 1) it can execute the listener when the element is first seen, then immediately after if the element is fully visible, and then again immediately after if the element exits the visible scrollable area.

To prevent any callback from being executed more than once, we can create a utility such as the following:

/**
* Restricts an async callback from being executed more than a fixed amount of times in a row.
*
* @param {Async Function} asyncCallback
* @param {Number} times numer of times allowed for the callback to be executed in a row
* @returns {Async Function} restricted callback
*/
export function restrictConcurrentTimes(asyncCallback, times = 1) {
let executed = 0
return async (...args) => {
if (executed >= times) {
return
}
try {
executed += 1
await asyncCallback(...args)
} finally {
executed -= 1
}
}
}

This utility has an internal counter and returns a new asynchronous callback that when executed it increases the counter and when it finalizes it decreases the counter. If the counter is greater than or equal to the allowed number of times it just skips the execution.

If you come from a computer science background, you might remember this as part of your parallel computing courses where you used locks and semaphores to prevent concurrent threads from accessing the same resources at the same time.

Notice that we use a try-finally block to make sure that the counter is decreased even if anything goes wrong while calling the original callback. Remember, async callbacks are aborted if any run-time errors occur, so we need to unblock our listeners to try again when the next event is triggered.

Usage

For the Intersection Observer API, we can do the following. Notice that we prevent the concurrent execution to one time only because we don’t want to fetch anything more than once.

new IntersectionObserver(
restrictConcurrentTimes(async ([entry], observer) => {
if (entry.isIntersecting) {
await fetch('...')
}
}),
options
)

If you are using fetch you might also be interested in preventing multiple requests by aborting a fetch request.

Any other API:

window.addEventListener(
'mousemove',
restrictConcurrentTimes(async () => {
await new Promise(r => setTimeout(r, 2000)) // or whatever
}, 2 /* concurrent executions */),
{ pasive: true }
)

--

--

Pablo Garcia
Pablo Garcia

Written by Pablo Garcia

Senior Engineer at Netflix, ex-Staff Architect 2 at PayPal. M.S. in Computer Science w/specialization in Computing Systems. B.Eng. in Computer Software.

No responses yet