Wednesday, April 24, 2013

Preempting Asynchronous Callbacks

With the single-threaded nature of JavaScript, the use of callbacks is innate to it. Promises (e.g. jQuery Deferred) are a nicer standardized way of making asynchronous function calls while at the same time tying up dependencies between different async tasks (e.g. $.when(), Promise.then(), etc.).
In the examples I present here I will use jQuery Deferreds/Promises.
[The difference in Deferred and Promise is the perspective. Deferred is from the perspective of the "producer", which will resolve or reject the deferred. While Promise is from the perspective of the "consumer", it attaches callbacks to the promise to be called upon failure or success. See jQuery Deffered]

The Problem

Most often we won't need to have multiple outstanding promises of the same type - e.g. let's say we have an input box for a search phrase and when one presses a submit button, the search phrase is submitted to a remote server and the result is displayed when the response comes back. Our code would look something like:
button.click( function() {
  var text = inputBox.text();
  $.post("test.php", { search: text } )
  .done(function (data) {
     //display the data
   });
});
Now lets say you typed something and clicked the submit button. Then before the server responded, you edited the text and clicked submit again. What would happen?
Whatever response comes first will be displayed and then overwritten with the response that comes second. Note, can you assume that the responses will arrive in the same order as the requests? Usually, NOT.

Lets prevent multiple outstanding requests

So, how would you solve this? [If you are wondering why would this be a problem in the first place, maybe in a real application, we perform some expensive operations on the response.] How about disabling the submit button as soon as one clicks on it and enabling it when the response comes back?
button.click( function() {
  inputBox.attr('disabled', 'disabled')
  var text = inputBox.text();
  $.post("test.php", { search: text } )
  .done(function (data) {
     //display the data
  })
  .fail(function (err) {
     //display err
  })
  .always(function () {
    inputBox.removeAttr('disabled');
  });
});
The reason we re-enabled the button in the always callback instead of the done callback is because we want to re-enable the button in case of either success(done) or failure(fail), so instead of repeating it in those two callbacks, always gives us a convenient shortcut to stay DRY.

But why make the user wait?

But lets say we are not happy with having to WAIT for success/failure. If we like to search something else and don't care about the current ongoing search, what should we do?

We preempt the obsolete callbacks. But how?
Would a boolean flag work, as follows?
button.click( function() {
  var text = inputBox.text();
  button.__preempt = false;
  $.post("test.php", { search: text } )
  .done(function (data) {
      if(button.__preempt) return;
      //display the data
      button.__preempt = true;
  });
});
NO the above is no different than the previous example; since we haven't gained any more expressive power. The attribute disabled could be thought of as the equivalent of __preempt variable!
Actually, now it's worse. We allow the user to hit submit button multiple times but we display only the first result instead of the last.

How about a counter?

button.click( function() {
  var text = inputBox.text();
  if(button.__clickCount) button.__clickCount++;
  else button.__clickCount = 1;
  $.post("test.php", { search: text } )
  .done(function (data) {
      if(--button.__clickCount) return;
      //display the data
  })
  .fail(function () {
      button.__clickCount--;
  });
});
While the above works, we are assuming that the responses come in the same order as the requests. If that is not the case, we might need to have the request (the search criterion) itself embedded in the response, which would enable us to place the related search phrase back in the input box whenever we receive a response. But chances are we cannot dictate the response payload (e.g. we don't control that server or it is shared by other consumers and we can't change the api).

Any other way to relax on response ordering?

How about we remember the request?
button.click( function() {
  var text = inputBox.text();
  botton.__unresolved = text;
  $.post("test.php", { search: text } )
  .done(function (data) {
      if(button.__unresolved !== text) return;
      //display the data
  });
});
The reason this works is because the callback is relying on the closure on two objects - button and text.
Since button is the same for all instances of the callback, it allows them to communicate and button.__unresolved is the means of that communication. On the other hand text is unique to each instance.

A slightly different and more flexible approach.
Instead of using "text" we should generalize by use of a GUID.
And how about preempting based not just on multiple click of the submit button but also another event (e.g. a cancel button)?
button.click( function() {
  var thisCallId = guid();
  botton.__validCallId = thisCallId;
  $.post("test.php", { search: inputBox.text() } )
  .done(function (data) {
      if(button.__validCallId !== thisCallId) return;
      //display the data
  });
});
cancelButton.click( function() {
  delete button.__validCallId;
});

No comments:

Post a Comment