Add queueing to angulars $http service
Asked Answered
P

6

17

I have a very quirky api that can only handle a single request at a time. Therefore, I need to ensure that every time a request is made, it goes into a queue, and that queue is executed one request at a time, until it is empty.

Normally, I just use jQuery's built in queue, since the site is already using jQuery. However, I was not sure if I could somehow decorate the $http service, or wrap it in another service that returns one promise at a time, or something else.

Putup answered 22/1, 2013 at 17:52 Comment(2)
How are you planning on using the queue? Are the callers all the same? Or do you want to have one promise returned per caller, but executed by $http in series?Timisoara
>>Or do you want to have one promise returned per caller, but executed by $http in series? - this is what I am looking for. Essentially if I could get an interceptor able to delay the http request until all other requests before it have succeeded, that would be great.Putup
N
40

Here is my solution for that: http://plnkr.co/edit/Tmjw0MCfSbBSgWRhFvcg

The idea is: each run of service add request to queue and return promise. When request to $http is finished resolve/refuse returned promise and execute next task from queue if any.

app.factory('srv', function($q,$http) {

  var queue=[];
  var execNext = function() {
    var task = queue[0];
    $http(task.c).then(function(data) {
      queue.shift();
      task.d.resolve(data);
      if (queue.length>0) execNext();
    }, function(err) {
      queue.shift();
      task.d.reject(err);
      if (queue.length>0) execNext();
    })
    ;
  }; 
  return function(config) {
    var d = $q.defer();
    queue.push({c:config,d:d});
    if (queue.length===1) execNext();            
    return d.promise;
  };
});

Looks quite simple :)

Nonplus answered 22/1, 2013 at 21:23 Comment(8)
This seems super promising, but I don't understand how you handle success/error responses using this. Thanks for your help!Putup
Rereading my comment made it seem less clear than I thought it was originally. When I call srv() in the controller, how do I set up a success and an error response (you use .then, which seems to mean there is only one possible reaction to the response). Thanks againPutup
@Putup when you call srv, it returns a promise that has success, error, and then methods. See $q.Timisoara
In case of successfull response I just resolve returned promise. In case of error I reject. So depending on response you will get called first callback or second callback of then function. Actually another solution is to chain promises, but it is not so ovious solution...Nonplus
@JoshDavidMiller actually success/error methods are extensions of $http and $q does not have these methods, so only then(successCallBack, errorCallBack) could be used.Nonplus
I used this post while writing service to wrap Google's Geocoding API (which limits the frequency of requests). I came up with something that 1) queries the server serially, 2) caches the results in localStorage, and 3) responds appropriately to the error codes from the API. Hope it's useful to someone : gist.github.com/benmj/6380466Rexrexana
Don't you also have to queue.shift() and execNext() from within the error handler?Christogram
Interesting. I may be extended with a cancelling mechanism, that dequeue or cancel the `$http-call depending on state of the queued-call.Cutshall
W
5

Building on Valentyn's great work above, I rolled this code into a standalone Angular (v1.2+) request/response interceptor. It will queue $http requests automatically without needing to rework your code to use srv() everywhere:

( function() {

'use strict';

angular.module( 'app' ).config( [ '$httpProvider', function( $httpProvider ) {

    /**
     * Interceptor to queue HTTP requests.
     */

    $httpProvider.interceptors.push( [ '$q', function( $q ) {

        var _queue = [];

        /**
         * Shifts and executes the top function on the queue (if any). Note this function executes asynchronously (with a timeout of 1). This
         * gives 'response' and 'responseError' chance to return their values and have them processed by their calling 'success' or 'error'
         * methods. This is important if 'success' involves updating some timestamp on some object which the next message in the queue relies
         * upon.
         */

        function _shiftAndExecuteTop() {

            setTimeout( function() {

                _queue.shift();

                if ( _queue.length > 0 ) {
                    _queue[0]();
                }
            }, 1 );
        }

        return {

            /**
             * Blocks each request on the queue. If the first request, processes immediately.
             */

            request: function( config ) {

                var deferred = $q.defer();
                _queue.push( function() {

                    deferred.resolve( config );
                } );

                if ( _queue.length === 1 ) {
                    _queue[0]();
                }

                return deferred.promise;
            },

            /**
             * After each response completes, unblocks the next request.
             */

            response: function( response ) {

                _shiftAndExecuteTop();
                return response;
            },

            /**
             * After each response errors, unblocks the next request.
             */

            responseError: function( responseError ) {

                _shiftAndExecuteTop();
                return $q.reject( responseError );
            },
        };
    } ] );
} ] );

} )();
Witness answered 22/1, 2015 at 4:56 Comment(0)
S
5

Richard: Your code works perfect but it also works with inner request like template or $templateProviders.

Here is solution to work only with external http requests

/**
 * Interceptor to queue HTTP requests.
 */
$httpProvider.interceptors.push(['$q', function ($q) {
    var _queue = [];

    /**
     * Executes the top function on the queue (if any).
     */
    function _executeTop() {
        if (_queue.length === 0) {
            return;
        }
        _queue[0]();
    }

    return {
        /**
         * Blocks each request on the queue. If the first request, processes immediately.
         */
        request: function (config) {
            if (config.url.substring(0, 4) == 'http') {
                var deferred = $q.defer();
                _queue.push(function () {
                    deferred.resolve(config);
                });
                if (_queue.length === 1) {
                    _executeTop();
                }
                return deferred.promise;
            } else {
                return config;
            }
        },
        /**
         * After each response completes, unblocks the next request.
         */
        response: function (response) {
            if (response.config.url.substring(0, 4) == 'http') {
                _queue.shift();
                _executeTop();
            }
            return response;
        },
        /**
         * After each response errors, unblocks the next request.
         */
        responseError: function (responseError) {
            if (responseError.config.url.substring(0, 4) == 'http') {
                _queue.shift();
                _executeTop();
            }
            return $q.reject(responseError);
        },
    };
}]);
Strage answered 14/2, 2015 at 15:15 Comment(4)
Thanks for the enhancement! I'm not sure why you need to re-check config.url in response/responseError though? If the call is non-http, I understand it won't block, but is there harm in shifting the queue anyway?Witness
You have to use it because response is always executedStrage
You have to use it because response and responseError are always executed - even without promise. That's why You have to check httpStrage
If you're using relative URL like /api/... then the check for config.url.substring will always fail. I've replaced with different property that I also use to queue specific type of calls, and not all. if (config.queue) { and then in the request e.g. $http.get(url, {queue:true}).. So you can choose which type of requests you want to queue.Gentleness
R
4
.factory('qHttp', function($q, $http) {
  var queue = $q.when();

  return function queuedHttp(httpConf) {
    var f = function(data) {
      return $http(httpConf);
    };
    return queue = queue.then(f, f);
  };
})

How to use:

var apis = ['//httpbin.org/ip', '//httpbin.org/user-agent', '//httpbin.org/headers'];

for (var i = 0; i < 100; i++) {
  qHttp({
    method: 'get', 
    url: apis[i % apis.length]
  })
  .then(function(data) { 
    console.log(data.data); 
  });
}
Rustice answered 18/4, 2015 at 16:9 Comment(2)
can you add a bit more context to explain how someone might use this in their app?Egor
Best and easy answer.Goodrich
R
2

If someone need solution to sequential http calls (as mentioned by OP) in Angular 5 then following is the solution:

    import { Injectable } from '@angular/core';
    import { Response } from '@angular/http';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs/Observable';
    import { Subject } from 'rxjs/Subject'

    export class PendingRequest {
      url: string;
      method: string;
      options: any;
      subscription: Subject<any>;

      constructor(url: string, method: string, options: any, subscription: Subject<any>) {
        this.url = url;
        this.method = method;
        this.options = options;
        this.subscription = subscription;
      }
    }

    @Injectable()
    export class BackendService {
      private requests$ = new Subject<any>();
      private queue: PendingRequest[] = [];

      constructor(private httpClient: HttpClient) {
        this.requests$.subscribe(request => this.execute(request));
      }

      /** Call this method to add your http request to queue */
      invoke(url, method, params, options) {
        return this.addRequestToQueue(url, method, params, options);
      }

      private execute(requestData) {
        //One can enhance below method to fire post/put as well. (somehow .finally is not working for me)
        const req = this.httpClient.get(requestData.url)
          .subscribe(res => {
            const sub = requestData.subscription;
            sub.next(res);
            this.queue.shift();
            this.startNextRequest();
          });
      }

      private addRequestToQueue(url, method, params, options) {
        const sub = new Subject<any>();
        const request = new PendingRequest(url, method, options, sub);

        this.queue.push(request);
        if (this.queue.length === 1) {
          this.startNextRequest();
        }
        return sub;
      }

      private startNextRequest() {
        // get next request, if any.
        if (this.queue.length > 0) {
          this.execute(this.queue[0]);
        }
      }
    }

In case of someone wants to look at working plunker then here is the working plunker.

Route answered 3/1, 2018 at 12:50 Comment(1)
isn't the http call result here lost ?Frostbite
W
0

My two queues:

  1. Sequential queue QueueHttp: executes requests one after the other

code:

app.factory('QueueHttp', ($http, $q) => {
    let promise = $q.resolve();

    return (conf) => {
        let next = () => {
            return $http(conf);
        };

        return promise = promise.then(next);
    };
});

usage:

return QueueHttp({
           url: url,
           method: 'GET'
       });
  1. Delay queue DelayHttp: executes requests by a delay amount

code:

app.factory('DelayHttp', ($http, $timeout) => {
    let counter = 0,
        delay = 100;

    return (conf) => {
        counter += 1;

        return $timeout(() => {
            counter -= 1;
            return $http(conf);
        }, counter * delay);
    };
});

usage:

return DelayHttp({
           url: url,
           method: 'GET'
       });
Wireworm answered 14/3, 2019 at 13:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.