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");
})
Related
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);
}
Transactions and batched writes can be used to write multiple documents by means of an atomic operation.
Documentation says that Using the Cloud Firestore client libraries, you can group multiple operations into a single transaction.
I cannot understand what is the meaning of client libraries here and if it's correct to use Transactions and batched writes within a Cloud Function.
Example given: suppose in the database I have 3 elements (which doc IDs are A, B, C). Now I need to insert 3 more elements (which doc IDs are C, D, E). The Cloud Function should add just the latest ones and send a Push Notification to the user telling him that 2 new documents are available.
The doc ID could be the same but since I need to calculate how many documents are new (the ones that will be inserted) I need a way to read the doc ID first and check for its existence. Hence, I'm wondering if Transactions fit Cloud Functions or not.
Also, each transaction or batch of writes can write to a maximum of 500 documents. Is there any other way to overcome this limit within a Cloud Function?
Firestore Transaction behaviour is different between the Clients SDKs (JS SDK, iOS SDK, Android SDK , ...) and the Admin SDK (a set of server libraries), which is the SDK we use in a Cloud Function. More explanations on the differences here in the documentation.
Because of the type of data contention used in the Admin SDK you can, with the getAll() method, retrieve multiple documents from Firestore and hold a pessimistic lock on all returned documents.
So this is exactly the method you need to call in your transaction: you use getAll() for fetching documents C, D & E and you detect that only C is existing so you know that you need to only add D and E.
Concretely, it could be something along the following lines:
const db = admin.firestore();
exports.lorenzoFunction = functions
.region('europe-west1')
.firestore
.document('tempo/{docId}') //Just a way to trigger the test Cloud Function!!
.onCreate(async (snap, context) => {
const c = db.doc('coltest/C');
const d = db.doc('coltest/D');
const e = db.doc('coltest/E');
const docRefsArray = [c, d, e]
return db.runTransaction(transaction => {
return transaction.getAll(...docRefsArray).then(snapsArray => {
let counter = 0;
snapsArray.forEach(snap => {
if (!snap.exists) {
counter++;
transaction.set(snap.ref, { foo: "bar" });
} else {
console.log(snap.id + " exists")
}
});
console.log(counter);
return;
});
});
});
To test it: Create one of the C, D or E doc in the coltest collection, then create a doc in the tempo collection (Just a simple way to trigger this test Cloud Function): the CF is triggered. Then look at the coltest collection: the two missing docs were created; and look a the CF log: counter = 2.
Also, each transaction or batch of writes can write to a maximum of
500 documents. Is there any other way to overcome this limit within a
Cloud Function?
AFAIK the answer is no.
There used to also be a one second delay required as well between 500 record chunks. I wrote this a couple of years ago. The script below reads the CSV file line by line, creating and setting a new batch object for each line. A counter creates a new batch write per 500 objects and finally asynch/await is used to rate limit the writes to 1 per second. Last, we notify the user of the write progress with console logging. I had published an article on this here >> https://hightekk.com/articles/firebase-admin-sdk-bulk-import
NOTE: In my case I am reading a huge flat text file (a manufacturers part number catalog) for import. You can use this as a working template though and modify to suit your data source. Also, you may need to increase the memory allocated to node for this to run:
node --max_old_space_size=8000 app.js
The script looks like:
var admin = require("firebase-admin");
var serviceAccount = require("./your-firebase-project-service-account-key.json");
var fs = require('fs');
var csvFile = "./my-huge-file.csv"
var parse = require('csv-parse');
require('should');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://your-project.firebaseio.com"
});
var firestore = admin.firestore();
var thisRef;
var obj = {};
var counter = 0;
var commitCounter = 0;
var batches = [];
batches[commitCounter] = firestore.batch();
fs.createReadStream(csvFile).pipe(
parse({delimiter: '|',relax_column_count:true,quote: ''})
).on('data', function(csvrow) {
if(counter <= 498){
if(csvrow[1]){
obj.family = csvrow[1];
}
if(csvrow[2]){
obj.series = csvrow[2];
}
if(csvrow[3]){
obj.sku = csvrow[3];
}
if(csvrow[4]){
obj.description = csvrow[4];
}
if(csvrow[6]){
obj.price = csvrow[6];
}
thisRef = firestore.collection("your-collection-name").doc();
batches[commitCounter].set(thisRef, obj);
counter = counter + 1;
} else {
counter = 0;
commitCounter = commitCounter + 1;
batches[commitCounter] = firestore.batch();
}
}).on('end',function() {
writeToDb(batches);
});
function oneSecond() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 1010);
});
}
async function writeToDb(arr) {
console.log("beginning write");
for (var i = 0; i < arr.length; i++) {
await oneSecond();
arr[i].commit().then(function () {
console.log("wrote batch " + i);
});
}
console.log("done.");
}
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'm on a simple application:
It's my first try with firebase functions + realtime database.
The functions are going to be called by an external client application (e.g.: android).
if it was not javascript + nosqldb I would not have a problem, but here I am stuck, because I'm not sure for the best db structure and about transaction-like operations.
I. stored data:
user profile (id, fullname, email, phone, photo)
tickets amount per user
history for tickets buyings
history for tickets usings
II. actions:
user buy some tickets - should add tickets to the user's amount AND add a record to buyings history
user use some tickets - should remove tickets from the user's amount AND add a record to usings history
So my base problem is this ANDs - if it was a SQL db i would use a transaction but here I'm not sure what is db structure and js code in order to achieve same result.
EDITED:
======== index.js =======
exports.addTickets = functions.https.onCall((data, context) => {
// data comes from client app
const buyingRecord = data;
console.log(‘buyingRecord: ‘ + JSON.stringify(buyingRecord));
return tickets.updateTicketsAmmount(buyingRecord)
.then((result)=>{
tickets.addTicketsBuyingRecord(buyingRecord);
result.userid = buyingRecord.userid;
result.ticketsCount = buyingRecord.ticketsCount;
return result;
});
});
====== tickets.js =======
exports.updateTicketsAmmount = function(buyingRecord) {
var userRef = db.ref(‘users/’ + buyingRecord.userid);
var amountRef = db.ref(‘users/’ + buyingRecord.userid + ‘/ticketsAmount’);
return amountRef.transaction((current)=>{
return (current || 0) + buyingRecord.ticketsCount;
})
.then(()=>{
console.log(“amount updated for userid [“ + buyingRecord.userid + “]”);
return userRef.once(‘value’);
})
.then((snapshot)=>{
var data = snapshot.val();
console.log(“data for userid [“ + snapshot.key + “]:” + JSON.stringify(data));
return data;
});
}
exports.addTicketsBuyingRecord = function(buyingRecord) {
var historyRef = db.ref(‘ticketsBuyingHistory’);
var newRecordRef = historyRef.push();
return newRecordRef.set(buyingRecord)
.then(()=>{
console.log(‘history record added.’);
return newRecordRef.once(‘value’);
})
.then((snapshot)=>{
var data = snapshot.val();
console.log(‘data:’ + JSON.stringify(data));
return data;
});
}
You would have to use the callback, the request on Android to add or read data have an onSuccess or OnFailure CallBack, i used this to trigger my new request.
You can check this on the doc here :)
Also if instead of using RealTime Database you use Firestore you can use the FireStore Transactions here is the info.
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.