I'd like my objects to cache the result of some network requests and answer the cached value instead of doing a new request. This answer here done using angular promises looks a lot like what I'm going for, but I'm not sure how to express it using the Parse.com promise library. Here's what I'm trying...
module.factory('CustomObject', function() {
var CustomObject = Parse.Object.extend("CustomObject", {
cachedValue: null,
getValue: function() {
if (this.cachedValue) return Parse.Promise.as(this.cachedValue);
return this.functionReturningPromise().then(function (theValue) {
this.cachedValue = theValue;
return this.cachedValue;
});
},
My idea is to return a promise whether or not the value is cached. In the case where the value is cached, that promise is resolved right away. The problem is, as I follow this in the debugger, I don't seem to get the cached result on the second call.
Your value is almost correct. Your design is correct the only issue you have here is dynamic this.
In the context of the .then handler, this is set to undefined (or the window object), however - since you're using Parse promises and I'm not sure those are Promises/A+ compliant it can be arbitrary things - the HTTP request, or whatever. In strict code and a good promise library - that would have been an exception.
Instead, you can do CustomObject.cachedValue explicitly instead of using this:
var CustomObject = Parse.Object.extend("CustomObject", {
cachedValue: null,
getValue: function() {
if (CustomObject.cachedValue) return Parse.Promise.as(this.cachedValue);
return this.functionReturningPromise().then(function (theValue) {
CustomObject.cachedValue = theValue;
return this.cachedValue;
});
},
If $q promises are also possible instead of Parse promises, I'd use those instead:
var cachedValue = null;
getValue: function() {
return $q.when(cachedValue || this.functionReturningPromise()).then(function(theValue){
return cachedValue = theValue;
});
}
You can just cache the promise and return that
module.factory('CustomObject', function() {
var CustomObject = Parse.Object.extend("CustomObject", {
cachedPromise: null,
getValue: function() {
if (!this.cachedPromise) {
this.cachedPromise = this.functionReturningPromise();
}
return this.cachedPromise;
},
...
}
...
}
I am not familiar with the Parse.com promise library, but it could be a plain JS error:
The this inside the function is not referring to the Promise object, but to the global object.
Change the code like that:
...
getValue: function() {
if (this.cachedValue) return Parse.Promise.as(this.cachedValue);
var that = this;
return this.functionReturningPromise().then(function (theValue) {
that.cachedValue = theValue;
return that.cachedValue;
});
},
Related
I'm trying to wrap a third party library to return an object that resolves into an object that can be displayed in the view, similar to how $resource() works. I'm aware that I can manually do .then() on the promise and then set the value, but I wanted the result to seamlessly return similar to how I can do:
this.Value = $resource("/someresource").get();
How would I change the below SomeThirdPartyFunction() to return an object that resolves in the view.
Here's an example of what I'm trying to do:
angular.module('testApp', []).controller('TestController', function ($timeout, $q) {
var TestController = this;
var SomeThirdPartyFunction = function () {
var Deferred = $q.defer();
var Promise = Deferred.promise;
$timeout(function () {
Deferred.resolve("abcd");
}, 3000);
return Promise;
};
TestController.Value = SomeThirdPartyFunction();
/* I don't want to do this:
SomeThirdPartyFunction().then(function(Value) {
TestController.Value = Value;
});*/
});
And here's a plunker: https://plnkr.co/edit/HypQMkaqXmFZkvYZXFXf?p=preview
Every example I've seen using promises just wraps $http calls, but I haven't seen any examples of calling third party libraries that return promises that resolve into objects.
From the AngularJS document:
It is important to realize that invoking a $resource object method
immediately returns an empty reference (object or array depending on
isArray). Once the data is returned from the server the existing
reference is populated with the actual data.
So instead of return a promise, you can do something like this:
var SomeThirdPartyFunction = function() {
var getAComplextObject = function() {
return {
number: 42,
method: function() {
return this.number + 1;
}
};
};
var returnValue = {};
$timeout(function() {
console.log("Resolved!");
Object.assign(returnValue, getAComplextObject());
}, 1000);
return returnValue;
};
You can wrap it in a promise and make the promise part of the return value, doing that you can make it thenable (aka a Promise)
Under-the-hood, $resource uses angular.copy:
function myResourceGet(params) {
var emptyObj = {};
emptyObj.$resolved = false;
emptyObj.$promise = $http.get(url, {params:params})
.then(function(response) {
angular.copy(response.data, emptyObj);
}).finally(function() {
emptyObj.$resolved = true;
});
return emptyObj;
}
From the Docs:
angular.copy Overview
Creates a deep copy of source, which should be an object or an array.
If a destination is provided, all of its elements (for arrays) or properties (for objects) are deleted and then all elements/properties from the source are copied to it.
The $resource service only works when the data is an object or an array. It does not work with primitives such as a string or number.
I have a series of services that either fetch data from an API server, or return data if it exists in local storage.
.factory('LogEntryResource', function(config, $resource) {
return $resource(config.apiUrl + 'logentries/:id/');
})
.factory('LogEntry', function(localStorageService, LogEntryResource) {
var localLogEntries = localStorageService.get("logEntries");
return {
all: function() {
if(localLogEntries){
return localStorageService.get("logEntries");
} else {
return LogEntry.query(function(data){
localStorageService.set("logEntries", data);
return data;
});
}
},
get: function(logEntryId){
...
},
delete: function(logEntryId){
...
},
update: function(logEntryId){
...
}
}
})
The problem is that in the app controllers sometimes a promise is returned, and sometimes the data is returned, so I need to handle the return value of LogEntry.all() to either wait for the promise to resolve or to use the data.
I'm not really sure how to go about it because I can either use a .then() which works for the promise, but is undefined if it has data, or vice-versa. I know I'm doing something wrong and looking for advice how to handle this situation of dealing with either data or a promise being returned.
.controller('LogEntryCtrl', function($scope, LogEntry) {
// var data = LogEntry.all();
// var promise = LogEntry.all();
$scope.logEntry = ???
}
I'm hoping there's a nice reusable solution instead of having to do a check to see what it is every time I use this code in my controllers/routes
// trying to avoid doing this
var logEntry = LogEntry.all();
if(logEntry.isPromise){
// do promise stuff here
} else if(logEntry.isData {
// do data stuff here
}
My suggestion would be always return a promise. You can use $q.resolve() to create a shortcut for a resolved promise
.factory('LogEntry', function(localStorageService, LogEntry, $q) {
var localLogEntries = localStorageService.get("logEntries");
return {
all: function() {
if(localLogEntries){
return $q.resolve(localLogEntries);
} else {
return LogEntry.query(function(data){
localStorageService.set("logEntries", data);
// update variable also
localLogEntries = data;
return localLogEntries ;
});
}
},
In controller you always use then() this way
LogEntry.all().then(function(data){
$scope.data = data;
});
I have the following pattern in my AngularJS which calls for refactoring:
$scope.advertisers = Advertiser.query()
$scope.advertisersMap = {};
$scope.advertiser.$then(function (response) {
arrayToMap(response.resource, $scope.advertisersMap)
}
arrayToMap is function that adds each item in an array to the object with it's ID as key.
Now, I would have liked that this will happen in Advertiser itself, e.g.
$scope.allAdvertisers = Advertiser.query()
// Somewhere else
var advertiser = Advertiser.get({id: 2})
Where Advertiser.get({id: 2}) will return from a cache populated earlier by the query method.
Advertiser is defined in factory:
.factory('Advertiser', ['djResource', 'Group', function ($djResource, Group) {
var Advertiser = $djResource('/campaigns/advertisers/:advertiserId/', {advertiserId: '#id'});
Advertiser.prototype.getGroups = function () {
return Group.getByAdvertiser({advertiserId: this.id});
};
return Advertiser;
}])
Sadly, DjangoRestResource (and $resource which it wraps) caches by URLs, so query() will cache /advertisers/ while get(2) will cache /advertisers/2/, but I want query to cache in such way that get will be able to retrieve it as well.
I've tried replacing the query function by a wrapper function that does the caching, but I want it to return an promise which is also an Array like $resource does. It was something like:
var oldQuery = Advertiser.query;
var cache = $cacheFactory('advertisers');
Advertiser.query = function () {
var promise = oldQuery();
return promise.then(function (response) {
angular.forEach(response.resource, function (resource) {
cache.put(resource.id, resource)
})
})
};
But then the returned promise is no longer an Array-like object, and it doesn't not encapsulate the returned results in an Advertiser object as it used to, which breaks most of my code expects that Advertiser.query() will eventually be an Array.
What other approaches should I try? This snippet repeats itself in every controller for multiple factories and it hurts my eyes.
Here is solution to the problem:
function makeCachingMethod(object, method, cache, config) {
var $q = angular.injector(['services']).get('$q');
var oldMethod = object[method];
object[method] = function () {
var result;
if (!config.isArray) {
var id = config.idParam ? arguments[0][config.idParam] : arguments[0];
result = cache.get(id);
if (result !== undefined) {
if (result.$promise === undefined) {
result.$promise = $q.when(result);
result.$resolved = true;
}
return result;
}
}
result = oldMethod.apply(this, arguments);
result.$promise.then(function (data) {
if (config.isArray) {
angular.forEach(data, function (item) {
cache.put(item.id, item);
})
} else {
cache.put(data.id, data);
}
return data;
});
return result;
}
}
And an example usage:
app.factory('Country', ['$resource', '$cacheFactory', function ($resource, $cacheFactory) {
var Country = $resource('/campaigns/countries/:id', {id: "#id"}, {
query: {method: 'GET', url: '/campaigns/countries/', isArray: true},
get: {method: 'GET', url: '/campaigns/countries/:id/', isArray: false}
});
var cache = $cacheFactory('countries');
makeCachingMethod(Country, 'query', cache, {isArray: true});
makeCachingMethod(Country, 'get', cache, {idParam: 'id'});
return Country;
}])
What is happening here?
I use makeCachingMethod to decorate the original method created by $resource. Following the pattern used by $resource itself, I use a configuration object to signal whether the decorated method returns an array or not, on how the id is passed in queries. I assume, though, that the key of the ID to save is 'id', which is correct for my models but might need to be changed.
Noticed that before returning an object from the cache, the decorator adds to it $promise and $resolved attributes, since my application expects objects originated from $resource which have these properties, and in order to keep using the promises API, e.g.:
$scope.advertiser = Advertiser.get({advertiserId: $scope.advertiserId});
$scope.advertiser.$promise.then(function () {
$scope.doSomething();
});
Notice that since the function is defined outside the scope of any Angular module it is required to inject the $q service using angular.injector(). A nicer solution will be to return a service for invoking the decorator function. Such service could also handle the generation of the caches themselves.
This solution does not handle the expiration of cached models, which isn't much of problem in my scenario, as these rarely change.
I'm not aware of any cache support directly with $resource, you could create a factory that abstracts the Advertiser resource and handle the caching yourself.
Here is some helpful info on caching:
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#validating-cached-responses-with-etags
I'm working on an app at the minute using $resource and every method is abstracting so I can check the cache to see if what I'm requesting is there and return it, if not, make the call using $resource.
I have a service method that has some caching logic:
model.fetchMonkeyHamById = function(id)
{
var that = this;
var deferred = $q.defer();
if( that.data.monkeyHam )
{
deferred.resolve( that.data.monkeyHam );
return deferred.promise;
} else {
return this.service.getById( id).then( function(result){
that.data.monkeyHam = result.data;
});
}
};
I know how to use $httpBackend to force the mocked data to be returned. Now, how do I force it to resolve (and then test the result) when I've set the data explicitly?
I want to test the result in the controller then() function:
MonkeyHamModel.fetchMonkeyHamById(this.monkeyHamId).then( function() {
$scope.currentMonkeyHam = MonkeyHamModel.data.monkeyHam;
});
Then my test I want to explicitly set the data (so it loads from memory "cache" instead of httpBackend)
MonkeyHamModel.data.monkeyHam = {id:'12345'};
MonkeyHamModel.fetchMonkeyHamById( '12345');
// How to "flush" the defer right here like I would have flushed httpBackend?
expect( $scope.currentMonkeyHam.id ).toEqual('12345'); //fails because it is not defined yet since the $q never resolved
Where $scope is just the scope of my controller, but called $scope here for brevity.
UPDATE:
The suggested answer does not work. I need the function to return a promise, not a value that is the result of a promise:
model._monkeyHams = {} // our cache
model.fetchMonkeyHamById = function(id){
return model.monkeyHams[id] || // get from cache or
(model.monkeyHams[id] = this.service.getById(id).then(function(result){
return result.data;
}));
};
The following requires that you have touched the server already. I create a model on the front end (currentMonkeyHam) or whatever, and don't load it back after the first POST (an unnecessary GET request). I just use the current model. So this does not work, it requires going out to the server at least once. Therefore, you can see why I created my own deferred. I want to use current model data OR get it from the server if we don't have it. I need both avenues to return a promise.
var cache = null;
function cachedRequest(){
return cache || (cache = actualRequest())
}
Your code has the deferred anti pattern which makes it complicated - especially since you're implicitly suppressing errors with it. Moreover it is problematic for caching logic since you can end up making multiple requests if several requests are made before a response is received.
You're overthinkig it - just cache the promise:
model._monkeyHams = {} // our cache
model.fetchMonkeyHamById = function(id){
return model.monkeyHams[id] || // get from cache or
(model.monkeyHams[id] = this.service.getById(id).then(function(result){
return result.data;
}));
};
In your case, you were caching all IDs as the same thing, the general pattern for caching promises is something like:
var cache = null;
function cachedRequest(){
return cache || (cache = actualRequest())
}
Creating deferred is tedious and frankly - not very fun ;)
You can use setTimeout (or $timeout) for resolving the promise.
You can modify your code as -
model.fetchMonkeyHamById = function(id)
{
var that = this;
var deferred = $q.defer();
if( that.data.monkeyHam )
{
setTimeout(function() {
deferred.resolve(that.data.monkeyHam);
}, 100);
return deferred.promise;
} else {
return this.service.getById( id).then( function(result){
that.data.monkeyHam = result.data;
});
}
};
EDIT:
Modified as per Benjamin's suggestion -
Using $rootScope.digest() - code should be something like this
MonkeyHamModel.data.monkeyHam = {id:'12345'};
MonkeyHamModel.fetchMonkeyHamById( '12345');
$rootScope.digest();
We've done something similar in our code base, but instead of having an object with state that constantly changed we went with something that looks more like a traditional repository.
someInjectedRepoistory.getMonkeyHamModel().then(x => $scope.monkeyHam = x);
Repository{
getMonkeyHamModel() {
var deferred = $q.defer();
if( this.cache.monkeyHam )
{
deferred.resolve( this.cache.monkeyHam );
} else {
return this.service.getById( id).then( function(result){
this.cache.monkeyHam = result.data;
});
}
return deferred.promise
}
}
There are no problems with returning a completed deferred. That's part of the purpose of the deferreds, it shouldn't matter when or how they are resolved, they handle all of that for you.
As for your test we do something like this.
testGetFromService(){
someInjectedRepoistory.getMonkeyHamModel().then(x => verifyMonkeyHam(x));
verifyHttpBackEndGetsCalled()
}
testGetFromCache(){
someInjectedRepoistory.getMonkeyHamModel().then(x => verifyMonkeyHam(x));
verifyHttpBackEndDoesNotGetCalled()
}
I'm trying to create a service that will hold the shopping cart content of my website using AngularJS. I will then use this to make sure the reference to the cart in all controllers etc should be to the same object and synced.
The problem is that the cart content must be initialized via an ajax call. My code below does not work but it shows what I'm trying to accomplish. I would like to somehow get the list of items and return with getItems(), but if the list of items is not yet fetched then I will need to fetch for it first and promise a return. I'm trying to wrap my head around the "promise" concept but so far I have not fully got it yet.
factory('cartFactory', function ($rootScope, Restangular) {
var cart = $rootScope.cart = {};
return {
getItems: function () {
if (undefined == cart.items) {
return Restangular.oneUrl('my.api.cart.show', Routing.generate('en__RG__my.api.cart.show')).get().then(function($cart){
cart = $rootScope.cart = $cart;
angular.forEach(cart.items, function(value, key){
cart.items[key]['path'] = Routing.generate('en__RG__my.frontend.product.info', {'slug': value.variant.product.slug});
});
return cart.items;
});
} else {
return cart.items
}
},
setItems: function ($items) {
cart.items = $items;
},
removeItem: function ($item) {
cart.splice(cart.indexOf($item), 1);
},
addItem: function ($item) {
cart.items.push($item)
}
}
})
I will try to explain this in a very simplified way.
A promises is just an object that is "passed around" and we use this objects to attach functions that will be executed whenever we resolve, reject or notify the promise.
Because in Javascript objects are passed by reference we are able to refer to the same object in several places, in our case inside the service and the controller.
In our service we execute:
getItems: function () {
var deferred = $q.defer();
// do async stuff
return deferred.promise;
}
Lets say that the variable deferred above is an object more os less like this:
{
reject: function (reason) {
this.errorCallback(reason);
},
resolve: function (val) {
this.successCallback(val);
},
notify: function (value) {
this.notifyCallback(value);
},
promise: {
then: function (successCallback, errorCallback, notifyCallback) {
this. successCallback = successCallback;
this.errorCallback = errorCallback;
this.notifyCallback = notifyCallback;
}
}
}
So when we call getItems() a promise (deferred.promise) is returned and this allows the callee to set the callbacks to be executed whenever the promise changes its state (resolve, reject or notify).
So inside our controller I am setting only the resolve callback, if the promises is rejected it will happen silently because there is no errorCallback to be executed.
cartFactory.getItems().then(function (items) {
$scope.items = items;
});
Of course there is much more behind it, but I think this simplistic promise will help you get the basic idea. Be aware that cartFactory.getItems() must always return a promise, even when the items are already loaded, otherwise cartFactory.getItems().then() would break if , for example, you return an array.
Here a JSBin with your cartFactory service, I am using $timeout to simulate an async call.
Hope this helps.