2

I need to keep calling remote API until I get the response I need, and I would like to use the Official A+ promises in node.js. Sync psudo code:

params = { remote api call params }
while (true) {
    result = callRemoteApi(params)
    if isGood(result) {
        onSuccess(result)
        break
    }
    params = modify(params)
}

I am using the request-promise lib for requests, so the result might be something like this:

new Promise(function (resolve, reject) {
    var task = request({params})
        .then(function (result) {
            if (isGood(result)) {
                resolve(result);
            } else {
                task = request({new params}).then(this_function);
            }
        });

P.S. This is very similar to https://stackoverflow.com/a/17238793/177275, but I would like a non-q-based implementation.

2 Answers 2

6

Something like this should work well:

var remoteApiCall = function(num){
    // fake function to resolve promise if random number in range
    return new Promise(function(resolve, reject){
        return ((Math.random()*10) < num)
            ? resolve(true)
            : reject(false);
    })
}

function getUntil(num){
    return remoteApiCall(num).then(function(result){
        if (result) {
            return result  
        } else {
            // call again until you get valid response
            return getUntil(num)
        }
    })
}

getUntil(num).then(...)
Sign up to request clarification or add additional context in comments.

4 Comments

Nice solution and more generally - thanks for helping with the promise tag. Note that the nice thing about this solution is that it's platform agnostic.
You are rejecing with undefined while the calling code simply expects a resolution with falsy value
@BenjaminGruenbaum thanks and my pleasure... just trying pay it forward the help I have received from all of you
@Esailija THANKS.. I have cleaned up :)
2

The following solution addresses a few specific problems:

  • how to break the (otherwise endless) loop
  • how to access the result of a previously failed attempt
  • how to incorporate your params = modify(params)

The problem can be broken down into two sections:

  • a repeater/promise runner (that itself returns a promise so we can hook onSuccess to it)
  • a promise provider - a function that creates promises for the repeater

The repeater would look like this:

function repeatUntilSuccess(promiseProvider) {
    return new Promise(function (resolve, reject) {
        var counter = 0;
        function run(failedResult) {
            var p = promiseProvider(failedResult, counter++);
            if (p instanceof Promise) {
                p.then(resolve).catch(run);
            } else {
                reject(p);
            }
        }
        run();
    });
}

The "loop" happens in this line: p.then(resolve).catch(run);. The repeater keeps calling the promise provider until the promise it returns resolves (in which case the repeater resolves) or until it no longer provides a promise (in which case the repeater rejects).

A promise provider can be any function(previousResult, counter) that returns a promise (or not, if you wish to stop the loop).

var params = {/* ... */};
function nextRequest(previousResult, counter) {
    if (counter >= 10) return "too many attempts";
    if (previousResult) params = modify(params);
    return apiRequest(params);
}

(This setup assumes that apiRequest() returns a promise)

Now you can do this:

repeatUntilSuccess(nextRequest).then(onSuccess).catch(onError);

Since your question includes the side-task of wrapping an HTTP request in a promise:

function apiRequest(params) {
    return new Promise(function (resolve, reject) {
        return request(params).then(function (result) {
            if (isGood(result)) {
                resolve(result);
            } else {
                reject(result);
            }
       });
    });
}

Open the browser console and run the following snippet to see it in action.

function repeatUntilSuccess(promiseProvider) {
    return new Promise(function (resolve, reject) {
        var counter = 0;
        function run(failedResult) {
            var p = promiseProvider(failedResult, counter++);
            if (p instanceof Promise) {
                p.then(resolve).catch(run);
            } else {
                reject(p);
            }
        }
        run();
    });
}

// mockup promise that resoves or rejects randomly after a timeout
function randomPromise(num){
    return new Promise(function(resolve, reject){
        setTimeout(function () {
            var randomNum = Math.floor(Math.random() * num * 10);
            if (randomNum < num) {
                resolve(randomNum);
            } else {
                reject(randomNum);
            }
        }, 300);
    });
}

// promise provider
function nextPromise(prev, i) {
    if (prev) console.info("failed attempt #" + i + ": " + prev);
    if (i >= 5) return "too many attempts:" + i;
    return randomPromise(100);
}

// run it!
repeatUntilSuccess(nextPromise).then(function (result) {
    console.log("success", result);
}).catch(function (result) {
    console.log("failed", result);
});

8 Comments

@BenjaminGruenbaum I disagree. My code looks like that, it isn't that. (Okay, apiReques() could be made marginally simpler, but the rest?)
1) Unfortunately your last version in that Gist isn't equivalent anymore to my original idea. 2) I don't understand how Promise.resolve() as a way to create a new promise is better than new Promise(). It's fewer lines, but is it better? (I do understand that Promise.resolve(p) is nicer, though.) 3) I don't subscribe to the idea that rejected promises are like exceptions. If a promise encapsulates a process that can fail naturally (like setting a value and getting back a "not allowed") then I would expect that promise to reject, not resolve with a falsey value.
Maybe I'm wrong with my last point. Assuming I talk to an HTTP API in order to set the value foo to 5. That request can fail on multiple levels. It can fail physically (timeout), it can fail on server (404 or 500), it can fail in the application (access denied). I would expect a rejected promise in every one of these cases, because the promise was to set the value foo to five, and it didn't work. Resolving the promise with "false" for the "access denied" case does not feel right.
Well, when properly abstracted, setFoo(5).then(joy).catch(noJoy) wouldn't even expose the fact that HTTP is involved. When the server replies "no" that's not an HTTP error, but but it still means noJoy for me. Right?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.