5

Summary: poll() functions with callbacks are available; I haven't found any using native promises. I've tried to adapt some without success. The problem I haven't solved yet is that when the first instance of the function called by setTimeout ends without any return, the .then() listening for it sees the termination as a false and a reject(). then() terminates and doesn't listen for later returns.

Question: How best to help the .then() function stick around for later returns with resolve() or reject()?

The rest of this post is detail. Read what helps.

Available poll functions: I like (https://stackoverflow.com/users/1249219/om-shankar) Om Shankar's response in Calling a function every 60 seconds. David Walsh's poll() is very similar (at https://davidwalsh.name/essential-javascript-functions). Both use callbacks and work well. I found poll in javascript which includes a poll() using bluebird-only promises.

Here's my attempt at implementing with native promises.

/**
 * poll - checks repeatedly whether a condition exists. When the condition
 *   exists, returns a resolved standard promise. When it has checked
 *   long enough, returns a rejected standard promise.
 * @param {function} fn - a caller-supplied synchronous function that
 *   detects a condition in the environment. Returns true if the
 *   condition exists; otherwise false.
 * @param {number} timeout - maximum number of milliseconds
 *   the caller wants to check param fn();
 *   reject() the promise at the expiration of param timeout.
 * @param {number} interval - minimum number of milliseconds between
 *   calls to param fn(); resolve() the promise when param fn() first
 *   reports true.
 * @return {promise} - resolved when param fn() returns true;
 *   rejected if param timeout expires without param fn() returning true
 */
function poll(fn, timeout, interval) {
  let endTime = Number(new Date()) + (timeout || 2000)
  interval = interval || 250
  return Promise.resolve     *2
  .then(() => {      *3
    (function p(fn, endTime, interval) {
      if (fn()) { return Promise.resolve("Condition is satisfied.") }   *4
      else {
        if (Number(new Date()) <= endTime) {)     *5
          window.setTimout(p, interval, fn, endTime, interval)    *6
        }
        else {
          return Promise.reject("Past endTime; condition not satisfied")
        }
      }
    }())     *7
  })      *8
}

Expected usage:

function waitIsOver() { return (<desired condition exists>) }
poll(waitIsOver, 2000, 250)        *1

The way I think this is running (please correct me if I'm wrong): After the call to poll() at *1, we quickly return a pending promise at *2 so that poll() knows to wait. Then, we call that promise's then() function at *3. Function p() starts. If fn() (known outside p() as waitIsOver()) returns true at *4, we're good: We return resolve() and poll() at *1 gets the settled promise it seeks.

Then the bad part: If fn() returns false at *4 and we're inside endTime at *5 (which is likely; the first call is unlikely to occur after endTime), we use setTimeout() at *6 to ask JS to make a note in the stack to instantiate another p() after interval time. After that, the first instance of p() terminates at *7. At *8, then() knows that p() terminated without returning anything and interprets the condition as returning false and reject(); with reject(), the promise is settled and can never change. However, after expiration of interval, a successor instance of p() fires up. Anything it returns is lost; the promise is settled and then() has terminated after sending execution on an unwanted path.

How do I convert an existing callback API to promises? recommends an approach with a Promise constructor, resolve() calling callback(), and reject() calling errback. I tried the technique, but I ran into the same problem of the then() function ending before I want it to. I haven't yet figured out how to make then() as patient in waiting as a callback function.

That sets up the question. Again:

Question: How best to help the .then() function stick around for later returns from resolve() or reject()?

Community
  • 1
  • 1
BaldEagle
  • 918
  • 10
  • 18
  • This is a what looks like an overly complicated question. Can you just take two paragraphs and describe what you're trying to accomplish without any regard to your current (and flawed) effort. I think a solution is way, way, way easier than you're making it, but your code is so complicated I can't see the forest from the trees to understand exactly what you are trying to accomplish. – jfriend00 Aug 05 '16 at 21:30
  • I also hope you know that promises are one-shot devices. They return a value or error exactly once so they don't by themselves work for polling results. They can be used in polling, but not by themselves. – jfriend00 Aug 05 '16 at 21:31
  • I'm trying to write a `poll()` function with native promises; I'll edit the title. Perhaps part of the motivation is learning more about native promises. I understand the one-shot nature of promises. Have you an example of something I can use to keep `.then()` open throughout a polling period? – BaldEagle Aug 05 '16 at 21:36
  • A `.then()` handler calls it's callbacks when the promise it is attached to resolves or rejects. That's it. There's no other magic than that. If you don't want it to call it's callbacks, then don't resolve the promise until you do. Still don't know what you're trying to accomplish. We're not going to try to reteach promises here. Tell us what your `poll()` function is supposed to do and we can help with an efficient implementation. – jfriend00 Aug 05 '16 at 21:46

2 Answers2

4

How best to help the .then() function stick around for later returns from resolve() or reject()

A .then() handler is called when the underlying promise is resolved or rejected. It's not called before that, ever. So, if you want to delay when the .then() handler is called, then you delay resolving or rejecting the underlying promise until the appropriate time.

As you can tell from my comments, it is hard to tell exactly what you're trying to accomplish because you don't just describe a straightforward objective you are trying to accomplish.

Given that, here's my guess at what you're trying to accomplish. A clear question could have receive an answer like this in a few minutes.

If you just want to repeatedly poll your function until it returns a truthy value or until the timeout time hits, you can do this with standard ES6 promies:

function poll(fn, timeout, interval) {
    return new Promise(function(resolve, reject) {
        // set timeout timer
        var timeoutTimer = setTimeout(function() {
            clearInterval(intervalTimer);
            reject("Past endTime; condition not satisfied");
        }, timeout);

        // set polling timer
        var intervalTimer = setInterval(function() {
            if (fn()) {
                clearTimeout(timeoutTimer);
                clearInterval(intervalTimer);
                resolve("Condition is satisfied");
            }
        }, interval);
    });
}

poll(yourFounction, 5000, 100).then(function(result) {
    // succeeded here
}).catch(function(err) {
    // timed out here
})

Or, with the Bluebird promise library, you can use its .timeout() method to do this:

function poll(fn, timeout, interval) {
    return new Promise(function(resolve, reject) {
        // set polling timer
        var intervalTimer = setInterval(function() {
            if (fn()) {
                clearInterval(intervalTimer);
                resolve("Condition is satisfied");
            }
        }, interval);
    }).timeout(timeout, "Past endTime; condition not satisfied");
}

poll(yourFounction, 5000, 100).then(function(result) {
    // succeeded here
}).catch(function(err) {
    // timed out here
})

Notice that both these schemes return a promise and then when the poll() function is done, they either call resolve or reject on that new promise and that will then trigger any .then() handlers to get called.


P.S. I should add that this all assumes your fn() is a synchronous function that returns a truthy or falsey value (which is what your code seems to presume). If your fn() is actually an asynchronous function with a callback or a promise, then that needs to be factored into the design. You would have to show us what the calling convention is for the function before we could write code to use that correctly.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • I agree with your post. It's pretty much an re-iteration of mine. It's a pretty wonky use case. I can't imagine a use case for this sort of problem. Most polling is done over the wire and will be async. I'm really curious to hear what his use case is. – Patrick Motard Aug 06 '16 at 00:34
  • That is beautiful and elegant code. It is perfect for my current project and I'll keep it in my "go-to" code collection. As to the question: I've heard, "It's not enough to write so you can be understood. You must write so you cannot be misunderstood!" I regret having failed at the latter for you. – BaldEagle Aug 06 '16 at 01:31
  • @BaldEagle - For next time, read about the [XY Problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). It's where the question describes problems with your attempted solution rather than first describing the goal/specification for what you are trying to accomplish. This makes it hard for us to offer the best solution to your actual problem or takes lots of questions. We also want to see what you have tried so that is good you included that, but if it's too far off, then it does not describe what you're actually trying to do well enough for us to offer a real solution. – jfriend00 Aug 06 '16 at 01:39
  • Fair enough. It was a good read. It applies. Also: I shouldn't assume anything. I assumed (though I didn't know I assumed) that "everybody" has a common understanding of "polling function". Maybe "lots" of people do, but had I been more clear, the question would have been better. For future readers, I added documentation to my intended function in a "common" format; much of this would have been avoided had I included that documentation in my first post. Thanks for the code and the coaching. All the best. – BaldEagle Aug 06 '16 at 02:25
2

Since you said you found polling functions that have callbacks, this basically boils down to "How do I promisify something?"

Use BluebirdJS's Promisify:

var poleYouFoundThatHasCallback = require('somePollLibrary');
var Promise = require('bluebird');
var poll = Promise.Promisify(poleYouFoundThatHasCallback);

poll.then((res) => { 
//dostuff with res here

}).catch(err => console.log(err))

You can also throw a timeout on it for shits and giggles.

Here's and example from the docs:

var Promise = require("bluebird");
var fs = Promise.promisifyAll(require('fs'));
fs.readFileAsync("huge-file.txt").timeout(100).then(function(fileContents) {

}).catch(Promise.TimeoutError, function(e) {
    console.log("could not read file within 100ms");
});
Patrick Motard
  • 2,650
  • 2
  • 14
  • 23
  • Thanks. Even closer: "How do I promisify a poll function with standard promises?" – BaldEagle Aug 05 '16 at 22:00
  • Good Bluebird solution. Thanks. Also, your code demonstrates a technique of splitting a collection of "go to' functions (poll() is one of mine) into separate small modules. Good technique. Thanks for that, too. – BaldEagle Aug 06 '16 at 16:27