I'm running into a bit of a problem with an Angular (1.4) service. The code is roughly as follows :
service.retrieveStuffFromServer = function() {
return httpCallFromServer().then(
function(data) {
if (Array.isArray(data)) {
return data;
}
return [];
}
);
};
I call this function in two distinct controllers. Most of the times, it works as intended, but I'm having problem in those conditions :
The HTTP call takes time to return the data
Controller A calls the service.
Controller B calls the service.
The service returns data to controller A
The call in the controller B is cancelled. The logic after it never executes
My first guess would be to slightly alter the service, to inform either of the controllers if the service is already busy so I can retry later, but I'm not sure if this is the best solution, so I'm looking for some advice.
Hard to say why it doesn't just work, but presumably something in httpCall() is preventing the same call from being made again before the 1st one completes, and it rejects if that happens. But if you want controller B call to share the response from an active previous call, you could cache the promise:
function myFunction() {
if (!myFunction.promise) {
myFunction.promise = httpCall()
.then(function(result) {
myFunction.promise = undefined;
return ...
}, function(err) {
myFunction.promise = undefined;
throw err;
});
}
return myFunction.promise;
}
This will cause the same promise from a prior call to be returned as long as the prior call is still unresolved.
Using a property of the function itself as a cache is a convenient way to keep state associated logically with the function itself. You could just use any variable defined outside the scope of myFunction though.
Related
I'm kind of newbie to AngularJS and struggling with the promise API. I've implemented a save() function in my controller in order to get:
The regulation version saved
Once the regulation version is saved, all of its installation types are saved in an async-parallel way
The saved! log displayed after everything is done (in my real application I want to go to other application state here)
That's the code I currently have in my controller:
var saveRegulationInstallationTypes = function (result) {
var saveOperations = [];
angular.forEach(vm.installationTypeRegs, function (
installationTypeReg, key) {
installationTypeReg.regulationVersion = result;
var res = InstallationTypeReg.update(installationTypeReg, function () {
console.log('Installation type updated');
});
saveOperations.push(res);
});
return $q.all(saveOperations);
}
function save() {
var regulationVersionSaved = RegulationVersion.update(vm.regulationVersion);
return regulationVersionSaved.$promise.then(
function (result) {
saveRegulationInstallationTypes(result).then(console.log('saved!'));
});
}
RegulationVersion and InstallationTypeReg are services returning $resource methods for each of the http operations over the entities. The problem when executing the save() method is that I get the saved! log before the Installation type updated ones. I guess, as I'm returning q.all from my function, it should wait to finish all the inner operations before calling the callback.
What am I doing wrong?
Seems two issues: 1) "saveOperations.push(res);" -- should be "saveOperations.push(res.$promise);" -- As in the array it should store the promise not whole object. and 2) saveRegulationInstallationTypes(result).then(console.log('saved!')); should be saveRegulationInstallationTypes(result).then(function(){console.log('saved!')});
I'm creating a factory to take a userId from one page, make a call to a REST API, and return the results on the following view. My initial attempts were largely taken from this answer but - unsurprisingly - I keep getting caught in a situation where the doesn't respond in time and the get() method returns an empty array.
Here's the factory itself
app.factory('GetMessages', function() {
var messages = []
function set(userId) {
Restangular.all('/api/messages/').getList({'_id': userId}).then(function(docs){
messages = docs
})
}
function get() {
return messages;
}
return {
set: set,
get: get
}
});
For what it's worth I'm having no trouble getting the userId into the factory as it's just passed in on a function like this
view:
<a ng-click='passToFactory(message.user.id)' href='/home/inbox/reply'>Reply</a>
controller:
$scope.passToFactory = function(id) {
GetMessages.set(id);
};
and the controller for the following view is just
$scope.messages = GetMessages.get()
The issue I'm having is that after the factory returns the empty set no further changes from the factory are recognized (even though after time elapses it does get the proper response from the API) and $scope.messages remains empty.
I've attempted to move the API call to the get method (this hasn't worked as the get method often does not get the userId in time) and I can't find a way to use a promise to force get() to wait on set() completing.
I'd prefer to keep using Restangular in the eventual solution but this is a small thing that has taken too much time so any fix works.
I'm fairly new to Angular so I'm sure there's something totally obvious but right now I'm just lost. Thanks.
The race condition that you have is that the function inside the .then method is executed asynchronously after the call to the set function. If the get function executes before the $q service fulfills the promise, the get function returns an empty array.
The solution is to save the promise and chain from the promise.
app.factory('GetMessages', function() {
var promise;
function set(userId) {
promise = Restangular.all('/api/messages/').getList({'_id': userId});
}
function get() {
return promise;
}
return {
set: set,
get: get
}
});
In your controller, chain from the promise.
GetMessages.get.then( function (docs) {
$scope.messages = docs;
}) .catch ( function (error) {
//log error
};
For more information on chaining promises, see the AngularJS $q Service API Reference -- chaining promises.
You are breaking the reference to the original messages array when you reassign it.
Try:
Restangular.all('/api/messages/').getList({'_id': userId}).then(function(docs){
messages.concat(docs) ; // keep same array reference
});
Simple example to explain why it isn't working
var arr = [];
var x = arr;
arr = [1,2,3]; // is now a different array reference
console.log(x); // is still empty array. x !== arr now
cherlietfl is right.
The problem is that you break the reference to the messages array since you assign a new array to messages inside your get function. But concat is doing this as well.
Try this:
Restangular.all('/api/messages/').getList({'_id': userId}).then(function(docs){
messages.splice(0, messages.length); // clear the array
messages.push.apply(messages, docs); //add the new content
});
Try assigning you function to the scope. Then call that function in the model. Like so:
// controller
$scope.getMessages = GetMessages.get;
View:
<div ng-repeat="message in getMessages()"></div>
This way when the request call finishes and the digest cycle goes through the watchers again, the get function will be called and you will get your messages.
Updated with HTTP and initial code based on requests/Please look at the bottom of the post:
I've been posting several questions on my AngularJS learning curve of late and the SO community has been fantastic. I've been a traditional C programmer when I used to program and have recently started writing my own ionic/Angular JS app. I'm struggling with the promise version of traditional async calls when it comes to converting a custom function to a promise. I don't think I really understood and I find various examples very contrived. I'd appreciate some help. I have some code which is not working, and I have some conceptual questions:
Let's take this simple function:
angular.module('zmApp.controllers').service('ZMDataModel', function() { return { getMonitors: function () { return monitors; } }
getMonitors is a simple function that basically returns an array of monitors. But here is the rub: When the app first starts, I call an http factory that does an http get and goes about populating this monitor list. This http factory is different from this service but invokes a setMonitor method in this service to populate the array. When the array is populated, a variable called 'monitorsLoaded' is set to 1. When this variable is set to 1, I know for sure monitors is loaded.
Now, I have a view with a controller called "MontageCtrl". I want to wait for the monitors to load before I show the view. In a previous post, one person suggested I use route resolve, but I had to first convert my getMonitors to a promise. So here is what I did:
angular.module('zmApp.controllers').service('ZMDataModel', function($q) {
getMonitors: function () {
var _deferred = $q.defer();
if (monitorsLoaded!=0)
{
console.log ("**** RETURNING MONITORS *****");
_deferred.resolve(monitors);
}
console.log ("*** RETURNING PROMISE ***");
return _deferred.promise;
},
Next up, in app.js I connected the route as follows:
.state('app.montage', {
data: {requireLogin:false},
resolve: {
message: function(ZMDataModel)
{
console.log ("Inside app.montage resolve");
return ZMDataModel.getMonitors();
}
},
Finally I modified my controller to grab the promise as such:
angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', function($scope,$rootScope, ZMHttpFactory, ZMDataModel,message) {
//var monsize =3;
console.log ("********* Inside Montage Ctrl");
It seems based on logs, I never go inside Montage Ctrl. Route resolve seems to be waiting for ever, whereas my logs are showing that after a while, monitorLoaded is being set to 1.
I have several conceptual questions:
a) In function getMonitors, which I crafted as per examples, why do people return a _deferred.promise but only assign a _deferred.resolve? (i.e. why not return it too?). Does it automatically return?
b) I noticed that if I moved var _deferred definition to my service and out of its sub function, it did work, but the next view that had the same route dependency did not. I'm very confused.
c) Finally I ready somewhere that there is a distinction between a service and a factory when it comes to route resolve as a service is only instantiated once. I am also very confused as in some route resolve examples people use when, and I am using .state.
At this stage, I'm deep into my own confusion. Can someone help clarify? All I really want is for various views to wait till monitorsLoaded is 1. And I want to do it via route resolves and promises, so I get the hang of promises once and for all.
Added: Here is the HTTP factory code as well as the app.run code that calls this when the app first starts. FYI, the http factory works well - the problems started when I crafted ZMDataModel - I wanted this to be a central data repository for all controllers to use -- so they did not have to call HTTP Factory each time to access data, and I could control when HTTP factory needs to be called
angular.module('zmApp.controllers').factory('ZMHttpFactory', ['$http', '$rootScope','$ionicLoading', '$ionicPopup','$timeout','ZMDataModel',
function($http, $rootScope, $ionicLoading, $ionicPopup, $timeout,ZMDataModel) {
return {
getMonitors: function() {
var monitors = [];
var apiurl = ZMDataModel.getLogin().apiurl;
var myurl = apiurl+"/monitors.json";
return $http({
url: myurl,
method: 'get'
}) //http
.then(function(response) {
var data = response.data;
//console.log("****YAY" + JSON.stringify(data));
// $rootScope.$broadcast ('handleZoneMinderMonitorsUpdate',monitors);
$ionicLoading.hide();
ZMDataModel.setMonitors(data.monitors);
ZMDataModel.setMonitorsLoaded(1);
//monitors = data.monitors;
return ZMDataModel.getMonitors();
},
function (result)
{
console.log ("**** Error in HTTP");
$ionicLoading.hide();
ZMDataModel.setMonitorsLoaded(1);
//$ionicPopup.alert ({title: "Error", template:"Error retrieving Monitors. \nPlease check if your Settings are correct. "});
return ZMDataModel.getMonitors();
}
); //then
}, //getMonitors
And here is the code in app.run that first calls this:
.run(function($ionicPlatform, $ionicPopup, $rootScope, $state,ZMDataModel, ZMHttpFactory)
{
ZMDataModel.init();
var loginData = ZMDataModel.getLogin();
if ( loginData.username && loginData.password && loginData.url && loginData.apiurl)
{
console.log ("VALID CREDENTIALS. Grabbing Monitors");
// this calls http factory getMonitors that eventually populated the ZMDataModel
// monitors array and sets monitorsLoaded to 1
ZMHttpFactory.getMonitors();
}
}
I finally solved all the problems. There were various issues with my initial attempts. My final resolved solution is here Am I returning this promise correctly?
The learnings:
a) Separating the HTTP get into a factory and the data model into another service was unnecessarily complicating life. But that separation was not the problem. Infact, the way the promise was coded above, on first run, if monitorsLoaded was 0, it would simply return the deferred promise and there was no ".success" or similar construct for me to get into the resolve code block again.
b) The biggest thing that was making me run around in loops was deferring or rejecting was simply setting a state. the return always has to be the promise - and it would return the state you set. I assumed return d.promise always means returning "in progress".
I have a promise that runs without a problem when it runs during application start, e.g.
myPromise
.success( function(data) { $scope.myvariable = data })
.error( function(msg, code) { console.log("msg: " + msg + "\nCode" + code) });
However if I try to run the promise dynamically, let's say when a button is clicked, (1) the promise executes successfully but none of my variables are updated.
running apply or digest only produces the following error: $digest already in progress
$scope.getContent = function() {
myPromise
.success( function(data) {
$scope.myVariable = data; //THIS WORKS
console.log(data); //THIS WORKS
})
}
//Running the below code afterwards still produces a null value
console.log($scope.myVariable);
This is what we called as async world.
When you are waiting for the callback of your promise the statement console.log($scope.myVariable); already executed but still you don't have any value in it.
In that case you can use $watch if you want to get it value outside.
$scope.$watch('myVariable',funciton(newVal){
console.log(newVal);
});
Little Detail:-
myPromise is invoked and waiting for the response in the meanwhile the statement console.log($scope.myVariable); after it executed which obviously doen't have any value for $scope.myVariable inside it (nobody gave it :-P). So when response came back it call the success or error method and initialize the value to your variable $scope.myVariable and print it.
Here your console.log($scope.myVariable); statement executes before success callback so here you need to do .then() chaining or apply watch on scope variable.
The reason your console.log, which comes after your promise logs null is because it is executing before your promise returns. Even though the function using the promise has run, the success part of the code has not fired yet. The code hits the promise, makes the calls, creates the promise object, and moves on. When the promise returns, it fills in the empty promise object with the returned data. This is happening after your
console.log($scope.myVariable);
A good way maybe to handle it is to store the returned value in a variable. Inside the success set the variable to the returned data, then use a function like
$scope.myVariable = undefined;
$scope.getContent = function() {
myPromise
.success( function(data) {
$scope.myVariable = data; //THIS WORKS
console.log(data); //THIS WORKS
})
}
function checker() {
if($scope.myVariable) {
console.log($scope.myVariable);
}
}
Then you can call that function as needed.
Depending on what you are doing and when your getContent function needs to run, you may want to use this with ui-router, and use resolve, which will run these functions before the page loads, thereby ensuring you have data to work with when the DOM loads.
AngularJS UI-Router: preload $http data before app loads
I think promise object makes call only once,when controller is initialized
you have to reinitialize controller,to get new updated values from server
A. Call sequence will be as follows when DOM ready:
1. $scope.getContent will get initialized.
2. then execution : console.log($scope.myVariable);
B. Async call:
1. If success then below statement will get executed.
$scope.myVariable = data; //THIS WORKS
Above point A and B are independent. Execution is asyc here.
Hope this will help you understand. Enjoy.
Maybe this can help :
// a service providing data with async call
app.service('myService', function(elasticQuery) {
this.getData = function (a,b,c) {
// don't forget the 2 return
return elasticQuery.search({
// make ajax call
}).then(function (response) {
return response
});
};
});
// in my controller
// scope.getData is lunch with ng-click in the view
$scope.getData = function(a,b,c){
myService.getData( a,b,c ).then(function (data) {
$scope.myVariable = data;
});
};
My service needs to retrieve a value asynchronously, but once I have it, I'd like to used a cached version of the value.
When two controllers call this service, I'd expect the first one to cache the retrieved value and the second one to use the cached value, but according to the log, I never find a cached value. When this runs, I see a log message that shows the value being cached, then, when I follow an angular route to a different controller, I do not see that the service finds the cached value. Why does it not run according to my expectation**?**
angular.module('myApp.services').factory('Config', function() {
var Config = { };
Config.currentYear = function() {
if (Config._currentYear) {
// sadly, we never execute here
console.log("returning cached year");
return Parse.Promise.as(Config._currentYear);
}
return Parse.Config.get().then(function(config) {
console.log("caching year");
Config._currentYear = config.get("currentYear");
return Config._currentYear;
});
};
return Config;
});
A couple notes: (1) I named the cached attribute _currentYear, adding the underscore to avoid colliding with the function name. Not sure if I need to do that. (2) I return a fulfilled promise when the value is cached, so the function always returns a promise...also not sure if that's needed, but figure it can't hurt.
Instead of caching the data, why don't you just cache the promise and return it. When you cache the data, you are setting the data Config._currentYear only within the success callback and there are chances that other subsequent call(s) happening before the success callback is run. So you end up making the same call again. You can easily see this when you have calls made to the same service method from different controllers which are instantiated, by their presence on the same template. Caching a promise upfront will avoid these issues.
angular.module('myApp.services').factory('Config', function() {
var config; //Just use to save the promise
Config.currentYear = function() {
/*If there is already a call made before return the promise else
make the actual call and store the promise in the variable.*/
return config || config = Parse.Config.get().then(function(config) {
return config.get("currentYear");
});
};
});