[ How can I create an Angular factory with a getter and setter method without running into a race condition ]
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.
Answer 1
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.
Answer 2
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
Answer 3
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
});
Answer 4
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.