28

I'm just getting my feet wet with Angularjs. I have an issue which I think has something to do with promises.

Let's say I load route 'A' which makes several ajax requests through it's controller:

allSites = AllSites.query({ id:categoryID });

allSites.$promise.then(function(allSites){
    //add stuff to the scope and does other things 
    //(including making another ajax request)
});

Then I have route 'B' which makes it's own API request through it's controller:

$scope.categories = Category.query();

Here's the factory service currently used by route 'A':

.factory('AllSites',function($resource){
    return $resource('api/categorySites/:id');
});

When I first view route 'A' but then switch to 'B' before 'A' is finished loading, route 'B' sits and waits for everything initially requested in 'A' to finish (actually, the query() request is made, but it won't resolve until the one from 'A' does, at that point, the stuff inside .then() continues to happen, even though I don't need it as I'm now on another route.

As you can see in my devtools timeline, the green line indicates when I switched to route 'B'. The request for route 'B' didn't resolve until the two requests above did (a request that is usually very fast). (at which point I'm able to use the view as a user). Then, after that, more promises resolve from route 'A'.

My devtools

I've searched everywhere for an answer and can only find people that want to "defer" the route loading until promises are resolved. But in my case I almost want the opposite. I want to kill those requests when I switch.

Here's someone else with the same, unanswered question: Reject Angularjs resource promises

Any help is appreciated.

Community
  • 1
  • 1
Joao
  • 2,696
  • 2
  • 25
  • 35
  • $location.path('url').replace() will replace the current route rather than adding it to the stack. I'm not sure you can cancel routes in angular - have you looked at the source to ngRoute to find out? – Jon Jun 26 '14 at 21:12

5 Answers5

15

First of all, I decided I needed to use $http since I couldn't find any solution that used $resource, nor could I get it to work on my own.

So here's what my factory turned into, based on @Sid's answer here, using the guide at http://www.bennadel.com/blog/2616-aborting-ajax-requests-using-http-and-angularjs.htm

.factory('AllSites',function($http,$q){

    function getSites(categoryID) {

        // The timeout property of the http request takes a deferred value
        // that will abort the underying AJAX request if / when the deferred
        // value is resolved.
        var deferredAbort  = $q.defer();

        // Initiate the AJAX request.
        var request = $http({
            method: 'get',
            url: 'api/categorySites/'+categoryID,
            timeout: deferredAbort.promise
        });

        // Rather than returning the http-promise object, we want to pipe it
        // through another promise so that we can "unwrap" the response
        // without letting the http-transport mechansim leak out of the
        // service layer.
        var promise = request.then(
            function( response ) {
                return( response.data );
            },
            function() {
                return( $q.reject( 'Something went wrong' ) );
            }
        );

        // Now that we have the promise that we're going to return to the
        // calling context, let's augment it with the abort method. Since
        // the $http service uses a deferred value for the timeout, then
        // all we have to do here is resolve the value and AngularJS will
        // abort the underlying AJAX request.
        promise.abort = function() {
            deferredAbort.resolve();
        };

        // Since we're creating functions and passing them out of scope,
        // we're creating object references that may be hard to garbage
        // collect. As such, we can perform some clean-up once we know
        // that the requests has finished.
        promise.finally(
            function() {
                promise.abort = angular.noop;
                deferredAbort = request = promise = null;
            }
        );

        return( promise );
    }

    // Return the public API.
    return({
        getSites: getSites
    });

});

Then, in my controller (route 'A' from my problem):

var allSitesPromise = AllSites.getSites(categoryID);

$scope.$on('$destroy',function(){
    allSitesPromise.abort();
});

allSitesPromise.then(function(allSites){
    // do stuff here with the result
}

I wish the factory wasn't so messy, but I'll take what I can get. However, now there's a separate, related issue Here where, though the promise was cancelled, the next actions are still delayed. If you have an answer for that, you can post it there.

Community
  • 1
  • 1
Joao
  • 2,696
  • 2
  • 25
  • 35
  • Interesting approach! Please accept your own answer if that solved your issue. – Alp Jun 27 '14 at 17:17
  • It won't let me until tomorrow :) – Joao Jun 27 '14 at 17:17
  • I used a similar approach, but the .abort() will not be defined on the promise chain, i.e. allSitesPromise.then().abort is undefined. Do you have any tricks to make the .abort also propagate down the promise chain? – Aditya Santoso Apr 19 '16 at 12:00
  • 1
    I don't understand the reasoning of trying to abort after the `then()` function is returned (actually, I don't even know if `then()` returns anything at all). By "then" your promise has resolved anyway and your data has been received - there's nothing to abort. Maybe your issue merits it's own question on SO. – Joao Apr 20 '16 at 14:05
  • `then()` returns a new promise, but time it isn't your wrapped promise that you augmented with the `abort` method. – gligoran Aug 08 '17 at 10:48
2

There is a similar question with the answer "How to cancel $resource requests".

While it does not address the question exactly it gives all ingredients to cancel resource request when route is switched:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Cancel resource</title>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-resource.js"></script>
  <script>
angular.module("app", ["ngResource"]).
factory(
  "services",
  ["$resource", function($resource)
  {
    function resolveAction(resolve)
    {
      if (this.params)
      {
        this.timeout = this.params.timeout;
        this.params.timeout = null;
      }

      this.then = null;
      resolve(this);
    }

    return $resource(
      "http://md5.jsontest.com/",
      {},
      {
        MD5:
        {
          method: "GET",
          params: { text: null },
          then: resolveAction
        },
      });
  }]).
controller(
  "Test",
  ["services", "$q", "$timeout", function(services, $q, $timeout)
  {
    this.value = "Sample text";
    this.requestTimeout = 100;

    this.call = function()
    {
      var self = this;

      self.result = services.MD5(
      {
        text: self.value,
        timeout: $q(function(resolve)
        {
          $timeout(resolve, self.requestTimeout);
        })
      });
    }
  }]);
  </script>
</head>
<body ng-app="app" ng-controller="Test as test">
  <label>Text: <input type="text" ng-model="test.value" /></label><br/>
  <label>Timeout: <input type="text" ng-model="test.requestTimeout" /></label><br/>
  <input type="button" value="call" ng-click="test.call()"/>
  <div ng-bind="test.result.md5"></div>
</body>
</html>

How it works

  1. $resource merges action definition, request params and data to build a config parameter for an $http request.
  2. a config parameter passed into an $http request is treated as a promise like object, so it may contain then function to initialize config.
  3. action's then function may pass timeout promise from params into the config.

Please look at "Cancel Angularjs resource request" for details.

Community
  • 1
  • 1
  • This does not provide an answer to the question. To critique or request clarification from an author, leave a comment below their post. – levi Feb 18 '15 at 04:07
  • The question was about cancelation of resource request when route is switched. My answer tells how to cancel resource request using timeout promise. To build a solution author needs to cancel a timeout promise when route is being switched. – Vladimir Nesterovsky Feb 18 '15 at 06:12
  • How would this translate to older versions of angular that still use the legacy promise extensions success() and error()? – g1de0n_ph Jan 06 '16 at 10:02
1

Take a look at this post

You could do what he is doing and resolve the promise to abort the request on a route change (or state change if using ui router).

It may not be the easiest thing to make happen but seems like it can work.

Nalaka526
  • 11,278
  • 21
  • 82
  • 116
Sid
  • 7,511
  • 2
  • 28
  • 41
  • Yep it did! I'm going to post an answer with some code for my own future reference (and for others too). – Joao Jun 27 '14 at 16:13
0

I cancel the promise with $q.reject(). I think that this way is more simple:

In SitesServices.js:

;(() => {
  app.services('SitesServices', sitesServices)
  sitesServices.$inject = ['$http', '$q']
  function sitesServices($http, $q) {

    var sitesPromise = $q.defer()
    this.getSites = () => {
      var url = 'api/sites'
      sitesPromise.reject()
      sitesPromise = $q.defer()
      $http.get(url)
        .success(sitesPromise.resolve)
        .error(sitesPromise.reject)

      return sitesPromise.promise
    }
  }
})()

In SitesController.js:

;(() => {
  app.controller('SitesController', sitesControler)
  sitesControler.$inject = ['$scope', 'SitesServices']
  function sitesControler($scope, SitesServices) {
    $scope.sites = []

    $scope.getSites = () => {
      SitesServices.getSites().then(sites => {
        $scope.sites = sites
      })
    }
  }
})()
zamarrowski
  • 483
  • 2
  • 7
0

Checking the docs for $resource I found a link to this little beauty. https://docs.angularjs.org/api/ng/service/$http#usage

timeout – {number|Promise} – timeout in milliseconds, or promise that should abort the request when resolved.

I've used it with some success. It go a little something like this.

export default function MyService($q, $http) {
  "ngInject";
  var service = {
    getStuff: getStuff,
  };

  let _cancelGetStuff = angular.noop;

  return service;

  function getStuff(args) {
    _cancelGetStuff(); // cancel any previous request that might be ongoing.

    let canceller = $q( resolve => { _cancelGetStuff = resolve; });

    return $http({
      method: "GET",
      url: <MYURL>
      params: args,
      timeout: canceller
    }).then(successCB, errorCB);

    function successCB (response) {
      return response.data;
    }

    function errorCB (error) {
      return $q.reject(error.data);
    }
  }
}

Keep in mind

  1. This assumes you only want the results from the last request
  2. The canceled requests still call successCB but the response is undefined.
  3. It may also call errorCB, the error.status will be -1 just like if the request timed out.
leff
  • 575
  • 1
  • 3
  • 12