Tristan
Tristan

Reputation: 1790

Why Won't This Firebase Callable Function Return A Value?

I have a callable function that should return a value, but the only thing ever returned is null. Below is the current version of the function. I have also tried having a return on the first promise (the original once call), and at the end in another then returning the GUID. It actually returned data in that case, but it returned immediately and the GUID was empty.

How can I accomplish my goal and still return the GUID? I don't know when the function is called if I will use a new GUID that I generate, or one that already exists in the database.

There is a similar question here: Receiving returned data from firebase callable functions , but in that case it was because he never returned a promise from the function. I am returning a promise on all code paths. Unless I have to return the initial promise from the once call? In which case, how can I return the GUID when I don't know it yet?

I am also trying to throw an error in a couple of places and the error shows up in the logs for the function, but is never sent to the client that called the function.

I am going off of the examples here: https://firebase.google.com/docs/functions/callable

Sorry for the code bomb.

Calling the function:

var newGame = firebase.functions().httpsCallable('findCreateGame');
        newGame({}).then(function(result) {
            // Read result of the Cloud Function.
            //var sGameID = result.data.guid;
            console.log(result);
        }).catch(function(error) {
            console.log(error);
        });

Function:

exports.findCreateGame = functions.https.onCall((data, context) => {
    console.log("findCurrentGame Called.")
    /**
    * WHAT NEEDS DONE
    *
    *
    * Pull in user's information
    * Determine their win/loss ratio and search for a game using transactions in either low medium or high queue
    * If there are no open games in their bracket, search the one above, then below
    * If no open games anywhere, create a new game in their bracket
    * If an open game is found, write the UID to the game and add the game's ID to the user's profile
    *
    */

    var uid = context.auth.uid;
    var section = "";
    var sUsername = "";
    var sProfilePic = "";
    var currentGames = null;
    var sGUID = "";

    //Get the user's info
    var userref = admin.database().ref('users/' + uid);
    userref.once("value", function(data) {
        var ratio = 0;
        var wins = parseInt(data.val().wins);
        var losses = parseInt(data.val().losses);
        var lives = parseInt(data.val().lives);

        if (lives < 1){
            //This user is out of lives, should not have been able to get here
            //Throw an exception so that we can see why it failed
            throw new functions.https.HttpsError('permission-denied', 'You do not have enough lives to start a new game.');
        }
        sUsername = data.val().username;
        sProfilePic = data.val().profilepicture;

        //Handle if they have no losses
        if (losses == 0){
            ratio = 100;
        } else {
            ratio = (wins / losses) * 100;
        }

        //If they have played less than 5 games, put them in noob tier
        if (wins + losses < 5){
            ratio = 0;
        }

        if (ratio <= 33){
            section = "noob";
        } else if (ratio > 33 && ratio <= 66){
            section = "average";
        } else {
            section = "expert";
        }
    }).then(() => {
        //Get all of the games this user is currently in
        admin.database().ref('games').orderByChild(uid).once('value', function(data) {
            currentGames = data.val();
        }).then(() => {

            //Generate a new GUID in case we need to set up a new game
            sGUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });



            var queueref = admin.database().ref('gamequeue/' + section);
            queueref.transaction(function(currentGUID) {

                if (currentGUID == null){
                    //Write our GUID in the queue
                    return sGUID;
                } else {
                    //Get the id of the game we just got
                    sGUID = currentGUID

                    return null;
                }
            }).then((res) => {

                if (res.snapshot.val() != null){
                    //This means we are creating the game lobby

                    //Generate a new answer
                    var newAnswer = "";
                    while (newAnswer.length < 4){
                        var temp = Math.floor(Math.random() * 9) + 1;

                        temp = temp.toString();

                        if (!newAnswer.includes(temp)){
                            newAnswer += temp;
                        }
                    }

                    var obj = {username: sUsername, score: 0, profilepicture: sProfilePic};

                    return admin.database().ref('games/' + sGUID).set({id: sGUID, requestor: uid, [uid]: obj, answer: newAnswer, turn: uid, status: 'pending'}).then(() => {
                        return {guid: sGUID};
                    });
                } else {
                    //We found a game to join

                    //If we are in a duplicate request situation, make sure the GUID is a string
                    if (typeof(sGUID) != 'string'){
                        sGUID = Object.keys(sGUID)[0];
                    }

                    //Make sure we didn't find our own game request
                    if (currentGames[sGUID] != null){
                        //Add this GUID back to the queue, we shouldn't have removed it
                        return admin.database().ref('gamequeue/' + section + '/' + sGUID).set('');

                        //Throw an exception that says you can only have one open game at a time
                        throw new functions.https.HttpsError('already-exists', 'We are still finding a match for your last request. You are only allowed one open request at a time.');

                    } else {
                        //Get the current game info
                        admin.database().ref('games/' + sGUID).once('value', function(data) {
                            var sRequestor = data.val().requestor;
                            var sOpponentUsername = data.val()[sRequestor].username;
                            var sOpponentProfilePic = data.val()[sRequestor].profilepicture;

                            //Write all of our info to the game
                            return admin.database().ref('games/' + sGUID).update({[sRequestor]: {opponentusername: sUsername, opponentprofilepicture: sProfilePic}, [uid]: {username: sUsername, score: 0, opponentusername: sOpponentUsername, opponentprofilepicture: sOpponentProfilePic}, status: 'active'}).then(() => {
                                return {guid: sGUID};
                            });
                        });
                    }
                }
            });
        });
    })
});

Upvotes: 3

Views: 2341

Answers (1)

Bob Snyder
Bob Snyder

Reputation: 38319

The documentation for callable functions explains:

To return data after an asynchronous operation, return a promise. The data returned by the promise is sent back to the client.

You have many asynchronous operations that must be chained together. A return needs to be added to each of these statements (as shown):

return userref.once("value", function(data) {...

return admin.database().ref('games').orderByChild(uid).once('value', function(data) {...

return queueref.transaction(function(currentGUID) {...

return admin.database().ref('games/' + sGUID).once('value', function(data) {...

Upvotes: 4

Related Questions