Is there any way to get the data from node on "child_added" event using cloud-function of firebase? - firebase

I was using the query "OnUpdate" on each client to get the data from that node and calculate the children-count but it is too costly.
So I decided to use a cloud-function and create another node of children-count based on the node in which all the users exist but there is an issue, I'm unable to find any query like "OnChildAdded".
The available queries listed on firebase documentation are "OnUpdate", "OnDelete", "OnWrite" and "OnCreate" that are useless for this case because using "OnCreate" on child node cannot return me the children of parent node or "OnUpdate" on parent node will again become costly because all the users update their states frequently.
So what about "OnOperation"? Is there any use of it or is there any other way to reduce the cost of query and also create a children-count node?
Here is the structure of my database
{
currentGame: {
players: {
playerId: {...playerGameData},
//,
},
noOfPlayer: // this is what i wanted to create based on above players node children_count.
}
}

Here is the solution to the above problem in case anyone else need to solve a similar issue.
const PLAYER_REF = "currentGame/players/{playerId}";
const PLAYER_COUNT_NODE = "currentGame/noOfPlayers";
exports.incPlayersCount = functions.database.ref (PLAYER_REF).onCreate (async (snap) =>
{
const countRef = snap.ref.root.child (PLAYER_COUNT_NODE);
await countRef.transaction((current) => {
return (typeof current !== "number" || current < 0) ? 1 : current + 1;
});
return null;
});
exports.decPlayersCount = functions.database.ref (PLAYER_REF).onDelete (async (snap) =>
{
const countRef = snap.ref.root.child (PLAYER_COUNT_NODE);
await countRef.transaction((current) => {
return (typeof current !== "number" || current <= 0) ? 0 : current - 1;
});
return null;
});
btw - it is exactly similar to the sample code that #FrankvanPuffelen have shared in the above comments.

Related

Firebase cloud functions not catching with fast database updates

Hello I am trying to build a multiplayer game.
I have got a working queue:
.ref('Multiplayer/Queue/{queueCategory}/Players/{playerid}')
.onCreate((snapshot, context) => {
const root = snapshot.ref.root;
var category = context.params.queueCategory;
const gameDir = "Multiplayer/Active/" + category;
var gameID = snapshot.ref.root.child(gameDir).push().key;
root.child("Multiplayer/Queue/" + category + "/Players").once("value").then(players => {
var secondplayer: DataSnapshot | null = null;
functions.logger.log(players.numChildren());
players.forEach(player => {
if(player.val() === "placeholder" && player.key !== context.params.playerid) {
secondplayer = player;
}
});
functions.logger.log(secondplayer);
if(secondplayer === null) return null;
root.child("Multiplayer/Queue/" + category + "/Players").transaction(function (matchmaking) {
//check if player joined differnet game
if (matchmaking === null || matchmaking === undefined || secondplayer === null || matchmaking[context.params.playerid] !== "placeholder" || matchmaking[secondplayer?.key || 1] !== "placeholder") return matchmaking;
matchmaking[context.params.playerid] = gameDir + "/" + gameID + "/player1";
matchmaking[secondplayer.key || -1] = gameDir + "/" +gameID + "/player2";
return matchmaking;
}).then(result => {
var playerval: string = result.snapshot.child(context.params.playerid).val();
var pPath = playerval.split('/');
pPath.pop();
playerval = pPath.join('/');
functions.logger.log("playervalue: " + playerval);
functions.logger.log("gamedir: " + gameDir + gameID);
if(playerval !== gameDir + "/" + gameID) return;
var game = {
gamestate: "init",
category: category,
Players: {
"player1": "",
"player2": ""
}
}
root.child(gameDir + "/" + gameID).set(game).then(snap => {
return null;
}).catch(error => {
console.log(error);
})
}).catch(error => {
console.log(error);
})
return null;
}).catch(error => {
console.log(error);
})
});
This script pairs up players and changes the value of the player in queue to the new gameroom
dir
. Everything works, except when to many players join the queue at once (
breaks down at roughly 1player/sec). I suspect the problem is in this part of the code:
var secondplayer: DataSnapshot | null = null;
functions.logger.log(players.numChildren());
players.forEach(player => {
if(player.val() === "placeholder" && player.key !== context.params.playerid) {
secondplayer = player;
}
});
functions.logger.log(secondplayer);
if(secondplayer === null) return null;
If to many players join the second player will be overlapping with other instances of the functions and ultimately the will terminate after the second player value has been set.
How can I fix this?
Please help me
Cloud functions do not guarantee order of execution. There is a Firecast explaining parallel execution. You can take a look at transactions but your use case doesn't seem to be straightforward as incrementing or decrementing a value.
Cloud functions (any serverless functions) may not be the best choice for all the cases.
You will have to make sure the user is deleted from the node immediately once is player is matched. Using transactions you can match and remove 2 players immediately. So when a third user who was about to match as well will be added to queue instead of in the queue.
You may need queues running on your server (if the reads and writes are so high speed) or refactor the logic that matches your players. Cloud Compute may be a better choice for this. You may be able to use something like OpenMatch with that.
I remember stacking up all the users trying to match in an array and then running a cron job every 2 seconds to make pairs of them. Then I had used realtime database to emit changes to relevant users. Although this may not not be possible in cloud functions as each functions runs independently of each other.

Firestore query "onSnapshot" called at the same time does not work (

I created an app with Ionic and Firestore that features live chat and I'm having a problem with it.
The conversation is loaded with the method:
refUneConversationMyUserCol.ref.orderBy('date', 'desc').limit(20).get()
To this is added an "onSnapshot" request to retrieve the last message sent live
this.unsubscribeDataUneConversation = refUneConversationMyUserCol.ref.orderBy('date', 'desc').limit(1).onSnapshot(result => {
console.log(result.docs[0].data());
if (this.isCalledBySnapshot === false) {
this.isCalledBySnapshot = true;
} else if (result.docs[0].data().expediteur !== this.authentificationService.uidUserActif) {
const data = result.docs[0].data();
const id = result.docs[0].id;
this.dataUneConversation.push({ id, ...data } as UneConversation);
}
});
It will work perfectly however, when I send a message at the same time (with 2 different accounts talking to each other), I encounter a problem, the onSnapshot is triggered only once and I only receive one message.
I specify that the two messages are sent well in the database, they are only not displayed both during the live session
Do you have any idea why?
Thank you
(Here is the whole method)
async getDataUneConversation(idI: string) {
if (this.loadedDataUneConversation !== idI) {
/* ANCHOR Msg en direct */
this.isCalledBySnapshot = false;
if (this.unsubscribeDataUneConversation) {
await this.unsubscribeDataUneConversation();
}
const refUneConversationMyUserCol = this.afs.collection<User>('users').doc<User>(this.authentificationService.uidUserActif).collection<Conversations>('conversations');
const result = await refUneConversationMyUserCol.ref.orderBy('date', 'desc').limit(20).get();
/* ANCHOR Msg en direct */
this.unsubscribeDataUneConversation = refUneConversationMyUserCol.ref.orderBy('date', 'desc').limit(1).onSnapshot(result => {
console.log(result.docs[0].data());
if (this.isCalledBySnapshot === false) {
this.isCalledBySnapshot = true;
} else if (result.docs[0].data().expediteur !== this.authentificationService.uidUserActif) {
const data = result.docs[0].data();
const id = result.docs[0].id;
this.dataUneConversation.push({ id, ...data } as UneConversation);
}
});
/* ANCHOR Msg en brut */
if (result.docs.length < 20) {
this.infiniteLastUneConversationMax = true;
} else {
this.infiniteLastUneConversationMax = false;
}
this.infiniteLastUneConversation = result.docs[result.docs.length - 1];
this.dataUneConversation = result.docs.map(doc => {
const data = doc.data();
const id = doc.id;
return { id, ...data } as UneConversation;
});
this.dataUneConversation.reverse();
this.loadedDataUneConversation = idI;
}
}
EDIT for working :
this.unsubscribeDataUneConversation = refUneConversationMyUserCol.ref.orderBy('date', 'asc').startAfter(this.dataUneConversation[this.dataUneConversation.length
- 1].date).onSnapshot(result => {
result.docs.forEach(element => {
const data = element.data();
const id = element.id;
if (!this.dataUneConversation.some(e => e.id === element.id)) {
this.dataUneConversation.push({ id, ...data } as UneConversation);
}
});
});
You're limiting live messages to only one last message. In a chat app, you want to listen to all new messages. So the issue is probably in your .limit(1) clause.
But if you do that, I understand that you'll get the whole conversation, with all messages, since the conversation started.
My approach would be like this:
Get the date of the last message from your refUneConversationMyUserCol... conversation loader.
When you do the onSnapshot() to get the last message, do not limit to 1 message, instead, start at a date after the date of the last loaded message.
Since you're ordering by date anyway, this will be an easy fix. Look into "Adding a cursor to your query".
Basically, you'll be saying to Firestore: give me LIVE new messages but start at NOW - and even if there are many messages posted at the same time, you'll get them all, since you're not limiting to 1.
Feel free to ask if this is not clear enough.

Firebase Realtime DB: Order query results by number of values for a key

I have a Firebase web Realtime DB with users, each of whom has a jobs attribute whose value is an object:
{
userid1:
jobs:
guid1: {},
guid2: {},
userid2:
jobs:
guid1: {},
guid2: {},
}
I want to query to get the n users with the most jobs. Is there an orderby trick I can use to order the users by the number of values the given user has in their jobs attribute?
I specifically don't want to store an integer count of the number of jobs each user has because I need to update users' jobs attribute as a part of atomic updates that update other user attributes concurrently and atomically, and I don't believe transactions (like incrementing/decrementing counters) can be a part of those atomic transactions.
Here's an example of the kind of atomic update I'm doing. Note I don't have the user that I'm modifying in memory when I run the following update:
firebase.database().ref('/').update({
[`/users/${user.guid}/pizza`]: true,
[`/users/${user.guid}/jobs/${job.guid}/scheduled`]: true,
})
Any suggestions on patterns that would work with this data would be hugely appreciated!
Realtime Database transactions run on a single node in the JSON tree, so it would be quite difficult to integrate the update of a jobCounter node within your atomic update to several nodes (i.e. to /users/${user.guid}/pizza and /users/${user.guid}/jobs/${job.guid}/scheduled). We would need to update at /users/${user.guid} level and calculate the counter value, etc...
An easier approach is to use a Cloud Function to update a user's jobCounter node each time there is a change to one of the jobs nodes that implies a change in the counter. In other words, if a new job node is added or removed, the counter is updated. If an existing node is only modified, the counter is not updated, since there were no change in the number of jobs.
exports.updateJobsCounter = functions.database.ref('/users/{userId}/jobs')
.onWrite((change, context) => {
if (!change.after.exists()) {
//This is the case when no more jobs exist for this user
const userJobsCounterRef = change.before.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return 0;
});
} else {
if (!change.before.val()) {
//This is the case when the first job is created
const userJobsCounterRef = change.before.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return 1;
});
} else {
const valObjBefore = change.before.val();
const valObjAfter = change.after.val();
const nbrJobsBefore = Object.keys(valObjBefore).length;
const nbrJobsAfter = Object.keys(valObjAfter).length;
if (nbrJobsBefore !== nbrJobsAfter) {
//We update the jobsCounter node
const userJobsCounterRef = change.after.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return nbrJobsAfter;
});
} else {
//No need to update the jobsCounter node
return null;
}
}
}
});

How can I keep track of the last updated at time for a document in Firestore?

I want to have an updatedAt field in my pizza document that should be updated every time there's an update happening in this particular document. I think the best place to handle this is in an onUpdate trigger:
exports.onUpdatePizza = functions.firestore
.document('pizzas/{pizzaId}')
.onUpdate(async (change, context) => {
return change.after.ref.update({ updatedAt: new Date() });
});
However, the above code will fall into an infinite loop. How can I implement this without the undesired side effect?
There is a good explanation there https://medium.com/#krngd2/prevent-infinity-loop-in-firebase-cloud-functions-ea8083afbd35
Add this inside the function:
// simply input data
const after: any = change.after.exists ? change.after.data() : null;
const before: any = change.before.exists ? change.before.data() : null;
const canUpdate = () => {
// if Update Trigger
if (before.updatedAt && after.updatedAt) {
if (after.updatedAt._seconds !== before.updatedAt._seconds) {
return false;
}
}
// if Create Trigger <--- assuming you use it
if (!before.createdAt && after.createdAt) {
return false;
}
return true;
}
if (canUpdate()) {
// update code here
}
Here is my code for universal updates: https://stackoverflow.com/a/60963531/271450
In your function, check to see if the update date is within some threshhold of the current time before updating it again. This will defend against unwanted writes, but the downside is that the update time may lag by that threshold, if there are very frequent updates.

Meteor subscription is not stopping

I've got what should be a relatively simple issue. I set a session, then a subscribe to a collection using the string stored in the session. But when that session changes, I need to clear the subscription data and start again.
My code is as follows:
let subscriptionReady;
let filteredResults = [];
let rawResults = [];
let county = Session.get('county');
let type = Session.get('type');
This is mostly just prep work to create some empty objects to populate later. This all gets set on a click event. After we set these placeholder objects we go and subscribe by those sessions:
if (county && !type) {
return function() {
if (subscriptionReady) {
subscriptionReady.stop();
}
filteredResults = [];
rawResults = [];
subscriptionReady = Meteor.subscribe('resourcesearch', county, {
onReady: () => {
rawResults = resourceCollection.find({}, { sort: {score: -1} }).fetch();
rawResults.forEach((result) => {
if (result.score) {
filteredResults.push(result);
}
});
}
});
}
At the third line I run a check to see if subscriptionReady exists, then it will have the stop method available. So then I run it. But, it doesn't actually stop anything.
What am I missing?
After trial and error, I've got it solved. The issue was the placement of the stop call. I no longer have to check if subscriptionReady exists, instead I stop the subscription inside of the onReady method:
return function() {
filteredResults = [];
rawResults = [];
subscriptionReady = Meteor.subscribe('resourcesearch', county, {
onReady: () => {
rawResults = resourceCollection.find({}, { sort: {score: -1} }).fetch();
rawResults.forEach((result) => {
if (result.score) {
filteredResults.push(result);
}
});
subscriptionReady.stop();
}
});
It's .stop() not .stop docs
Also you can probably avoid your filtering loop by including score in your query. Are you looking for documents where the score key exists {score: {$exists: true}} or just where it is non zero {$score: {$ne: 0}}?
Also you shouldn't need to clear the subscription and start again. If you make your subscription parameter resourcesearch a reactive data source then the subscription will automatically update to give you the documents you need. Starting/stopping a subscription in response to a search would be an anti-pattern.

Resources