Avraam Mavridis
Avraam Mavridis

Reputation: 8920

Which is the "right" way to handle a response that came late

Lets say that we have two buttons, each on are calling the following method:

var NUMBER_OF_IMAGE_REQUEST_RETRIES = 3;
var IMAGE_REQUEST_TIMEOUT = 3000;


processImage: function(image_data) {
    var main_response = $q.defer();
    var hash = getImageHash(image_data);

    var requestsCounter = -1;

    requestImage = function() {
      $http.post(apiUrl, {params: {data: hash},timeout: IMAGE_REQUEST_TIMEOUT})
       .then(function(response) {
           return main_response.resolve(response.data);
        }, function(error) {
        if (++requestsCounter < NUMBER_OF_IMAGE_REQUEST_RETRIES) {
          requestLabelsImage();
        } else {
          return main_response.reject();
        }
      });
    };

    requestLabelsImage();

    return main_response.promise;
}

The method passes an image related data to the server, the server process the data and then response. Every time a user press a different button different image_data is being send to the server.

The problem:

The user press button 1, the method is called with image_data_1, and then he/she immediately press button 2 and the method is called with image_data_2. The processImage function is called by another method, lets say doSomethingWithTheResponse which only cares about the latest user's action, but the image_data_2 is proceed faster by the servers, so the client gets image_data_2 before image_data_1, so the client believes that image_data_1 was related to the user's latest action, which is not the case. How can we ensure that the client is always getting the response that is related to the users latest action?

Note: The hash is different for the differente image_data requests.

I was thinking something like:

var oldhash = null;

processImage: function(image_data) {
    var main_response = $q.defer();
    var hash = getImageHash(image_data);
    oldhash = hash;

    var requestsCounter = -1;

    requestImage = function(hash) {
     if(hash === oldhash){

      $http.post(apiUrl, {params: {data: hash},timeout: IMAGE_REQUEST_TIMEOUT})
       .then(function(response) {
           return main_response.resolve(response.data);
        }, function(error) {
        if (++requestsCounter < NUMBER_OF_IMAGE_REQUEST_RETRIES) {
          requestLabelsImage(hash);
        } else {
          return main_response.reject();
        }
      });

    }
    else {
      main_response.reject();
    }
   }

    requestLabelsImage(hash);

    return main_response.promise;
}

But I am not 100% sure that this is the right approach.

Upvotes: 0

Views: 85

Answers (3)

If your UI allows for initiation multiple actions, while processing of those actions are mutually exclusive, then you should probably use promises, and track active promises.

button1.addEventListener("click", function(evt) {
  startRunning( task1.start() );
});

button2.addEventListener("click", function(evt) {
  startRunning( task2.start() );
});

With a task runner like:

function startRunning( promise ) {
  while(runningTasks.length>0) {
    cancel( runningTasks.unshift() );
  });
  runningTasks.push( promise );
}

Your cancel function can come from anything that can deal with promises, like Angular's service.cancelRequest, or you can write your own code that takes the promise and smartly breaks off its operation.

Of course, if you're not using promises, then you probably want to start doing so, but if you absolutely can't you can use a manager object like:

button1.addEventListener("click", function(evt) { task1(); });

button2.addEventListener("click", function(evt) { task2(); });

with

var manager = [];

function cancelAll() {
  while(manager.lenght>0) {
    var cancelfn = manager.unshift()
    cancelfn();
  }
  return true;
}

function task1() {
  var running = cancelAll();
  manager.push(function() { running = false; });
  asyncDo(something1, function(result) {
    if(!running) return;
    // do your real thing
  });
}

function task1() {
  var running = cancelAll();
  manager.push(function() { running = false; });
  asyncDo(something2, function(result) {
    if(!running) return;
    // do your real thing
  });
}

And you can put cancels on as many aspects as you need. If you need to cancel running XHRs, you might be able to do so, if you have multiple steps in your result handling, cut off at each step start, etc.

Upvotes: 1

ton
ton

Reputation: 1145

Simply disregard the previous requests.

You can create a repository of requests (array or dictionary implementation is okay). Call .abort() on the previous ones once another request is made -- when you add it in your storage.

If you want a dictionary, there is a good example here (tackles a different topic, though), but here is a modified snippet of his code which is related to your case:

var _pendingRequests = {};

function abortPendingRequests(key) {
    if (_pendingRequests[key]) {
        _pendingRequests[key].abort();
    }
}

Where the key can be.. say... a category of your action. You can name constants for it, or it can be just the name of the button pressed. It can even be a URL of your request; completely up to you.

There is an excellent explanation of the whole concept here:

jquery abort() ajax request before sending another https://stackoverflow.com/a/3313022/594992

Upvotes: 1

Sidd
Sidd

Reputation: 1397

This sounds like an ideal use-case for promises. Basically, whenever a new request is made, you want to cancel any existing promises. I am not versed in AngularJS, but the following ng-specific links might prove useful:

Angularjs how to cancel resource promise when switching routes

Canceling A Promise In AngularJS

Upvotes: 0

Related Questions