Use Promise to wait until polled condition is satisfied
Here's a waitFor
function that I use quite a bit. You pass it a function, and it checks and waits until the function returns a truthy value, or until it times out.
- This is a simple version which illustrates what the function does, but you might want to use the full version, added further in the answer
let sleep = ms => new Promise(r => setTimeout(r, ms));
let waitFor = async function waitFor(f){
while(!f()) await sleep(1000);
return f();
};
Example usages:
- wait for an element to exist, then assign it to a variable
let bed = await waitFor(() => document.getElementById('bedId'))
if(!bed) doSomeErrorHandling();
- wait for a variable to be truthy
await waitFor(() => el.loaded)
- wait for some test to be true
await waitFor(() => video.currentTime > 21)
- add a specific timeout to stop waiting
await waitFor(() => video.currentTime > 21, 60*1000)
- pass it some other test function
if(await waitFor(someTest)) console.log('test passed')
else console.log("test didn't pass after 20 seconds")
Full Version:
This version takes cares of more cases than the simple version, null, undefined, empty array, etc., has a timeout, a frequency can be passed as an argument, and logs to the console what it is doing with some nice colors
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));}
/**
* Waits for the test function to return a truthy value
* example usage:
* wait for an element to exist, then save it to a variable
* let el = await waitFor(() => document.querySelector('#el_id')))
* timeout_ms and frequency are optional parameters
*/
async function waitFor(test, timeout_ms = 20 * 1000, frequency = 200) {
if (typeof (test) != "function") throw new Error("test should be a function in waitFor(test, [timeout_ms], [frequency])")
if (typeof (timeout_ms) != "number") throw new Error("timeout argument should be a number in waitFor(test, [timeout_ms], [frequency])");
if (typeof (frequency) != "number") throw new Error("frequency argument should be a number in waitFor(test, [timeout_ms], [frequency])");
let logPassed = () => console.log('Passed: ', test);
let logTimedout = () => console.log('%c' + 'Timeout : ' + test, 'color:#cc2900');
let last = Date.now();
let logWaiting = () => {
if(Date.now() - last > 1000) {
last = Date.now();
console.log('%c' + 'waiting for: ' + test, 'color:#809fff');
}
}
let endTime = Date.now() + timeout_ms;
let isNotTruthy = (val) => val === undefined || val === false || val === null || val.length === 0; // for non arrays, length is undefined, so != 0
let result = test();
while (isNotTruthy(result)) {
if (Date.now() > endTime) {
logTimedout();
return false;
}
logWaiting();
await sleep(frequency);
result = test();
}
logPassed();
return result;
}
A small variation would be to use a named IIFE so that your code is a little more concise and avoids polluting the external scope:
function ensureFooIsSet() {
return new Promise(function (resolve, reject) {
(function waitForFoo(){
if (lib.foo) return resolve();
setTimeout(waitForFoo, 30);
})();
});
}
Is there a more concise approach to this problem?
Well, with that waitForFoo
function you don't need an anonymous function in your constructor at all:
function ensureFooIsSet() {
return new Promise(waitForFoo);
}
To avoid polluting the scope, I would recommend to either wrap both in an IIFE or to move the waitForFoo
function inside the ensureFooIsSet
scope:
function ensureFooIsSet(timeout) {
var start = Date.now();
return new Promise(waitForFoo);
function waitForFoo(resolve, reject) {
if (window.lib && window.lib.foo)
resolve(window.lib.foo);
else if (timeout && (Date.now() - start) >= timeout)
reject(new Error("timeout"));
else
setTimeout(waitForFoo.bind(this, resolve, reject), 30);
}
}
Alternatively, to avoid the binding that is needed to pass around resolve
and reject
you could move it inside the Promise
constructor callback like @DenysSéguret suggested.
Is there a better approach?
Like @BenjaminGruenbaum commented, you could watch the .foo
property to be assigned, e.g. using a setter:
function waitFor(obj, prop, timeout, expected) {
if (!obj) return Promise.reject(new TypeError("waitFor expects an object"));
if (!expected) expected = Boolean;
var value = obj[prop];
if (expected(value)) return Promise.resolve(value);
return new Promise(function(resolve, reject) {
if (timeout)
timeout = setTimeout(function() {
Object.defineProperty(obj, prop, {value: value, writable:true});
reject(new Error("waitFor timed out"));
}, timeout);
Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
get: function() { return value; },
set: function(v) {
if (expected(v)) {
if (timeout) cancelTimeout(timeout);
Object.defineProperty(obj, prop, {value: v, writable:true});
resolve(v);
} else {
value = v;
}
}
});
});
// could be shortened a bit using "native" .finally and .timeout Promise methods
}
You can use it like waitFor(lib, "foo", 5000)
.