2

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.

Sam_Butler
  • 293
  • 2
  • 14
  • Try recursing without promises first, i.e. how you'd do it with synchronous code. And second, write your current code with promises, i.e. no passing of callbacks to `$.getJSON` and `setTimeout`, but using `then` instead. – Bergi Jul 27 '16 at 15:18

3 Answers3

3

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.

danh
  • 62,181
  • 10
  • 95
  • 136
  • I'm trying this out but there are a couple of errors I've had to correct, I'll comment again when I've debugged. I'm getting result undefined on recursion so I'm looking up how and what the getJSON() deferred object returns so I can suggest corrections for future readers of this answer. – Sam_Butler Jul 28 '16 at 14:30
  • Doc says promise in version >=1.5 "As of jQuery 1.5, all of jQuery's Ajax methods return a superset of the XMLHTTPRequest object. This jQuery XHR object, or "jqXHR," returned by $.getJSON() implements the Promise interface, giving it all the properties, methods, and behavior of a Promise" – danh Jul 28 '16 at 14:34
  • You're right, it's a promise. I'll edit your answer with corrections, a couple of typos. But the result was undefined because you've employed .then() rather than .done() and the conditional only checks for result.status, which would only be returned once getJSON is actually done, i.e. the API has responded. I won't change that here in case you've got a reason to use .then() that I'm not aware of. Thanks for your help. – Sam_Butler Jul 28 '16 at 14:40
  • Nope, I must be missing something. The caller is `checkTransaction(trxid).then(function(result){ ... });` -- I'm logging something in there, and I've added some logging to `checkTransaction()` but it's returning to the caller when the trx is still pending. Still mashing my head. – Sam_Butler Jul 28 '16 at 15:27
  • Oooh. I think that's my bad. Try `return wait(3000).then(function() { return checkTransaction(trxid); });`. – danh Jul 28 '16 at 15:36
  • No, that's still not working. I get the idea it's more to do with the `checkTransaction()` function. At what point does it resolve? I want it to resolve when `result` is returned, so caller.then() will execute the next stage (it's a multi-stage setup action). – Sam_Butler Jul 28 '16 at 15:57
  • Okay, but that missing return is definitely required. Edited. And I see what you're going for, and I think the solution outlines it: checkTransaction's job is to return a promise to do some http work, which is really a promise to repeat that work under some (pending) condition. – danh Jul 28 '16 at 16:07
  • Hey - I've got to start my day job, but it occurred to me that the idea could be made clearer by decomposing the tasks a little differently. Rather than clutter this answer, consider the following at paste bin. http://pastebin.com/vycaUJtK – danh Jul 28 '16 at 16:37
1

"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>
Community
  • 1
  • 1
Michael Bromley
  • 4,792
  • 4
  • 35
  • 57
  • 1
    Avoid the [deferred antipattern](http://stackoverflow.com/q/23803743/1048572)! There is indeed a bug in there. – Bergi Jul 27 '16 at 16:23
  • Since he was already using jQuery, I opted to use the jQuery implementation of promises. Is there a way to do A+ promises without a separate promise lib or ES6? – Michael Bromley Jul 27 '16 at 16:56
  • The antipattern is not *that* you are using deferreds, it is how you are using them. – Bergi Jul 27 '16 at 17:21
  • 1
    I assume then the answer by danh above is a better approach? The promisifying of the setTimeout is what I was missing, which allows composing & chaining the whole thing. – Michael Bromley Jul 27 '16 at 19:01
  • Yes, exactly that's the point :-) And your current solution does not propagate errors, which you'd get for free when composing via `then`. – Bergi Jul 27 '16 at 19:03
  • Learned something new there! – Sam_Butler Jul 28 '16 at 14:40
  • Check the comments above though, I'm still not getting anywhere because `caller.then(function() { ..thisStuffHere... });` is executing before the transaction has returned completed. – Sam_Butler Jul 28 '16 at 15:44
  • 1
    See updated answer - presenting both approaches, an explanation of why the jQuery one does not work, plus working code snippets! Thanks @Bergi, I learned something from this question! – Michael Bromley Jul 29 '16 at 07:39
  • @MichaelBromleyyour edit actually better understands the use case, I'm going to try with Bluebird and see how this sits in my application. – Sam_Butler Jul 29 '16 at 10:12
  • 1
    This definitely works, nice work on improving your answer too, I guess we all learned something today! – Sam_Butler Jul 29 '16 at 13:08
0

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

Bamieh
  • 10,358
  • 4
  • 31
  • 52