I have a series of nested async calls that need to finish before my code continues. Function save_part1 calls the sqlite database and returns the rows of interest. For each of those rows, I make an ajax call to a save them remotely.
From what I have read about promises and deferred, I have only seen them being used in the context of ajax calls. And on top of that, it makes my brain hurt.
Question: how do I wait until all the ajax calls have completed before starting save_part2?
function save()
{
save_part1();
//this should only happen after all the ajax calls from save_part1 are complete
save_part2();
}
function save_part1()
{
db.transaction(function (tx) {
tx.executeSql("SELECT * FROM TABLE1", [],
function (transaction, results) {
for (var i = 0; i < results.rows.length; i++)
{
var row = results.rows.item(i);
ajaxCall_item1(row);
}
}, errorHandler)
});
}
function save_part2()
{
db.transaction(function (tx) {
tx.executeSql("SELECT * FROM TABLE2", [],
function (transaction, results) {
for (var i = 0; i < results.rows.length; i++)
{
var row = results.rows.item(i);
ajaxCall_item2(row);
}
}, errorHandler)
});
}
As long as you have ajaxCall_item returning the jQuery deferred object, you can have save_part1 return a deferred object, collect the returned promises into an array, call them with $.when and resolve the "promise" once all of the requests have completed. Then you will be able to write: save_part1().then(save_part2);. Here is an untested example:
function save() {
save_part1().then(save_part2).fail(function(err) {
// ohno
});
}
function ajaxCall_item1(row) {
return $.ajax({ ... });
}
function save_part1()
{
var dfd = jQuery.Deferred();
var promises = [];
db.transaction(function (tx) {
tx.executeSql("SELECT * FROM TABLE1", [],
function (transaction, results) {
for (var i = 0; i < results.rows.length; i++) {
var row = results.rows.item(i);
promises.push(ajaxCall_item1(row));
}
$.when.apply($, promises).then(function() {
dfd.resolve.apply(dfd, arguments);
});
}, function(err) {
dfd.reject(err);
});
});
return dfd;
}
Related
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
here's the code I currently using,
function loopArrayWithAsync(array, doSthWithElement, finalCallback) {
var count = 0;
var _data = [];
var _errs = [];
for (var i = 0; i < array.length; i++) {
doSthWithElement(array[i], function (err, data) {
count++;
if (err) {
_errs.push(err);
}
if (data) {
_data.push(data);
}
if (count === data.length) {
finalCallback(_errs, _data);
}
}
}
}
then, I will use the function in this way:
loopArrayWithAsync(array, function (element, finish) {
// element - element in the array
asyncFunc(element, function (err, result) {
if (err) {
finish(err);
} else {
finish(null, result);
}
});
}, function (errs, finalData) {
// when the for loop is finished,
// i.e. (count === data.length)
// this callback will be executed
// errs - array of err from the above callback function
// finalData - array of result from the above callback function
outerCallback(errs, finalData);
});
with this implementation, I can loop through an array with async function and execute the callback function when all elements in the array have been processed.
but now I want to add a delay/interval feature to loopArrayWithAsync()
something like loopArrayWithAsync(array, {interval : 1000}, function (element, finish) {..., after it processed the first element, it should wait for 1000ms, then starts to process the second element, and vice versa...
I've found another question talking about adding delay to for loop
but it seems to be more complicated while dealing with async functions.
Any answers will be appreciated
============================== update ==============================
this is the function after refactoring,
function loopArrayWithAsync(array, options, doSthWithElement, finalCallback) {
if (isFunction(options)) {
finalCallback = doSthWithElement;
doSthWithElement = options;
options = {};
}
options.interval = options.interval || 0;
options.oneByOne = options.oneByOne || false;
var _data = [];
var _errs = [];
var count = 0;
var length = array.length;
var i = 0;
(function handleIteration() {
if (i < length) {
var element = array[i];
doSthWithElement(element, function (err, data) {
if (err) {
_errs.push(err);
}
if (data) {
_data.push(data);
}
count++;
if (count === length) {
finalCallback(_errs, _data);
} else if (options.oneByOne) {
if (options.interval) {
setTimeout(handleIteration, options.interval);
} else {
process.nextTick(handleIteration);
}
}
});
i++;
if (!options.oneByOne) {
if (options.interval) {
setTimeout(handleIteration, options.interval);
} else {
process.nextTick(handleIteration);
}
}
}
}());
};
so that I can use the function in this way now:
loopArrayWithAsync(array, {interval : 1000}, function (element, finish) {
asyncFunc(element, function (err, result) {
if (err) {
finish(err);
} else {
anotherAsyncFunc(result, function (err, doc) {
if (err) {
finish(err);
} else {
finish(null, doc);
}
});
}
});
}, function (errs, finalData) {
outerCallback(errs, finalData);
});
or
loopArrayWithAsync(array, {oneByOne : true}, function (element, finish) {...
loop through the elements one by one
loopArrayWithAsync(array, {interval : 5000, oneByOne : true}, function (element, finish) {...
loop through the elements one by one and with 5 seconds delay
available options :
interval is the amount of milliseconds between each iteration, default : 0
If oneByOne is true, the method would only proceed to the next element until finish has been invoked for the current element, default : false
The code suits my case now, but I will still try the suggested libraries to make life easier, thank you
Please leave a comment if you found that the code can be further improved, looking forward to any suggestions!
As suggested by #thefourtheye you can use the concept of Promises, and Bluebird is a fast and good library for this. Promise.settle lets you resolve and reject your promises and then inspect the result.
function loopArray(array) {
var arrayOfPromises = [];
for (var i = 0; i < array.length; i++) {
arrayOfPromises.push(doSomethingAsync(array[i]));
}
Promise.settle(arrayOfPromises).then(function (results) {
console.log("All async calls done! You can inspect the result!");
console.log(results);
});
}
function doSomethingAsync(item) {
return new Promise(function(resolve, reject){
//Do you async work here!
console.log("Entering async function call " + item);
if(item === "three"){
reject("bad value!");
}
resolve(item + " promise done!");
});
}
loopArray(["one","two","three"]);
I made a JSFiddle of the below example. Working with asynchronous functions, promises can help you out a lot, so I'd really suggest you look into it.
You can use a local function to make the asynchronous loop. For the next iteration the function calls itself with a delay:
function loopArrayWithAsync(array, doSthWithElement, finalCallback, delay) {
var _data = [], _errs = [], i = 0;
loop();
function loop() {
doSthWithElement(array[i], function (err, data) {
if (err) {
_errs.push(err);
}
if (data) {
_data.push(data);
}
i++;
if (i === array.length) {
finalCallback(_errs, _data);
} else {
window.setTimeout(loop, delay);
}
}
}
}
To start the calls at a certain interval instead of having a delay between calls, just use setInterval with different times:
function loopArrayWithAsync(array, doSthWithElement, finalCallback, delay) {
var _data = [], _errs = [], count = 0;
for (var i = 0; i < array.length; i++) {
window.setTimeout(function() {
doSthWithElement(array[i], function (err, data) {
if (err) {
_errs.push(err);
}
if (data) {
_data.push(data);
}
count++;
if (count === array.length) {
finalCallback(_errs, _data);
}
});
}, i * delay);
}
}
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;
}
SaveData.js
function queryDB(callback) {
var sqlTxt = "SELECT * FROM DEMO";
db.transaction(
function(tx) {
tx.executeSql(sqlTxt, [],
function(tx, results) {
var item_Codes = [];
for (var i = 0; i < results.rows.length; i++) {
item_Codes.push({item_code: results.rows.item(i).itemCode});
}
callback(item_Codes);
})
, errorCB;
});
return false;
}
main.js
queryDB();
console.log( item_Codes);
I have above two methods to retrieve data from database .It works fine but i need to combine these two methods to one method and return the itemCodes array.
var processResult = function(items)
{
//process returned array 'items'
}
function queryDB(callback) {
var sqlTxt = "SELECT * FROM DEMO";
db.transaction(
function(tx) {
tx.executeSql(sqlTxt, [],
function (tx, results) {
var item_Codes = [];
for (var i = 0; i < results.rows.length; i++) {
item_Codes.push({item_code: results.rows.item(i).itemCode});
}
callback(item_Codes);
})
, errorCB);
});
return false;
}
and call it with:
queryDB(processResult);
or you can use global variable item_Codes instead of function processResult
How to retrieve an array of data arrMyLatLng function.
db.transaction(queryDB, errorCB);
function queryDB(tx)
{
tx.executeSql('SELECT * FROM Attractions', [], querySuccess, errorCB);
}
function querySuccess(tx, results)
{
var arrMyLatLng = [];
var len = results.rows.length;
var arrLatitude=[], arrLongitude=[];
for (var i=0; i<len; i++)
{
arrLatitude[i] = results.rows.item(i).latitude;
arrLongitude[i] = results.rows.item(i).longitude;
arrMyLatLng[i] = new google.maps.LatLng(arrLatitude[i], arrLongitude[i]);
}
return arrMyLatLng ; // Need this array to manipulate the data from it is outside of the function.
}
Thanks
You're performing an asynchronous operation which will execute its callback whenever the operation completes. Hence there is nothing that you can return arrMyLatLng to. All work with arrMyLatLng will need to be done in the callback. One thing you can do is call another function from querySuccess and pass arrMyLatLng to it:
function querySuccess(tx, results) {
var arrMyLatLng = [];
var len = results.rows.length;
var arrLatitude=[], arrLongitude=[];
for (var i=0; i<len; i++) {
arrLatitude[i] = results.rows.item(i).latitude;
arrLongitude[i] = results.rows.item(i).longitude;
arrMyLatLng[i] = new google.maps.LatLng(arrLatitude[i], arrLongitude[i]);
}
someOtherFunction(arrMyLatLng);
}