Execute Action After Multiple Javascript Async Processes Complete - javascript

So I have a longrunning query status page for my company, and it shows bars for the different database instances that change color based on number of longrunners + other criteria.
The issue is, every time the call is made to update the info, the colors all go back to default, and build from the ground up. This is because I'm using a $scope.variable object to hold the color information as the longrunner data is retrieved.
I want to switch this to using a local standard variable within the function, and only after all data has been retrieved, assign this variable to the $scope.variable.
Context - our instances are organized into swimlanes, so I create an object for the swimlane color and for the instance color. When all are collapsed, you only see the swimlane, so I needed a way for the instance color to bubble up to the swimlane.
So it amounts to something like this:
var getLongrunners = function(){
$scope.longrunnersByInstance = {};
for (var l = 0; l < $scope.swimlanes.length; l++){
$scope.slColor[$scope.swimlanes[l].swimlane] = 0;
}
for (var j = 0; j < instances.length; j++){
$scope.longrunnersByInstance[instances[j].instance] = [];
$scope.instanceColor[instances[j].instance] = 0;
}
for (var i = 0; i < instances.length; i++){
(function(e){
$http
.get('/getLongrunners',{params: {envFlag: '',instance: instances[e].instance}})
.then(function(response){
var longrunners = response.data;
for(var k = 0; k < longrunners.length; k++){
$scope.longrunnersByInstance[instances[e].instance].push(longrunners[k]);
}
if(longrunners.length > $scope.dangerThresh){
$scope.instanceColor[instances[e].instance] = 2;
}else if(longrunners.length >= $scope.warningThresh){
$scope.instanceColor[instances[e].instance] = 1;
}
if($scope.slColor[instances[e].swimlane] < $scope.instanceColor[instances[e].instance]) {
$scope.slColor[instances[e].swimlane] = $scope.instanceColor[instances[e].instance]
}
},getLongrunnersFail);
}(i));
So I want the $scope.slColor and $scope.instanceColor to be regular local variables until this loop finishes.
I look into promises, but that seemed to only be useful with $http before .then() is called on it.
Is there a way to make a custom promise type architecture and contain more than one function, and only return the promise when everything has been completed?
Thanks!
EDIT:
Most recent try:
var promises = [];
var longrunnersByInstance = {};
var instancesPerf = {};
var slColor = {};
var instanceColor = {};
var promiseTest = function() {
$scope.longrunnersByInstance = {};
for (var l = 0; l < $scope.swimlanes.length; l++){
slColor[$scope.swimlanes[l].swimlane] = 0;
}
for (var j = 0; j < instances.length; j++){
instanceColor[instances[j].instance] = 0;
}
instances.forEach(function (instance) {
promises.push($http
.get('/getLongrunners', {
params: {envFlag: 'bh', instance: instance.instance}
})
.then(function (response) {
var longrunners = response.data;
longrunnersByInstance[instance.instance] = [];
for (var k = 0; k < longrunners.length; k++) {
longrunnersByInstance[instance.instance].push(longrunners[k]);
}
if (longrunners.length > $scope.dangerThresh) {
instanceColor[instance.instance] = 2;
} else if (longrunners.length >= $scope.warningThresh) {
instanceColor[instance.instance] = 1;
}
console.log(instance.instance);
if (slColor[instance.swimlane] < instanceColor[instance.instance]) {
slColor[instance.swimlane] = instanceColor[instance.instance]
}
return true;
}, getLongrunnersFail)
);
function getLongrunnersFail(response){
console.log("getting longrunners failed" + response.status);
}
$q.all(promises).then(function () {
// longrunnersByInstance to $scope
console.log('calling all promises callback!');
instances.forEach(function (instance) {
$scope.longrunnersByInstance[instance.instance] = longrunnersByInstance[instance.instance];
});
// instancesPerf to $scope
instances.forEach(function (instance) {
$scope.instancesPerf[instance.instance] = instancesPerf[instance.instance];
});
// slColor to $scope
instances.forEach(function (instance) {
$scope.slColor[instance.instance] = slColor[instance.instance];
});
// instanceColor to $scope
instances.forEach(function (instance) {
$scope.instanceColor[instance.instance] = instanceColor[instance.instance];
});
}, allPromisesFail);
function allPromisesFail(){
console.log("all promises failed")
}
});
};

Angular uses the $q service for dealing with promises.
It has a function called all to deal with exactly the type of problem you encountered.
here is a simple fiddle to demonstrate it: http://jsfiddle.net/ThomasBurleson/QqKuk/
var myApp = angular.module('myApp', []);
function MyCtrl($scope, $q, $timeout) {
var thenFn = function(value){
console.log('resolved ', value);
return value;
},
q1 = $scope.q1 = $q.defer(),
q2 = $scope.q2 = $q.defer(),
p1 = $scope.q1.promise,
p2 = $scope.q2.promise;
$scope.fromThen = $q.all([
p1.then(thenFn),
p2.then(thenFn)
])
.then(function(values) {
console.log(values);
return values;
});
// Must start the AngularJS digest process
// to allow $q.resolve() to work properly
// So use $timeOut() or $apply()
setTimeout(function () {
$scope.$apply( function() {
console.log('resolving delayed promises');
q1.resolve({value : 1});
q2.resolve({value : 2});
});
}, 100, this);
/*
* Alternative approach
*
$timeout( function() {
console.log('resolving delayed promises');
q1.resolve({value : 1});
q2.resolve({value : 2});
});
*/
}
Here is how you would apply this to your code (haven't tested it, so it's just a direction, but it should get you going):
var promises = [];
for (var i = 0; i < instances.length; i++){
//$http return a promise, so you can just push it
promises.push( $http
.get('/getLongrunners',{params: {envFlag: '',instance: instances[e].instance}}));
}
$q.all(promises).then(function(values){
//values should contain an array with all the results you got from all the requests, so you can run through it and aggregate the results
});

Promises are chainable: when you return something inside the success callback of a promise you get a new promise that resolves with the returned value.
Example from angular documentation ("Chaining Promises" part):
promiseB = promiseA.then(function(result) {
return result + 1;
});
// promiseB will be resolved immediately after promiseA is resolved and its value
// will be the result of promiseA incremented by 1
So, in your /getLongRunners callbacks you can return a value that immediately resolves (like true) so that you get a promise that resolves as soon as the callback is done. If you collect all these "child" promises in an array you can than pass that array to $.all, and it will resolves when all the promises resolve, i.e. as soon as all the callbacks are done.
Here I replace the for loop and embedded immediately-executed function with the forEach method: it's clearer and avoids the closure problem you encountered
var promises = [];
instances.forEach(function(instance, i) {
promises.push($http
.get('/getLongrunners', {
params: {envFlag: '', instance: instances[e].instance}
})
.then(function(response) {
var longrunners = response.data;
// whatever you have to do
return true;
}, getLongrunnersFail);
});
$q.all(promises).then(function() {
// When you are here, all your callbacks will have been executed
});

Related

How to run a function once all the ajax calls have been done

Say I have this function :
function testAjax(url) {
return $.ajax({
url: url,
type: 'GET'
});
}
Now I have a number of nodes (I am using D3), I wish to loop through. Each one may or may not have some files associated. To find out if it does I get the url on the chosen node, inspect the returned data, if it has a file/files I add it to the array of files. I then wish to log the file array only after I have gone through each node and inspected it to see if it has files.
Rest of the code is similar to this :
var allFiles = [];
nodes.each(function(d) {
testAjax(d.url)
.success(function(data) {
if (data.files) {
if (data.files.length > 0) {
for (var i = 0; i < data.files.length; i++) {
allFiles.push(data.files[i])
}
}
}
})
})
//Here is where I want to log/use (call a function passing the files array as an argument) the array of files after its been completed.
Create array of promises and use $.when() to be resolved when full array is resolved
var allFiles = [];
var promises = [];
nodes.each(function(d) {
// reference to promise
var req= testAjax(d.url)
.success(function(data) {
if (data.files) {
if (data.files.length > 0) {
for (var i = 0; i < data.files.length; i++) {
allFiles.push(data.files[i])
}
}
}
})
});
// push to array
promises.push(req);
});
$.when.apply(null, promises).then(function(){
// all promises have been resolved here
}).fail(function(){
alert('Opps ... something went wrong with one of the requests')
})
You can use a check in the success callback to see if you have reached the end of the nodes array:
var allFiles = [];
nodes.each(function(i, d) {
testAjax(d.url)
.success(function(data) {
if (data.files) {
if (data.files.length > 0) {
for (var i = 0; i < data.files.length; i++) {
allFiles.push(data.files[i])
}
}
}
// Check if the length-1 is equal to the index
if (i == nodes.length-1) doneLooping();
})
})
doneLooping() should be called as the last callback completes.
Here is a useful page on the .each() function.
Please try something like
var allFiles = [];
var totalNodes = 0,
totalSuccessResponses = 0;
nodes.each(function(d) {
totalNodes++;
testAjax(d.url)
.success(function(data) {
totalSuccessResponses++;
if (data.files) {
if (data.files.length > 0) {
for (var i = 0; i < data.files.length; i++) {
allFiles.push(data.files[i])
}
}
}
if(totalNodes == totalSuccessResponses) {
// callback function()
// is excuted after all success responses received
}
})
})
I would use $.when, to be sure all promises have been resolved before perform any treatment.
Here is an example (not directly related to you code, but using mocked calls):
var test = function (i) {
var result = $.Deferred();
setTimeout(function () {
console.log(i + ": done");
result.resolve(i);
}, 1000 + Math.floor(Math.random() * 2000));
return result;
};
// with apply to allow the possible usage of $.map on an array of objects, and $.then on each promises
$.when.apply($, [test(1), test(2), test(3)]).done(function (i1, i2, i3) {
console.log(i1 + " " + i2 + " " + i3 + " are done");
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
If you want to perform post treatment on you ajax call results before invoking when you can combine the above approach with $.then and $.map

Returning the array before the function is complete & Angular JS

I am trying to return the array 'self.results' with all the arrays pushed in, which is after the self.yelpResults is completed. I want to use the returned array in another function. For now, self.parsedYelpArray is suppose to accept that array.
I am having trouble getting the self.results return all the arrays that are being pushed in. Instead, it asynchronously push the original empty array into the self.parsedYelpArray function.
How do I resolve this?
This is the code in my controller:
self.MapRouteArray = CompileMapArray.compileRoutes(data);
self.yelpResults = CompileYelpResults.compileYelp(self.MapRouteArray);
self.parsedYelpArray = ParsingYelpResults.parsingData(self.yelpResults);
And, these are the relevant services:
.service('CompileMapArray', function () {
var self = this;
self.MapRouteArray = [];
self.compileRoutes = function (data) {
for (var i = 0; i < data.response.route[0].leg[0].maneuver.length; i += 2) {
self.MapRouteArray.push(data.response.route[0].leg[0].maneuver[i].position.latitude + ',' + data.response.route[0].leg[0].maneuver[i].position.longitude);
}
return self.MapRouteArray;
};
})
.service('CompileYelpResults', function (YelpResource) {
var self = this;
self.results = [];
self.compileYelp = function (mapArray) {
for (var j = 0; j < mapArray.length; j++) {
YelpResource.getListings({term: self.yelpSearch, ll: mapArray[0]}, function (response) {
self.results.push(response.businesses);
console.log(self.results);
});
}
return self.results;
};
})
.service('ParsingYelpResults', function () {
var self = this;
self.parsingData = function (results) {
console.log(results);
};
});
You are trying to return from an asynchronous function; you'll always get unreliable results from that, you need to pass in a callback function that handles whatever operation you want at the end of your async... Like:
.service('CompileYelpResults', function (YelpResource) {
var self = this;
self.results = [];
self.compileYelp = function (mapArray, callbackFn) {
for (var j = 0; j < mapArray.length; j++) {
YelpResource.getListings({term: self.yelpSearch, ll: mapArray[0]}, function (response) {
self.results.push(response.businesses);
console.log(self.results);
});
}
callbackFn(self.results);
};
});
Then call the function with a callback function like so:
var parsed = CompileYelpResults.compileYelp(self.MapRouteArray, function(result) {
console.log(result);
});
This goes for all your asynchronous functions.
Relating to your comment the callback function you pass as second parameter to compileYelp takes the place of parsingData, so whatever processing you want to do with the results will be in the body of the callback function. It gives extra advantage in that you can use the results whichever way you like. For example.
var logged = CompileYelpResults.compileYelp(self.MapRouteArray, function(result) {
console.log(result);
});
var stringified = CompileYelpResults.compileYelp(self.MapRouteArray, function(result) {
JSON.stringify(result);
});

How to call function after API gets are finished inside of for loop?

Below is a for loop which will run a max of time times, Inside of that for loop I make a GET call to return some data that needs to be added to my obj object.
I need some way to tell when all 3 GETS are finished as well as the for loop before calling the TagFactory.buildSavedView(obj) line. Thoughts?
for (var i = 0; i < termIDs.length; i++) {
ApiFactory.getTagData(tickers[i], termIDs[i]).then(function(data) {
singleTagArray.push(data.data.tickers);
var updatedTag = TagFactory.renderDirections(singleTagArray, null, period);
newTagObject = updatedTag[0];
tags.push(newTagObject);
finishObjSetup(tags);
console.log('viewHeaderDirect > obj: ', obj);
});
}
TagFactory.buildSavedView(obj);
vm.loadSavedModal = false;
You need to use $q.all, but creating a promise array and pass it to $q.all that will execute its .then only when all the promises gets executed.
Code
var promises = [];
for (var i = 0; i < termIDs.length; i++) {
var promise = ApiFactory.getTagData(tickers[i], termIDs[i]).then(function(data) {
singleTagArray.push(data.data.tickers);
var updatedTag = TagFactory.renderDirections(singleTagArray, null, period);
newTagObject = updatedTag[0];
tags.push(newTagObject);
finishObjSetup(tags);
console.log('viewHeaderDirect > obj: ', obj);
});
promise.push(promise); //creating promise array.
}
$q.all(promise).then(function(){
//here the call will goes after all calls completed.
})
You could use a simple counter:
var y = 0;
for (var i = 0; i < termIDs.length; i++) {
ApiFactory.getTagData(tickers[i], termIDs[i]).then(function (data) {
y++;
singleTagArray.push(data.data.tickers);
var updatedTag = TagFactory.renderDirections(singleTagArray, null, period);
newTagObject = updatedTag[0];
tags.push(newTagObject);
finishObjSetup(tags);
console.log('viewHeaderDirect > obj: ', obj);
if (y === termIDs.length) {
TagFactory.buildSavedView(obj);
vm.loadSavedModal = false;
}
});
}

Nodejs ORM2 callback with parameters

I am creating models in the for statement:
for (var j = 0; j < data.length; j++) {
models.MyModel1.create({ name : data[j].name },
function(err, model){
if (err) {
throw err
}
models.OtherMyModel.create({ model_id : model.id, index : j }],
function(err,submodule){
});
});
}
So here I want to create submodel that will use parent model id and it's index j. And because of async var j will be data.length - 1 for all callback. How I can pass index parameter to the model creation callback?
You can use promises to achieve that.
The following snippet uses when:
var when = require('when');
var nodefn = require('when/node/function');
var promises = [];
// Wrap model creation functions by a promise
var createMyModel1 = nodefn.lift(models.MyModel1.create);
var createOtherMyModel = nodefn.lift(models.OtherMyModel.create);
for (var j = 0; j < data.length; j++) {
// Store the index into a local variable because when the first promise
// will resolve, `i` would be equal to `data.length`!
var index = j;
// Create the first model
var promise = createMyModel1({
name: data[j].name
}).then(function(model) {
// Once the model is created, create the submodel
return createOtherMyModel({
model_id: model.id,
index: index
});
}, function(err) {
throw err;
});
// Store the promise in order to synchronize it with the others
promises.push(promise);
}
// Wait for all promises to be resolved
when.all(promises).then(function() {
console.log('All models are created');
});
Another solution is to use yield and any coroutine runner (available since node.js 0.11.x):
var co = require('co');
// Thunkify model creation functions
var createMyModel1 = function(data) {
return function(fn) { models.MyModel1.create(data, fn); };
};
var createOtherMyModel = function(data) {
return function(fn) { models.OtherMyModel.create(data, fn); };
};
co(function *() {
for (var j = 0; j < data.length; j++) {
// Create first model
var model = yield createMyModel1({
name: data[j].name
});
// Create its submodel
var submodel = yield createOtherMyModel({
model_id: model.id,
index: j
});
}
})();

Function not returning value synchronously

I have the following condition where syncAnalytics is called first. Inside this function, there is another function, retreiveAnalyticsData, written to retrieve the locally stored data.
But before the value is returned from retreiveAnalyticsData, the rest of function syncAnalytics gets executed.
DemoModule.factory('ApplicationAnalytics', function ($http, $location, DBO) {
return {
syncAnalytics: function () {
console.log("syncAnalytics called");// Getting displayed 1st
// Retrieve analytics data available data from db
var inputData = this.retreiveAnalyticsData();
console.log("Input Data : " + JSON.stringify(inputData)); // Getting displayed 4th
},
retreiveAnalyticsData: function () {
console.log('retreiveAnalyticsData called');// Getting displayed 2nd
// Select all rows from app_analytics table in db
var selectQuery = "SELECT * FROM app_analytics";
var analyticsData = [];
DBO.execQry(selectQuery, function (results) {
var len = results.rows.length,
i;
for (i = 0; i < len; i++) {
analyticsData.push(results.rows.item(i).text);
}
console.log(analyticsData); //Getting displayed 5th
return analyticsData;
});
console.log('retreiveAnalyticsData ended');// Getting displayed 3rd
}
};
});
So basically :
var inputData = this.retreiveAnalyticsData(); //This should be executed before the below line.
console.log("Input Data : " + JSON.stringify(inputData)); // Getting displayed 4th
Any insight will be greatly appreciated.
Note : I am using AngularJS
DBO.execQry is an asynchronous function. You may see this because of the callback pattern - e.g. the second paramter of execQry is a function that is called if execQry is ready retrieving the data. I guess what you see is, that console.log('retreiveAnalyticsData ended'); is printed out before console.log(analyticsData);
How to handle this?
1) The oldschool way would be using a callback function:
syncAnalytics: function () {
this.retreiveAnalyticsData(function(inputData){
console.log("Input Data : " + JSON.stringify(inputData));
});
},
retreiveAnalyticsData: function (callback) {
var selectQuery = "SELECT * FROM app_analytics";
var analyticsData = [];
DBO.execQry(selectQuery, function (results) {
var len = results.rows.length,
for (var i = 0; i < len; i++) {
analyticsData.push(results.rows.item(i).text);
}
callback(analyticsData);
});
}
But this way has a lot of disadvantages. What if you would like to handle erros or need to to make multiple asynchronous calls or sync them together? So we came to the promise pattern.
2) The Promise Pattern by $q
syncAnalytics: function () {
this.retreiveAnalyticsData().then(function(inputData){
console.log("Input Data : " + JSON.stringify(inputData));
});
},
retreiveAnalyticsData: function () {
var selectQuery = "SELECT * FROM app_analytics";
var analyticsData = [];
var deferred = $q.defer();
DBO.execQry(selectQuery, function (results) {
var len = results.rows.length,
for (var i = 0; i < len; i++) {
analyticsData.push(results.rows.item(i).text);
}
deferred.resolve(analyticsData);
});
return deferred.promise;
}

Categories

Resources