3

I would like to secure my code to make sure several Ajax requests triggered by user actions in a certain order will have their responses handled in the same order.

I want to keep the asynchonous mechanism of Ajax. Just secure my code to avoid out of sequence responses that can lead to mess on my web page.

To make it perfectly clear. Let's take an example:

$('#button1').click(function(){
  $.ajax({url: 'dosomething1.html'})
    .done( function(){ console.log('something 1 success'); } )
    .fail( function(){ console.log('something 1 failure'); } );
});

$('#button2').click(function(){
  $.ajax({url: 'dosomething2.html'})
    .done( function(){ console.log('something 2 success'); } )
    .fail( function(){ console.log('something 2 failure'); } );
});

$('#button3').click(function(){
  $.ajax({url: 'dosomething3.html'})
    .done( function(){ console.log('something 3 success'); } )
    .fail( function(){ console.log('something 3 failure'); } );
});

If the user clicks on "#button1" then "#button2" and then "button3", I want to see in the console:

>something 1 success
>something 2 success
>something 3 success

It can happen that the responses are not received in the order the server sent them. So I want to get prepared for this scenario.

Note: I can't know in advance the sequence of events triggered by the user. So I need to find a way to chain the response handlers "on the fly".

What would be the best solution to achieve this?

I'm new to Ajax tricky stuff and I read a large amount of things today without finding THE solution (I guess that somehow deferred and promise objects could do the trick). Please help me to get rid of this terrible headache. :)


Edit to comment the solution by Kevin B.

I struggled with my brain to FULLY understand the example from Kevin B (that does work) until I read a book on Deferreds that explains that the "then" function is actually creating a new Deferred and returns its promise.

This is this new promise that is "chained" to the previous one. It calls its successfull of failure callbacks depending on the result of the previous promise evaluation (resolved or rejected).

In our case that means that when the previous promise is evaluated, the "then" promise is also evaluated and takes as an input the result (resolved or rejected) of the previous promise to decide which callback to call. In kevin B's code the ajax request promise is returned in both cases (resolved or rejected). Hence, the .fail and .done callback of the promise are called ONLY ONCE the "then" promise is evaluated AND the returned promise (ajax request one) is "resolved" (.done function) or rejected (.fail function).

To go further: My understanding is that the promise is a kind of listener on an event that can potentially happen in the future.

In classical cases, when the event happens, the deferred is changed to "resolved" or "rejected" state and the promise callbacks are called. The promise is "listening" to the state of the deferred to be changed. The event trigerring this state change is the resolution or rejection of the initial event (ajax request, timeout, other...).

In "then" cases, the trigerring event for evaluating the promise is: the referenced promise (previous promise in the chain) is evaluated (either resolved or rejected). Given the evaluation result, the success or failure callback is called.

I propose this slightly re-organized code inspired by Kevin's code to help out dummies like me to better understand:

var currentPromise = $.Deferred().resolve().promise();

$('#button1').click(function(){

    var button1Promise = $.ajax({url: 'dosomething1.html'})

    var thenPromise = currentPromise.then(
        function () { return button1Promise;}, 
        function () { return button1Promise;}); // to also handle fail conditions gracefully

    currentPromise = thenPromise;

    // "thenPromise" callback functions are returning the promise linked to 
    // the ajax request. So this promise is "replacing" the "thenPromise". 
    // Hence "done" and "fail" functions are finally defined for the ajax request promise.
    thenPromise.done( function(){ console.log('something 1 success'); } );
    thenPromise.fail( function(){ console.log('something 1 failure'); } );
});

Hopefully it will help people not totally comfortable with jquery concepts to fully understand promises chaining with "then" function.

Don't hesitate to comment if I misundertood something.

Nitseg
  • 1,267
  • 1
  • 12
  • 21
  • Could you just disable the other buttons while one request is being processed? For instance, On button1 press, disable button2, disable button3. Then, on either done or fail, re-enable all buttons. – Wyatt Earp Mar 17 '15 at 19:55
  • Nop, since I don't want to prevent the user from doing an action. Not only the Ajax request is sent. Some objects of the DOM are modified right away. – Nitseg Mar 17 '15 at 19:58
  • there is **1 easy option**, that is making the UI to have the 3 responses areas and you fill them as they're coming... because **async** means that the **first call could take 20sec** from the server and the second and third call **could only take 40ms**... without blocking the buttons as you said, the easiest way is to **have space in the UI for the 3 responses** like `
    ` and fill them up as you getting the responses...
    – balexandre Mar 17 '15 at 20:04
  • OK, this was just an example. Don't focus on it. :) I really need to handle the responses in correct order. Hacks won't do the trick. – Nitseg Mar 17 '15 at 20:15
  • 1
    Ideally you would use $.when when you want to send requests off as fast as possible but still get them in the order that they were sent, however, in this case $.when won't work because you don't know up front how many you are sending since it is triggered by the user clicking. You should be able to create a promise up front, and then just keep chaining request to it, however that isn't ideal either because you'll only be sending 1 request at a time. – Kevin B Mar 17 '15 at 20:16
  • @KevinB: Please develop. I don't really care about sending the requests in one single shoot. This is unlikely to happen anyway. – Nitseg Mar 17 '15 at 20:22
  • The downside of the sending being delayed is you will end up with a long chain of delayed requests. I came up with a workaround to that anyway, and included it in the answer. – Kevin B Mar 17 '15 at 20:35

4 Answers4

3

If you create a promise up front, you could keep chaining off of it to get your desired effect.

// ajax mock
function sendRequest(v, delay) {
    var def = $.Deferred();
    setTimeout(function () {
        def.resolve(v);
    }, delay);
    return def.promise();
}

var ajaxPromise = $.Deferred().resolve().promise();
var delay = 600; // will decrement this with each use to simulate later requests finishing sooner

// think of this as a click event handler
function doAction (btnName) {
    delay -= 100;
    var promise = sendRequest(btnName, delay);
    ajaxPromise = ajaxPromise.then(function () {
        return promise;
    }).done(function () {
        console.log(btnName);
    });
}

doAction("1");
doAction("2");
doAction("3");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

Since i delayed them by 500ms, 400ms, and 300ms, none of them logged until after 500ms.

Here's your code with the same technique:

var ajaxPromise = $.Deferred().resolve().promise();
$('#button1').click(function(){
  var promise = $.ajax({url: 'dosomething1.html'})

  ajaxPromise = ajaxPromise.then(function () {
    return promise;
  }, function () {
    return promise; // to also handle fail conditions gracefully
  }).done( function(){ console.log('something 1 success'); } )
    .fail( function(){ console.log('something 1 failure'); } );
});

// repeat for other two buttons

The important thing is all of the ajax requests will always be sent immediately, but the done and fail handlers won't be executed until their turn.

Kevin B
  • 94,570
  • 16
  • 163
  • 180
  • Oh thanks. I re-read the jquery documentation and with your examples everything becomes crystal clear. I was close but not familiar enough with the concept to write the logical as you did. Thanks a lot. – Nitseg Mar 18 '15 at 14:04
0

I can't know in advance the sequence of events triggered by the user. So I need to find a way to chain the response handlers "on the fly".

You need to pipe ajax requests to have the respective responses in the same order, one way to do that is using this plugin https://code.google.com/p/jquery-ajaxq/

elsadek
  • 1,028
  • 12
  • 39
  • I don't want to queue the requests, I would like to make sure the answers are handled in the same order as the requests in case they take different routes on the web and arrive not in the correct order. – Nitseg Mar 17 '15 at 20:13
  • So queuing the responses (not the requests) might be the desired effect. The requests queued would have a similar effect but waste time. – Menace Jan 26 '19 at 12:48
0

As you say:

I can't know in advance the sequence of events triggered by the user. So I need to find a way to chain the response handlers "on the fly".

The right way to go about this is definitely to use deferred objects / promises and NOT set the async parameter to false, which can cause a lot of unwanted problems.

Read the canonical introduction on how to do it here.

EDIT:

An example of synchronizing parallel tasks with $.when(), taken from here:

var promiseOne, promiseTwo, handleSuccess, handleFailure;

// Promises
promiseOne = $.ajax({ url: '../test.html' });
promiseTwo = $.ajax({ url: '../test.html' });


// Success callbacks
// .done() will only run if the promise is successfully resolved
promiseOne.done(function () {
   console.log('PromiseOne Done');
});

promiseTwo.done(function () {
    console.log('PromiseTwo Done');
});

// $.when() creates a new promise which will be:
// resolved if both promises inside are resolved
// rejected if one of the promises fails
$.when(
   promiseOne,
   promiseTwo
)
.done(function () {
   console.log('promiseOne and promiseTwo are done');
})
.fail(function () {
   console.log('One of our promises failed');
});
Community
  • 1
  • 1
Miqi180
  • 1,670
  • 1
  • 18
  • 20
  • OK, I read your reference quickly. I need to dig into it a little bit more deeply but I don't think it can be useful to solve my issue. I was thinking of something like calling the deferred.then function but I don't think I can do it directly on the jqXHR object returned by the .ajax call. deferred.then is "easy" to use when you know in advance the chaining. But here it depends on user actions... . – Nitseg Mar 17 '15 at 20:19
-2

The easiest way here will be to use async : false parameter for $.AJAX(), just to be sure that your requests run one after another. http://api.jquery.com/jquery.ajax/

NYS
  • 64
  • 3