Use Promise to wait until polled condition is sati

2019-03-09 08:59发布

问题:

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.

回答1:

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


回答2:

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



回答3:

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