javascriptnode.jspromisebluebird

Promise.some() with a timeout?


Use case - get a bunch of urls and cache the results. The ones that complete fast (say 500ms) get incorporated on this pass, any that take longer than that still complete and get saved to cache so next time round (~10 mins later in our app) they are in cache .

I'd like to be able to do something like Promise.someWithTimeout(promises, timeout) where the result is an array of resolved values that completed before the timeout expired. (Promise.some() is really close by it takes a count not a timeout)

Something like

Promise.someWithTimeout([promises], <timeout value>)
.then((results) => {
  //stuff
})

Where each of the promises looks like

return memcache.getAsync(keyFromUrl(url))
.then((cacheRes) => {
  if(cacheRes) return cacheRes;
  //not in cache go get it
  return rp(url)
  .then((rpRes) => {
    //Save to cache but don't wait for it to complete
    memcache.setAsync(keyFromUrl(url), rpRes, 86400).catch((e) => { /*error logging*/ }) 
    return rpRes;
  })
})

The idea being that if the url comes back fast we use it, if it takes longer we bail but still cache the result so next time through we have it. So the rp() timeout would be much longer than the Promise.someWithTimeout() one

Has anybody written a lib to do this (I can't find one), or is there a better pattern? I though about using Promise.all().timeout().finally() with the promises stashing their results into an array but it doesn't feel right for reason I can't quite put my finger on.


Solution

  • For one-off coding, you can start two separate promise chains and use a flag to see if a timeout already happened:

    var timedOut = false;
    // your regular code
    Promise.all(...).then(function() {
        // cache the result, even if it came in after the timeout
    
        if (!timedOut) {
           // if we didn't time out, then show the response
        }
    });
    
    Promise.delay(1000).then(function() {
        timedOut = true;
        // do whatever you want to do on the timeout
    });
    

    If you want to package this functionality into one method and have the ability to access to both the fact that it timed out AND to the eventual results even after the timeout (so you could cache them, for example), then you have to resolve with something that has more than one piece of info in it. Here's one way to allow access to both the timeout and the eventual data by resolving with an object that can contain more than one piece of info.

    Promise.someWithTimeout = function(promiseArray, cnt, timeout) {
        var pSome = Promise.some(promiseArray, cnt).then(function(results) {
            return {status: "done", results: results};
        });
        var pTimer = Promise.delay(timeout).then(function() {
            return {status: "timeout", promise: pSome}
        });
        return Promise.race([pSome, pTimer]);
    }
    
    // sample usage
    Promise.someWithTimeout(arrayOfPromises, 3, 1000).then(function(result) {
        if (result.status === "timeout") {
            // we timed out
            // the result of the data is still coming when result.promise is resolved if you still want access to it later
            result.promise.then(function(results) {
                // can still get access to the eventual results here 
                // even though there was a timeout before they arrived
            }).catch(...);
        } else {
            // result.results is the array of results from Promise.some()
        }
    });