Lately I've been stuck with a problem that I don't know how to solve. I asked this question and after some efforts we've found that Firebase works differently with promises than normal requests, and I couldn't use them properly.
As explained in the question, I'm filling an array with some informations from Firebase, and I need to call another method when I'm sure the array is filled, in other words when I'm sure the call to Firebase has finished.
This is my code as I'm using it now:
var user_pref = firebase.database().ref('/users/'+ self.api.email_id+'/preferenze/');
var ref = firebase.database().ref('/tag/')
var userTags = [];
var self1 = self;
user_pref.once('value', function(preferenze) {
preferenze.forEach(function(t) {
ref.once('value', function(tags) {
tags.forEach(function(t1) {
if (t.key == t1.key) {
console.log("Found " + t1.key)
userTags.push(t1.key)
}
return false;
});
})
return false;
});
}).then(a =>{
await this.sleep(1000) //----> WORKAROUND
console.log("Out")
this.myTags = userTags
this.findPoiByTag(this.myTags) //method I have to call when finished
})
I'm using this orrible workaround with sleep to be sure the code outside is executed after the one inside. Without that, it prints "Out" before and then all the "Found" in the loop. I've tried using it with promises in every way, but it still doesn't work. Having a look at the docs here I couldn't find anything that would help me.
That's indeed pretty bad.
This should be closer to what you need:
var userTags = [];
var self1 = self;
user_pref.once('value', function(preferenze) {
var promises = [];
preferenze.forEach(function(t) {
promises.push(ref.child(t.key).once('value'));
});
Promise.all(promises).then(function(snapshots) {
snapshots.forEach(function(snapshot) {
if (snapshot.exists()) {
userTags.push(snapshot.key);
}
});
})
this.myTags = userTags
this.findPoiByTag(this.myTags) //method I have to call when finished
});
What this does differently:
It loads each preference key with a direct look (removing the need for a deeply nested loop that was loading way too much data).
It puts all load of the categories into an array of promises.
It then calls your function after all promises have resolved.
Related
I have a function that calls another function which then does two things:
It does an http get to grab a list of IDs
It then loops through that list of IDs, makes another http get for each ID, and adds the result to the var 'dataList'.
I need to have it return the fully populated dataList as the result, then I can take that list and do something with it. I know I need to use promises for this but I'm having trouble getting the behavior I'm looking for. My latest attempt is below. This does return the expected list, but then it seems to get stuck in a loop - I think it's doing a return for every iteration of the map loop. Any suggestions would be appreciated.
getDataList(searchString, matchCase, rows, start).then(function(result) {
// Do something with result
});
getDataList: function(searchString, matchCase, rows, start) {
let body = {};
let dataList = [];
var url = getUrl();
var defer = $q.defer();
return $http.get(url).success(function(response) {
let dataIdList = [];
body = response.response.docs;
body.map(function getDataId(wfr) {
if (wfr.referenceType === 'data') {
dataIdList.push(wfr.referenceId);
}
});
dataList = dataIdList.map(function getData(dataId) {
dataSvc.getDataDetails(dataId).then(function(response) {
dataList.push(response);
});
});
defer.resolve(dataList);
}).error(function(result) {
defer.reject();
});
}
The .success and .error methods have been removed from the AngularJS framework.1 Avoid using the deferred anti-pattern.2 Use $q.all to resolve multiple AngularJS promises:
getDataList: function(searchString, matchCase, rows, start) {
var url = getUrl();
return $http.get(url).then(function(response) {
let body = response.data.response.docs;
let dataIdList =
body.filter(wfr => wfr.referenceType === 'data').map(_ => _.referenceId);
let dataListPromiseArr = dataIdList.map(dataId => {
return dataSvc.getDataDetails(dataId).then(function(response) {
return response.data;
});
});
return $q.all(dataListPromiseArr);
}).catch(function(response) {
console.log(response);
throw response;
});
}
For more information, see
AngularJS $q Service API Reference - $q.all
I think Promise.all() is what you are looking for.
You can push all the getDataDetails(dataId) requests as promises to an array, then do Promise.all(yourPromiseArray) and return what you want only after Promise.all has successfully finished.
I'd highly recommend looking into async/await during your research, as it could be quite helpful. Plenty of articles on the internet on how you can implement it, and many many StackOverflow questions to look through if you feel like you're stuck with it.
Hope this helps, good luck! :)
In the following code I save each item's key and an email address in one table, and to retrieve the object to fetch from the original table using said key. I can see that the items are being put into the rawList array when I console.log, but the function is returning this.cartList before it has anything in it, so the view doesn't receive any of the data. How can I make it so that this.cartList waits for rawList to be full before it is returned?
ionViewWillEnter() {
var user = firebase.auth().currentUser;
this.cartData.getCart().on('value', snapshot => {
let rawList = [];
snapshot.forEach(snap => {
if (user.email == snap.val().email) {
var desiredItem = this.goodsData.findGoodById(snap.val().key);
desiredItem.once("value")
.then(function(snapshot2) {
rawList.push(snapshot2);
});
return false
}
});
console.log(rawList);
this.cartList = rawList;
});
}
I have tried putting the this.cartList = rawList in a number of different locations (before return false, even inside the .then statement, but that did not solve the problem.
The following function call is asynchronous and you're falling out of scope before rawList has a chance to update because this database call takes a reasonably long time:
desiredItem.once("value").then(function(snapshot2) {
rawList.push(snapshot2);
});
You're also pushing the snapshot directly to this list, when you should be pushing snapshot2.val() to get the raw value.
Here's how I would fix your code:
ionViewWillEnter() {
var user = firebase.auth().currentUser;
this.cartData.getCart().on('value', snapshot => {
// clear the existing `this.cartList`
this.cartList = [];
snapshot.forEach(snap => {
if (user.email == snap.val().email) {
var desiredItem = this.goodsData.findGoodById(snap.val().key);
desiredItem.once("value")
.then(function(snapshot2) {
// push directly to the cartList
this.cartList.push(snapshot2.val());
});
}
return false;
});
});
}
The problem is the Promise (async .once() call to firebase) inside the forEach loop (sync). The forEach Loop is not gonna wait for the then() statement so then on the next iteration the data of the previous iteration is just lost...
let snapshots = [1, 2, 3];
let rawList = [];
snapshots.forEach((snap) => {
console.log(rawList.length)
fbCall = new Promise((resolve, reject) => {
setTimeout(function() {
resolve("Success!");
}, 2500)
});
fbCall.then((result) => {
rawList.push(result);
});
})
You need forEach to push the whole Promise to the rawList and Then wait for them to resolve and do sth with the results.
var snapshots = [1, 2, 3];
var rawList = [];
var counter = 0;
snapshots.forEach((snap) => {
console.log(rawList.length)
var fbCall = new Promise((resolve, reject) => {
setTimeout(function() {
resolve("Success!" + counter++);
}, 1500)
});
rawList.push(fbCall);
})
Promise.all(rawList).then((res) => {
console.log(res[0]);
console.log(res[1]);
console.log(res[2]);
});
The thing is, it is still a bit awkward to assign this.cartList = Promise.all(rawList) as it makes it a Promise. So you might want to rethink your design and make something like a getCartList Service? (dont know what ur app is like :p)
Since you're using angular you should also be using angularfire2, which makes use of Observables which will solve this issue for you. You will still be using the normal SDK for many things but for fetching and binding data it is not recommended to use Firebase alone without angularfire2 as it makes these things less manageable.
The nice things about this approach is that you can leverage any methods on Observable such as filter, first, map etc.
After installing it simply do:
public items$: FirebaseListObservable<any[]>;
this.items$ = this.af.database.list('path/to/data');
And in the view:
{{items$ | async}}
In order to wait for the data to appear.
Use AngularFire2 and RxJS this will save you a lot of time, and you will do it in the proper and maintainable way by using the RxJS operators, you can learn about those operators here learnrxjs
I have an Angularjs project that uses Restangular to access the database. I have three layers of data (say mydata, mysubdata, mysubsubdata) and there is a one-to-many relationship between each layer. My problem is that, for my display, I need to concatenate the mysubsubdata to the mysubdata. When I try to get data back from the database, I am hitting a complaint in the compiler that says I can't have a function in a loop. Here is what I am trying to do:
DataService.one(mydata.id).getList('mysubdata')
.then(function(data) {
var dataList = data;
for (returnedData in dataList) {
DataService.one(mydata.id).one('mysubdata',returnedData.id).getList('mysubsubdata')
.then(returnedSubData) {
dataList = angular.extend(dataList, returnedSubData);
});
}
});
All the examples I've found have loops inside the .then function or are trying to get a bunch of promises back first. I don't think those apply. I'm still pretty new to Angular, so I may be flailing a bit. Not sure about the extend either, but that's likely a separate question.
Edit: I suspect this should be done with a $q.all but haven't grasped the method yet.
Adding a then() method inside your loop won't work because of the simple reason that loop does not wait for the promises to be resolved. You can achieve this using recursive method.
var myCustomData = null,
dataList = null,
dataListCounter = 0;
DataService.one(mydata.id).getList('mysubdata')
.then(function (data){
dataList = data;
myCustomData = mydata;
$scope.getSubSubData();
});
$scope.getSubSubData = function () {
if (dataList.length >= dataListCounter)
return;
DataService.one(myCustomData.id).one('mysubdata',dataList[dataListCounter].id).getList('mysubsubdata')
.then(function (returnedSubData) {
dataList = angular.extend(dataList, returnedSubData);
dataListCounter++;
$scope.getSubSubData();
});
};
Please let me know if this helps!
Minor corrections to #Anadi Sharma's response.
$scope.getSubSubData = function () {
if (dataList.length == dataListCounter)
return;
DataService.one(myCustomData.id).one('mysubdata',dataList[dataListCounter].id).getList('mysubsubdata')
.then(function (returnedSubData) {
dataList[dataListCounter].mysubsubdata = returnedSubData;
dataListCounter++;
$scope.getSubSubData();
});
};
Note that I then use a filter when I display the data to concatenate the subsubdata values.
I'm using the axios promise library, but my question applies more generally I think. Right now I'm looping over some data and making a single REST call per iteration.
As each call completes I need to add the return value to an object. At a high level, it looks like this:
var mainObject = {};
myArrayOfData.forEach(function(singleElement){
myUrl = singleElement.webAddress;
axios.get(myUrl)
.then(function(response) {
mainObject[response.identifier] = response.value;
});
});
console.log(convertToStringValue(mainObject));
What's happening of course is when I call console.log the mainObject doesn't have any data in it yet, since axios is still reaching out. What's a good way of dealing with this situation?
Axios does have an all method along with a sister spread one, but they appear to be of use if you know ahead of time how many calls you'll be making, whereas in my case I don't know how many loop iterations there will be.
You need to collect all of your promises in an array and then use Promise.all:
// Example of gathering latest Stack Exchange questions across multiple sites
// Helpers for example
const apiUrl = 'https://api.stackexchange.com/2.2/questions?pagesize=1&order=desc&sort=activity&site=',
sites = ['stackoverflow', 'ubuntu', 'superuser'],
myArrayOfData = sites.map(function (site) {
return {webAddress: apiUrl + site};
});
function convertToStringValue(obj) {
return JSON.stringify(obj, null, '\t');
}
// Original question code
let mainObject = {},
promises = [];
myArrayOfData.forEach(function (singleElement) {
const myUrl = singleElement.webAddress;
promises.push(axios.get(myUrl));
});
Promise.all(promises).then(function (results) {
results.forEach(function (response) {
const question = response.data.items[0];
mainObject[question.question_id] = {
title: question.title,
link: question.link
};
});
console.log(convertToStringValue(mainObject));
});
<script src="https://unpkg.com/axios#0.19.2/dist/axios.min.js"></script>
It's described in axios docs (Performing multiple concurrent requests section).
Before May 2020 it was possible to do with axios.all(), which is now deprecated.
I have an array and i need to send values of array to webservice through http post request one by one . For the node.js , i'm using "async" package to do that for ex: async.eachSeries doing it well , how can i do that same thing for angular.js , my normal async code;
//this code sends all queries of array (maybe 5.000 request at same time , it is hard to process for webservice :=) ) at same time and wait for all responses.
//it works but actually for me , responses should wait others at end of loop should work one by one
//like async.eachSeries module!
for (var i = 0; i < myArr.lenght; i++) {
(function (i) {
var data = {
"myQuery": myArr[i].query
};
$http.post("/myServiceUrl", data).success(function (result) {
console.log(result);
});
})(i);
}
Both Matt Way and Chris L answers Correct , you can investigate Chris's answer for understanding about async to sync functions in for loops.
You can use $q to create a similar requirement by chaining promises together. For example:
var chain = $q.when();
angular.forEach(myArr, function(item){
chain = chain.then(function(){
var data = {
myQuery: item.query
};
return $http.post('/myServiceUrl', data).success(function(result){
console.log(result);
});
});
});
// the final chain object will resolve once all the posts have completed.
chain.then(function(){
console.log('all done!');
});
Essentially you are just running the next promise once the previous one has completed. Emphasis here on the fact that each request will wait until the previous one has completed, as per your question.
function logResultFromWebService(value)
{
$http.post("/myServiceUrl", value).success(console.log);
}
angular.forEach(myArray, logResultFromWebService);
If I understand your question correctly. You want to run a for loop in a synchronized manner such that the next iteration only occurs once the previous iteration is completed. For that, you can use a synchronized loop/callbacks. Especially if the order matters.
var syncLoop = function (iterations, process, exit) {
var index = 0,
done = false,
shouldExit = false;
var loop = {
next: function () {
if (done) {
if (shouldExit && exit) {
return exit(); // Exit if we're done
}
}
// If we're not finished
if (index < iterations) {
index++; // Increment our index
process(loop); // Run our process, pass in the loop
// Otherwise we're done
} else {
done = true; // Make sure we say we're done
if (exit) exit(); // Call the callback on exit
}
},
iteration: function () {
return index - 1; // Return the loop number we're on
},
break: function (end) {
done = true; // End the loop
shouldExit = end; // Passing end as true means we still call the exit callback
}
};
console.log('running first time');
loop.next();
return loop;
}
For your particular implementation:
syncLoop(myArray.length, function (loop) {
var index = loop.iteration();
var data = {
"myQuery": myArray[index].query
};
$http.post("/myServiceUrl", data).success(function (result) {
console.log(result);
loop.next();
});
}, function () {
console.log('done');
});
If you intend on doing something with the data once returned (such as perform calculations) you can do so with this method because you will return the data in a specified order.
I implemented something similar in a statistical calculation web app I built.
EDIT:
To illustrate the problem I had when using $q.when I have set up a fiddle. Hopefully this will help illustrate why I did this the way I did.
https://jsfiddle.net/chrislewispac/6atp3w8o/
Using the following code from Matt's answer:
var chain = $q.when(promise.getResult());
angular.forEach(myArr, function (item) {
chain = chain.then(function () {
$rootScope.status = item;
console.log(item);
});
});
// the final chain object will resolve once all the posts have completed.
chain.then(function () {
console.log('all done!');
});
And this fiddle is an example of my solution:
https://jsfiddle.net/chrislewispac/Lgwteone/3/
Compare the $q version to my version. View the console and imagine those being delivered to the user interface for user intervention in the process and/or performing statistical operations on the sequential returns.
You will see that it does not sequentially give the numbers 1,2,3,4 etc. either in the console or in the view in Matt's answer. It 'batches' the responses and then returns them. Therefore, if step 3 is not to be run depending on the response in step 2 there is not, at least in the answer provided, a way to break out or explicitly control the synchronous operation here. This presents a significant problem when attempting to perform sequential calculations and/or allow the user to control break points, etc.
Now, I am digging through both the $q libraries and the Q library to see if there is a more elegant solution for this problem. However, my solution does work as requested and is very explicit which allows me to place the function in a service and manipulate for certain use cases at my will because I completely understand what it is doing. For me, that is more important than using a library (at least at this stage in my development as a programmer and I am sure there are lots of other people at the same stage on StackOverflow as well).
If the order doesn't matter in which they are sent
var items = [/* your array */];
var promises = [];
angular.forEach(items, function(value, key){
var promise = $http.post("/myServiceUrl", { "myQuery": value.query });
promises.push(promise);
});
return $q.all(promises);