mardi 26 janvier 2016

Angular pattern for ensuring single active promise

In our application, we have an search input field. Typically a request is sent while the user types (a la Google Instant) and the results are displayed.

Obviously, the following can happen:

  1. User types, which results in ajaxRequest1
  2. User continues typing, resulting in ajaxRequest2
  3. results2 corresponding to ajaxRequest2 are received and displayed
  4. After this, results1 corresponding to ajaxRequest1 are received. Obviously, since ajaxRequest2 was sent after ajaxRequest1, we only care about results2, not results1.

EDIT: The obvious answer here is "Use debounce". For reasons of confidentiality and brevity, I'll just say here that it won't work in our particular scenario. I know what debounce does and I have considered it.

In pseudo-code, we used to handle it like this:

$scope.onInput = function() {
  var inputText = getInput();
  SearchService.search(inputText).then(function(results) {
    // only display if input hasn't changed since request was sent
    if(inputText === getInput()) {
      displayResults(results);
    }
  });
};

Since this involves a lot of boilerplate and looks ugly, we moved to a pattern where the SearchService manages things a bit better

$scope.onInput = function() {
  var inputText = getInput();
  SearchService.search(inputText).then(function(results) {
    displayResults(results);
  });
}

function SearchService() {
  var cachedSearchDeferred;
  this.search = function(inputText) {
    if(cachedSearchDeferred) {
      //means there's an unresolved promise corresponding to an older request
      cachedSearchDeferred.reject();
    }

    var deferred = $q.deferred();
    $http.post(...).then(function(response) {
      // saves us having to check the deferred's state outside
      cachedSearchDeferred = null;
      deferred.resolve(response.data);
    });
    cachedSearchDeferred = deferred;
    return deferred.promise;
  }
}

This works fine. The SearchService creates a deferred containing the promise corresponding to the most recent call to SearchService.search. If another call is made to SearchService.search the old deferred is rejected and a new deferred is created corresponding to the new call.

Two questions:

  1. Is this a good pattern to do what we need - essentially request locking? We want to ensure that only the most recent request's promise resolves successfully
  2. If we had other SearchService methods that needed to behave similarly, then this deferred boilerplate needs to be inside every method. Is there a better way?

Aucun commentaire:

Enregistrer un commentaire