AngularJS service retry when promise is rejected

本小妞迷上赌 提交于 2019-12-29 03:15:09

问题


I'm getting data from an async service inside my controller like this:

myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
    $scope.getData = function(query) {
        return AsyncService.query(query).then(function(response) {
            // Got success response, return promise
            return response;
        }, function(reason) {
            // Got error, query again in one second
            // ???
        });
    }
}]);

My questions:

  1. How to query the service again when I get error from service without returning the promise.
  2. Would it be better to do this in my service?

Thanks!


回答1:


You can retry the request in the service itself, not the controller.

So, AsyncService.query can be something like:

AsyncService.query = function() {
  var counter = 0
  var queryResults = $q.defer()

  function doQuery() {
    $http({method: 'GET', url: 'https://example.com'})
      .success(function(body) {
        queryResults.resolve(body)
      })
      .error(function() {
        if (counter < 3) {
          doQuery()
          counter++ 
        }
      })
  }

  return queryResults.promise
}

And you can get rid of your error function in the controller:

myApp.controller('myController', ['$scope', 'AsyncService',
  function($scope, AsyncService) {
    $scope.getData = function(query) {
      return AsyncService.query(query).then(function(response) {
        // Got success response
        return response;
      });
    }
  }
]);



回答2:


This actually works:

angular.module('retry_request', ['ng'])
  .factory('RetryRequest', ['$http', '$q', function($http, $q) {
    return function(path) {
      var MAX_REQUESTS = 3,
          counter = 1,
          results = $q.defer();

      var request = function() {
        $http({method: 'GET', url: path})
          .success(function(response) {
            results.resolve(response)
          })
          .error(function() {
            if (counter < MAX_REQUESTS) {
              request();
              counter++;
            } else {
              results.reject("Could not load after multiple tries");
            }
          });
      };

      request();

      return results.promise;
    }
  }]);

Then just an example of using it:

RetryRequest('/api/token').then(function(token) {
  // ... do something
});

You have to require it when declaring your module:

angular.module('App', ['retry_request']);

And in you controller:

app.controller('Controller', function($scope, RetryRequest) {
  ...
});

If someone wants to improve it with some kind of backoff or random timing to retry the request, that will be even better. I wish one day something like that will be in Angular Core




回答3:


I wrote an implementation with exponential backoff that doesn't use recursion (which would created nested stack frames, correct?) The way it's implemented has the cost of using multiple timers and it always creates all the stack frames for the make_single_xhr_call (even after success, instead of only after failure). I'm not sure if it's worth it (especially if the average case is a success) but it's food for thought.

I was worried about a race condition between calls but if javascript is single-threaded and has no context switches (which would allow one $http.success to be interrupted by another and allow it to execute twice), then we're good here, correct?

Also, I'm very new to angularjs and modern javascript so the conventions may be a little dirty also. Let me know what you think.

var app = angular.module("angular", []);

app.controller("Controller", ["$scope", "$http", "$timeout",
    function($scope, $http, $timeout) {

  /**
   * Tries to make XmlHttpRequest call a few times with exponential backoff.
   * 
   * The way this works is by setting a timeout for all the possible calls
   * to make_single_xhr_call instantly (because $http is asynchronous) and
   * make_single_xhr_call checks the global state ($scope.xhr_completed) to
   * make sure another request was not already successful.
   *
   * With sleeptime = 0, inc = 1000, the calls will be performed around:
   * t = 0
   * t = 1000 (+1 second)
   * t = 3000 (+2 seconds)
   * t = 7000 (+4 seconds)
   * t = 15000 (+8 seconds)
   */
  $scope.repeatedly_xhr_call_until_success = function() {
    var url = "/url/to/data";
    $scope.xhr_completed = false
    var sleeptime = 0;
    var inc = 1000;
    for (var i = 0, n = 5 ; i < n ; ++i) {
      $timeout(function() {$scope.make_single_xhr_call(url);}, sleeptime);
      sleeptime += inc;
      inc = (inc << 1); // multiply inc by 2
    }
  };

  /**
   * Try to make a single XmlHttpRequest and do something with the data.
   */
  $scope.make_single_xhr_call = function(url) {
    console.log("Making XHR Request to " + url);

    // avoid making the call if it has already been successful
    if ($scope.xhr_completed) return;
    $http.get(url)
      .success(function(data, status, headers) {
        // this would be later (after the server responded)-- maybe another
        // one of the calls has already completed.
        if ($scope.xhr_completed) return;
        $scope.xhr_completed = true;
        console.log("XHR was successful");
        // do something with XHR data
      })
      .error(function(data, status, headers) {
        console.log("XHR failed.");
      });
  };

}]);



回答4:


Following this article Promises in AngularJS, Explained as a Cartoon

you need to retry only when the response comes under 5XX category

I have written a service called http which can be called by passing all http configs as

 var params = {
  method: 'GET',
  url: URL,
  data: data
 }

then call the service method as follows:

   <yourDefinedAngularFactory>.http(params, function(err, response) {});

http: function(config, callback) {
  function request() {
    var counter = 0;
    var queryResults = $q.defer();

    function doQuery(config) {
      $http(config).success(function(response) {
        queryResults.resolve(response);
      }).error(function(response) {
        if (response && response.status >= 500 && counter < 3) {
          counter++;
          console.log('retrying .....' + counter);
          setTimeout(function() {
            doQuery(config);
          }, 3000 * counter);
        } else {
          queryResults.reject(response);
        }
      });
    }
    doQuery(config);
    return queryResults.promise;
  }
  request(config).then(function(response) {
    if (response) {
      callback(response.errors, response.data);
    } else {
      callback({}, {});
    }
  }, function(response) {
    if (response) {
      callback(response.errors, response.data);
    } else {
      callback({}, {});
    }
  });
}



回答5:


I ended up doing this a lot so I wrote a library to help address this problem : )

https://www.npmjs.com/package/reattempt-promise-function

In this example you could do something like

myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
    var dogsQuery = { family: canine };
    $scope.online = true;
    $scope.getDogs = function() {
        return reattempt(AsyncService.query(dogsQuery)).then(function(dogs) {
            $scope.online = true;
            $scope.dogs = dogs;
        }).catch(function() {
            $scope.online = false;
        });
    }
}]);


来源:https://stackoverflow.com/questions/19595886/angularjs-service-retry-when-promise-is-rejected

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!