Working on an mobile cordova/angular project. Below is a simple service call:
this.getSomeData = function (businessId) {
var deferred = $q.defer();
var query = "SELECT * FROM Stuff";
$cordovaSQLite.execute(db, query).then(function (res) {
deferred.resolve(res.rows);
}, function (err) {
deferred.reject(err);
});
return deferred.promise;
};
The issue is simple:
for (var k = 0; k < count; k++) {
myService.getSomeData($scope.model.stuff[k].id, k).then(function (data) {
// whatever
}
);
getSomeData is async, so by the time it returns, the k of the for cycle is far from correct.
I thought of passing k to the service method as a parameter:
for (var k = 0; k < count; k++) {
myService.getSomeData($scope.model.stuff[k].id, k).then(function (data) {
// whatever
}
);
And change the service method accordingly:
this.getSomeData = function (id, index) {
var deferred = $q.defer();
var query = "SELECT * FROM Stuff";
$cordovaSQLite.execute(db, query).then(function (res) {
deferred.resolve(res.rows, index);
}, function (err) {
deferred.reject(err);
});
return deferred.promise;
};
But that second parameter is ignored and is always undefined.
How to overcome this?
It sounds like you're running into a problem called "closure over the loop variable" and this issue is discussed in detail here:
JavaScript closure inside loops – simple practical example
In your case, however, the clean solution is to combine Array#map with $q.all():
$q.all($scope.model.visits.map(function (stuff) {
return myService.getSomeData(stuff.id);
})).then(function (results) {
// results is an array of the results of all the calls to getSomeData() in the correct order
});
Also, as Bergi points out, avoid the deferred antipattern:
this.getSomeData = function (id) {
var query = "SELECT * FROM Stuff";
return $cordovaSQLite.execute(db, query).then(function (res) {
return res.rows;
});
};
This is how I got it working. I tried using #JLRishe's suggestion but it wouldn't work. Turns out, I managed to pass more than one parameter through to the service method and back to the controller as well (by building an object than contains as many parameters I need).
myService.getSomeData().then(
function (stuff) {
// whatever
}
).then(function () {
for (var i = 0; i < $scope.model.stuff.length; i++) {
// HERE I SEND TWO PARAMETERS TO THE SERVICE METHOD
myService.getSomeMoreData($scope.model.stuff[i].id, i).then(
function (data) {
// whatever
}
);
}
});
this.getSomeMoreData = function (id, index) {
var deferred = $q.defer();
var query = "SELECT * FROM stuff";
$cordovaSQLite.execute(db, query).then(function (res) {
var moreStuff = [];
for (var i = 0; i < res.rows.length; i++) {
var junk = res.rows.item(i);
moreStuff.push(junk);
}
// HERE I RESOLVE AN OBJECT INSTEAD OF TWO PARAMETERS
deferred.resolve({
moreStuff: moreStuff,
index: index
});
}, function (err) {
deferred.reject(err);
});
return deferred.promise;
};
Related
I have an array of tags which may contain up to 3 items.
Next I pass tags into my getTagQuotes function and am trying to set a promise variable to it. Right now I'm getting promise is undefined.
// If there are tags, the wait for promise here:
if (tags.length > 0) {
var promise = getTagQuotes(tags).then(function() {
console.log('promise =',promise);
for (var i=0; i<tweetArrayObjsContainer.length; i++) {
chartObj.chartData.push(tweetArrayObjsContainer[i]);
}
chartDirective = ScopeFactory.getScope('chart');
chartDirective.nvd3.drawChart(chartObj.chartData);
});
}
else {
chartDirective = ScopeFactory.getScope('chart');
chartDirective.nvd3.drawChart(chartObj.chartData);
}
^ The goal above is to make sure that arrays in tweetArrayObjsContainer have been filled and pushed into chartObj.chartData before I draw my nvd3 chart.
Below is my getTagQuotes function which does another loop through the tags (up to 3) and calls another service GetTweetVolFactory.returnTweetVol which makes the actual API call to retrieve data.
I have code which checks if the we're on the final loop step, then calls the formatTagData function.
Inside formatTagData is where I fill the tweetArrayObjsContainer.
// Check for and GET tag data:
////////////////////////////////////////////////////////////////////
function getTagQuotes(tags) {
console.log('getTagQuotes called with',tags);
var deferred = $q.defer(); // <- setting $q.defer here
var url = 'app/api/social/twitter/volume/';
for (var i=0; i<tags.length; i++) {
var loopStep = i;
rawTagData = [];
GetTweetVolFactory.returnTweetVol(url+tags[i].term_id)
.success(function(data, status, headers, config) {
rawTagData.push(data);
if (loopStep === (rawTagData.length - 1)) {
// formatTagData fills the tweetArrayObjsContainer with data:
formatTagData(rawTagData);
deferred.resolve(); // <-- last step, so resolve
return deferred.promise;
}
})
.error(function(data, status) {
return 'error in returning tweet data';
});
}
function formatTagData(rawData) {
tweetArrayObjsContainer = [];
for (var i=0; i<rawData.length; i++) {
var data_array = [];
var loopNum = i;
_(rawData[i].frequency_counts).reverse().value();
for (var j=0; j<rawData[loopNum].frequency_counts.length; j++) {
var data_obj = {};
rawData[loopNum].frequency_counts[j].start_epoch = addZeroes(rawData[loopNum].frequency_counts[j].start_epoch);
data_obj.x = rawData[loopNum].frequency_counts[j].start_epoch;
data_obj.y = rawData[loopNum].frequency_counts[j].tweets;
data_array.push(data_obj);
}
var tweetArrayObj = {
"key" : "Quantity"+(i+1),
"type" : "area",
"yAxis" : 1,
"color" : tagColorArray[i],
"values" : data_array
};
tweetArrayObjsContainer.push(tweetArrayObj);
}
}
}
Your getTagQuotes function should return a promise with the $q service, check it out.
It should look like this:
function getTagQuotes(tags) {
var deferred = $q.defer();
(...)
// When the action is finished here, call resolve:
deferred.resolve();
(...)
// And in the end, return the promise
return deferred.promise;
}
And finally, in your main code, you'll do:
var promise = getTagQuotes(tags).then(function(){
console.log('promise =',promise);
// Fill chartObj with tweet data arrays
(...)
});
This function has no return statement.
function getTagQuotes(tags)
I'm messing with SteamAPI in order to learn some NodeJS. Right now I'm trying to get games's info after an initial request to get the player's profile, once I have the games IDs stored in an array. The point is that I don't know how to "return" an array AFTER the whole ID array is iterated and all results has come from the server.
function getThumbs(game) {
return rq(
'http://store.steampowered.com/api/appdetails?appids=' + game,
{json: true},
function (error, response, bd) {
if(response.statusCode === 200 && bd[game].data) {
return bd[game].data.screenshots;
}
});
}
function getGamesThumbnails(games) {
var deferred = $q.defer(),
queue = [];
for (var y = 0; y < games.length; y++) {
var game = games[y];
var thumbs = getThumbs(game);
queue.push(thumbs);
}
$q.all(queue).then(
function (data) {
deferred.resolve(data);
},
function (err) {
deferred.reject(err)
}
);
return deferred.promise;
}
app.get('/blog',function(client_req,client_res){
rq('http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=' + key + '&steamid=blablabla&format=json', function (error, response, body) {
var data = JSON.parse(body);
var games = data.response.games.map(function (game) {
return game.appid;
});
getGamesThumbnails(games).then(function (data) {
console.log(data)
})
});
});
Basically, you should use a callback, because like you are doing in getThumbsyou are returning the object, while you should return the value bd[game].data.screenshots;
function getThumbs(game, cb) {
return rq(
'http://store.steampowered.com/api/appdetails?appids=' + game,
{json: true},
function (error, response, bd) {
if(response.statusCode === 200 && bd[game].data) {
cb(null, bd[game].data.screenshots);
}
});
}
function getGamesThumbnails(games) {
var deferred = $q.defer(),
queue = [];
for (var y = 0; y < games.length; y++) {
var game = games[y];
getThumbs(game, function(err, value) {
queue.push(value);
});
}
$q.all(queue).then(
function (data) {
deferred.resolve(data);
},
function (err) {
deferred.reject(err)
}
);
return deferred.promise;
}
And plust to return the response to the client you have to use the client_res.send(VALUE)
so the bottom part would become like this:
app.get('/blog',function(client_req,client_res){
rq('http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=' + key + '&steamid=blablabla&format=json', function (error, response, body) {
var data = JSON.parse(body);
var games = data.response.games.map(function (game) {
return game.appid;
});
getGamesThumbnails(games).then(function (data) {
client_res.send(data);
console.log(data)
})
});
});
Your getThumbs() function does not return a promise. $q.all only works on an array containing promises, whereas rq uses callbacks.
Try this:
function getThumbs(game) {
var deferred = $q.defer(),
rq(
'http://store.steampowered.com/api/appdetails?appids=' + game,
{json: true},
function (error, response, bd) {
if(response.statusCode === 200 && bd[game].data) {
deferred.resolve(bd[game].data.screenshots);
}
});
return deferred.promise;
}
Thank you both!
I tried Yuri's approach, but $q.all it doesn't seem to resolve the array of promises (nothing happens after the last request from getThumbs())
for (var y = 0; y < games.length; y++) {
var game = games[y];
var thumbs = getThumbs(game);
queue.push(thumbs);
}
$q.all(queue).then(
function (data) {
console.log(data)
deferred.resolve(data);
},
function (err) {
deferred.reject(err)
}
);
As I understand it the 100 asyncFunctions in the code below will not be executed until after func has returned true, and at this point the referens to i will not be valid. Nothing in this code would work as expected I guess.
Psedo example code:
function func(){
var needToKnow = []
for i = 1 to 100 {
needToKnow[i] = asyncFunction( i )
}
//Do some work on needToKnow[i]
return true
}
What would be the Javascript way to do something like this?
Use callbacks:
function func(callback) {
var needToKnow = [],
max = 100;
for (var i = 0; i < max; i++) {
asyncFunction(i, function (result) {
needToKnow.push(result);
if (needToKnow.length == max) { // or something that let you know that its finished
callback(needToKnow);
}
});
}
}
function asyncFunction(i, callback) {
setTimeout(function () {
callback({ index: i });
}, 1000); // Im an async func!
}
And use it this way:
func(function (result) {
console.log(result);
});
Be careful, don't get in callback hell
Here is an example using the Q Promise library:
function functionThatCouldThrowError(i){
//It doesn't, but just to give an idea of error propagation.
return { index: i };
}
function asyncFunction(i) {
var deferred = Q.defer();
setTimeout(function () {
try{
var data = functionThatCouldThrowError(i);
deferred.resolve(data);
} catch (error) {
deferred.reject({ index: i, error: error });
}
}, 1000);
return deferred.promise;
}
function doAll() {
var needToKnow = []
for (var i = 0; i < 100; i++) {
needToKnow[i] = asyncFunction( i );
}
return Q.all(needToKnow);
}
doAll().then(function(arg) {
//arg contains all 100 elements
alert("All done");
})
Update: expanded the example to demonstrate how to handle errors.
Plunker:http://plnkr.co/edit/djWpTKxgvzK2HmkVwvTy?p=preview
I have a conceptual problem with NodeJS... I need to call a deferred function within a loop and use the response for the next iteration. How can I do that without blocking? Here is what I did but of course, response.rev doesn't get updated for the next iteration:
first.function().then(function(response) {
for (var int = 0; int < $scope.files.length; int++) {
call.function(response.rev).then(function (res) {
response.rev = res.rev;
}, function (reason) {
console.log(reason);
});
}
});
Edit, my real code with Benjamin's help (still not working):
pouchWrapper.updateClient(clientManager.clientCleaner($scope.client)).then(function(response) {
if ($scope.files != null) {
var p = $q.when();
for (var int = 0; int < $scope.files.length; int++) {
var doc = $scope.files[int].slice(0, $scope.files[int].size);
p = p.then(function(formerRes){
return pouchWrapper.uploadFiletoDoc($scope.client._id, $scope.contract.policy_number + '&' + $scope.damage.number + '-' + $scope.files[int].name, res.rev, doc, $scope.files[int].type).then(function (res) {
return res;
}, function (reason) {
console.log(reason);
});
});
}
return p;
}
});
You use .then for that. .then is the asynchronous version of the semicolon:
first.function().then(function(response) {
var p = $q.when(); // start with an empty promise
for (var int = 0; int < $scope.files.length; int++) {
p = p.then(function(formerValue){
return call.function(formerValue).then(function (res) {
// use res here as the _current_ value
return res; // return it, so that the next iteration can use it;
});
});
}
return p; // this resolves with the _last_ value
});
Here are the cloud functions namely 'batchReq1' and batchPromises.
In any case, if I know the exact number of promises pushed (Consider the size of results to be '2' in function batchPromises(results)) and executed through when(), I can handle the success response by passing that number of result parameters (In the below example request1, request2) in successCallBack of .then().
If I have to process the number of promises pushed to .when() dynamically, then, how can we handle this in SuccessCallBack? Unlike earlier scenario, we can't expect fixed number of results in the then method (batchPromises(results).then(function (result1, result2) {....)
batchReq1
Parse.Cloud.define("batchReq1", function (request, response) {
var results = request.params.imageArray;
batchPromises(results).then(function (result1, result2) {
console.log("Final Result:: Inside Success");
console.log("Final Result:: Inside Success result 1::::"+result1);
console.log("Final Result:: Inside Success result 2::::"+result2);
response.success();
}
// batchPromises(results).then(function (arraySuccess) {
//
// console.log("Final Result:: Inside Success");
// console.log("Final Result:: Inside Success:: Length:: "+arraySuccess.length);
// //Fetch all responses from promises and display
// var _ = require('underscore.js');
// _.each(arraySuccess, function (result) {
//
// console.log("Final Result:: " + result)
//
// });
//
//
// response.success();
//
// }
, function (error) {
console.log("Final Result:: Inside Error");
response.error(error);
});
});
batchPromises
function batchPromises(results) {
var promise = Parse.Promise.as();
var promises = [];
var increment = 0;
var isFromParallelExecution = false;
var _ = require('underscore.js');
_.each(results, function (result) {
var tempPromise = Parse.Promise.as("Promise Resolved ");
promises.push(tempPromise);
}
promise = promise.then(function () {
return Parse.Promise.when(promises);
});
}
increment++;
});
return promise;
}
this is how i handle this...
Parse.Cloud.define("xxx", function(request, response)
{
var channels = ["channel1", "channel2", "channel3", "channel4", "channel5"];
var queries = new Array();
for (var i = 0; i < channels.length; i++)
{
var query = new Parse.Query(channels[i]);
queries.push(query.find());
}
Parse.Promise.when(queries).then(function()
{
var msg = "";
for (var j = 0; j < arguments.length; j++)
{
for (var k = 0; k < arguments[j].length; k++)
{
var object = arguments[j][k];
msg = msg + " " + object.get('somefield');
}
}
response.success(msg);
});
});
Either you just use the arguments object to loop over the results, or you build your arraySuccess without when - it doesn't make much sense here anyway as you batch the requests (executing them sequentially), instead of executing them in parallel:
function batchPromises(tasks) {
var _ = require('underscore.js');
_.reduce(tasks, function (promise, task) {
return promise.then(function(resultArr) {
var tempPromise = Parse.Promise.as("Promise Resolved for "+taks);
return tempPromise.then(function(taskResult) {
resultArr.push(taskResult);
return resultArr;
});
});
}, Parse.Promise.as([]));
}
If you actually wanted to execute them in parallel, use a simple
function batchPromises(tasks) {
var _ = require('underscore.js');
return Parse.Promise.when(_.map(tasks, function (task) {
return Parse.Promise.as("Promise Resolved for "+taks);
}).then(function() {
return [].slice.call(arguments);
});
}