8

I've built a somewhat complex method for returning a resources via $http.

The method returns a promise and then checks my local cache if the resources exists yet. If it does it will return the cached resources, if not it will make the $http request. This works great after the resource has been cached, but I have multiple functions through out the application that is hitting this method on load, and every one of them will make the http request because the resources hasn't been returned and cached yet.

I came up with a simple check that fixes this, but I feel like there should be a better way. I added a boolean that is set to true if the method is in the middle of getting the resource, and if it is I resolve the method with a half second timeout, to give the request time to resolve. Code is below.

So, is there a better way?

   var schools = [];
   var loadingSchools = false;

   function getAllSchools(forceUpdate) {
        return $q(function (resolve, reject) {
            if(loadingSchools) resolve($timeout(getAllSchools, 500));

            else{

                loadingSchools = true;

                if (schools.length && !forceUpdate) {
                    loadingSchools = false;
                    resolve(schools);
                    return;
                }

                console.log('$http: Getting All Schools - schoolService.js');

                $http.get(API_PATH + 'schools_GetAll', {cache:true})
                .success(function(result) {
                    schools = result;
                    loadingSchools = false;
                    resolve(schools);
                })
                .error(function(error) {
                    schools = [];
                    loadingSchools = false;
                    reject(error);
                });
            }
        });
    }
6
  • 1
    maybe create a function that manages the waiting promises so you basically instead of setting timeout you register to that function and when the original promise returns you resolve all of the ones that registered Commented Oct 5, 2015 at 21:23
  • 2
    far simpler to use a resolve in router, especially if using ui-router. WHich router are you using? Commented Oct 5, 2015 at 21:28
  • @Saar - Good thinking, I was considering doing something like that, but wanted to make this post first. I figured there would be a method of doing this built into $http that I could leverage. Commented Oct 5, 2015 at 21:34
  • 1
    @Charlietfl - I'm using angular-route-segment.com Commented Oct 5, 2015 at 21:34
  • OooooK .... never even knew that one existed. In angular-ui router which also supports nested views , a resolve in a parent route is available in all child routes. Thus one api call is made and data is available throughout from that point Commented Oct 5, 2015 at 21:39

2 Answers 2

24
+50

First off, I don't think it's necessary to wrap the HttpPromise into another promise. With the success/error methods deprecated, you should rely solely on the then() method, and treat the HttpPromise as just a regular promise.

In order to make sure that the request is sent out only once, you can actually keep track of the first HttpPromise you create, and on subsequent calls of the function, return that same promise.

Here is a service which will accept an API endpoint as an argument, and ensure that only one request is sent out to that API.

app.factory('$httpOnce', [ '$http', '$cacheFactory',
  function ($http, $cacheFactory) {
    var cache = $cacheFactory('$httpOnce');

    return function $httpOnce(url, options) {
      return cache.get(url) || cache.put(url, $http.get(url, options)
        .then(function (response) {
          return response.data;
        }));
    };
  }
]);

Usage

function log(data) {
  console.log(data);
}

// issues an HTTP request
$httpOnce('https://api.github.com/').then(log);
// does not issue an HTTP request, returns the same promise as above
$httpOnce('https://api.github.com/').then(log);

// ...
// HTTP request completes somewhere, both promises above are resolved
// ...

setTimeout(function () {
  // immediately resolved
  $httpOnce('https://api.github.com/').then(log);
}, 5000);

Here is a demo. You can see in the dev tools that only one request is issued.

Sign up to request clarification or add additional context in comments.

6 Comments

Exactly what I was looking for. Thanks for the answer.
I have a question regarding this solution, I want to understand better why it works. When you put the $http.get result into the cache, you also run the promise (so the $http.get(url, options) returns a promise and you use it) before adding it to cache). Why can you use another .then() when getting the cache ? There is no promise returned from the 'cache.get'. $http.get returns one promise and returning the result - returns another promise or the same ? Thank you for your time, let me know if you have any reference for me to read more about it.
Surely the cache: true flag does the same thing? Am I missing something?
@florinmtsc then() method of a promise returns a new promise. I use it to create a promise which is resolved with the response data rather than the response object. See the Promise chaining documentation for an explanation and some examples. There is no way to "run the promise" as you describe it.
@zilj It's close, but when using cache: true, the cache is only populated after the first request has completed. If I issue three back-to-back requests with an empty cache, all three will reach the server, which is not what OP wanted. My solution solves this issue.
|
0

I just the exact same problem and here is my service

app = angular.module('MM_Graph')

class Team_Data
  constructor: ($routeParams,$rootScope, $cacheFactory, $q, API)->
    @.$routeParams  = $routeParams
    @.$rootScope    = $rootScope
    @.$cacheFactory = $cacheFactory
    @.cache         = $cacheFactory('team_Data')
    @.$q            = $q
    @.deferred      = null
    @.API           = API
    @.project       = null
    @.data          = null
    @.team          = null
    @.schema        = null


  call_With_Cache: (method, params, callback)=>                                 # method that uses promises to prevent multiple parallel calls

    cache_Key = "#{method}_#{JSON.stringify(params)}"                           # create cache key using method and params

    if not @.cache.get cache_Key                                                # if this is the first require for this type of data (method_project_team)
      deferred = @.$q.defer()                                                   # create a new instance of deferred
      @.cache.put cache_Key, deferred.promise                                   # put its promise in the cache

      on_Data_Received = (data)->                                               # simple callback method to resolve the promise
        deferred.resolve(data)                                                  # resolve promise (this is the data that will show as the 1st param in 'then')

      method_Params = params.concat(on_Data_Received)                           # append on_Data_Received callback method to the current 'method' params list

      @.API[method].apply(null, method_Params)                                  # invoke 'method' in @.API

    @.cache.get(cache_Key).then (data)->                                        # invoke the 'then' from the promise (will happen async until data is recevied)
      callback (data)                                                           # finally call the original callback with the data received from 'method'

  clear_Data: ()=>
    @.cache.removeAll()
    @.scores   = null
    @.schema   = null
    @.data     = null
    @.deferred = null

  load_Data: (callback)=>
    if not (@.$routeParams.project and @.$routeParams.team)                     # check that project and team are set
      return callback()

    if (@.$routeParams.project is @.project and @.$routeParams.team is @.team)  # check if either the project or team have changed
      # do nothing here since project or team have not changed
    else
      @.clear_Data()                                                            # when project changes remove all cache entries
      @.project = @.$routeParams.project
      @.team    = @.$routeParams.team
      @.project_Schema (schema)=>                                               # get projecg schema
        @.data_Score (scores)=>                                                 # get current team scores
          @.team_Get (data)=>                                                   # get team data
            @.scores = scores
            @.schema = schema
            @.data   = data

            @.deferred.resolve()                                                # trigger promise resolve

    @.deferred ?= @.$q.defer()                                                  # ensure @.deferred object exits
    @.deferred.promise.then ->                                                  # schedule execution
      callback()                                                                # invoke original caller callback

  data_Score    : (             callback) => @.call_With_Cache 'data_Score'       , [@.project, @.team      ], callback
  project_Schema: (             callback) => @.call_With_Cache 'project_Schema'   , [@.project              ], callback
  radar_Fields  : (             callback) => @.call_With_Cache 'data_Radar_Fields', [@.project              ], callback
  radar_Team    : (target_Team, callback) => @.call_With_Cache 'data_Radar_Team'  , [@.project, target_Team ], callback
  team_Get      : (             callback) => @.call_With_Cache 'team_Get'         , [@.project, @.team      ], callback

  save: (callback)=>
    @.API.file_Save @.project, @.team , @.data, callback

app.service 'team_Data', ($routeParams, $rootScope, $cacheFactory, $q, API)=>
  return new Team_Data $routeParams, $rootScope, $cacheFactory, $q, API

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.