1

Hello I'm having some issues with Firebase Functions, but the issue is most likely more so on the JavaScript side. You see I need a function to return to the client an array of objects that are within the range of a client's location. The problem is that this function needs to iterate and test the documents in a collection to see if they're within range. If it is, push the object onto the array. Once it finishes iterating each thing, return the array back to the client.

The main issue is that JavaScript isn't synchronous when it comes to loops. So my next idea was to try to force waiting after the iteration was finished using promises. Down below is the code I currently have:

export const getNearbyBison = functions.https.onCall((data, context) => {
    let distance = data.distance;
    let thisUid = data.uid;
    let Lat1 = data.x;
    let Lon1 = data.y;
    let returnUIDs: any[] = [];
    console.log(distance, thisUid, Lat1, Lon1);
    var needDocs = admin.firestore().collection('users').doc('user').collection('user').where('locationEnabled', '==', true).get();

    return needDocs.then(documents => {
        console.log(documents);
        let theseDocs = documents.docs;
        let promiseCheck: any[] = [];
        theseDocs.forEach(doc => {
            let pUid = doc.data().uid;
            console.log('Uid of this user' + pUid);
            admin.firestore().collection('location').doc(pUid).get().then(async location => {
                if (location.exists) {
                    console.log('Location of this user' + location);
                    let locID = location.id;
                    let thisData = location.data();
                    let Lat2 = thisData!.x;
                    let Lon2 = thisData!.y;
                    console.log(locID, Lat2, Lon2);
                    //**********Location equation to solve distance*********
                    var R = 6371000;
                    var phi1 = Lat1 * Math.PI / 180;
                    var phi2 = Lat2 * Math.PI / 180;
                    var deltaPhi = Lat2 - Lat1;
                    var deltaLambda = Lon2 - Lon1;
                    var dLat = deltaPhi * Math.PI / 180;
                    var dLon = deltaLambda * Math.PI / 180;

                    var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                        Math.cos(phi1) * Math.cos(phi2) * Math.sin(dLon / 2) *
                        Math.sin(dLon / 2);

                    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
                    var d = R * c;
                    //*******Convert to miles**************
                    var miles = d / 1609.34;
                    console.log('Miles of user: ' + miles);

                    let pusher = new Promise((resolve, reject) => {
                        if (miles <= distance) {
                            if (thisUid != locID) {
                                //Remember this setup for when you have to use the info for this page...
                                returnUIDs.push({
                                    farAway: miles,
                                    userInfo: doc
                                });
                                resolve();
                            } else { resolve() }
                        } else { resolve() }
                    });

                    promiseCheck.push(
                      pusher  
                    )
                }
            }).catch();
        })
        Promise.all(promiseCheck).then(whatever => {
            console.log(returnUIDs);
            return ({
                nearbyArray: returnUIDs
            });
        }).catch();
    }).catch();

});

The problem is that the function is still sending the array to the client before the forEach loop finishes iterating. Is there a way I can force it to wait? What am I doing wrong? I have looked at other similar issues on this forum and that's how I got to the conclusion that I need to use Promises in some way to cause the program to wait for the loop to be complete. Thank you in advance.

Edit:: Okay so I made a fair bit of progress. For one I put everything that's within the forEach() loop into a returnable Promise and then pushed each one onto the Promise array. I looked up how Javascript deals with asynchronous operations and that was the reason why it was logging an empty array to my console before the loops were finished. I checked my console and now it does log the returned array AFTER the forEach loops finish. Here's the edited code:

export const getNearbyBison = functions.https.onCall((data, context) => {
let distance = data.distance;
let thisUid = data.uid;
let Lat1 = data.x;
let Lon1 = data.y;
let returnUIDs: any[] = [];
console.log(distance, thisUid, Lat1, Lon1);
var needDocs = admin.firestore().collection('users').doc('user').collection('user').where('locationEnabled', '==', true).get();

needDocs.then(documents => {
    console.log(documents);
    let theseDocs = documents.docs;
    let promiseCheck: any[] = [];
    theseDocs.forEach(doc => {
        let pusher = new Promise((resolve, reject) => {
            let pUid = doc.data().uid;
            console.log('Uid of this user' + pUid);
            admin.firestore().collection('location').doc(pUid).get().then(location => {
                if (location.exists) {
                    console.log('Location of this user' + location);
                    let locID = location.id;
                    let thisData = location.data();
                    let Lat2 = thisData!.x;
                    let Lon2 = thisData!.y;
                    console.log(locID, Lat2, Lon2);
                    //**********Location equation to solve distance*********
                    var R = 6371000;
                    var phi1 = Lat1 * Math.PI / 180;
                    var phi2 = Lat2 * Math.PI / 180;
                    var deltaPhi = Lat2 - Lat1;
                    var deltaLambda = Lon2 - Lon1;
                    var dLat = deltaPhi * Math.PI / 180;
                    var dLon = deltaLambda * Math.PI / 180;

                    var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                        Math.cos(phi1) * Math.cos(phi2) * Math.sin(dLon / 2) *
                        Math.sin(dLon / 2);

                    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
                    var d = R * c;
                    //*******Convert to miles**************
                    var miles = d / 1609.34;
                    console.log('Miles of user: ' + miles);

                    if (miles <= distance) {
                        if (thisUid != locID) {
                            //Remember this setup for when you have to use the info for this page...
                            returnUIDs.push({
                                farAway: miles,
                                userInfo: doc
                            });
                            resolve();
                        } else { resolve() }
                    } else { resolve() }

                }
            }).catch();
        });
        promiseCheck.push(pusher);
    });

    Promise.all(promiseCheck).then(whatever => {
        console.log(returnUIDs);
        return ({
            nearbyArray: returnUIDs
        });
    }).catch();
}).catch();

});

The problem now is that it's STILL returning empty information to the client. Not only that but this empty information is being sent before the loops are finished. So on my console I can see that the array the function should return is populated with the correct information to send back. The problem is however that the function is still sending nothing, I suspect because it's still sending before the loops are finished. Can anyone tell me why this is the case?

  • 1
    Simply calling `then` or `catch` on a promise doesn't really cause anything to wait. Your code needs to return a single promise that resolves with the data to send to the client after **all** the async work completes in the function. That means every other promise returned by every async call needs to be factored into that final promise. – Doug Stevenson Nov 02 '19 at 18:29
  • Hmmm can you clarify a bit more for me? Because I thought that's what I was doing with adding promises to an array and then waiting for that array of promises to resolve. But clearly I'm not lol. – Jibri Wright Nov 02 '19 at 18:35
  • As I said, calling `then` does not "wait" for anything to finish. It is asynchronous and just returns immediately with yet another promise that resolves with the value returned by the callback that you passed. `Promise.all()` is also async and returns another promise. Maybe you want to do something with that, since you're currently ignoring it? – Doug Stevenson Nov 02 '19 at 18:38
  • Ohh okay thank you! So how would I send the array after the array of promises resolves? – Jibri Wright Nov 02 '19 at 18:40
  • 1
    As I said: "Your code needs to return a single promise that resolves with the data to send to the client after all the async work completes in the function." There's a lot of code here and I can't really debug it all for you. I can just tell you that I can see that you're ignoring promises here, and that's causing your function to break. Perhaps try something simple first to experiment and learn, then work your way up to a final solution. – Doug Stevenson Nov 02 '19 at 18:43
  • Well see that's the thing, I am lol. Each loop creates a promise that resolves when the information is pushed onto the array that needs to be returned to the user. And that Promise itself is pushed onto the Promise array I specified earlier. And when that Promise array is resolved then it should return the necessary array to the client. So that isn't particularly helpful lol. – Jibri Wright Nov 02 '19 at 18:48
  • @DougStevenson in fact I'm looking at another post on this forum about how to resolve this issue. And the answer given to the problem was the answer you gave! Lol. The very solution that I'm trying in this function is the same solution that you gave a while back! Lol. https://stackoverflow.com/questions/47470690/how-to-resolve-return-a-foreach-function-in-cloud-functions – Jibri Wright Nov 02 '19 at 19:09
  • @DougStevenson okay so I actually made a fair bit of progress. For one I decided to put everything in the forEach function within a returnable promise and pushed that onto the Promise array. I also checked my Firebase log and it is in fact logging the return array after every forEach() Promise is finished. The problem is that it's still returning to the client before everything is finished. Why is this the case? – Jibri Wright Nov 02 '19 at 19:51
  • You're still ignoring some promises. As I stated before, simply calling `then` or `catch` on a promise isn't enough. Your function can't return until all of the promises are fully resolved. – Doug Stevenson Nov 03 '19 at 06:26
  • I posted the answer to the problem below. I was actually using Promises correctly. The issue was how Firebase(or Google Cloud Functions) returns after an asynchronous function. You have to put a return in front of an asynchronous function to return whatever happens after that Promise. Look at the answer below, I was using Promises correctly the entire time. – Jibri Wright Nov 06 '19 at 03:59

1 Answers1

0

Okay guys so I figured out what the problem was! Anytime you want a Firebase function to return after an asynchronous operation, you have to return the promise. So in the above code every time an asynchronous operation was performed I had to place return in front of it. Not only that but I also had to nest everything one after another inside of .then() functions. Not the easiest feat as when you're dealing with large amounts of code that require a substantial amount of asynchronous operations, it's extremely easy to get lost in your code. But hey that's how Firebase works(or Google cloud functions, one of the two). Maybe they'll make it easier to perform asynchronous functions in the future, but that was the main issue. In my code below you can see that every asynchronous function is returned, and all of the major parts of the code are nestled in the .then() of the previous function.

I do note that it could be simplified a bit, as while Promises are an important part they weren't the main issue with the code. The issue was how Firebase(or Google) deals with asynchronous code.

export const getNearbyBison = functions.https.onCall((data, context) => {
    let counter = 0;
    let distance = data.distance;
    let thisUid = data.uid;
    let Lat1 = data.x;
    let Lon1 = data.y;
    let returnUIDs: any[] = [];
    let promiseCheck: any[] = [];
    console.log(distance, thisUid, Lat1, Lon1);
    var needDocs = admin.firestore().collection('users').doc('user').collection('user').where('locationEnabled', '==', true).get();

    return needDocs.then(documents => {
        return new Promise((r, j) => {
            console.log(documents);
            let theseDocs = documents.docs;

            theseDocs.forEach(doc => {
                let pusher = new Promise((resolve, reject) => {
                    let pUid = doc.data().uid;
                    console.log('Uid of this user' + pUid);
                    admin.firestore().collection('location').doc(pUid).get().then(location => {
                        if (location.exists) {
                            console.log('Location of this user' + location);
                            let locID = location.id;
                            let thisData = location.data();
                            let Lat2 = thisData!.x;
                            let Lon2 = thisData!.y;
                            console.log(locID, Lat2, Lon2);
                            //**********Location equation to solve distance*********
                            var R = 6371000;
                            var phi1 = Lat1 * Math.PI / 180;
                            var phi2 = Lat2 * Math.PI / 180;
                            var deltaPhi = Lat2 - Lat1;
                            var deltaLambda = Lon2 - Lon1;
                            var dLat = deltaPhi * Math.PI / 180;
                            var dLon = deltaLambda * Math.PI / 180;

                            var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                                Math.cos(phi1) * Math.cos(phi2) * Math.sin(dLon / 2) *
                                Math.sin(dLon / 2);

                            var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
                            var d = R * c;
                            //*******Convert to miles**************
                            var miles = d / 1609.34;
                            console.log('Miles of user: ' + miles);

                            if (miles <= distance) {
                                if (thisUid != locID) {
                                    //Remember this setup for when you have to use the info for this page...
                                    returnUIDs.push({
                                        farAway: miles,
                                        userInfo: doc
                                    });
                                    counter++;
                                    resolve();
                                } else {
                                    counter++;
                                    resolve();
                                }
                            } else {
                                counter++;
                                resolve();
                            }
                            if (counter >= documents.size) {
                                r();
                            }

                        }
                    }).catch();
                });
                promiseCheck.push(pusher);
            });

        }).then(() => {
            return Promise.all(promiseCheck).then(whatever => {
                console.log(returnUIDs);
                return { nearbyArray: returnUIDs };
            }).catch();
        }).catch();
    }).catch();

});

As you can see the constant nestling can get pretty overwhelming, but at the moment it's a necessary evil and maybe this will change in the future to be handled more elegantly.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441