2

I have a web page that can submit an unlimited number of items back to the server for processing.

I've decided to submit these items in groups of 25 using AJAX calls to a web service. So my loop looks something like this:

// Submit elements
for (var i = 0; i < ids.length; i += 25) {

    var productIds = ids.slice(i, Math.min(i + 25, ids.length - 1));

    $.post('/Services/ImportProducts.asmx/ImportProducts', JSON.stringify({ importProductIds: productIds }))
    .done(function (result, statusText, jqxhr) {

        // TODO: Update progress

    })
    .always(function () {
        // TODO: Test for error handling here
    });
}

So far, this seems right. However, when all processing is complete, I want to refresh the page. And given the code above, I'm not seeing an easy way to perform a task when the last AJAX call has completed.

Since $.post() is asynchronous, this loop will complete before the AJAX calls have. And since the AJAX calls could complete in a different order than they were submitted, I can't simply test for when my last-submitted call is done.

How do I know when this code is done?

Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
  • possible duplicate of [Javascript, jQuery multiple AJAX requests at same time](http://stackoverflow.com/questions/19571323/javascript-jquery-multiple-ajax-requests-at-same-time) – Populus Aug 25 '14 at 18:07

4 Answers4

6

You can do this utilizing jQuery's promises. The general workflow involves you adding each of your promises to an array, then applying that array using jQuery when to execute another callback when all of the promises have returned.

Something like this should work:

var promises = []
for (var i = 0; i < ids.length; i += 25) {

    var productIds = ids.slice(i, Math.min(i + 25, ids.length - 1));

    var promise = $.post('/Services/ImportProducts.asmx/ImportProducts', JSON.stringify({ importProductIds: productIds }))
    .done(function (result, statusText, jqxhr) {

        // TODO: Update progress

    })
    .always(function () {
        // TODO: Test for error handling here
    });

    promises.push(promise);
}

/*
    Note, the "apply" function allows one to unwrap an array to parameters for a
    function call. If you look at the jQuery.when signature, it expects to be 
    called like: $.when(promise1, promise2, promise3). Using .apply allows you 
    to satisfy the signature while using an array of objects instead.

    See MDN for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
*/
$.when.apply($, promises)
    .done(function() {
        console.log("All done!") // do other stuff
    }).fail(function() {
        // something went wrong here, handle it
    });
klyd
  • 3,939
  • 3
  • 24
  • 34
  • Thanks, but can you explain what could cause the `fail()` handler to be called? I'm handling errors already but planned to continue processing after an error. Would one error cause `when().done()` to not be called? – Jonathan Wood Aug 25 '14 at 18:24
  • From the documentation on `when` it looks like if any one fails, call the fail callback: "In the multiple-Deferreds case where one of the Deferreds is rejected, jQuery.when immediately fires the failCallbacks for its master Deferred." If you always want to fire something after the `.when` just use `.always` – klyd Aug 25 '14 at 18:27
  • One more thing to add. `$.when` returns a promise, which is the same thing that `$.ajax` returns, all methods `done`, `fail`, `then`, `always` etc are valid for the new `when` promise. – klyd Aug 25 '14 at 18:34
  • One more thing. :) Do you know anything about how this works? What is the meaning of `$.when().done()`, for example, in relationship to all of the `promise.done()`? How does the array of object methods map to the methods of a single object? – Jonathan Wood Aug 25 '14 at 19:08
  • When in doubt: https://github.com/jquery/jquery/blob/master/src/deferred.js#L99 A `Deferred` (the master) is returned, then a listener is added to each passed child. When they're all done it resolves the Deferred (which will then fire the `done`, `failed`, etc callbacks). For the difference between a deferred/promise check out [this question](http://stackoverflow.com/questions/6801283/what-are-the-differences-between-deferred-promise-and-future-in-javascript) Here's the deferred docs: http://api.jquery.com/category/deferred-object/ – klyd Aug 25 '14 at 19:21
  • Sorry to beat a dead horse. You've provided lots of helpful info and I've marked your answer as the correct one. But reading the source didn't clear things up for me. Specifically, each time `$.post().done()` is called, there are values passed as arguments. So how are all those arguments passed to a single occurrence of `$.when().done()`? – Jonathan Wood Aug 25 '14 at 19:55
  • Check this line: https://github.com/jquery/jquery/blob/master/src/deferred.js#L146 It dumps each of the resolved values in to an array, and then passes that to the done callback. – klyd Aug 25 '14 at 20:01
  • Ok, so the arguments are different (arrays instead of the original values). That's definitely what I was after. Thanks. – Jonathan Wood Aug 25 '14 at 20:04
4

You'd do that by pushing all the promises in an array, and using $.when to determine when they are all done

var ajaxArr = [];

for (var i = 0; i < ids.length; i += 25) {

    var productIds = {
        importProductIds : ids.slice(i, Math.min(i + 25, ids.length - 1))
    }

    ajaxArr.push(
        $.post('/Services/ImportProducts.asmx/ImportProducts', productIds, function(result, statusText, jqxhr) {
            // TODO: Update progress
        })
    );
}


$.when.apply(undefined, ajaxArr).done(function() {

    window.location.href = 'something'

});
adeneo
  • 312,895
  • 29
  • 395
  • 388
2

You could set a count that is equal to the number of loops that you have to process and then on returning from the ajax request, decrement the counter and check for it being zero. When it reaches zero you can refresh the page

var remains = ids.length;

// Submit elements
for (var i = 0; i < ids.length; i += 25) {

    var productIds = ids.slice(i, Math.min(i + 25, ids.length - 1));

    $.post('/Services/ImportProducts.asmx/ImportProducts', JSON.stringify({ importProductIds: productIds }))
    .done(function (result, statusText, jqxhr) {

        // TODO: Update progress

    })
    .always(function () {

       // TODO: Test for error handling here

       remains--;
       if ( remains===0 ) {

             // TODO: refresh here
       }
    });
}
Code Uniquely
  • 6,356
  • 4
  • 30
  • 40
0

Just count how many callbacks are done; while callbacks are asynchronous, the code is run in a single thread, so they will be called sequentially, and you would be safe to assume that when count == 25 you're done.

Jaime Gómez
  • 6,961
  • 3
  • 40
  • 41
  • I didn't quite understand this. Since the callbacks are asynchronous, they won't necessarily return in order since some calls could potentially take longer than others. So while I know when all the requests have been made, I don't know when those calls have returned. And 25 is the number of items to be processed per callback. The number of callbacks is unlimited. – Jonathan Wood Aug 25 '14 at 18:10
  • Order doesn't seem to matter in your case, you just want to do something once every callback is completed right? So you could count how many calls are made, and then when that many callbacks are invoked, you're done. Just declare those variables outside of the for loop :) – Jaime Gómez Aug 25 '14 at 18:14
  • Yes, but I don' just need to know when all the requests have been made. That's very easy. I need to know when all requests have been processed. – Jonathan Wood Aug 25 '14 at 18:20
  • That's why I'm saying to count when the **callback** has been invoked, that is when the request returned. In your case, when `done` is called. – Jaime Gómez Aug 25 '14 at 18:27