Atomic update using transaction in firebase android - javascript

I want to create a simple match making application using android and firebase.
The working is as follows
UserA comes online and a node is created on firebase.
UserB comes online and 2nd node is created on firebase. and so on.
UserA and UserB objects are
Now I want to update UserA with userB fields and userB with UserA fields.
Ensuring that no other user should interrupt in between this update. like
I tried using transaction and found that only single database reference can be changed using transaction. Is there any way by which I can update both user at the same time using transaction and ensuring that both references are either updated or failed at same time.(no in between. like one updated and other is not)
A cloud function can also be used but not know how to use multi path update inside a transaction for real time database.
EDIT-1
I have moved to firebase fire store and so far tried below code with firebase functions. Not sure it will server the purpose when user base increases.
function matchedUser(xSnapshot, mSnapshot) {
return db.runTransaction(t => {
console.log(mSnapshot.mUserId)
console.log(xSnapshot.mUserId)
console.log("Other Snap ID" + otherSnapshot.mUserId)
console.log("My Snap ID" + mySnapshot.mUserId)
const xUserRef = db.collection("Users").doc(otherSnapshot.mUserId)
return t.get(xUserRef).then(doc => {
t.update(xUserRef, otherSnapshot)
const mUserRef = db.collection("Users").doc(mySnapshot.mUserId)
t.update(mUserRef, mySnapshot)
}).catch(error => {
console.log(error)
})
})
.then(result => {
console.info('Transaction success!')
return "Success"
})
.catch(err => {
console.error('Transaction failure:', err)
return "Failed"
})
}

Related

Firestore transactions showing unexpected behaviour when used in cloud functions

I am writing an app that features an inventory in which users can reserve products. I want to ensure that 2 users cannot simultaneously reserve a product at the same time, for this, I intend on using transactions. When using transactions from the Firebase SDK, everything works as intended, but I am getting unexpected behavior when using transactions from a callable cloud function. To simulate the use case where 2 users happen to reserve the same product, I use setTimeout in my cloud function to halt the function for 3 seconds. I am launching this function from 2 different clients with different user contexts.
export const reserveProduct = functions.https.onCall(async (data,context) => {
function testTimeout(){
return new Promise((resolve,reject) => {
setTimeout(()=> {
return resolve(true)
},3000)
})
}
if(!context.auth){
return {
error: `You must be logged in to reserve products`
}
}else{
const productRef = admin.firestore().collection('products').doc(data.productID)
const userRef = admin.firestore().collection('users').doc(context.auth.uid)
return admin.firestore().runTransaction((transaction) => {
return transaction.get(productRef).then(async(doc) => {
if(doc.get('status') == 'reserved'){
throw "Document already reserved!"
}else{
console.log("Product not reserved, reserving now!")
}
await testTimeout()
transaction.update(productRef, {status: 'reserved'});
transaction.update(userRef, {reserved: admin.firestore.FieldValue.arrayUnion(data.productID)})
})
}).then(() => {
console.log("Transaction Successfully committed !")
}).catch((error) => {
throw "Transaction failed, product already reserved"
})
}
After running this function call from 2 different clients simultaneously, The function call from my first client returns successfully as expected, but only after roughly 35s (which is way too long for the simplicity of the transaction). However, the second function call times out without returning any value. I have not seen any documentation explicitly stating the use of transactions in callable cloud functions, nor should it be affected when used within the emulator.
I am expecting to simply get a return value for whichever function call is able to modify the data first, and catch the error from the function which has retried and validated the reserved state.
Any help would be appreciated, thanks!
One major difference between the two places is in the way the SDKs used handle transactions:
The client-side SDKs use an optimistic compare-and-set approach for transactions, meaning that they pass the values you read in the transaction with the data you're writing. The server then only writes the new data if the documents you read haven't been updated.
The server-side SDKs (used in your Cloud Function) use a more traditional pessimistic approach for transactions, and place a lock on each document that you read in the transaction.
You can read more about database contention in the SDKs in the documentation.
While I'm not exactly certain how this is affecting your code, I suspect it is relevant to the difference in behavior you're seeing between the client-side and server-side implementations.

Firebase Realtime Database - Determine if user has access to path

I have updated my Firebase Realtime Database access rules, and have noticed some clients now tries to access paths they do not have access to. This is ok - but my problem is that my code stops after being unable to read a restricted node.
I see below error in my console, and then loading of subsequent data stops:
permission_denied at /notes/no-access-node
I begin by collecting access nodes from /access_notes/uid and continue to read all data from /notes/noteId.
My code for collecting notes from the database below:
//*** SUBSCRIPTION */
database.ref(`access_notes/${uid}`).on('value', (myNotAccessSnaps) => {
let subscrPromises = []
let collectedNots = {}
// Collect all categories we have access to
myNotAccessSnaps.forEach((accessSnap) => {
const noteId = accessSnap.key
subscrPromises.push(
database.ref(`notes/${noteId}`)
.once('value', (notSnap)=>{
const notData = notSnap.val()
const note = { id: notSnap.key, ...notData}
collectedNotes[note.id] = note
},
(error) => {
console.warn('Note does not exist or no access', error)
})
)
})
Promise.all(subscrPromises)
.then(() => {
const notesArray = Object.values(collectedNotes)
...
})
.catch((error) => { console.error(error); return Promise.resolve(true) })
I do not want the client to halt on permission_denied!
Is there a way to see if the user has access to a node /notes/no_access_note without raising an error?
Kind regards /K
I do not want the client to halt on permission_denied!
You're using Promise.all, which MDN documents as:
Promise.all() will reject immediately upon any of the input promises rejecting.
You may want to look at Promise.allSettled(), which MDN documents as:
[Promise.allSettled()] is typically used when you have multiple asynchronous tasks that are not dependent on one another to complete successfully, or you'd always like to know the result of each promise.
Is there a way to see if the user has access to a node /notes/no_access_note without raising an error?
As far as I know the SDK always logs data access permissions errors and this cannot be suppressed.
Trying to access data that the user doesn't have access to is considered a programming error in Firebase. In normal operation you code should ensure that it never encounters such an error.
This means that your data access should follow the happy path of accessing data it knows it has access to. So you store the list of the notes the user has access to, and then from that list access each individual note.
So in your situation I'd recommend finding out why you're trying to read a note the user doesn't have access to, instead of trying to hide the message from the console.

How to (using React JS web) and Firestore, can you find out when a chatRoom (on the Firestore Database) receives new messages?

I am trying to build an app using FireStore and React JS (Web)
My Firestore database basically has:
A collection of ChatRooms ChatRooms
Every chat-room has many messages which is a subcollection, for example:
this.db.collection("ChatRooms").doc(phone-number-here).collection("messages")
Also, every chat-room has some client info like first-name, last-name etc, and one that's very important:
lastVisited which is a timestamp (or firestamp whatever)
I figured I would put a React Hook that updates every second the lastVisited field, which means to try to record as accurately as possible on Firestore the last time I left a chat-room.
Based on that, I want to retrieve all the messages for every customer (chat-room) that came in after the last visit,
=> lastVisited field. :)
And show a notification.
I have tried from .onSnapshot listener on the messages subcollection, and a combination of Firestore Transactions but I haven't been lucky. My app is buggy and it keeps showing two, then one, then nothing, back to two, etc, and I am suffering much.
Here's my code!
Please I appreciate ANY help!!!
unread_messages = currentUser => {
const chatRoomsQuery = this.db.collection("ChatRooms");
// const messagesQuery = this.db.collection("ChatRooms");
return chatRoomsQuery.get().then(snapshot => {
return snapshot.forEach(chatRoom => {
const mess = chatRoomsQuery
.doc(chatRoom.id)
.collection("messages")
.where("from", "==", chatRoom.id)
.orderBy("firestamp", "desc")
.limit(5);
// the limit of the messages could change to 10 on production
return mess.onSnapshot(snapshot => {
console.log("snapshot SIZE: ", snapshot.size);
return snapshot.forEach(message => {
// console.log(message.data());
const chatRef = this.db
.collection("ChatRooms")
.doc(message.data().from);
// run transaction
return this.db
.runTransaction(transaction => {
return transaction.get(chatRef).then(doc => {
// console.log("currentUser: ", currentUser);
// console.log("doc: ", doc.data());
if (!doc.exists) return;
if (
currentUser !== null &&
message.data().from === currentUser.phone
) {
// the update it
transaction.update(chatRef, {
unread_messages: []
});
}
// else
else if (
new Date(message.data().timestamp).getTime() >
new Date(doc.data().lastVisited).getTime()
) {
console.log("THIS IS/ARE THE ONES:", message.data());
// newMessages.push(message.data().customer_response);
// the update it
transaction.update(chatRef, {
unread_messages: Array.from(
new Set([
...doc.data().unread_messages,
message.data().customer_response
])
)
});
}
});
})
.then(function() {
console.log("Transaction successfully committed!");
})
.catch(function(error) {
console.log("Transaction failed: ", error);
});
});
});
});
});
};
Searching about it, it seems that the best option for you to achieve that comparison, would be to convert your timestamps in milliseconds, using the method toMillis(). This way, you should be able to compare the results better and easier - more information on the method can be found in the official documentation here - of the timestamps of last message and last access.
I believe this would be your best option as it's mentioned in this Community post here, that this would be the only solution for comparing timestamps on Firestore - there is a method called isEqual(), but it doesn't make sense for your use case.
I would recommend you to give it a try using this to compare the timestamps for your application. Besides that, there is another question from the Community - accessible here: How to compare firebase timestamps? - where the user has a similar use cases and purpose as yours, that I believe might help you with some ideas and thoughts as well.
Let me know if the information helped you!

How to query database with firebase cloud functions

I am trying to query my firestore database using cloud functions.
I want to trigger an email notification every time a new reading in my database is under the value of 10.
Here is the relevant database structure for reference: database structure.
The "readings" field is an array and each "reading" is a map which holds the fields "date" and "value".
Currently I am at the point where I can send an email notification every time a new user is created however I want this to work for the database. I am unsure how to query for the "readings" array and then for each individual reading.
Here is my code so far which sends an email when a new user is created
exports.sendNotification = functions.auth.user().onCreate((user) => {
const mailOptions = {
from: '"Spammy Corp." <noreply#firebase.com>',
to:"fakeEmail#btopenworld.com",
text: "TEST"
};
return mailTransport.sendMail(mailOptions)
.then(() => console.log("It worked"))
.catch((error) =>
console.error('There was an error while sending the email:', error));
});
See: https://firebase.google.com/docs/firestore/extend-with-functions
For example, to fire on all new readings added to that first child:
exports.sendEmail = functions.firestore
.document('sensor/UGt.../readings')
.onCreate((snap, context) => {
const newValue = snap.data();
const value = newValue.value;
if (value < 10) {
// send email
}
});
In further comments you mentioned listening for new readings in all sensor elements, not just your first one. This is unfortunately not possible in an efficient / simple way (source). Instead you will have to listen to all onUpdate events on /sensor/, check if the update is adding a reading, then check the value & send your email.
It may be easier to call the cloud function directly from wherever adds the reading, depending on how many times the /sensor/ path is going to be updated for other reasons (since every time this happens, it's a waste of resources).

What is the correct way to load data from thousands of users without slowing down our vue.js and firebase app?

We recently launched an application for a company that provides staff for music festivals using vue.js and a firebase realtime database. We have about 2,000 users and currently 8 events that they can apply for. Each user has profiles and admins can manage their events and hires from the app. The app fires up right away, but the data in "getInitialUserState" takes a long time to populate. I'm worried that once we have 5K or 10K users, load time will only get worse.
In our App.vue file:
created () {
this.$store.dispatch("getInitialState")
this.$store.dispatch("getInitialEventsState")
}
Then in Users.vue:
created () {
this.$store.dispatch("getInitialUserState")
}
In our store:
getInitialState({ commit }) {
firebase.auth().onAuthStateChanged(function(user) {
console.log(user);
const db = firebase.database();
db.ref('users/').child(user.uid).on('value', (snap)=>{
let u = snap.val();
u.id = user.uid
commit('setUser', u);
})
})
},
getInitialUserState({commit}, payload){
const db = firebase.database()
db.ref("users").once("value", (snap) => {
let data = []
for(var key in snap.val()){
let user = snap.val()[key]
user.id = key
data.push(user)
}
commit('setUsers', data)
commit('setUsersMap', snap.val());
})
},
and:
getInitialEventsState({ commit }) {
let db = firebase.database();
db.ref("/events").on("value", function (snapshot) {
let data = []
for (var key in snapshot.val()) {
let event = snapshot.val()[key];
if(event.title){
event.id = key
data.push(event)
}
}
commit("setLoadedEvents", data)
commit("setEventsMap", snapshot.val());
})
}
We've tried adding v-cloak to different elements so don't render without all of the correct data, but that either doesn't work or we are doing it wrong. I've also tried loading data at different lifecycle hooks, but that doesn't make any difference either.
What I want to happen is for my data (as a logged-in user) and for event data to load immediately. Then, EITHER for the "Users" data to only load if the user in a admin (so it won't slow down the experience for regular users), or for Users data to load asynchronously and without interfering with the rest of the app. Currently, while the Users data is loading, the page is effectively frozen and you can't click to go to any other pages.
Firebase should be fast. We are using firebase hosting as well. I'm hoping you guys can at least point us in the right direction.
What is the correct way to load data from thousands of users without slowing down the user experience?
Thanks!

Categories

Resources