AngularJS: Using $q to fire ajax calls synchronously

Problem

Is it possible to use $q to fire ajax requests synchronously in AngularJS?

I have a long list of vehicles, each vehicle has events associated with them and I need to retrieve the eventdetails of each event when the user expands the listing.

Right now, if the user expands the listing, I am firing up to 15 calls asynchronously and it seems to be causing issues with the API I'm consuming, so I'd like to see if performance is improved if I wait for each request finishes before firing the next.

I'm attempting to implement $q to delay the next request until the previous is finished, however I can't seem to wrap my head around using the service, here is what I currently have:

// On click on the event detail expander
$scope.grabEventDetails = function(dataReady, index) {
    if (dataReady == false) {
        retrieveEventDetails($scope.vehicles[index].events);
    }
}

var retrieveEventDetails = function(events) {
    // events is array

    var deferred = $q.defer();
    var promise = deferred.promise;

    var retrieveData = function(data) {
        return $http({
            url: '/api/eventdetails',
            method: 'POST',
            data: {
                event_number: data.number
            },
            isArray: true
        });
    }

    _.each(events, function(single_event) {
        promise.then(retrieveData(single_event).success(function(data) {
            console.log(data);
        }));
    });
}

This is still firing asynchronously, Where am I going wrong with this?

I understand firing the requests synchronously isn't the best idea, at the moment I just want to see if performance is improved with the API at all.

Problem courtesy of: Neil

Solution

  1. You don't need $q to implement a promise as $http returns one.

  2. _.each fires all the callbacks without especially waiting the promise.

  3. All you do is call retrieveData for all events whenever your promise is resolved, and since you don't do a first call, it shouldn't even be working

You could do some recursive call like this :

var retrieveEventDetails = function(events) {
    var evt = events.shift();
    $http({
        url: '/api/eventdetails',
        method: 'POST',
        data: {
            event_number: evt.number
        },
        isArray: true
    }).then(function(response){
        console.log(response.data);
        retrieveEventDetails(events);
    });
}
Solution courtesy of: Florian F.

Discussion

I'm not sure you even need $q here. In this example, each piece of data is registered in the controller as soon as it comes back from the call.

Live demo (click).

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

app.controller('myCtrl', function($scope, myService) {
  $scope.datas = myService.get();
});

app.factory('myService', function($http) {
  var myService = {
    get: function() {
      var datas = {};

      var i=0;
      var length = 4;
      makeCall(i, length, datas);

      return datas;
    }
  }

  function makeCall(i, length, datas) {
    if (i < length) {
      $http.get('test.text').then(function(resp) {
        datas[i] = resp.data+i;
        ++i;
        makeCall(i, length, datas);
      }); 
    }
  }

  return myService;
});

Here's a way using $q.all() that you can wait for all of the data to come through before passing it to the controller: Live demo (click).

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

app.controller('myCtrl', function($scope, myService) {
  myService.get().then(function(datas) {
    $scope.datas = datas;
  })
});

app.factory('myService', function($q, $http) {
  var myService = {
    get: function() {
      var deferred = $q.defer();

      var defs = [];
      var promises = [];

      var i=0;
      var length = 4;

      for(var j=0; j<length; ++j) {
        defs[j] = $q.defer();
        promises[j] = defs[j].promise;
      }

      makeCall(i, length, defs);

      $q.all(promises).then(function(datas) {
        deferred.resolve(datas);
      });

      return deferred.promise;
    }
  }

  function makeCall(i, length, defs) {
    if (i < length) {
      $http.get('test.text').then(function(resp) {
        defs[i].resolve(resp.data+i);
        ++i;
        makeCall(i, length, defs);
      })
    }
  } 

  return myService;
});
Discussion courtesy of: m59

I do think you should use $q as some other part of your application might need to get a promise.

A good example would be $routeProvider resolve option.

I made a little demo in plunker.

Solution:

  • retrieveData function should return a function (which returns a promise) instead of a just a promise.
  • That way we can create a promise chain: promise.then(fn).then(fn).then(fn).then(null,errorFn)
  • We must resolve the first promise to kick the chain.
var retrieveEventDetails = function(events) {
    // events is array

    var deferred = $q.defer();
    var promise = deferred.promise;

    var retrieveData = function(data) {
        return function(){      
            return $http({
                url: '/api/eventdetails',
                method: 'POST',
                data: {
                    event_number: data.number
                },
                isArray: true
            })
        }
    }

    deferred.resolve();

    return events.reduce(function(promise, single_event){
        return promise.then(retrieveData(single_event));
    }, promise);
}
Discussion courtesy of: Ilan Frumer

This recipe can be found in it's original form on Stack Over Flow.