Reading snapshot of other user in nested firebase function - javascript

I am trying to loop through my database that is structured like this:
Realtime Structure
I want to use the RBUserID (RB: Referred By) key which references another user. I'm using the code below to cycle through the UserIDs and find which userID matches the RBUserID.
function readOnceandFindRef() {
refRead.once("value", function (snapshot) {
snapshot.forEach(function (childSnapshot) {
var childKey = childSnapshot.key;
var childData = childSnapshot.val();
var RBUserID = childData.RBUserID;
console.log(RBUserID);
debugger; // 1
refRead.child(RBUserID).once("value", function (snapshot) {
if (snapshot.exists()) {
console.log(snapshot.key);
debugger; // 2
}
debugger; // 3
});
});
});
}
The forEach loop reads the users and console.log(RBUserID) works, but then it skips through
refRead.child(RBUserID).once("value", function (snapshot) {
and doesn't runs Debugger 2 and Debugger 3 until after the forEach loop has ended. The chrome debugger shows this:
Chrome Debugger
Any help of why it's skipping the read function inside would be appreciated

That's because once() is asynchronous and returns immediately, before the query is complete. The callback you pass to it is invoked some time later, while the code immediately after it continues to execute.
Note that once() also returns a promise that resolves with the snapshot object, and it's more common to use that instead of the callback argument in order to make it easier to do asynchronous programming.

As #Doug rightly pointed out in his answer, since once() is an asynchronous operation, you might need to tweak your function readOnceandFindRef for checking all values synchronously.
One of the solution would be using for-of loop instead of forEach loop.
async function readOnceandFindRef() {
const snapshots = await refRead.once("value");
for (const snapshot of snapshots) {
var childKey = snapshot.key;
var childData = snapshot.val();
var RBUserID = childData.RBUserID;
console.log(RBUserID);
debugger; // 1
const childSnapshot = await refRead.child(RBUserID).once("value");
if(childSnapshot.exists()){
console.log(snapshot.key);
debugger; // 2
}
debugger; // 3
}
}
However, in this case, you will be checking through each user-id one after another.
But using the solution of Promise.all, you can execute all such asynchronous operations in parallel.
Solution 2
async function readOnceandFindRef() {
const snapshots = await refRead.once("value");
// Following part allows us to execute all the asynchronous operations
// at once and still store their outcomes synchronously in the results array.
const results = await Promise.all(snapshots.map(snapshot => {
var childKey = snapshot.key;
var childData = snapshot.val();
var RBUserID = childData.RBUserID;
console.log(RBUserID);
debugger; // 1
return refRead.child(RBUserID).once("value").then(snapshot => {
const doesRefReadExist = false;
if(childSnapshot.exists()){
console.log(snapshot.key);
doesRefReadExist = true;
}
return { rbUserID: RBUserID, doesRefReadExist}
}) ;
}))
}
The results array would look something like:
[
{
rbUserID: RBUserID-1,
doesRefReadExist: true/false
},
{
rbUserID: RBUserID-2,
doesRefReadExist: true/false
},
.
.
.
]

Related

Asyncronicity in a reduce() function WITHOUT using async/await

I am patching the exec() function to allow subpopulating in Mongoose, which is why I am not able to use async/await here -- my function will be chained off a db call, so there is no opportunity to call await on it, and within the submodule itself, there I can't add async/await outside of an async function itself.
With that out of the way, let's look at what I'm trying to do. I have two separate arrays (matchingMealPlanFoods and matchingMealPlanRecipeFoods) full of IDs that I need to populate. Both of them reside on the same array, foods. They each require a db call with aggregation, and the problem in my current scenario is that only one of the arrays populates because they are happening asynchronously.
What I am trying to do now is use the reduce function to return the updated foods array to the next run of reduce so that when the final result is returned, I can replace the entire foods array once on my doc. The problem of course is that my aggregate/exec has not yet returned a value by the time the reduce function goes into its next run. Is there a way I can achieve this without async/await here? I'm including the high-level structure here so you can see what needs to happen, and why using .then() is probably not viable.
EDIT: Updating code with async suggestion
function execute(model, docs, options, lean, cb) {
options = formatOptions(options);
let resolvedCount = 0;
let error = false;
(async () => {
for (let doc of docs) {
let newFoodsArray = [...doc.foods];
for (let option of options) {
const path = option.path.split(".");
// ... various things happen here to prep the data
const aggregationOptions = [
// // $match, then $unwind, then $replaceRoot
];
await rootRefModel
.aggregate(aggregationOptions)
.exec((err, refSubDocuments) => {
// more stuff happens
console.log('newFoodsArray', newFoodsArray); // this is to check whether the second iteration is using the updated newFoods Array
const arrToReturn = newFoodsArray.map((food) => {
const newMatchingArray = food[nests[1]].map((matchingFood) => {
//more stuff
return matchingFood;
});
const updatedFood = food;
updatedFood[`${nests[1]}`] = newMatchingArray;
return updatedFood;
});
console.log('arrToReturn', arrToReturn);
newFoodsArray = [...arrToReturn];
});
}
};
console.log('finalNewFoods', newFoodsArray); // this should log after the other two, but it is logging first.
const document = doc.toObject();
document.foods = newFoodsArray;
if (resolvedCount === options.length) cb(null, [document]);
}
})()
EDIT: Since it seems it will help, here is the what is calling the execute function I have excerpted above.
/**
* This will populate sub refs
* #param {import('mongoose').ModelPopulateOptions[]|
* import('mongoose').ModelPopulateOptions|String[]|String} options
* #returns {Promise}
*/
schema.methods.subPopulate = function (options = null) {
const model = this.constructor;
if (options) {
return new Promise((resolve, reject) => execute(model, [this], options, false, (err, docs) => {
if (err) return reject(err);
return resolve(docs[0]);
}));
}
Promise.resolve();
};
};
We can use async/await just fine here, as long as we remember that async is the same as "returning a Promise" and await is the same as "resolving a Promise's .then or .catch".
So let's turn all those "synchronous but callback-based" calls into awaitables: your outer code has to keep obeying the API contract, but since it's not meant to a return a value, we can safely mark our own version of it as async, and then we can use await in combination with promises around any other callback based function calls in our own code just fine:
async function execute(model, docs, options, lean, andThenContinueToThis) {
options = formatOptions(options);
let option, resolvedCount = 0;
for (let doc of docs) {
let newFoodsArray = [...doc.foods];
for (option of options) {
// ...things happen here...
const aggregationOptions = [/*...data...*/];
try {
const refSubDocuments = await new Promise((resolve, reject) => rootRefModel
.aggregate(aggregationOptions)
.exec((err, result) => err ? reject(err) : resolve(result));
// ...do some work based on refSubDocuments...
}
// remember to forward errors and then stop:
catch (err) {
return andThenContinueToThis(err);
}
}
// remember: bind newFoodsArray somewhere so it doesn't get lost next iteration
}
// As our absolutely last action, when all went well, we trigger the call forwarding:
andThenContinueToThis(null, dataToForward);
}

How to use async call inside forEach when using firebase calls

the question that I have is that I can't figure out how to make this code work properly using Firestore (not sure if this is irrelevant).
The actual code is the following:
prestamoItems() {
var myarray = [];
var myobject = {};
//here comes the first async method (works OK)
fb.prestamosCollection
.orderBy("fechaPrestamo", "desc")
.get()
.then(val => {
if (!val.empty) {
//here comes forEach
val.docs.forEach(doc => {
myobject = doc.data();
myobject.id = doc.id;
console.log("The doc id is " +myobject.id)
//here comes second async call inside the forEach loop, but it doesnt wait for this
//to be finished, and immediately goes to the other step
fb.equiposCollection.doc(myobject.id).get().then(eqp => {
console.log("The doc id from the other collection is " +eqp.id)
})
myarray.push(myobject)
console.log("myobject pushed to myarray")
});
}
});
}
Please note that I'm calling an async method inside a forEach loop that comes from another async method. In every variation of the code, the output that I'm getting (the console logs) are the following:
11:13:14.999 Prestamos.vue?18d2:71 The doc id is 1yTCUKwBvlopXX2suvVu
11:13:14.999 Prestamos.vue?18d2:78 myobject pushed to myarray
11:13:15.000 Prestamos.vue?18d2:71 The doc id is Z5TE15Fj3HFrn1zvceGe
11:13:15.000 Prestamos.vue?18d2:78 myobject pushed to myarray
11:13:15.000 Prestamos.vue?18d2:71 The doc id is JNN9aN65XE1tUTmlzkoJ
11:13:15.000 Prestamos.vue?18d2:78 myobject pushed to myarray
11:13:15.000 Prestamos.vue?18d2:71 The doc id is NF2hHCpM8leZezHbmnJx
11:13:15.001 Prestamos.vue?18d2:78 myobject pushed to myarray
11:13:15.364 Prestamos.vue?18d2:74 The doc id from the other collection is 1yTCUKwBvlopXX2suvVu
11:13:15.368 Prestamos.vue?18d2:74 The doc id from the other collection is Z5TE15Fj3HFrn1zvceGe
11:13:15.374 Prestamos.vue?18d2:74 The doc id from the other collection is JNN9aN65XE1tUTmlzkoJ
11:13:15.379 Prestamos.vue?18d2:74 The doc id from the other collection is NF2hHCpM8leZezHbmnJx
So, the forEach loop is not waiting to the async function inside it (which actually is the expected behavior, AFAIK).
The question is how can I make it wait for the inner call to be finished before adding the obect to the array? Thanks in advance.
either you nest code, which depends on previous results into then() callbacks or you wrap the loop (forEach does not support async) in async block to make use of await inside. eg.:
fb.prestamosCollection
.orderBy("fechaPrestamo", "desc")
.get()
.then(val => {
if (!val.empty) {
// wrap loop in async function call iife so we can use await inside
(async () => {
for (var i = 0; i < val.docs.length; i++) {
const doc = val.docs[i];
myobject = doc.data();
myobject.id = doc.id;
// this will be synchronous now
let eqp = await fb.equiposCollection.doc(myobject.id).get();
console.log(eqp.id);
myarray.push(myobject)
}
})();
}
});
The root of the problem is that you're trying to turn an asychronous operation (waiting for Firestore to return values) into a synchronous one. This isn't really possible in a meaningful way in JavaScript without causing lots of issues!
You'll need to populate your array inside of the .then() callback and return the promise as a result of the function. Any caller that calls your prestamoItems() function will also have to use .then() callbacks to access the underlying myarray value:
const _ = {
async prestamoItems() {
const val = await fb.prestamosCollection.orderBy("fechaPrestamo", "desc").get();
if (val.empty) {
return myarray
}
// Promise.all() will take a list of promises and will return their results once they have all finished.
return await Promise.all(
// Array.prototype.map() will take an existing array and, for each item, call the given function and return a new array with the return value of each function in that array.
// This is functionally equivalent to making a new array and push()ing to it, but it reads a lot nicer!
val.docs.map(async doc => {
const myobject = doc.data();
const eqp = await fp.equiposCollection.doc(myobject.id).get()
// I presume you want to do something with eqp here
return myobject
})
);
}
}
The above code sample uses Array.prototype.map() to do away with myarray as it's not necessary.
A caller would have to use this code like this:
_.prestamoItems().then((myarray) => {
...
})
Promises are a way of saying that a value may be avaliable at some point in the future. Because of this, you have to make sure that any interaction you have with a promise is written in such a way that assumes the value is not avaliable immediately. The easiest way to do this is by using async/await and ensuring that you return promise objects.
just move the push inside then like this
fb.equiposCollection.doc(myobject.id).get().then(eqp => {
console.log("The doc id from the other collection is " +eqp.id)
myarray.push(myobject)
console.log("myobject pushed to myarray")
})

How to run a firebase function correctly?

This function will return the value of initiate_session, initiate_session[user_session].events, user_session
before the firebase function runs.
How to run the firebase function first ??
function createSession() {
var user_session = randomCharacters(20) //8iKhA4Aq2!6gZ)890ip#;
var ip = '127.0.0.1';
var initiate_session = new Object();
var session_started = new Date().getTime();
firebase.database().ref(user_session).once('value', function (snapshot) {
if (snapshot.exists()) {
console.log('session exists');
createSession();
} else {
console.log('session not exists')
initiate_session[user_session] = {
ip: ip,
session_started: session_started,
events: {}
};
firebase.database().ref(user_session).set({
ip: ip,
session_started: session_started,
events: {}
});
}
});
console.log('new session', initiate_session);
return [initiate_session, initiate_session[user_session].events, user_session];}
This is because the Firebase function is asynchronous - the code inside the method that gets the snapshot is a callback that is executed after the Firebase read is finished, which may take several seconds. However, as soon as you dispatch the read request via .once(...) your execution flow continues and the return is called.
There are a few possible solutions:
Pass a callback argument to your createSession() method that is called with the values you are trying to return, instead of returning them directly.
Return a promise from your method that resolves to give the values you're trying to return.
Use async/await syntax for your Firebase call. This is covered already in this question: running queries in firebase using async / await
Rough Example of #1
function createSession(onSessionCreated) {
var user_session = "whatever";
var initiate_session = new Object();
firebase.database().ref(user_session).once('value', function (snapshot) {
// do things with the snapshot
onSessionCreated(initiate_session, initiate_session[user_session].events, user_session)
});
}
// usage:
createSession(function (initiate_session, events, user_session) {
// do things with initiate_session, events and user_session
});
Rough Example of #2
function createSession() {
var user_session = "whatever";
var initiate_session = new Object();
firebase.database().ref(user_session).once('value').then(function (snapshot) {
// do things with the snapshot
return [initiate_session, initiate_session[user_session].events, user_session];
});
}
// usage:
createSession().then(function (results) {
// do things with results (i.e. the three elements in the array above)
});
Rough Example of 3
async function createSession() {
var user_session = "whatever";
var initiate_session = new Object();
const snapshot = await firebase.database().ref(user_session).once('value');
// do things with the snapshot
return [initiate_session, initiate_session[user_session].events, user_session];
}
// usage:
const results = await createSession();
If you're new to async/await code it probably won't be the easiest place to start as it may require changes elsewhere in your code, but this article is a good resource if you're keen to learn.

how to access the value of variable outside its scope in snapshot.foreach

return statment return null value outside the foreach of the variable, I know the foreach loop creates its own scope , i need to return the value , how can i do that .....
this.selectedUserMessages = this.changeUserList.switchMap(Userid => {
if (Userid) {
var self = this;
this.loadingSerivice.isLoading.next(true);
var ref = db.database.ref(`wfffsdf/usesdfdsfsrs/${Userid}/ress`);
var i = 0;
ref.once("value")
.then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
var key = childSnapshot.key;
// childData will be the actual contents of the child
var invitedList = childSnapshot.val();
self.invList.push({
invitedid: invitedList
});
var invitedlistid = self.invList[i]['invitedid']['invitedBy'];
//console.log(invitedlistid);
if (invitedlistid == "asfsafafasfsafsafs") {
//console.log("thread"+threadKey);
self.ChatKey = key;
//console.log(self.ChatKey+""+ db.database.ref(`1dfgsdfgdfhfdh/tdfsdfhsfhfhfdh/${key}/messages`));
}
i++;
});
self.chatMessages = self.getMessages(self.ChatKey);
// console.log(self.chatMessages);
});
return this.chatMessages;
}
return of(null);
}
return this.chatMessages give null value...
The short answer is that the scope of the variables aren't the problem. In Javascript, var is scoped to surrounding function. In the example code provided, forEach is being passed a function, so all of its variables are scoped as declared. Outer variables are captured in the inner function so they behave properly as well.
The problem is that the inner promise is not resolving before the switchmap returns a value.
To understand fully what's going on, let's take a look at what the code is currently doing and what it should be doing instead.
For the sake of argument, I'm going to assume the following definitions (even though I know they are not correct):
selectedUserMessages : Observable<[]>;
changeUserList : Observable<String>;
Here, changeUserList is a stream of string events that represents the current selected UserId. selectedUserMessages will be an array of messages associated with the currently selected user.
The code above is simplified to the following meaning:
this.selectedUserMessages =
// when changeUserList changes, switch selectedUserMessages to this new stream
this.changeUserList.switchMap(Userid => {
if (Userid) {
// grab DB reference to user child list
var ref = db.database.ref(`wfffsdf/usesdfdsfsrs/${Userid}/ress`);
// CREATE A PROMISE TO QUERY THE DATABASE
ref.once("value") // query once
.then(function (snapshot) {
snapshot.forEach(function (childSnapshot) {
var key = childSnapshot.key;
var val = childSnapshot.val();
// process child
});
// store result
this.chatMessages = this.getMessages(this.ChatKey);
}); // END OF PROMISE
// RETURN IMMEDIATELY, BEFORE PROMISE RESOLVES
return this.chatMessages;
}
return of(null); // return null stream
}
The issue being that the switchmap call returns this.chatMessages before the inner promise resolves.
switchmap has an interesting feature: you can return a promise from inside switchmap and it will wait on the promise. This is one way to mix Promises and Observables.
Returning a promise from inside the switchmap looks like:
this.selectedUserMessages =
// when changeUserList changes, switch selectedUserMessages to this new stream
this.changeUserList.switchMap(Userid => {
if (Userid) {
// grab DB reference to user child list
let ref = db.database.ref(`wfffsdf/usesdfdsfsrs/${Userid}/ress`);
// !!!! RETURN A PROMISE TO QUERY THE DATABASE !!!!
return ref.once("value") // query once
.then(function (snapshot) {
let results = [];
snapshot.forEach(function (childSnapshot) {
let key = childSnapshot.key;
let val = childSnapshot.val();
// process child //
results.push(/*processing results*/);
});
return results;
}); // END OF PROMISE
}
else
return of(null); // return null stream
}
With that being said, there's an alternative to mixing Promises and Observables for Firebase called RxFire. It's a relatively new package from the Firebase team for using RxJS Observables. Since you are using the Realtime Database, you will want to look at these examples. List() will emit a stream of child changes. You can pipe() this stream through a map() operator to do additional processing (or extract the val() and include the key as in the example). what you do in map() is similar to what you do in forEach.
AngularFirebase has an example using RxFire on FireStore.

How to wait for a Firebase retrieve value and only then exit the function?

I have a Firebase query.
Because Firebase works asynchronously, the function continue to run without waiting for the Firebase retrieve value.
Is there a way to wait for the result from the Firebase query and only then to make the return from the function?
function CheckBuyingCondition(prefix){
var Res= "";
var Current_Price_Open_ref = firebase.database().ref("dailyT/Current_Price_Open/"+nextDayTrading).orderByChild("Prefix").equalTo(prefix)
Current_Price_Open_ref.once("value").then(function(snapshot) {
if(snapshot.exists()){
snapshot.forEach(function(childSnapshot) {
var val = childSnapshot.val();
res =""+ val.Current_Price_Open;
});
}else{
res = "NA";
}
});
return res; //(Here i got res = "" instead of the correct value from Firebase query
}
Use async/await:
async function checkBuyingCondition(prefix) {
var res = '';
var currentPriceOpenRef = firebase.database()
.ref(`dailyT/currentPriceOpen/${nextDayTrading}`)
.orderByChild('prefix')
.equalTo(prefix);
var snapshot = await currentPriceOpenRef.once('value');
if(snapshot.exists()) {
snapshot.forEach(function(childSnapshot) {
var val = childSnapshot.val();
res = `${val.currentPriceOpen}`;
});
} else {
res = 'NA';
}
return res;
}
Take note that this does not make your function synchronous at all, thus the async keyword at the beginning of your function declaration; it just makes your function look like one.
On the 3rd line inside the function you'll notice the await keyword. This waits for your promise to resolve then returns the result which in your case, is the snapshot from Firebase. You can only use await inside async functions.
More Reading: Javascript Async/Await
What you're proposing is making the Firebase SDK asynchronous call into an synchronous call. This is not a good idea, and to be honest, not even possible in JavaScript. If you need to make a helper function that deals with Firebase APIs, that function should instead accept a callback function to be invoked when the work completes, or return a promise so that the caller of the function can decide what to do next.
Read here to learn more about why Firebase APIs are asynchronous.
Try this:
function CheckBuyingCondition(prefix){
var Res= "";
var Current_Price_Open_ref = firebase.database().ref("dailyT/Current_Price_Open/"+nextDayTrading).orderByChild("Prefix").equalTo(prefix)
return Current_Price_Open_ref.once("value").then(function(snapshot) {
if(snapshot.exists()){
snapshot.forEach(function(childSnapshot) {
var val = childSnapshot.val();
res =""+ val.Current_Price_Open;
});
return res;
}else{
res = "NA";
}
});
}
Firebase queries are promises, so you because you can return the result from the promise and get it with another promise.

Categories

Resources