setInterval that blocks on `await`
setInterval
and promises in javascript
David Walsh asks on twitter
Does anyone have a good example of chaining promises with setInterval so that multiple promises aren’t created concurrently?
Example of problem
setInterval(async () => {
const result = await someAsyncFunc();
// The problem is that `someAsyncFunc` could hang during one of the loops,
// which would allow multiple active awaits
}, 2000);
To expound on this issue:
- Suppose
someAysncFunc
takes 1m to resolve - Given this
setInterval
func,someAsyncFunc
would actually be invoked 30x! - Ideally, we want invoke
someAsyncFunc
and then not reinvoke it until the initial invocation had resolved.
^ Given my interpretation of this issue, let’s continue.
Take 1: hacking it
My solution: take advantage of js’s ability to set props on functions to track “state”
// this func will wait a random period of time from 1 - 10s
const someAsyncFunc = _ => new Promise((resolve, reject) => {
const t = Math.random()*10000;
const now = new Date()
setTimeout(_ => resolve({t, now}), t)
});
k = setInterval(async function onInterval() {
// let's recall that functions are just objects in JS
// we can track a property on the func, isRunning, that
// will tell us if we are waiting on the asyncFunc to resolve
if (onInterval.isRunning) {
// if we are still waiting for the previous func's
// promise to resolve, just exit
return
}
// if we are here, this is our first iteration OR the async func
// has resolved
onInterval.isRunning = true
const output = await someAsyncFunc()
const now = new Date()
console.log(`--------
someAsyncFunc Ran For: ${output.t},
Time now vs when someAsyncFunc resolved: ${now.getTime() - output.now.getTime()},
Time now: ${new Date()}
---------`)
onInterval.isRunning = false;
}, 1000)
Output:
--------
someAsyncFunc Ran For: 8261.615781686205,
Time now vs when someAsyncFunc resolved: 8261,
Time now: Wed Jan 19 2022 18:51:38 GMT-0500 (Eastern Standard Time)
---------
--------
someAsyncFunc Ran For: 7269.033684469877,
Time now vs when someAsyncFunc resolved: 7272,
Time now: Wed Jan 19 2022 18:51:46 GMT-0500 (Eastern Standard Time)
---------
--------
someAsyncFunc Ran For: 7857.849152060381,
Time now vs when someAsyncFunc resolved: 7850,
Time now: Wed Jan 19 2022 18:51:54 GMT-0500 (Eastern Standard Time)
---------
clearInterval(k)
undefined
--------
someAsyncFunc Ran For: 8935.12200335714,
Time now vs when someAsyncFunc resolved: 8933,
Time now: Wed Jan 19 2022 18:52:03 GMT-0500 (Eastern Standard Time)
---------
// proving clearInterval works
new Date()
Wed Jan 19 2022 18:52:18 GMT-0500 (Eastern Standard Time)
This works! Neat.
Note that:
- the
Time now
differs by ~ the magnitude of the time seen insomeAsyncFunc Ran For
- the
setInterval
actually exectutes every 1s but we avoid re-invoking the promise until is running is completed
Take 2: wrapping into a function
To make this code more easily reusable, let’s wrap into a function:
// this func will wait a random period of time from 1 - 10s
const someAsyncFunc = _ => new Promise((resolve, reject) => {
const t = Math.random()*10000;
const now = new Date()
setTimeout(_ => resolve({t, now}), t)
});
// this returns a func that tracks the "state" of
// our onInterval callback
function setIntervalWithPromise(target) {
return async function(...args) {
if (target.isRunning) return
// if we are here, we can invoke our callback!
target.isRunning = true
await target(...args)
target.isRunning = false
}
}
// Now, we can run setInterval as per usual but wrapped in
// `setIntervalWithPromise`
k = setInterval(setIntervalWithPromise(async function() {
// here, we can do as we please with promises, etc
// all BAU
const output = await someAsyncFunc()
// this is just to prove timing works as expected
const now = new Date()
console.log(`someAsyncFunc timeout: ${output.t}, Time now: ${new Date()}`)
}), 1000)
The result is the same:
someAsyncFunc timeout: 8475.130126684691, Time now: Thu Jan 20 2022 02:13:47 GMT-0500 (Eastern Standard Time)
someAsyncFunc timeout: 8176.9764095892515, Time now: Thu Jan 20 2022 02:13:56 GMT-0500 (Eastern Standard Time)
someAsyncFunc timeout: 3362.7717183204654, Time now: Thu Jan 20 2022 02:14:00 GMT-0500 (Eastern Standard Time)
someAsyncFunc timeout: 7939.80037795154, Time now: Thu Jan 20 2022 02:14:09 GMT-0500 (Eastern Standard Time)
someAsyncFunc timeout: 7984.381100145164, Time now: Thu Jan 20 2022 02:14:17 GMT-0500 (Eastern Standard Time)
someAsyncFunc timeout: 5770.4932457582945, Time now: Thu Jan 20 2022 02:14:22 GMT-0500 (Eastern Standard Time)
someAsyncFunc timeout: 1932.7454950995527, Time now: Thu Jan 20 2022 02:14:25 GMT-0500 (Eastern Standard Time)
clearInterval(k)
someAsyncFunc timeout: 7951.747669489057, Time now: Thu Jan 20 2022 02:14:33 GMT-0500 (Eastern Standard Time)
// proof that clearInterval works
new Date()
Thu Jan 20 2022 02:22:05 GMT-0500 (Eastern Standard Time)
Ta da!