Search code examples
angularjsangular-httpangular-http-interceptors

How to prioritize requests in angular $http service?


I'm working on an application with a large amount of lazy data loading. I would like to prioritize http requests based on 'priority' param.

This is the concept of using it.

$http.get(url, {params: query, priority: 1})

I was thinking of using $http interceptors. Something like that:

 angular.module('myModule')
.factory('httpPriorityInterceptor', function ($interval, $q) {
    var requestStack = [];

    return {
        request: function (config) {

            config.priority = config.priority || 3;

            requestStack.push(config);
            requestStack.sort(sortByPriority);

            if (isFirstToGo(item)) return requestStack.pop();

            deferred = $q.defer();

            var intervalPromise = $interval(function(){

                if (isFirstToGo(item)) {
                    deferred.resolve(requestStack.pop());
                    $interval.cancel(intervalPromise);
                };

            }, 100);

            return deferred.promise;

        }   
    };
});

But I can't return promise here. Any ideas?


Solution

  • You can do this by making use of $http's timeout property, and use both request and responseError callbacks to save and execute each $http requests respectively.

    Steps:

    1. Lazily inject the $http service within the request callback process, this will be the only way to get the $http service because injecting it in the factory's function causes circular dependency.

    2. Determine if the configuration passed in the request callback has been processed. If it has not been processed then add the configuration in the request stack and sort it by priority. Add a resolved promise in the timeout property of the configuration object, to cancel the current $http request. Finally return the configuration object.

    3. Once the $http request has been cancelled, catch it in the responseError callback. If there are items in the request stack, pop the first item(config) and invoke it using the lazy loaded $http service. Lastly return a rejected promise using the rejection parameter provided by the callback.

    DEMO

    angular.module('demo', [])
    
      .config(function($httpProvider) {
        $httpProvider.interceptors.push('httpPriorityInterceptor');
      })
    
      .factory('httpPriorityInterceptor', function($q, $injector) {
    
    
        var requestStack = [], // request stack
            $http = null; // http service to be lazy loaded
    
        return {
          request: request, // request callback
          responseError: responseError // responseError callback
        };
    
        // comparison function to sort request stack priority
        function sort(config1, config2) {
          return config1.priority < config2.priority;
        }
    
        function request(config) {
    
          // Lazy load $http service
          if(!$http) {
            $http = $injector.get('$http');
          }
    
          // check if configuration has not been requested
          if(!config.hasBeenRequested) {
    
            // set indicator that configuration has been requested
            config.hasBeenRequested = true;
    
            // set default priority if not present
            config.priority = config.priority || 3;
    
            // add a copy of the configuration
            // to prevent it from copying the timeout property
            requestStack.push(angular.copy(config));
    
            // sort each configuration by priority
            requestStack = requestStack.sort(sort);
    
            // cancel request by adding a resolved promise
            config.timeout = $q.when();
          }
    
          // return config
          return config;
        }
    
    
        function responseError(rejection) {
    
          // check if there are requests to be processed
          if(requestStack.length > 0) {
    
            // pop the top most priority
            var config = requestStack.pop();
            console.log(config);
    
            // process the configuration
            $http(config);
          }
    
          // return rejected request
          return $q.reject(rejection);
        }
    
      })
    
      .run(function($http) {
    
        // create http request
        var createRequest = function(priority) {
          $http.get('/priority/' + priority, {priority: priority});
        };
    
        createRequest(3);
        createRequest(1);
        createRequest(4);
        createRequest(2);
    
      });
    

    To make sure that each request has been invoked in the right order, you can check the logs in the console tab or the requests in the network tab.

    Update:

    If you want your requests invoked in order (when the first request must finish before the next request invokes) then you can tweak my solution in the responseError callback to something like this:

    DEMO

    function responseError(rejection) {
    
      // check if there are requests to be processed
      if(requestStack.length > 0) {
    
        requestStack.reduceRight(function(promise, config) {
          return promise.finally(function() {
            return $http(config);
          });
        }, $q.when());
    
        requestStack.length = 0;
    
      }
    
      // return rejected request
      return $q.reject(rejection);
    }
    

    UPDATE 06/16/2019

    As mentioned in the comments, the promise returned by prioritized requests do not return the expected promise resolution or rejection. I have updated the interceptor to accommodate such scenario by:

    1. Saving a deferred promise relative to each http config.
    2. Return the deferred promise in the responseError interceptor for sake keeping the resolution or rejection of the request.
    3. Finally use the deferred promise in the iteration of prioritized requests.

    DEMO

    angular.module('demo', [])
    
      .config(function($httpProvider) {
        $httpProvider.interceptors.push('httpPriorityInterceptor');
      })
    
      .factory('httpPriorityInterceptor', function($q, $injector) {
    
    
        var requestStack = [], // request stack
            $http = null; // http service to be lazy loaded
    
        return {
          request: request, // request callback
          responseError: responseError // responseError callback
        };
    
        // comparison function to sort request stack priority
        function sort(config1, config2) {
          return config1.priority < config2.priority;
        }
    
        function request(config) {
    
          // Lazy load $http service
          if(!$http) {
            $http = $injector.get('$http');
          }
    
          // check if configuration has not been requested
          if(!config.hasBeenRequested) {
    
            // set indicator that configuration has been requested
            config.hasBeenRequested = true;
    
            // set default priority if not present
            config.priority = config.priority || 3;
    
            // add a defered promise relative to the config requested
            config.$$defer = $q.defer();
    
            // add a copy of the configuration
            // to prevent it from copying the timeout property
            requestStack.push(angular.copy(config));
    
            // sort each configuration by priority
            requestStack = requestStack.sort(sort);
    
            // cancel request by adding a resolved promise
            config.timeout = $q.when();
          }
    
          // return config
          return config;
        }
    
    
        function responseError(rejection) {
    
          // check if there are requests to be processed
          if(requestStack.length > 0) {
    
            requestStack.reduceRight(function(promise, config) {
              var defer = config.$$defer;
              delete config.$$defer;
              return promise.finally(function() {
                return $http(config)
                  .then(function(response) {
                    defer.resolve(response);
                  })
                  .catch(function(error) {
                    defer.reject(error);
                  });
    
              });
            }, $q.when());
    
            requestStack.length = 0;
    
          }
    
          return rejection.config.$$defer.promise;
        }
    
      })
    
      .run(function($http) {
    
        // create http request
        var createRequest = function(priority) {
          return $http.get(priority + '.json', {priority: priority});
        };
    
        createRequest(3);
        createRequest(1).then(function(data) { console.log(data); })
        createRequest(4);
        createRequest(2);
    
      });