Latency compensation and Firestore transactions - google-cloud-firestore

I have an onSnapshot keeping track of the documents in a collection:
db.collection('/.../').onSnapshot(querySnapshot=> mylocalvariable = querySnapshot.docs)
Now, I want to select the first (in some order) element of this collection of documents that my user has not yet handled. When a user is done handling a document, I use a transaction to update the document according to the user's needs (transaction is better for me than .update() because I might have multiple users changing different parts of the document).
The problem is that unlike a .update (which would update mylocalvariable immediately), it seems like the transaction finishes without updating mylocalvariable. So, when I go to grab the "next" document, it just grabs the same document, because the function runs before the variable gets updated.
Code sample:
db.collection('/mycollection').onSnapshot(querySnapshot=> mylocalvariable = querySnapshot.docs)
function selectnextrecord(){
nextrecord = mylocalvariable.find(x=>!x.data().done)
console.log(nextrecord)
//expected: Get something different than the current record
//observed: This is being run with old data, so it returns the same record that I currently have with the old data.
}
let nextrecord;
selectnextrecord();
function submitchanges(){
let sfDocRef = db.collection('/mycollection').doc(nextrecord.id);
return db.runTransaction(function(transaction) {
return transaction.get(sfDocRef).then(function(sfDoc) {
if (!sfDoc.exists) {
throw "Document does not exist!";
}
transaction.update(sfDocRef, {done:true});
});
}).then(function() {
selectnextrecord();
}).catch(function(error) {
console.log("Transaction failed: ", error);
});
}```

After going through the documentation, I think this is expected behavior.
Do not modify application state inside of your transaction functions. Doing so will introduce concurrency issues, because transaction functions can run multiple times and are not guaranteed to run on the UI thread. Instead, pass information you need out of your transaction functions
In any case, you could filter the documents that are not done with .where() and then place your transaction inside a foreach:
db.collection('cities')
.where("done", "==", true)
.get()
.then(snapshot => {
snapshot.forEach(doc => {
return db.runTransaction(function(transaction) {
return transaction.get(sfDocRef).then(function(sfDoc) {
if (!sfDoc.exists) {
throw "Document does not exist!";
}
transaction.update(sfDocRef, {done:true});
});
}).catch(function(error) {
console.log("Transaction failed: ", error);
});
})
})

Related

Dart/Flutter stream to a field/data in firebase [duplicate]

How can I listen to a specific field change with firestore js sdk ?
In the documentation, they only seem to show how to listen for the whole document, if any of the "SF" field changes, it will trigger the callback.
db.collection("cities").doc("SF")
.onSnapshot(function(doc) {
console.log("Current data: ", doc && doc.data());
});
You can't. All operations in Firestore are on an entire document.
This is also true for Cloud Functions Firestore triggers (you can only receive an entire document that's changed in some way).
If you need to narrow the scope of some data to retrieve from a document, place that in a document within a subcollection, and query for that document individually.
As Doug mentioned above, the entire document will be received in your function. However, I have created a filter function, which I named field, just to ignore document changes when those happened in fields that I am not interested in.
You can copy and use the function field linked above in your code. Example:
export const yourCloudFunction = functions.firestore
.document('/your-path')
.onUpdate(
field('foo', 'REMOVED', (change, context) => {
console.log('Will get here only if foo was removed');
}),
);
Important: The field function is not avoiding your function to be executed if changes happened in other fields, it will just ignore when the change is not what you want. If your document is too big, you should probably consider Doug's suggestion.
Listen for the document, then set a conditional on the field you're interesting in:
firebase.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(word).collection('Pronunciations').doc('Castilian-female-IBM').onSnapshot(function(snapshot) {
if (snapshot.data().audioFiles) { // eliminates an error message
if (snapshot.data().audioFiles.length === 2) {
audioFilesReady++;
if (audioFilesReady === 3) {
$scope.showNextWord();
}
}
}
}, function(error) {
console.error(error);
});
I'm listening for a document for a voice (Castilian-female-IBM), which contains an array of audio files, in webm and mp3 formats. When both of those audio files have come back asynchronously then snapshot.data().audioFiles.length === 2. This increments a conditional. When two more voices come back (Castilian-male-IBM and Latin_American-female-IBM) then audioFilesReady === 3 and the next function $scope.showNextWord() fires.
Just out of the box what I do is watching before and after with the before and after method
const clientDataBefore = change.before.data();
console.log("Info database before ", clientDataBefore);
const clientDataAfter = change.after.data();
console.log("Info database after ", clientDataAfter );
For example now you should compare the changes for a specific field and do some actions or just return it.
Some more about before.data() and after.data() here

Can a firestore snapshot contain x number of changed documents

I want to ask about Firestore
Reading the docs
When a document is added, removed or modified then I get a signal about that event here in this code from the doc:
db.collection("cities").where("state", "==", "CA")
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New city: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified city: ", change.doc.data());
}
if (change.type === "removed") {
console.log("Removed city: ", change.doc.data());
}
});
});
When I start this listener will I then get all document matching "state", "==", "CA" even if it's 100.000 of them? Will they come all at once or in batches?
after the above initial all (100.000) will I after that always get one(1) document, like when a doc is modified, added or removed, or can there be like a batch collapsing latency from firestore so I will get 1-to-many in the snapshot?
When you first run the query, you'll get everything that matches that query, with a change.type === "added". Then you will receive changes as they are made, one by one (unless someone writes a batch at once).
The way to manage this is to add a filter to the collection. For example, you may want to sort the collection by a date field or a name field. Then limit the results to a manageable number and paginate.
db.collection("cities")
.where("state", ">=", "CA")
.orderBy("state")
.limit(50)
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New city: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified city: ", change.doc.data());
}
if (change.type === "removed") {
console.log("Removed city: ", change.doc.data());
}
});
});
Don't forget to add an unsubscribe so that you can remove the listener
You get only the documents that were added/modified/removed however the documentation clearly states the first time when the listener is set up it will return all the matching documents documents.
The first query snapshot contains added events for all existing
documents that match the query. This is because you're getting a set
of changes that bring your query snapshot current with the initial
state of the query. This allows you, for instance, to directly
populate your UI from the changes you receive in the first query
snapshot, without needing to add special logic for handling the
initial state.
So they do come all along. After that they'll be only the changes i.e. the amount of documents being changed.
The initial state can come from the server directly, or from a local
cache. If there is state available in a local cache, the query
snapshot will be initially populated with the cached data, then
updated with the server's data when the client has caught up with the
server's state.
Now with that being said you may be charged for it if the data is coming from the server and not from the local cache.

How to transfer data of one collection into another of mongodb

Am working on nestjs.I have two collections one in orders and second is payment and i want to retrieve one single entry from orders collections and save that same entry into payment collection.
Here is the code of services:
async order(name){
const list=this.usersmodel.find({name:name}).exec()
//return list
try{
if(list){
const x=this.usersmodel.aggregate([
{$out:"payment"}
])
return "data saved in payment collection"
}
}
catch(error){
return(error.message)
}
}
Here is the code of controller:
#Get('orderdata')
async orderdata(#Body('name')name){
return this.usersService.order(name)
}
By using these code lines neither i got desired output nor i got any error. I got "data saved in payment collection" when am hitting API in postman but i didn't get the entries in my payment collection.
I think the issue is here, in this line
const list = this.usersmodel.find({ name: name }).exec()
this is an asynchronous block of code, so the next lines will be executed without waiting for this list to be resolved
you have to use await keyword to enforce javascript to wait until this line is executed before executing the next lines
const list = await this.usersmodel.find({ name: name }).exec()
also, the aggregate pipeline is taking the whole order documents to the payment collection, as there is no filtering applied to that orders in the aggregate pipeline
so you have to add $match stage to your aggregate pipeline, in order to add the list of orders that have the name you specified
also note that we need await in the aggregate too as this is an asynchronous block of code, so wait until this aggregation to be done then execute the return statement
so the whole function should look something like
async order(name) {
const list = await this.usersmodel.find({ name: name }).exec()
//return list
try {
if(list){
await this.usersmodel.aggregate([ // note the await here
{ $match: { name: name } }, // filtering the orders
{ $out: "payment" } // move them to the payment collection
])
return "data saved in payment collection"
}
}
catch (error) {
return(error.message)
}
}
hope it helps

Meteor: Increment DB value server side when client views page

I'm trying to do something seemingly simple, update a views counter in MongoDB every time the value is fetched.
For example I've tried it with this method.
Meteor.methods({
'messages.get'(messageId) {
check(messageId, String);
if (Meteor.isServer) {
var message = Messages.findOne(
{_id: messageId}
);
var views = message.views;
// Increment views value
Messages.update(
messageId,
{ $set: { views: views++ }}
);
}
return Messages.findOne(
{_id: messageId}
);
},
});
But I can't get it to work the way I intend. For example the if(Meteor.isServer) code is useless because it's not actually executed on the server.
Also the value doesn't seem to be available after findOne is called, so it's likely async but findOne has no callback feature.
I don't want clients to control this part, which is why I'm trying to do it server side, but it needs to execute everytime the client fetches the value. Which sounds hard since the client has subscribed to the data already.
Edit: This is the updated method after reading the answers here.
'messages.get'(messageId) {
check(messageId, String);
Messages.update(
messageId,
{ $inc: { views: 1 }}
);
return Messages.findOne(
{_id: messageId}
);
},
For example the if(Meteor.isServer) code is useless because it's not
actually executed on the server.
Meteor methods are always executed on the server. You can call them from the client (with callback) but the execution happens server side.
Also the value doesn't seem to be available after findOne is called,
so it's likely async but findOne has no callback feature.
You don't need to call it twice. See the code below:
Meteor.methods({
'messages.get'(messageId) {
check(messageId, String);
var message = Messages.findOne({_id:messageId});
if (message) {
// Increment views value on current doc
message.views++;
// Update by current doc
Messages.update(messageId,{ $set: { views: message.views }});
}
// return current doc or null if not found
return message;
},
});
You can call that by your client like:
Meteor.call('messages.get', 'myMessageId01234', function(err, res) {
if (err || !res) {
// handle err, if res is empty, there is no message found
}
console.log(res); // your message
});
Two additions here:
You may split messages and views into separate collections for sake of scalability and encapsulation of data. If your publication method does not restrict to public fields, then the client, who asks for messages also receives the view count. This may work for now but may violate on a larger scale some (future upcoming) access rules.
views++ means:
Use the current value of views, i.e. build the modifier with the current (unmodified) value.
Increment the value of views, which is no longer useful in your case because you do not use that variable for anything else.
Avoid these increment operator if you are not clear how they exactly work.
Why not just using a mongo $inc operator that could avoid having to retrieve the previous value?

subsub document id returns whole document

When I findOne based on deliverables.steps._id it's returning my entire document rather than just my step with the particular id. Can I not have it return my individual step rather than the whole document? The reason I ask is so when I need to update I just update this step rather than updating the whole document each time.
exports.findStep = function(req, res) {
Project.findOne({'deliverables.steps._id': req.params.stepId}).sort('-created').exec(function(err, step) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(step);
}
});
};