Sam_Butler
Sam_Butler

Reputation: 303

How can I return a promise from a recursive request and resolve it when the data matches a condition?

I'm consuming an API that returns JSON, and on this page I have a progress bar indicating various steps toward setting something up at the user's request. Each subsequent AJAX request's success callback initiates the next request, because they need to be done in sequence. One step issues a server-side background job and the endpoint returns a transaction ID.

Outside this flow there is a function that checks another endpoint to see if this transaction is complete or not. If it's "pending", I need to reissue the request after a small delay.

I had this working with a recursive function:

function checkTransaction(trxid) {
    window.profileTrx[trxid] = 0;
    trxurl = 'https://example.com/transaction/'+trxid;
    $.getJSON(trxurl,function(result) {
        if(result.status === 'pending') {
            setTimeout(function () {
                checkTransaction(trxid);
            },3000);
        } else {
            window.profileTrx[trxid] = result;
        }
    });
}

The reason I was using window is so I could access the transaction by its ID in the callback it came from - a good use case for a promise if ever there were one. But it got messy, and my lack of experience began to get in my way. Looping over the state of window.profileTrx[trxid] seemed like double work, and didn't behave as expected, looping too quickly and crashing the page. Again, a promise with the next step in .then() was my idea, but I can't figure out how.

How could I implement this with promises such that the callback function that initiated the recursive "transaction check" would only continue with the rest of its execution once the API returns a non-pending response to the check?

I could get my head round recursing, and returning a promise, but not both at once. Any and all help massively appreciated.

Upvotes: 2

Views: 1079

Answers (3)

Michael Bromley
Michael Bromley

Reputation: 4822

"deferred"-based solution (not recommended)

Since you are using jQuery in your question, I will first present a solution that uses jQuery's promise implementation based on the $.Deferred() object. As pointed out by @Bergi, this is considered an antipattern.

// for this demo, we will fake the fact that the result comes back positive
// after the third attempt.
var attempts = 0;

function checkTransaction(trxid) {
  var deferred = $.Deferred();
  var trxurl = 'http://echo.jsontest.com/status/pending?' + trxid;

  function poll() {
    console.log('polling...');
    // Just for the demo, we mock a different response after 2 attempts.
    if (attempts == 2) {
      trxurl = 'http://echo.jsontest.com/status/done?' + trxid;
    }
    
    $.getJSON(trxurl, function(result) {
      if (result.status === 'pending') {
        console.log('result:', result);
        setTimeout(poll, 3000);
      } else {
        deferred.resolve('final value!');
      }
    });

    // just for this demo
    attempts++;
  }

  poll();

  return deferred.promise();
}

checkTransaction(1).then(function(result) {
  console.log('done:', result)
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

This should work (run the snippet to see), but as mentioned in the linked answer, there are issues with this "deferred" pattern, such as error cases not being reported.

The issue is that jQuery promises (until possibly recent versions - I've not checked) have massive issues that prevent better patterns from being used.

Another approach would be to use a dedicated promise library, which implements correct chaining on then() functions, so you can compose your function in a more robust way and avoid the "deferred" antipattern:

Promise composition solution (better)

For real promise composition, which avoids using "deferred" objects altogether, we can use a more compliant promise library, such as Bluebird. In the snippet below, I am using Bluebird, which gives us a Promise object that works as we expect.

function checkTransaction(trxid) {
  var trxurl = 'http://echo.jsontest.com/status/pending?' + trxid;

  var attempts = 0;

  function poll() {
    if (attempts == 2) {
      trxurl = 'http://echo.jsontest.com/status/done?' + trxid;
    }
    attempts++;
    console.log('polling...');
    
    // wrap jQuery's .getJSON in a Bluebird promise so that we
    // can chain & compose .then() calls.
    return Promise.resolve($.getJSON(trxurl)
      .then(function(result) {
        console.log('result:', result);
        if (result.status === 'pending') {
          
          // Bluebird has a built-in promise-ready setTimeout 
          // equivalent: delay()
          return Promise.delay(3000).then(function() {
            return poll();
          });
        } else {
          return 'final value!'
        }
      }));
  }

  return poll();
}

checkTransaction(1).then(function(result) {
  console.log('done:', result);
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.4.1/bluebird.min.js"></script>

Upvotes: 1

danh
danh

Reputation: 62676

My head is always clearer when I factor out promises first:

// wrap timeout in a promise
function wait(ms) {
    var deferred = $.Deferred();
    setTimeout(function() {
        deferred.resolve();
    }, ms);
    return deferred.promise();
}

// promise to get a trxid
function getTRX(trxid) {
    var trxurl = 'https://example.com/transaction/'+trxid;
    return $.getJSON(trxurl);
}

Now the original function seems easy...

function checkTransaction(trxid) {
    window.profileTrx[trxid] = trxid;
    return getTRX(trxid).then(function(result) {
        if (result.status === 'pending') {
            return wait(3000).then(function() {
                return checkTransaction(trioxid);
            });
        } else {
            window.profileTrx[trxid] = result;
            return result;
        }
    });
}

The caller will look like this:

return checkTransaction('id0').then(function(result) {
    return checkTransaction('id1');
}).then(function(result) {
    return checkTransaction('id2');
}) // etc

Remember, if the checkTransaction stays pending for a very long time, you'll be building very long chains of promises. Make sure that the get returns in some very small multiple of 3000ms.

Upvotes: 3

Bamieh
Bamieh

Reputation: 10906

You can return promises from functions, and the .then of the parent function will resolve when all the returned promises are resolved.

check this out for full details.

https://gist.github.com/Bamieh/67c9ca982b20cc33c9766d20739504c8

Upvotes: 0

Related Questions