Ernesto Ernesto - 5 months ago 5
Javascript Question

Keeping track and progress of various ajax post requests

I'm using jQuery to make various ajax POST requests. I need to keep track of the success or failure of each one of them, along with the overall progress of the complete batch, so that I can update the UI with a progress bar and info about how many requests have succeeded, and how many have failed, out of the total.

Before attempting to implement the feature in my app, I've been playing with some code in jsfiddle as a proof of concept, with no luck so far. This is what I've got:

// an alternative to console.log to see the log in the web page
var fnLog = function(message) {
$('#console').append($("<p>" + message + "</p>"));
};

// keeping track of how many ajax calls have been finished (successfully or not)
var count = 0;

// a dummy ajax call that succeeds by default
var fn = function(shouldFail) {
return $.get(shouldFail ? '/echo/fail/' : '/echo/json/')
.done(function() { fnLog("done") })
.fail(function() { fnLog("FAIL") });
};

// a set of different asynchronous ajax calls
var calls = [fn(),fn(),fn(),fn(true),fn(),fn()];

// an attempt to make a collective promise out of all the calls above
$.when.apply($, calls)
.done(function() { fnLog("all done") })
.fail(function() { fnLog("ALL FAIL") })
.always(function() { fnLog("always") })
.progress(function(arg) { fnLog("progress" + arg) })
.then(function() { fnLog("finished") });


It's all in this fiddle: http://jsfiddle.net/mmtbo7v6/1/

What I need is the ability to provide a callback that ought to be called after all promises are resolved (either successfully or not).

When all calls above are set to succeed (by removing the
true
argument to the fourth
fn
call in the array) it works fine. The output prints the following:

done
done
done
done
done
done
all done
always
finished


But when even a single call is set to fail (as it is by default in the jsfiddle), the output is the following:

done
FAIL
ALL FAIL
always
done
done
done
done


So none of the collective promise callbacks (the one generated by the
$.when
call) is called after all promises are resolved. The final
.then
is not called at all if a single ajax call fails.

Additionally, I would appreciate some insight on how to keep track of the progress of this batch of ajax calls, to update a progress bar in the UI.

Answer

Well... I'm going to be unfair. jQuery actually comes bundled with progression events but I myself hate them because I don't think they compose or aggregate well - so I'll show a simpler alternative approach for that progress bar that I believe is superior instead.

First thing's first:

The 'all promises resolved but some possibly rejected' issue is called a 'settle' typically. I've provided an answer to a similar question here with just giving the results and here providing an implementation that gives you access to all results even rejected ones.

 function settle(promises){
     var d = $.Deferred();
     var counter = 0;
     var results = Array(promises.length);
     promises.forEach(function(p,i){ 
         p.then(function(v){ // add as fulfilled
              results[i] = {state:"fulfilled", promise : p, value: v};
         }).catch(function(r){ // add as rejected
              results[i] = {state:"rejected", promise : p, reason: r};
         }).always(function(){  // when any promises resolved or failed
             counter++; // notify the counter
             if (counter === promises.length) {
                d.resolve(results); // resolve the deferred.
             }
         });
     });
     return d.promise();
 }

You'd use settle in place of $.when to get your desired results.

As for progression - I personally recommend passing a progression callback to the method itself. The pattern goes something like this:

function settle(promises, progress){
     progress = progress || function(){}; // in case omitted
     var d = $.Deferred();
     var counter = 0;
     var results = Array(promises.length);
     promises.forEach(function(p,i){ 
         p.then(function(v){ // add as fulfilled 
              results[i] = {state:"fulfilled", promise : p, value: v};
         }).catch(function(r){ // add as rejected
              results[i] = {state:"rejected", promise : p, reason: r};
         }).always(function(){  // when any promises resolved or failed
             counter++; // notify the counter
             progress((promises.length - counter) / promises.length);
             if (counter === promises.length) {
                d.resolve(results); // resolve the deferred.
             }
         });
     });
     return d.promise();
 }

Which would let you do something like:

settle([url1, url2, ... url100].map($.get), function(soFar){
    $("#myProgressBar").css("width", (soFar * 100)+"%");
}).then(function(results){
    console.log("All settled", results);
]);