Use Promise to wait until polled condition is satisfied

JavascriptPromise

Javascript Problem Overview


I need to create a JavaScript Promise that will not resolve until a specific condition is true. Let's say I have a 3rd party library, and I need to wait until a certain data condition exists within that library.

The scenario I am interested in is one where there is no way to know when this condition is satisfied other than by simply polling.

I can create a promise that waits on it - and this code works, but is there a better or more concise approach to this problem?

function ensureFooIsSet() {
    return new Promise(function (resolve, reject) {
        waitForFoo(resolve);
    });
}

function waitForFoo(resolve) {
    if (!lib.foo) {
        setTimeout(waitForFoo.bind(this, resolve), 30);
    } else {
        resolve();
    }
}

Usage:

ensureFooIsSet().then(function(){
    ...
});

I would normally implement a max poll time, but didn't want that to cloud the issue here.

Javascript Solutions


Solution 1 - Javascript

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);
		})();
    });
}

Solution 2 - Javascript

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;
}

Solution 3 - Javascript

> 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).

Solution 4 - Javascript

Here's a utility function using async/await and default ES6 promises. The promiseFunction is an async function (or just a function that returns a promise) that returns a truthy value if the requirement is fulfilled (example below).

const promisePoll = (promiseFunction, { pollIntervalMs = 2000 } = {}) => {
  const startPoll = async resolve => {
    const startTime = new Date()
    const result = await promiseFunction()

    if (result) return resolve()

    const timeUntilNext = Math.max(pollIntervalMs - (new Date() - startTime), 0)
    setTimeout(() => startPoll(resolve), timeUntilNext)
  }

  return new Promise(startPoll)
}

Example usage:

// async function which returns truthy if done
const checkIfOrderDoneAsync = async (orderID) => {
  const order = await axios.get(`/order/${orderID}`)
  return order.isDone
}

// can also use a sync function if you return a resolved promise
const checkIfOrderDoneSync = order => {
  return Promise.resolve(order.isDone)
}

const doStuff = () => {
  await promisePoll(() => checkIfOrderDone(orderID))
  // will wait until the poll result is truthy before
  // continuing to execute code
  somethingElse()
}

Solution 5 - Javascript

function getReportURL(reportID) {
  return () => viewReportsStatus(reportID)
  .then(res => JSON.parse(res.body).d.url);
}

function pollForUrl(pollFnThatReturnsAPromise, target) {
  if (target) return P.resolve(target);
  return pollFnThatReturnsAPromise().then(someOrNone => pollForUrl(pollFnThatReturnsAPromise, someOrNone));
}

pollForUrl(getReportURL(id), null);

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJoseph GabrielView Question on Stackoverflow
Solution 1 - JavascriptDenys SéguretView Answer on Stackoverflow
Solution 2 - JavascriptaljgomView Answer on Stackoverflow
Solution 3 - JavascriptBergiView Answer on Stackoverflow
Solution 4 - JavascriptLeopold WView Answer on Stackoverflow
Solution 5 - JavascriptAlex CusackView Answer on Stackoverflow