I am working on a chat app which is Node.js + MongoDB (Mongoose library) on the server side, and Angular.js on the client side.
I have a database collection (MongoDB) for rooms (all the rooms in the app), which looks like this:
// ------- creating active_rooms model -------
var active_rooms_schema = mongoose.Schema({
room_name: String,
users: [String]
});
var active_rooms = mongoose.model('active_rooms', active_rooms_schema);
This database contains a room with all its users (i.e. "my cool room" with users: "mike", "joe", and "dave").
What I want to do is - every time a user wants to be in a chat room (with some room name) in my Angular.js client, I want to:
Create the room if it is not exists
Push that user into the users array of the room.
I know that because of 1, I will always have a room with an array of users.
This is my Angular relevant code: (I cannot give here the whole app because it is way too large and not relevant.)
$scope.enterRoom = function(info) {
$q.when(create_room_if_not_exists($scope.room)).then(add_user_to_room($scope.name, $scope.room));
$location.path("chat");
}
var create_room_if_not_exists = function(room_name) {
var deferred = $q.defer();
is_room_already_exists({
'name': room_name
}).then(function(response) {
if (!response.data.is_room_exists) {
register_room({
'name': room_name
});
console.log("room: " + room_name + ", was created");
deferred.resolve();
}
}, function(error) {
deferred.reject(error.data);
});
return deferred.promise;
}
var add_user_to_room = function(user_name, room_name) {
console.log(user_name)
add_user_to_room_request({
'user_name': user_name,
'room_name': room_name
});
}
var is_room_already_exists = function(info) {
return $http({
url: '/is_room_already_exists',
method: 'POST',
data: info
});
}
var add_user_to_room_request = function(info) {
$http({
url: '/add_user_to_room',
method: 'POST',
data: info
});
}
var register_room = function(info) {
return $http({
url: '/register_room',
method: 'POST',
data: info
});
}
What happens is that the 2nd action happens before the 1st one. When I print a log into the console, I see that, and I don't know why.
Both of these actions arrive through an HTTP request to the server - so I don't think the problem is there.
I am talking about the XHRs not chaining
A common cause of problems with chaining is failure to return promises to the chain:
var add_user_to_room = function(user_name, room_name) {
console.log(user_name)
//add_user_to_room_request({
return add_user_to_room_request({
//^^^^^^ ----- be sure to return returned promise
'user_name': user_name,
'room_name': room_name
});
}
If chaining is done properly, $q.defer is not necessary:
var create_room_if_not_exists = function(room_name) {
//var deferred = $q.defer();
//is_room_already_exists({
return is_room_already_exists({
//^^^^^^ --- be sure to return derived promise
'name': room_name
}).then(function(response) {
if (!response.data.is_room_exists) {
console.log("room: " + room_name + ", to be created");
//register_room({
return register_room({
//^^^^^^ ----- return promise to further chain
'name': room_name
});
//deferred.resolve();
} else {
return room_name;
//^^^^^^ ----- return to chain data
};
}, function(error) {
//deferred.reject(error.data);
throw error;
//^^^^^ ------- throw to chain rejection
});
//return deferred.promise;
}
If a promise is returned properly, $q.when is not necessary:
$scope.enterRoom = function(info) {
//$q.when(create_room_if_not_exists($scope.room))
// .then(add_user_to_room($scope.name, $scope.room));
return create_room_if_not_exists($scope.room)
.then(function() {
return add_user_to_room($scope.name, $scope.room));
}).then(function()
$location.path("chat")
});
}
The rule of thumb with functional programming is -- always return something.
Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises. It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.
-- AngularJS $q Service API Reference - Chaining Promises.
Try this:
$scope.enterRoom = function (info) {
return create_room_if_not_exists($scope.room)).then(function(){
return add_user_to_room_request({ 'user_name': $scope.name, 'room_name': $scope.name });
}).then(function(){
$location.path('chat');
});
}
var create_room_if_not_exists = function (room_name) {
var deferred = $q.defer();
return is_room_already_exists({ 'name': room_name }).then(function (response) {
if (!response.data.is_room_exists) {
console.log("room: " + room_name + ", is being created");
return register_room({ 'name': room_name });
}
return response;
})
}
var is_room_already_exists = function (info) {
return $http({
url: '/is_room_already_exists',
method: 'POST',
data: info
});
}
var add_user_to_room_request = function (info) {
return $http({
url: '/add_user_to_room',
method: 'POST',
data: info
});
}
var register_room = function (info) {
return $http({
url: '/register_room',
method: 'POST',
data: info
});
}
Related
In the following code I want to execute a series of $http requests that modify a list. When all the responses are received, I want to process the list and remove part of the content.
The problem is that when I print the list after $q.all, the Chrome console shows a length of 3, but when I expand it to read the content only 2 elements are shown. On JSFiddle I have no issues, though.
var app = angular.module('MyApp',[]);
app.controller('MyController',['$scope','$q',"$http", function($scope,$q,$http){
var loopPromises = [];
var workorders = null;
$scope.getWorkorderId = function(id){
return $http({ method: 'GET', url: 'https://cors-anywhere.herokuapp.com/https://blk.clojure.xyz/interdiv/api/v1/service/' + id })
.then(function success(response) {
return response.data;
}, function error(response) {
console.log(response);
});
}
$http({ method: 'GET', url: 'https://cors-anywhere.herokuapp.com/https://blk.clojure.xyz/interdiv/api/v1/workorder' })
.then(function success(response) {
workorders = response.data;
}, function error(response) {
console.log(response);
})
.then(function() {
if (workorders == null) {
return;
}
angular.forEach(workorders, function(value, index, obj) {
var deferred = $q.defer();
loopPromises.push(deferred.promise);
var waitResponse = $scope.getWorkorderId(value.id);
waitResponse
.then(function(res) {
obj[index].services = res;
deferred.resolve();
})
});
$q.all(loopPromises)
.then(function() {
// Should contain 3 elements, only 2 are shown
console.log(workorders);
});
});
}]);
see better in the screenshots. Console Requests
The problem was in the second part of the code not copied in the question: I was using .splice() inside angular.forEach() which changes the indices of the elements within the array.
I am trying to set up a service that, when I feel like it, I can flip to live data coming from an API. The getData function takes skip/take parameters to define start record and number of records.
My data is currently in a json file:
{
"Data":[{
"Id":"1462",
"Name":"Cynthia"
},{
"Id":"1463",
"Name":"Bob"
},{
...
}],
"Total":71
}
My service currently pulls all json data at once:
var _getData = function (optionsData) {
return $http({
method: 'GET',
url: 'data/packages.json'
})
.then(function successCallback(response) {
return response;
},
function errorCallback(response) {
return response;
});
}
It seems to me that I have to write the paging logic right into the service:
.then(function successCallback(response) {
var records = response.data.Data;
var firstRecord = 0;//optionsData.skip;
var numberOfRecords = 1;//optionsData.take;
response.data.Data = records.slice(firstRecord, firstRecord + numberOfRecords);
return response;
},
Is this the right way in principle?
[ UPDATE ] The controller method:
var getPackageData = function (options){
return dataService.getData(options.data).then(
function successCallback(response) {
options.success(response.data.Data);
$scope.totalCount = response.data.Total;
},
function errorCallback(response) {
// handle error
}
);
};
my errorCallback is wrong? How?
The errorCallback is converting a rejected promise to a fulfilled promise.
To avoid conversion, either throw the response or return $q.reject:
var _getData = function (optionsData) {
return $http({
method: 'GET',
url: 'data/packages.json'
})
.then(function successCallback(response) {
return response;
},
function errorCallback(response) {
//To avoid converting reject to success
//DON'T
//return response;
//INSTEAD
throw response;
//OR
//return $q.reject(response);
});
}
Another common error is to fail to include either a throw or return statement. When a function omits a return statement, the function returns a value of undefined. In that case, the $q service will convert rejected promises to fulfilled promises which yield a value of undefined.
I have a model that I am using to hold my data in angular:
var FuelProcessingModel = function (carrierService) {
this.myArray = [];
};
That model has an array of MyObjects that I get from the DB:
var MyObject = function () {
//stuff
}
I update this using a REST call:
$scope.add = function () {
var myObject = new MyObject();
$scope.model.MyObjects.push(myObject);
service.add(myObject);
};
Which I use a service to hit the Server:
this.add = function (myObject) {
$http({
method: "POST",
url: "theServer",
data: myObject
});
}
The REST service just adds to the database, It doesn't return anything.
I need to reload the data from the database after the update is finished, so that my records now have all newly associated ID's and pertinent data.
I cannot just do:
window.location.reload();
The user starts by selecting a value from a drop down list to decide which list of data they start off seeing. I cannot / do not want to pass the value to it, mainly because it is in its own partial view, with its own controller, because it is used on many pages.
I tried doing:
$scope.add = function () {
//same as above
//this
service.get().then(function(result) { $scope.model.myArray = result.data; });
};
Obviously the problem here is the promise isn't complete before the DOM reloads the page. So the user saw themself add an item to the array and it vanished.
Do I want to load the page after the promise is complete? (How would I do that?)
should I return the updated data from the REST service and reset the current value? (seems like the same promise issue)
Is there a better practice that I do not know about?
UPDATE
For Bergi:
this.get = function (key) {
return $http({
method: "GET",
url: "theServer" + key
})
.success(function (data) {
return data;
});
}
I think you want to chain your two promises:
$scope.add = function () {
var myObject = new MyObject();
$scope.model.MyObjects.push(myObject);
return service.add(myObject).then(function() {
return service.get();
}).then(function(result) {
$scope.model.myArray = result.data;
});
};
and
this.add = function(myObject) {
return $http({
// ^^^^^^ return a promise here
method: "POST",
url: "theServer",
data: myObject
});
};
You can wrap your service call in a deferred promise, and on return success re-init your data from the controller..
$scope.add = function () {
var myObject = new MyObject();
$scope.model.MyObjects.push(myObject);
service.add(myObject).then(function (response) {
// here's where you'd do whatever you want to refresh your model
}),
function (err) {console.log(err);};
};
And the service:
this.add = function (myObject) {
var deferred = $q.defer();
$http({
method: "POST",
url: "theServer",
data: myObject,
success: function (response) {
deferred.resolve(err);
},
error: function (err) {
deferred.reject(err);
}
});
return deferred.promise;
}
I have created a factory, to handle all my http related calls. It returns following inline code:
return {
get: function (opts) {
var deferred = $q.defer();
var def = defaultOptions(HttpMethod.Get);
$.extend(def, opts);
$http({ method: 'get', url: config.remoteServiceName + def.url }).then(function (result, status, header) {
def.success(deferred, { data: result });
}, function (data) {
def.error(deferred, data);
});
return deferred.promise;
},
post: function (opts) {
var deferred = $q.defer();
var def = defaultOptions(HttpMethod.Post);
$.extend(def, opts);
$http.post(config.remoteServiceName + def.url, def.data).then(function (result) {
def.success(deferred, result);
}, function (data) {
def.error(deferred, data);
});
return deferred.promise;
},
remove: function (opts) {
var deferred = $q.defer();
var def = defaultOptions(HttpMethod.Delete);
$.extend(def, opts);
$http({ method: 'delete', url: config.remoteServiceName + def.url }).then(function (result) {
def.success(deferred, result);
}, function (data) {
def.error(deferred, data);
});
return deferred.promise;
}
};
Now, when i am making the calls, if there are few parallel calls being made, all promise resolution is getting mixed up. I am getting the resultset from one request in another's resolution.
Not able to solve the problem. What am i doing wrong?
Issue was not of parallel calls. It was somewhere else. JS model for ajax execution works fine, and no issue with $http either. We were using WebAPI as backend, and some of our global filters had state, which got altered on each call. Hence the data was mixed up. No issue on client end.
Thanks everyone
I have two $http posts that each post arrays of objects in a loop. The problem is that the second $http post is reliant on the first one completing. Is there a way to make them not async calls? I tried to used deferred but something is wrong in it as it is not working. It still fires group saving while tag saving is going on.
Angular:
var deferred = $q.defer();
var all = $q.all(deferred.promise);
for (var property in data.tagAdded) {
if (data.tagAdded.hasOwnProperty(property)) {
$http({
method: "POST",
url: '/api/projects/' + data.Project.Id + '/tags',
data: ({ Name: data.tagAdded[property].tag.Name })
}).success(function (response) {
deferred.resolve(response);
data.tagAdded[property].tag.Id = response.Data[0].Id;
data.tagAdded[property].tag.ProjectId = response.Data[0].ProjectId;
}).error(function (response) {
tagError = true;
$.jGrowl("Error saving new tags. Contact support.", { header: 'Error' });
});
}
}
deferred.promise.then(function() {
console.log(data);
});
all.then(function() {
groups.forEach(function(group) {
$http({
headers: { 'Content-Type': 'application/json; charset=utf-8' },
method: "POST",
url: '/api/projects/' + data.Project.Id + '/recruiting-groups',
data: angular.toJson(group, false)
}).success(function(response) {
}).error(function(response) {
recError = true;
$.jGrowl("Error saving recruiting group. Contact support.", { header: 'Error' });
});
});
});
Going without promises is totally not what you want to do here. In fact, this is exactly the kind of situation where promises shine the most! Basically, you weren't using $q.all properly. You can just pass it a list of promises, and it will be resolved when they are all resolved. If any one of them fails, it will yield a rejected promise with the same rejection as the first one that failed. You can of course swallow that rejection via a .catch invocation that returns anything other than a $q.reject value.
I reimplemented what you had using promises. Both .success and .error are fairly limited, so I used the traditional .then and .catch methods here.
/**
* Here we define a function that takes a property, makes a POST request to
* create a tag for it, and then appends the promise for the request to a list
* called tagRequests.
*/
var tagRequests = [];
var createTag = function(property) {
tagRequests.push($http({
method: "POST",
url: '/api/projects/' + data.Project.Id + '/tags',
data: ({ Name: data.tagAdded[property].tag.Name })
}).then(function(response) {
var responseData = response.data;
data.tagAdded[property].tag.Id = responseData.Data[0].Id;
data.tagAdded[property].tag.ProjectId = responseData.Data[0].ProjectId;
return responseData;
}).catch(function (err) {
var errorMsg = "Error saving new tags. Contact support.";
$.jGrowl(errorMsg, { header: 'Error' });
// If we don't want the collective promise to fail on the error of any given
// tag creation request, the next line should be removed.
return $q.reject(errorMsg);
}));
};
/**
* We then iterate over each key in the data.tagAdded object and invoke the
* createTag function.
*/
for (var property in data.tagAdded) {
if (Object.prototype.hasOwnProperty.call(data.tagAdded, property)) {
createTag(property);
}
}
/**
* Once all tag requests succeed, we then map over the list of groups and
* transform them into promises of the request being made. This ultimately
* returns a promise that is resolved when all group POST requests succeed.
*/
$q.all(tagRequests)
.then(function(tagsCreated) {
return $q.all(groups.map(function(group) {
return $http({
headers: { 'Content-Type': 'application/json; charset=utf-8' },
method: "POST",
url: '/api/projects/' + data.Project.Id + '/recruiting-groups',
data: angular.toJson(group, false)
}).then(function(response) {
return response.data;
})
.catch(function(err) {
var errorMsg = "Error saving recruiting group. Contact support.";
$.jGrowl(errorMsg, { header: 'Error' });
// If we want this collective promise to not fail when any one promise is
// rejected, the next line should be removed.
return $q.reject(errorMsg);
});
}));
});
I highly suggest brushing up on Promises in general, and then taking another look at the $q documentation. I've also written this blog post on the way promises work in Angular and how they differ from most promise implementations.
This is exactly what promises do. Angular uses Kris Kowal's Q Library for promises. Check out the Angular docs page, has a perfect example of the promise pattern. Basically you would do the first call, then once it's promise returns success, make the second call.
I had to alter my code a bit, and get some coworker team work. I went completely without promises.
I use a counter to track when counter = 0 by adding to the counter then upon success/fail i decrement from the counter taking it backwards for each completed transaction. When counter is at 0 I am done then call my next bit $http post.
Angular Deferred:
var counter = 0;
var tags = [];
for (var property in data.tagAdded) {
if (data.tagAdded.hasOwnProperty(property)) {
tags.push({ Name: property });
}
}
if (tags.length == 0) {
groupInsert();
} else {
tags.forEach(function (tag) {
counter ++;
$http({
method: "POST",
url: '/api/projects/' + data.Project.Id + '/tags',
data: ({ Name: tag.Name })
}).success(function (response) {
console.log(response)
counter--;
data.tagAdded[property].tag.Id = response.Data[0].Id;
data.tagAdded[property].tag.ProjectId = response.Data[0].ProjectId;
if (counter == 0) {
groupInsert();
}
}).error(function (response) {
counter--;
tagError = true;
$.jGrowl("Error saving new tags. Contact support.", { header: 'Error' });
if (counter == 0) {
groupInsert();
}
});
});
}
function groupInsert() {
groups.forEach(function (group) {
console.log(group)
$http({
headers: { 'Content-Type': 'application/json; charset=utf-8' },
method: "POST",
url: '/api/projects/' + data.Project.Id + '/recruiting-groups',
data: angular.toJson(group, false)
}).success(function (response) {
}).error(function (response) {
recError = true;
$.jGrowl("Error saving recruiting group. Contact support.", { header: 'Error' });
});
});
}