Retrieve and assign gameId to multiple players in Cloud Function transaction - javascript

I've been following this tutorial and trying to add a third player to the game. Here is the source code that works for two players. Below is my data structure:
"matchmaking" : {
"32idNK8OndcujN8CFNe3VBTxsXL2" : {
"gameId" : "placeholder"
},
"LnMnOtdLJYbsRuTeUpeHfaAhpX32" : {
"gameId" : "placeholder"
},
"plpISHWWtuNJvhSlFwfik6DOHR53" : {
"gameId" : "placeholder"
}
}
I have a cloud function that is called every time a user joins the matchmaking room. Depending on whichever two users are available, the function generates a unique game room for all three users. I am not well-versed in Javascript so I've been struggling on this for a while.
How can I retrieve and assign the gameId value to three players during a transaction?
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var database = admin.database();
exports.matchmaker = functions.database.ref('matchmaking/{playerId}')
.onCreate((snap, context) => {
var gameId = generateGameId();
database.ref('matchmaking').once('value').then(snapshot => {
var secondPlayer = null;
var thirdPlayer = null;
// add two players into the queue
snapshot.forEach(child => { // check that its not the playerId who just joined
var playerId = child.key
var gameId = child.val().gameId
if (gameId === "placeholder" && playerId !== context.params.playerId) {
secondPlayer = child;
}
});
snapshot.forEach(child => { // check that its not the playerId who just joined
var playerId = child.key
var gameId = child.val().gameId
if (gameId === "placeholder" && playerId !== secondPlayer.key && playerId !== context.params.playerId) {
thirdPlayer = child;
}
});
if (secondPlayer === null || thirdPlayer === null) return null; // one room needs three players
database.ref("matchmaking").transaction(function (matchmaking) {
console.log(matchmaking)
// ================================================
// PROBLEM STARTS HERE
// ================================================
console.log("playerId gameId:",context.params.playerId.gameId) // returns undefined
// If any of the players gets into another game during the transaction, abort the operation
if (matchmaking === null ||
context.params.playerId.gameId !== "placeholder" ||
matchmaking[secondPlayer.key].gameId !== "placeholder" ||
matchmaking[thirdPlayer.key].gameId !== "placeholder") return matchmaking;
// matchmaking.forEach(player => { // check that its not the playerId that just joined
// var playerId = player.key
// var gameId = player.val().gameId
// if (gameId !== "placeholder") {
// player["gameId"] = gameId;
// }
// });
context.params.playerId.gameId = gameId; // assign game id to player
matchmaking[secondPlayer.key].gameId = gameId;
matchmaking[thirdPlayer.key].gameId = gameId;
return matchmaking;
}).then(result => {
if (result.snapshot.child(context.params.playerId).val() !== gameId) return;
// ENDS HERE
// ================================================
var game = {
gameInfo: {
gameId: gameId,
playersIds: [context.params.playerId, secondPlayer.key, thirdPlayer.key]
},
turn: context.params.playerId
}
database.ref("games/" + gameId).set(game).then(snapshot => {
console.log("Game created successfully!")
return null;
}).catch(error => {
console.log(error);
});
return null;
}).catch(error => {
console.log(error);
});
return null;
}).catch(error => {
console.log(error);
});
});
function generateGameId() {
var possibleChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var gameId = "";
for (var j = 0; j < 20; j++) gameId += possibleChars.charAt(Math.floor(Math.random() * possibleChars.length));
return gameId;
}
I've been getting errors such as matchmaking.forEach is not a function or Cannot read property 'params' of undefined or Cannot read property 'val' of undefined
console.log(matchmaking) shows the following:
{ '32idNK8OndcujN8CFNe3VBTxsXL2': { gameId: 'placeholder' },
LnMnOtdLJYbsRuTeUpeHfaAhpX32: { gameId: 'placeholder' },
plpISHWWtuNJvhSlFwfik6DOHR53: { gameId: 'placeholder' } }
I would really appreciate any help.
Update:
Solution:
database.ref("matchmaking").transaction(function (matchmakingSnapshot) {
if (matchmakingSnapshot) {
matchmakingSnapshot[context.params.playerId].gameId = gameId; // assign game id to player
matchmakingSnapshot[secondPlayer.key].gameId = gameId;
matchmakingSnapshot[thirdPlayer.key].gameId = gameId;
return matchmakingSnapshot;
} else if ( // If any of the players gets into another game during the transaction, abort the operation
matchmakingSnapshot === null ||
context.params.playerId.gameId !== "placeholder" ||
matchmakingSnapshot[secondPlayer.key].gameId !== "placeholder" ||
matchmakingSnapshot[thirdPlayer.key].gameId !== "placeholder") {
return matchmakingSnapshot;
}
}).then(result => {
The problem was that when I was trying to update the gameId, the transaction returns a null data. I had to make sure matchmaingSnapshot is not null before accessing the context params playerId. I'm not sure why calling matchmakingSnapshot.val() returns function undefined, but yeah that's it!

You should not currently be getting the error `matchmaking.forEach is not a function`.
The ".forEach" function can work on an array or on a DataSnapshot (where it iterates over the children, providing a DataSnapshot of each in turn).
But if you have an Object, ".forEach" will give you the error message you describe. The code as it stands appears to be having "matchmaking" as a DataSnapshot, so it should be ok for ".forEach" at the moment.
Explanation for error: Cannot read property 'params' of undefined
I think you have accidentally inserted a variable name, instead of telling javascript to use the variable name's value
if (matchmaking === null ||
matchmaking.context.params.playerId.gameId !== "placeholder" ||
matchmaking.secondPlayer.key.gameId !== "placeholder" ||
matchmaking.thirdPlayer.key.gameId !== "placeholder"
) return matchmaking;
I think you don't mean matchmaking.secondPlayer.key.gameId. I think you mean matchmaking[secondPlayer].key.gameId, so that if secondPlayer is "ABC123", you would be referring to matchmaking.ABC123.key.gameId
Same for thirdPlayer.
Explanation for error: Cannot read property 'params' of undefined
Did you mean just context.params.playerId.gameId, not matchmaking.context.params.playerId.gameId? You might have added the "matchmaking" by mistake?
Exploring the error: TypeError: Cannot read property 'gameId' of undefined
If you get the above error with this:
matchmaking[context.params.playerId].key.gameId`
First, check that you need the step ".key". Looking at your object, it looks to me that ".gameId" comes straight after the matchmaking[playerId], with no ".key" needed. If you are sure you need the ".key", then I would debug as follows. Immediately before that statement, insert the following console logs.
console.log("playerId:",context.params.playerId)
console.log("matchmaking[context.params.playerId]:",
matchmaking[context.params.playerId])
Then check that the playerId is something sensible. If it isn't, console.log the entire context with
console.log("context:",JSON.stringify(context)) // This avoids getting something useless logged like `[Object object]`.
If the playerId is correct, you will then see if matchmaking has an entry for that player. This approach to debugging will eventually expose where the problem lies.
I think the final problem is the confusion between 'matchmaking' being a DataSnapshot versus a straightforward, writeable Object
This was hard for me to understand because matchmaking is currently a DataSnapshot, which is a somewhat confusing type of thing. It seems to get console.logged as though it is an object, but it isn't writeable.
I suggest we extract an actual object, as follows:
database.ref("matchmaking").transaction(function (matchmaking) {
let matchmakingObject = matchmaking.val();
// ... remainder of code as before, and then:
matchmakingObject[context.params.playerId.gameId] = gameId; // assign game id to player
matchmakingObject[secondPlayer.key].gameId = gameId;
matchmakingObject[thirdPlayer.key].gameId = gameId;
return matchmakingObject;
I hope this fixes things!
My way to avoid such problems is to ALWAYS call snapshots xxxxSnapshot. That way I never forget that while it may console.log as though it was an object, in reality it is some sort of mystery thing.
For example, if I was writing this code, I would call things as follows:
database.ref("matchmaking")
.transaction(function (matchmakingSnapshot) {
let matchmakingObject = matchmakingSnapshot.val();
// ... remainder of code as before, and then:
matchmakingObject[context.params.playerId.gameId] = gameId; // assign game id to player
matchmakingObject[secondPlayer.key].gameId = gameId;
matchmakingObject[thirdPlayer.key].gameId = gameId;
return matchmakingObject;
It's a bit ugly but less painful than trying to debug a name that has two powerfully different meanings that are easily conflated.

Related

Getting "Function returned undefined, expected Promise or value " while using Firebase Functions

I've started using Firebase recently with my Unity game to create a simple turnbased game.
First time using Firebase functions as well, not to mention not that skilled in JS.
I'm using this code that handles a simple matchmaking system for newly added players to the matchmaking sub-database and match them with other players who are idle, then creates a random id for the game set it to them and then creates a new object in a "games" sub-database.
I've uploaded the core to Firebase and started testing it manually on the real-time database by adding "users" but it's not working and the log says :
Function returned undefined, expected Promise or value
I've looked online to find out what to do but I'm lost about "Promises"
I'd appreciate help in this matter.
Here's the JS code:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
const database = admin.database();
exports.matchmaker = functions.database.ref("matchmaking/{playerId}")
.onCreate((snap, context) => {
const gameId = generateGameId();
database.ref("matchmaking").once("value").then((players) => {
let secondPlayer = null;
players.forEach((player) => {
if (player.val() == "searching" &&
player.key !== context.params.playerId) {
secondPlayer = player;
}
});
if (secondPlayer === null) return null;
database.ref("matchmaking").transaction(function(matchmaking) {
if (matchmaking === null ||
matchmaking[context.params.playerId] !== "" ||
matchmaking[secondPlayer.key] !== "searching") {
return matchmaking;
}
matchmaking[context.params.playerId] = gameId;
matchmaking[secondPlayer.key] = gameId;
return matchmaking;
}).then((result) => {
if (result.snapshot.child(
context.params.playerId).val() !== gameId) {
return null;
}
const game = {
gameInfo: {
gameId: gameId,
playersIds: [context.params.playerId, secondPlayer.key],
},
turn: context.params.playerId,
};
database.ref("games/" + gameId).set(game).then((snapshot) => {
console.log("Game created successfully!");
return null;
}).catch((error) => {
console.log(error);
});
return null;
}).catch((error) => {
console.log(error);
});
return null;
}).catch((error) => {
console.log(error);
});
});
/**
* Generates random game id
* #return {int} Game id
*/
function generateGameId() {
const possibleChars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let gameId = "";
for (let j = 0; j < 20; j++) {
gameId +=
possibleChars.charAt(Math.floor(Math.random() * possibleChars.length));
}
return gameId;
}
UPDATE:
I was able to fix it by adding a return value at the end of the onCreate method.
return context.
I fixed it by adding "return context" at the of the onCreate method.

Wait for all Firebase data query requests before executing code

I am trying to fetch data from different collections in my cloud Firestore database in advance before I process them and apply them to batch, I created two async functions, one to capture the data and another to execute certain code only after all data is collected, I didn't want the code executing and creating errors before the data is fetched when i try to access the matchesObject after the async function to collect data is finished, it keeps saying "it cannot access a property matchStatus of undefined", i thought took care of that with async and await? could anyone shed some light as to why it is undefined one moment
axios.request(options).then(function(response) {
console.log('Total matches count :' + response.data.matches.length);
const data = response.data;
var matchesSnapshot;
var marketsSnapshot;
var tradesSnapshot;
var betsSnapshot;
matchesObject = {};
marketsObject = {};
tradesObject = {};
betsObject = {};
start();
async function checkDatabase() {
matchesSnapshot = await db.collection('matches').get();
matchesSnapshot.forEach(doc => {
matchesObject[doc.id] = doc.data();
console.log('matches object: ' + doc.id.toString())
});
marketsSnapshot = await db.collection('markets').get();
marketsSnapshot.forEach(doc2 => {
marketsObject[doc2.id] = doc2.data();
console.log('markets object: ' + doc2.id.toString())
});
tradesSnapshot = await db.collection('trades').get();
tradesSnapshot.forEach(doc3 => {
tradesObject[doc3.id] = doc3.data();
console.log('trades object: ' + doc3.id.toString())
});
betsSnapshot = await db.collection('bets').get();
betsSnapshot.forEach(doc4 => {
betsObject[doc4.id] = doc4.data();
console.log('bets object: ' + doc4.id.toString())
});
}
async function start() {
await checkDatabase();
// this is the part which is undefined, it keeps saying it cant access property matchStatus of undefined
console.log('here is matches object ' + matchesObject['302283']['matchStatus']);
if (Object.keys(matchesObject).length != 0) {
for (let bets of Object.keys(betsObject)) {
if (matchesObject[betsObject[bets]['tradeMatchId']]['matchStatus'] == 'IN_PLAY' && betsObject[bets]['matched'] == false) {
var sfRef = db.collection('users').doc(betsObject[bets]['user']);
batch11.set(sfRef, {
accountBalance: admin.firestore.FieldValue + parseFloat(betsObject[bets]['stake']),
}, {
merge: true
});
var sfRef = db.collection('bets').doc(bets);
batch12.set(sfRef, {
tradeCancelled: true,
}, {
merge: true
});
}
}
}
});
There are too many smaller issues in the current code to try to debug them one-by-one, so this refactor introduces various tests against your data. It currently won't make any changes to your database and is meant to be a replacement for your start() function.
One of the main differences against your current code is that it doesn't unnecessarily download 4 collections worth of documents (two of them aren't even used in the code you've included).
Steps
First, it will get all the bet documents that have matched == false. From these documents, it will check if they have any syntax errors and report them to the console. For each valid bet document, the ID of it's linked match document will be grabbed so we can then fetch all the match documents we actually need. Then we queue up the changes to the user's balance and the bet's document. Finally we report about any changes to be done and commit them (once you uncomment the line).
Code
Note: fetchDocumentById() is defined in this gist. Its a helper function to allow someCollectionRef.where(FieldPath.documentId(), 'in', arrayOfIds) to take more than 10 IDs at once.
async function applyBalanceChanges() {
const betsCollectionRef = db.collection('bets');
const matchesCollectionRef = db.collection('matches');
const usersCollectionRef = db.collection('users');
const betDataMap = {}; // Record<string, BetData>
await betsCollectionRef
.where('matched', '==', false)
.get()
.then((betsSnapshot) => {
betsSnapshot.forEach(betDoc => {
betDataMap[betDoc.id] = betDoc.data();
});
});
const matchDataMap = {}; // Record<string, MatchData | undefined>
// betIdList contains all IDs that will be processed
const betIdList = Object.keys(betDataMap).filter(betId => {
const betData = betDataMap[betId];
if (!betData) {
console.log(`WARN: Skipped Bet #${betId} because it was falsy (actual value: ${betData})`);
return false;
}
const matchId = betData.tradeMatchId;
if (!matchId) {
console.log(`WARN: Skipped Bet #${betId} because it had a falsy match ID (actual value: ${matchId})`);
return false;
}
if (!betData.user) {
console.log(`WARN: Skipped Bet #${betId} because it had a falsy user ID (actual value: ${userId})`);
return false;
}
const stakeAsNumber = Number(betData.stake); // not using parseFloat as it's too lax
if (isNaN(stakeAsNumber)) {
console.log(`WARN: Skipped Bet #${betId} because it had an invalid stake value (original NaN value: ${betData.stake})`);
return false;
}
matchDataMap[matchId] = undefined; // using undefined because its the result of `doc.data()` when the document doesn't exist
return true;
});
await fetchDocumentsById(
matchesCollectionRef,
Object.keys(matchIdMap),
(matchDoc) => matchDataMap[matchDoc.id] = matchDoc.data()
);
const batch = db.batch();
const queuedUpdates = 0;
betIdList.forEach(betId => {
const betData = betDataMap[betId];
const matchData = matchDataMap[betData.tradeMatchId];
if (matchData === undefined) {
console.log(`WARN: Skipped /bets/${betId}, because it's linked match doesn't exist!`);
continue;
}
if (matchData.matchStatus !== 'IN_PLAY') {
console.log(`INFO: Skipped /bets/${betId}, because it's linked match status is not "IN_PLAY" (actual value: ${matchData.matchStatus})`);
continue;
}
const betRef = betsCollectionRef.doc(betId);
const betUserRef = usersCollectionRef.doc(betData.user);
batch.update(betUserRef, { accountBalance: admin.firestore.FieldValue.increment(Number(betData.stake)) });
batch.update(betRef, { tradeCancelled: true });
queuedUpdates += 2; // for logging
});
console.log(`INFO: Batch currently has ${queuedUpdates} queued`);
// only uncomment when you are ready to make changes
// batch.commit();
}
Usage:
axios.request(options)
.then(function(response) {
const data = response.data;
console.log('INFO: Total matches count from API:' + data.matches.length);
return applyBalanceChanges();
}

how to fix this TypeError: Cannot read property 'name' of null

how to fix this error
music.on('voiceStateUpdate',(lama, baru) => {
var state = null;
let Role = baru.roles.find((r) => ["IRON", "BRONZE"].includes(r.name));
const kategorikanal = '700743802574602260'
const channelid = '700743824346972231'
if(!lama.voiceChannel && !baru.voiceChannel) return;
if(!lama.voiceChannel && baru.voiceChannel) {state = "join"}
else if(lama.voiceChannel && !baru.voiceChannel) {state = "leave"}
else if(lama.voiceChannel.id !== baru.voiceChannel.id) {state = "move"}
else if(lama.voiceChannel.id == baru.voiceChannel.id) return;
console.log(state);
//!baru.member.roles.has(allowedRole)
if(baru.voiceChannelID === channelid || !baru.voiceChannelID === Role || Role !== null && Role !== '') {
console.log(baru.displayName + ' gabisabgo hrus ada rank ranked ');
// const Role = baru.guild.roles.get("724997095236304987");
baru.guild
.createChannel(`${Role.name} | ${baru.user.username}`,"voice")
.then(tempChannel => {
tempChannel.overwritePermissions(baru.guild.defaultRole.id, {
CONNECT: false,
})
tempChannel.overwritePermissions(Role.id, {
CONNECT: true
})
tempChannel.setParent(kategorikanal);
baru.setVoiceChannel(tempChannel.id);
tempChannel.setUserLimit("5");
})
.catch(console.error)
}
if(lama.voiceChannelID || !lama.voiceChannelID === Role || Role !== null && Role !== '') {
console.log(lama.displayName + ' gabisabgo hrus ada rank ranked ');
const voicelama = lama.guild.channels.get(lama.voiceChannelID);
let Role = baru.roles.find((r) => ["IRON", "BRONZE"].includes(r.name));
if(voicelama.name.startsWith(`${Role.name} | ${baru.user.username}`)){
let sawadikap = `**${baru.user.username}'s**` + " **Team**"
var koko = new Discord.RichEmbed()
.setColor("#FF4654")
.setThumbnail(`${baru.user.avatarURL}`)
.addField('**Good Game Well Played**',`${sawadikap}`)
.setFooter("#Valorant Indonesia Community." , 'https://i.imgur.com/yPWqxxu.png')
voicelama.delete()
.then(function() {
music.channels.get('725080861392896101').send(koko)
})
.catch(console.error);
}
}
})
ERROR VIEW
.createChannel(${Role.name} | ${baru.user.username},"voice")
^ TypeError: Cannot read property 'name' of null
Have you stepped through the code in debug mode? I recommend setting breakpoints, creating watches, and checking the value of the variables as you step through. If you don't feel comfortable doing so, can you please put in the following, and tell me what the console logs? :
console.log(Role)
console.log(Role.name)
Although Role is not null, The value of Role.name is null, meaning that it has no value assigned to it. That issue occurs here:
let Role = baru.roles.find((r) => ["IRON", "BRONZE"].includes(r.name));
So I see two possibilities:
No roles contain those names.
I thought that find should only result one result, but I can't seem to find good documentation of that method. Is it possible that both roles are found and a collection is returned? This would mean that there would be a collection of multiple roles, meaning that Role would not contain the data members that a Role object type would. This means that you would have to index one of the roles before using the name.
//
//if there isn't a matching role, then terminate the method.
if (Role == null)
{
return;
}
//if there are multiple roles that match the criterion, just use the first one.
//The alternative is that we could make it into a loop that handles it for all of them.
else if (Role instanceof Array)
{
if (Role.length == 0)
{
return;
}
Role = Role[0]
}
Add the above lines before calling baru.guild.createChannel.

TypeError: Cannot read property of null

I get the error in the title even though I have tried all the solutions. About the value of null;
If my code is ;
Output: TypeError: Cannot read property of null
case '!facebook':
face.Profile(facebook, function(result) {
let like = result.profiles.like;
let comments = result.profiles.comments;
if(like === null || comments === null){
//code.
}
else {
//code.
}
});
break;
You have to verify that each child of your object exists before you reference the next level. In your case, that means testing the existence of result and then result.profiles before trying to use any of the object values in profiles. See the code below.
Without seeing the rest of your case statement or how you're setting the value of face, it's hard to tell if you're going to run into other issues.
case '!facebook':
face.Profile(facebook, function(result) {
// Set default values
let like = null;
let comments = null;
// Set values only if there's a result containing profiles
if (result && result.profiles) {
like = result.profiles.like;
comments = result.profiles.comments;
}
if (like === null || comments === null){
//code
} else {
//code
}
});
break;

Firebase cloud functions check db for non-existant data

I'm looking for how to check if a documents exists in my cloud functions
My functions belows works fine when just incrementing an existing value, but now I'm trying to add functionality where it checks to see if the previous value exists and if it doesn't set as 1.
I've tried a different methods but I get things like "snapshot.exists" or "TypeError: Cannot read property 'count' of undefined at docRef.get.then.snapshot
var getDoc = docRef.get()
.then(snapshot => {
if (typeof snapshot._fieldsProto.count !== undefined) {
console.log("haha3", snapshot._fieldsProto.count)
var count = Number(jsonParser(snapshot._fieldsProto.count, "integerValue"));
docRef.set({
count: count + 1
});
}
else {
docRef.set({
count: 1
});
}
});
below is the code for the exists() error
var getDoc = docRef.get()
.then(snapshot => {
if snapshot.exists() {
console.log("haha3", snapshot._fieldsProto.count)
var count = Number(jsonParser(snapshot._fieldsProto.count, "integerValue"));
docRef.set({
count: count + 1
});
}
else {
docRef.set({
count: 1
});
}
});
The error for this code is:
TypeError: snapshot.exists is not a function at docRef.get.then.snapshot
It seems like docRef either points to a collection or is a query. In that case your snapshot is of type QuerySnapshot.
To check if a query has any result, use QuerySnapshot.empty.
I kept getting errors saying either empty or exists were not functions (tried many iterations) so eventually I landed on just using an undefined check and it works perfectly.
var db = event.data.ref.firestore;
var docRef = db.collection(userID).doc("joined").collection("total").doc("count");
var getDoc = docRef.get()
.then(snapshot => {
console.log("augu1", snapshot)
if (snapshot._fieldsProto === undefined) {
console.log("digimon1")
docRef.set({
count: 1
});
}
else {
console.log("haha31", snapshot._fieldsProto.count)
var count = Number(jsonParser(snapshot._fieldsProto.count, "integerValue"));
docRef.set({
count: count + 1
});
}
});
It turns out the problem is much simpler than I imagined: DocumentSnapshot.exists is a read-only property, not a function. So the proper way to use it is:
if snapshot.exists()

Categories

Resources