So I have this website, that acts like a stat sheet and when I press the -1 button, the score of the game goes down by 1. I'm using javascript to access the firebase database and then subtract the score by 1.
const docRef = doc(db, "Games", game_id);
const docSnap = await getDoc(docRef);
var score = docSnap.data().team2_score
await updateDoc(docRef, {
team2_score: score -= number
});
The issue is that if a user clicks it really fast multiple times, then Firebase doesn't do all those operations. So if a user clicks the button 5 times really fast, the Firebase database would only keep track of 4 of them. This is my issue.
Is there a way to make sure that every single one of those clicks updated in the database, even when clicked really fast?
You have two options:
Option 1
Use Firebase Increment: https://cloud.google.com/firestore/docs/samples/firestore-data-set-numeric-increment
const docRef = doc(db, "Games", game_id);
await updateDoc(docRef, {
team2_score: firebase.firestore.FieldValue.increment(-1)
});
Option 2
Use a Transaction. https://firebase.google.com/docs/firestore/manage-data/transactions#transactions
const docRef = doc(db, "Games", game_id);
try {
const result = await db.runTransaction((transaction) => {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(docRef).then((sfDoc) => {
if (!sfDoc.exists) {
throw "Document does not exist!";
}
// Note: this could be done without a transaction
// by updating the score using FieldValue.increment()
var newScore= sfDoc.data().team2_score - 1;
transaction.update(docRef, { team2_score: newScore});
});
})
console.log("Transaction successfully committed!");
} catch(error) {
console.log("Transaction failed: ", error);
}
Related
I am writing a cloud function that will move expired events from a collection to another. It is not working as expected and I am very novice at Javascript. Please save me.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const db = admin.firestore();
exports.expireEvents = functions.region("us-west2").pubsub.schedule('* * * * *').onRun(async (context) => {
await db.collection("Events").where('endDate', '<=', admin.firestore.Timestamp.now().millisecondsSinceEpoch).get().then((snapshot) => {
snapshot.forEach(async (doc) => {
// For all expired events, we will perform operations on some of their fields
const event = doc.data();
// Write document to collection of "expired events"
await db.collection("ArchivedEvents").doc(event.eid).set(event);
// For all the guests in that expired event, do stuff; guests should be a list of strings.
event.guests.forEach(async (uid) => {
// Create a new write batch
const batch = db.batch();
// Get user and update some attributes
const guest = await db.collection("Users").doc(uid);
// Add all operations to be performed for given user to this batch
batch.update(guest, {eventsAttended: admin.firestore.FieldValue.arrayUnion(event.eid)});
batch.update(guest, {eventsAttending: admin.firestore.FieldValue.arrayRemove(event.eid)});
// Execute batch of operations
await batch.commit();
});
// Delete doc from "not expired" collection
await db.collection("Events").doc(event.eid).delete();
});
console.log(`Successfully expired events ending on ${admin.firestore.Timestamp.now()}.`);
return true;
})
.catch((err) => {
console.error(`Could not get or update documents. Error ${err}.`);
return false;
});
});
This is the next part of the error. I tried with a collection with no documents and a few documents, but I am starting to think that because none of them have expired yet, that's why I am getting this error?
Rest of error log
If you want to ignore undefined values, enable `ignoreUndefinedProperties`.
at Object.validateUserInput (/workspace/node_modules/#google-cloud/firestore/build/src/serializer.js:277:19)
at validateQueryValue (/workspace/node_modules/#google-cloud/firestore/build/src/reference.js:2230:18)
at CollectionReference.where (/workspace/node_modules/#google-cloud/firestore/build/src/reference.js:1061:9)
at /workspace/index.js:139:33
at cloudFunction (/workspace/node_modules/firebase-functions/lib/cloud-functions.js:131:23)
at /layers/google.nodejs.functions-framework/functions-framework/node_modules/#google-cloud/functions-framework/build/src/function_wrappers.js:144:25
at processTicksAndRejections (node:internal/process/task_queues:96:5)
You've at least two problems:
I think you want a JavaScript Date object (not Firebase Timestamp object) in your query, i.e. where('endDate', '<=', new Date())
Firebase Timestamp doesn't have a millisecondsSinceEpoch property which is -- I think -- causing the "undefined" error that you're encountering.
I have developed a game using Firestore, but I have noticed some problems in my scheduled cloud function which deletes rooms that were created 5 minutes ago and are not full OR are finished.
So for that, I am running the following code.
async function deleteExpiredRooms() {
// Delete all rooms that are expired and not full
deleteExpiredSingleRooms();
// Also, delete all rooms that are finished
deleteFinishedRooms();
}
Deleting finished rooms seems to work correctly with this:
async function deleteFinishedRooms() {
const query = firestore
.collection("gameRooms")
.where("finished", "==", true);
const querySnapshot = await query.get();
console.log(`Deleting ${querySnapshot.size} expired rooms`);
// Delete the matched documents
querySnapshot.forEach((doc) => {
doc.ref.delete();
});
}
But I am experiencing concurrency problems when deleting rooms created 5 minutes ago that are not full (one room is full when 2 users are in the room, so that the game can start).
async function deleteExpiredSingleRooms() {
const currentDate = new Date();
// Calculate the target date
const targetDate = // ... 5 minutes ago
const query = firestore
.collection("gameRooms")
.where("full", "==", false)
.where("createdAt", "<=", targetDate);
const querySnapshot = await query.get();
console.log(`Deleting ${querySnapshot.size} expired rooms`);
// Delete the matched documents
querySnapshot.forEach((doc) => {
doc.ref.delete();
});
}
Because during the deletion of a room, a user can enter it before it is completely deleted.
Any ideas?
Note: For searching rooms I am using a transaction
firestore.runTransaction(async (transaction) => {
...
const query = firestore
.collection("gameRooms")
.where("full", "==", false);
return transaction.get(query.limit(1));
});
You can use BatchWrites:
const query = firestore
.collection("gameRooms")
.where("full", "==", false)
.where("createdAt", "<=", targetDate);
const querySnapshot = await query.get();
console.log(`Deleting ${querySnapshot.size} expired rooms`);
const batch = db.batch();
querySnapshot.forEach((doc) => {
batch.delete(doc.ref);
});
// Commit the batch
batch.commit().then(() => {
// ...
});
A batched write can contain up to 500 operations. Each operation in
the batch counts separately towards your Cloud Firestore usage.
This should delete all the rooms matching that criteria at once. Using a loop to delete them might take a while as it'll happen one by one.
If you are concerned about the 500 docs limit in a batch write, consider using Promise.all as shown:
const deleteOps = []
querySnapshot.forEach((doc) => {
deleteOps.push(doc.ref.delete());
});
await Promise.all(deleteOps)
Now to prevent users from joining the rooms that are being delete, it's kind of harder in a Cloud Function to do so as all the instances run independently and there may be a race condition.
To avoid that, you many have to manually check if the room that user is trying to join is older than 5 minutes and has less number of players. This is just a check to make sure the room is being deleted or will be deleted in no time.
function joinRoom() {
// isOlderThanMin()
// hasLessNumOfPlayers()
// return 'Room suspended'
}
Because the logic to filter which rooms should be deleted is same, this should not be an issue.
Maybe you are looking for transactions check out the documentation out here: https://firebase.google.com/docs/firestore/manage-data/transactions
Or watch the YouTube video that explains the concurrency problem and the differences between batched writes and transactions: https://youtu.be/dOVSr0OsAoU
I am using Firebase in my React Native app. I have a collection of users and in that, I have documents and the ids of the documents are custom (using users' phone numbers as ids) and in that, I have blocked users array field in which I will add the users who are blocked by the user. I want to show the list where user can only see the users who are not blocked.
I am getting all the users list and I want to filter them and fetch only the people not blocked by the user.
var getUsersList = async() => {
const findUser = await firestore().collection('users').get();
if (findUser.docs[0] != undefined && findUser.docs[0]._exists){
setUserList(findUser.docs)
}
}
I understand that your firestore collection is similar to this one:
If that is the case, then I have structured your requirement in the three functions below:
1.readBlockedNumbers to return the array of blocked numbers that the user has.
2.show_nonBlockedUsersthat receives the array of the blocked numbers from the previous method and displays the users who are not in this array.
3. test to coordinate the execution of the above two methods.
const admin = require('firebase-admin');
const serviceAccount = require('/home/keys.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
const db = admin.firestore();
async function readBlockedNumbers(docId){
const userRef = db.collection('users').doc(docId);
const doc = await userRef.get();
if (!doc.exists) {
console.log('No such document!');
return [];
}
else
{
return doc.data()['blocked_numbers'];
}
async function show_nonBlockedUsers(blocked_users){
console.log('Array length:', blocked_users.length);
if(blocked_users.length == 0)
return;
const userRef = db.collection('users');
const snapshot = await userRef
.where(admin.firestore.FieldPath.documentId(), 'not-in', blocked_users)
.get();
if (snapshot.empty) {
console.log('No matching documents.');
return;
}
snapshot.forEach(doc => {
console.log(doc.id, '=>', doc.data());
});
}
async function test(){
const docId = '202-555-0146';
//'202-555-0102'
const blocked_users = await readBlockedNumbers(docId);
await show_nonBlockedUsers(blocked_users);
}
Important here is how to use the not-in operator and the method admin.firestore.FieldPath.documentId().
I have found the not-in operator here and the method firebase.firestore.FieldPath.documentId() referenced in this other stackoverflow question since the id cannot be passed like the other document fields in the where clause.
Please refer to the firebase documentation as well for the limitations of the not-in operator.
I hope you find this useful.
I am running a series of stripe charges in promise structure. When I run just a single execution of a charge everything works fine. But when I try to run more than one the stripe charge line of code simply won't execute.
Does anyone have insight into my code structure or google cloud functions that would prevent the execution of several stripe calls from occurring?
const idempotency_key = randomstring.generate(); // prevent duplicate charges
const amount = total + 30;
const currency = "usd";
console.log("Blue Scarfs", idempotency_key);
console.log("Blue Scarfs", user_ID);
return docRef2.doc(`/users_stripe/${user_ID}`).get()
.then(snapshot => {
console.log("the dollars", snapshot);
console.log("the dollars", user_ID);
const customer = snapshot.data().id;
const charge = {amount, currency, customer};
console.log("Bills", charge);
return stripe.charges.create(charge , { idempotency_key });
console.log("Bills");
})
This code is being executed in a firebase cloud function to make a stripe charge after being triggered by a document being added to firestore. This is just a snippet where the problem lies. The code is supposed to charge a card, and upon success, update the 'dexCoinBal' value in firestore with a newBalance. I approached this by getting the previous value and then moving along with the update. However, the update doesn't seem to run at all. It's probably something to do with chaining the promises but I can't figure out what it is exactly. If someone could point out what I'm doing wrong that'd be great. I clearly don't understand promises well enough.
function charge (tok, amt, curr, uid) {
const token = tok;
const amount = amt;
const currency = curr;
const fStor = admin.firestore();
stripe.charges.create({
amount,
currency,
description: 'Example charge',
source: token
}).then(function(result){
return fStor.collection('users').get();
}).then(function(doc){
if(doc.exists){
const prev = doc.data().dexCoinBal;
var numDexCoins = amt / (0.25 * 100);
var newBal = numDexCoins + prev;
//Update the number of dexcoins in database
fStor.collection('users').doc(uid).update({
dexCoinBal: newBal
});
}
});
}
Full code below. Fixed the issue.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const fStor = admin.firestore();
const settings = {timestampsInSnapshots: true};
fStor.settings = settings;
// TODO: Remember to set token using >> firebase functions:config:set stripe.token="SECRET_STRIPE_TOKEN_HERE"
const stripe = require('stripe')(functions.config().stripe.token);
function charge (tok, amt, curr, uid) {
const token = tok;
const amount = amt;
const currency = curr;
console.log("Outside");
return stripe.charges.create({
amount,
currency,
description: 'Example charge',
source: token
}).then(function(result){
console.log("First Then");
return fStor.collection('users').doc(uid).get();
}).then(function(doc){
console.log("Second Then");
if(doc.exists){
const prev = doc.data().dexCoinBal;
var numDexCoins = amt / (0.25 * 100);
var newBal = numDexCoins + prev;
//Update the number of dexcoins in database
fStor.collection('users').doc(uid).update({
dexCoinBal: newBal
});
}
});
}
exports.createCharge = functions.firestore
.document('charges/{chargeId}')
.onCreate((snap, context) => {
// Get an object representing the document
// e.g. {'name': 'Marie', 'age': 66}
const data = snap.data();
// access a particular field as you would any JS property
const token = data.token;
const amount = data.amount;
const currency = data.currency;
const uid = data.uid;
// perform desired operations ...
return charge(token, amount, currency, uid);
});
I see two problems in your code:
1/ By doing return fStor.collection('users').get(); you are returning a QuerySnapshot and not a DocumentSnapshot. See the doc of the get() method here.
The doc says that "A QuerySnapshot contains zero or more DocumentSnapshot objects representing the results of a query. The documents can be accessed as an array via the docs property or enumerated using the forEach method. "
Therefore when, in then next then(), you do doc.data() you most probably get an error.
2/ The second problem, IMHO, is that you don't return anything in your charge function. Since, in a Cloud Function, you shall return a promise (or a value in specific cases) you should probably return a promise from your function. You should probably do as follows, but it is difficult to be 100% sure as you didn't publish the entire code of your Cloud Function.
return stripe.charges.create({
....
}).then(function(result){
....
}).then(function(collection){
....
});
If you add the entire code we may be able to help you more precisely.
I would also suggest that you look at these two must see videos from the Firebase team, about Cloud Functions and promises: https://www.youtube.com/watch?v=7IkUgCLr5oA and https://www.youtube.com/watch?v=652XeeKNHSk.