0

The scenario is when a player clicks "join game" in an app, a cloud function is invoked to add the player to firebase database and the result is passed as response.

Database structure:

/games
|--/game_id
|----/players => this is a json array of uids and their status([{"uid1": true}, {"uid2":true},...])
|----/max_players = 10

The cloud function should check if the /players size is less than then /max_players and only if so, it should update the /players with the uid of the player and return the response.

Here is my cloud function:

export const addUserToGame = functions.https.onCall((data, context) => {

    // Expected inputs - game_id(from data) and UID(from context)

    if (context.auth == null) {
        return {
            "status": 403,
            "message": "You are not authorized to access this feature"
        };
    }

    const uid = context.auth.uid;
    const game_id = data.game_id;

    let gameIDRef = gamesRef.child(game_id);

    return gameIDRef.transaction(function (t) {

        if (t === null) {
            // don't do anything, can i just do return;
        } else {

            let playersNode;
            let isAlreadyPlayer = false;
            if (t.players) { 
                console.log("players is not empty");

                playersNode = t.players;
                let players_count = playersNode.length;
                let max_players: Number = t.max_players;

                if (players_count >= max_players) {
                    // how can i return an error from here?
                }

                for (var i = 0; i < playersNode.length; i++) {
                    if (playersNode[i].uid === uid) {
                        isAlreadyPlayer = true;
                    }
                    break;
                }

                if (!isAlreadyPlayer) {
                    playersNode.push({
                        [uid]: "active"
                    });
                }
              
             t.players = playersNode;
             return t;

            } else {
                playersNode = [];
                playersNode.push({
                    [uid]: "active"
                });

                t.players = playersNode;
                return t;
            }

        }

    },function(error, committed, snapshot) {

        if(committed) {
            return {
                "status": 200,
                "message": "Player added successfully"
            };
        } else {
            return {
                "status": 403,
                "message": "Error adding player to the game"
            };
        }

    });

});


Please see the comments in the above code, how can i send back a response when a condition fails?


Thanks @Doug and @Renaud for quick response, i still have few questions:

Q1. So i have to use

 return gameIDRef.transaction(t => {

    }).then(result => {

    }).catch(error => {

    });

but when is this used? is this the callback version that @Renaud is referring to?

function (error, committed, snapshot) {
});

Q2. In the first way, when gameIDRef.transaction(t => {}) is called, what happens when there is no value with the game_id and when there is no value with game_id, i want to tell the user that there is no game with that specified id. How can i achieve that?

return gameIDRef.transaction(t => {
      if(t === null)
        return; 
//Where does the control go from here? To then? 

// Also i have a conditional check in my logic, which is to make sure the numbers of players doesn't exceed the max_players before adding the player

t.players = where the existing players are stored
t.max_players = maximum number of players a game can have

if(t.players.length >= t.max_players)
// How do i send back a message to client saying that the game is full?
// Throw an error like this?
throw new functions.https.HttpsError('failed-condition', 'The game is already full');

    }).then(result => {

    }).catch(error => {
// and handle it here to send message to client?
    });
Vamsi Challa
  • 11,038
  • 31
  • 99
  • 149

3 Answers3

1

You are trying to access a function property of an undefined variable. In this case, t does not have a property players (you are already checking that on an if statement above). But then, outside your if statement you are still accessing t.players.update, and since t.players is undefined, you get the error

BravoZulu
  • 1,140
  • 11
  • 24
1

There are several key aspects to adapt in your Cloud Function:

  1. As explained in the Cloud Functions doc, you need to manage the asynchronous Firebase operations by using promises. In your code, you use the callback "version" of the Transaction. You should use the "promise" version, as shown below.
  2. In the transaction, you need to return the new desired state you would like to write to the database node. In a previous version of your question you were returning the object you intended to send back to the Cloud Function caller: this is not correct.
  3. You need to send data back to the client (i.e. the caller) when the promise returned by the transaction is resolved, as shown below (i.e. in .then(r => {..})).

Finally, to answer to your question at the bottom of your answer ("how can i send back a response when a condition fails?"). Just do return; as explained in the doc "if undefined is returned (i.e. you return with no arguments) the transaction will be aborted and the data at this location will not be modified".

So, based on the above, you could think that the following should work. However, you will rapidly see that the if (t === null) test does not work. This is because the first invocation of a transaction handler may return a null value, see the documentation, as well as the following posts: Firebase transaction api call current data is null and https://groups.google.com/forum/#!topic/firebase-talk/Lyb0KlPdQEo.

export const addUserToGame = functions.https.onCall((data, context) => {
    // Expected inputs - game_id(from data) and UID(from context)

    // ...

    return gameIDRef
        .transaction(t => {

            if (t === null) {  // DOES NOT WORK...
                return;
            } else {
                // your "complex" logic
                return { players: ..., max_players: xxx };
            }
        })
        .then(r => {
            return { response: 'OK' };
        })
        .catch(error => {
            // See https://firebase.google.com/docs/functions/callable?authuser=0#handle_errors
        })
});

You will find a workaround in the SO post I referred above (Firebase transaction api call current data is null). I've never tested it, I'm interested by knowing how it worked for you, since this answer got a lot of votes.


Another, more radical, workaround would be to use Firestore, instead of the Realtime Database. With Firestore Transactions, you can very well check in a document exists or not.


UPDATE Following the comments:

If you want to send back different responses to the Cloud Function caller, depending on what was checked in the transaction, use a variable as shown with the dummy following example.

Do not re-execute the checks outside of the transaction (i.e. in the then()): Only in the transaction you can be sure that if another client writes to the database node before your new value is successfully written, the update function (i.e. t) will be called again with the new current value, and the write will be retried.

// ...
let result = "updated";
return gameIDRef
    .transaction(t => {
        console.log(t);
        if (t !== null && t.max_players > 20) {
            result = "not updated because t.max_players > 20"
            return;
        } else if (t !== null && t.max_players > 10) {
            result = "not updated because t.max_players > 10"
            return;
        }
        else {
            return { max_players: 3 };
        }
    })
    .then(r => {
        return { response: result };
    })
    .catch(error => {
        console.log(error);
        // See https://firebase.google.com/docs/functions/callable?authuser=0#handle_errors
    })
Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
  • Thank you, i have edited my question with my doubts, can you please look at it again? – Vamsi Challa Jul 26 '20 at 08:02
  • to answer to your question at the bottom of your answer ("how can i send back a response when a condition fails?"). Just do return; => i understand that the value is not changed, but to send the message back to client, should i check the values at /game_id again in "then" and return a message? – Vamsi Challa Jul 26 '20 at 08:50
  • To catch the case when you return `undefined` just look, in the `then()`, at the value of the `committed` property. It will be ` `false`. – Renaud Tarnec Jul 26 '20 at 08:59
  • Okie, that makes sense. Thank you. If i want to send a conditional response to user, let's say if the max players limit is reached, then inside then() if committed is false, then compare the players array with max_players and send the response? Is that how it should be written? – Vamsi Challa Jul 26 '20 at 09:12
  • So basically for all conditions that the data should not be modified, i should just use return, and once inside then() have to check for my conditions and construct the response to client? is this correct? – Vamsi Challa Jul 26 '20 at 09:13
  • The answer to the two questions of your last comment is yes. For the previous comment ("then inside then() if committed is false, then compare the players array with max_players and send the response? Is that how it should be written?") I am not sure to understand: you should not deal with the values of the node in the `then()` but only in the transaction. In the `then()` you just deal with the result of the transaction (which may include the response to the Cloud Function caller), not with the data/node to be modified. – Renaud Tarnec Jul 26 '20 at 11:14
  • I was maybe not totally clear in my last comment! By "In the `then()` you just deal with the result of the transaction (which may include the response to the Cloud Function caller)" I mean "dealing with the result of the transaction may include sending the response to the Cloud Function caller ". – Renaud Tarnec Jul 26 '20 at 11:45
  • Thanks for taking time in answering my questions. Just to clarify what i meant in then() section, in the transaction block ```if(t.players.length >= t.max_players) return;``` so now in the then() block, is it ok to do ```if(snapshot.child("players").numChildren() >= snapshot.child("max_players").val()) return "Game is already full";```?? – Vamsi Challa Jul 26 '20 at 12:00
  • I am trying to understand how to send a response back based on whether the condition passed or failed in the transaction. There may be more than 1 condition in the transaction block, and i may need to send different response to client based on these conditions. For ex., in transaction, ```if(a.length > 10) return; if(b.length > 10) return;``` and in then() i need to send ```if(a.length > 10) return "a is greater than 10"``` or ```if(b.length) > 10 return "b is greater than 10";``` – Vamsi Challa Jul 26 '20 at 12:06
  • You should not check the values **outside of the transaction** (i.e. in the `then()`): doing so kind of invalidates the use of a transaction. Only in the transaction you can be sure that if another client writes to the database node before your new value is successfully written, the update function (i.e. `t`) will be called again with the new current value, and the write will be retried. – Renaud Tarnec Jul 26 '20 at 12:55
  • See the update: it shows how to pass outside the transaction the results of the checks done within the transaction. – Renaud Tarnec Jul 26 '20 at 13:15
  • Thank you. Will try it out and get back to you. Really appreciate your time. – Vamsi Challa Jul 26 '20 at 13:43
  • Thanks for your time, i have made changes to my cloud function and also instead of using transaction on /game_id i am using it now /game_id/players. But i get an error while setting the data. I have posted a new question - https://stackoverflow.com/questions/63111633/nodejs-firebase-transaction-maximum-call-stack-size-exceeded – Vamsi Challa Jul 27 '20 at 08:43
  • 1
    Accepting your answer for your guidance – Vamsi Challa Jul 27 '20 at 08:48
1

With callable functions, you have to return a promise that resolves with the data object to send to the client. You can't just return any promise from a promise chain.

After the transaction promise resolves, you will need to chain another then callback to it to return the value you want. The general structure is:

return child.transaction(t => {
    return { some: "data" }
})
.then(someData => {
   // transform the result into something to send to the client
   const response = convertDataToResponse(someData)
   return response
})
.catch(err => {
   // Decide what you want to return in case of error
})

Be sure to read the API docs for transaction.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Thank you, i have edited my question with my doubts, can you please look at it again? – Vamsi Challa Jul 26 '20 at 08:02
  • If you have additional questions, you should post those as new questions. It's not acceptable on Stack Overflow to "overload" a single post with many questions and updates. – Doug Stevenson Jul 26 '20 at 17:18
  • Sure will do.. but seems like the issue is resolved. Will accept the answer or will post another question – Vamsi Challa Jul 26 '20 at 17:21