Firestore getCountFromServer not real time updating? - javascript

I am using the getCountFromServer() for my React App to display the number of docs in a collection.
However, when the user deletes a doc on the front end or creates one, the count is not updated in real-time.
Is it possible to fix this or must the user always reload the page to see the latest data?

The documentation says, emphasis mine:
count() aggregation queries are currently only supported via direct server response. Queries are served only by the Cloud Firestore backend, skipping the local cache and any buffered updates. This behavior is identical to operations performed inside Cloud Firestore transactions. You cannot currently use count() queries with real-time listeners and offline queries.
So, no, you can't get realtime count updates. You will have to make the request again to get an update. Whether or not you require a page refresh is up to you.

As #DougStevenson mentioned in his comment, there is no way you can get real-time updates when you're using count(). It's true you can call count() every time it is needed, but there is however a workaround.
To solve this, you should consider storing a counter in a document at a known path, and then increment its value every time a new document is added or decrement the value every time a document is deleted. To get real-time updates, simply attach a persistent listener on that document and you'll be up to date with the number of documents in the collection.
Besides that, if your collection grows in size (> 10,000,000 documents), then the above solution might be the only solution you have. For that, please check #FrankvanPuffelen's answer from the following post:
How fast is counting documents in Cloud Firestore?

If you are using getCountFromServer() to load the initial count and then want to decrement it for all active users when a document is deleted, you can use a listener on the same collection and update the state on frontend like this:
import { collection, query, where, onSnapshot } from "firebase/firestore";
// initial state loaded by getCountFromServer()
const [count, setCount] = useState(10);
const q = query(collection(db, "collectionName"));
const unsubscribe = onSnapshot(q, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "removed") {
console.log("Removed document: ", change.doc.data());
// TODO: Decrement the count in state
// e.g. setCount(count - 1)
}
});
});
This way you won't have to calculate the count again or maintain the count anywhere.

Related

Best way to batch create if not exists in firestore

I am working with a project where we create a bunch of entries in firestore based on results from an API endpoint we do not control, using a firestore cloud function. The API endpoint returns ids which we use for the document ids, but it does not include any timestamp information. Since we want to include a createdDate in our documents, we are using admin.firestore.Timestamp.now() to set the timestamp of the document.
On subsequent runs of the function, some of the documents will already exist so if we use batch.commit with create, it will fail since some of the documents exist. However, if we use batch.commit with update, we will either not be able to include a timestamp, or the current timestamp will be overwritten. As a final requirement, we do update these documents from a web application and set some properties like a state, so we can't limit the permissions on the documents to disallow update completely.
What would be the best way to achieve this?
I am currently using .create and have removed the batch, but I feel like this is less performant, and I occasionally do get the error Error: 4 DEADLINE_EXCEEDED on the firestore function.
First prize would be a batch that can create or update the documents, but does not edit the createdDate field. I'm also hoping to avoid reading the documents first to save a read, but I'd be happy to add it in if it's the best solution.
Thanks!
Current code is something like this:
const createDocPromise = docRef
.create(newDoc)
.then(() => {
// success, do nothing
})
.catch(err => {
if (
err.details &&
err.details.includes('Document already exists')
) {
// doc already exists, ignore error
} else {
console.error(`Error creating doc`, err);
}
});
This might not be possible with batched writes as set() will overwrite the existing document, update() will update the timestamp and create() will throw an error as you've mentioned. One workaround would be to use create() for each document with Promise.allSettled() that won't run catch() if any of the promise fails.
const results = [] // results from the API
const promises = results.map((r) => db.doc(`col/${r.id}`).create(r));
const newDocs = await Promise.allSettled(promises)
// either "fulfilled" or "rejected"
newDocs.forEach((result) => console.log(result.status))
If any documents exists already, create() will throw an error and status for that should be rejected. This way you won't have to read the document at first place.
Alternatively, you could store all the IDs in a single document or RTDB and filter out duplicates (this should only cost 1 read per invocation) and then add the data.
Since you prefer to keep the batch and you want to avoid reading the documents, a possible solution would be to store the timestamps in a field of type Array. So, you don't overwrite the createdDate field but save all the values corresponding to the different writes.
This way, when you read one of the documents you sort this array and take the oldest value: it is the very first timestamp that was saved and corresponds to the document creation.
This way you don't need any extra writes or extra reads.

Eliminating documents from Firebase Realtime database (Not the entire collection)

I have deployed an app with React and I am using Firebase Realtime database to store some info about attention tickets in a call center. The database will store aprox 80 tickets info per day, but this is cumulative. I want to avoid this so I will not reach the firebase storage limit.
My idea so far is to delete every day at noon the tickets from the database, so It will only store the data from the current date and eliminate it at noon.
I am using remove() function from firebase, but when I tried referencing to the collection, It was entired deleted, I just want to delete the documents but not the entire collection.
Is there a way to specify Firebase to delete docs only, maybe to delete every docs except one?
This is the bunch of code that I pretend to use for deleting (Based on JS)
function deletedata(){
const dbRef = query(ref(db,'/mycollectionpath'));
onValue(dbRef, (snapshot)=>{
snapshot.forEach(childSnapshot=>{
let keyName = childSnapshot.key;
remove(dbRef);
})
});
}
setInterval(deletedata,1000)
The Firebase Realtime Database automatically creates parent nodes when you write a value under them, and it automatically deletes parent nodes when there are no longer any values under them. There is no way to have a node without a value.
If you want to keep the node, consider writing a dummy value. For example, this would remove all child nodes from mycollectionpath and replace them with a single true value:
const dbRef = ref(db, '/mycollectionpath');
dbRef.set(true);

Firestore - Skip document update if it doesn't exists, without need of failures

I have a collection
/userFeed
Where I create/delete docs (representing users) when the current user starts following/unfollowing them.
...
/userFeed (C)
/some-followed-user (D)
-date <timestamp>
-interactions <number>
When the user likes a post, the interactions field will be updated. But... what if the user doesn't follow the post owner? Then, I will just need to skip the document update, without necessity of producing failures/errors.
const currentUserFeedRef = firestore
.collection("feeds")
.doc(currentUserId)
.collection("userFeed")
.doc(otherUserId);
const data = {
totalInteractions: admin.firestore.FieldValue.increment(value),
};
const precondition = {
exists: false, // I am trying weird things
};
if (batchOrTransaction) {
return batchOrTransaction.update(
currentUserFeedRef,
data,
precondition
);
}
Is it possible to just "skip the update if the doc doesn't exist"?
Is it possible to just "skip the update if the doc doesn't exist"?
No, not in the way that you're explaining it. Firestore updates don't silently fail.
If you need to know if a document exists before updating it, you should simply read it first and check that it exists. You can do this very easily in a transaction, and you can be sure that the update won't fail due to the document being missing if you check it this way first using the transaction object.
In fact, what you are trying to do is illustrated as the very first example in the documentation.

Does Firestore on web charge two reads for two listeners?

Using Firestore Web API v9, suppose I have a collection listener and a document listener for a document in that collection. An update for that document comes through.
Two questions:
Am I charged for two reads?
Is this an acceptable practice in general? Does the SDK send the data back twice, or send it once and dispatch an update twice?
Example:
collectionSnapshots(collection(firestore, 'people')).subscribe(...);
docSnapshots(doc(firestore, 'people/john')).subscribe(...);
Am I charged for two reads?
You are charged for number of documents being read irrespective of number of listeners you have. If 'people' collection has 50 documents and the listener returns 50 docs, you are charged 50 reads. Any updates received later will be charged 1 read per update.
Does the SDK send the data back twice, or send it once and dispatch an update twice?
When the listener is added, it'll return all documents that matched your query (in this case, the complete people collection). After that, whenever a document is added/updated/deleted the update will be received by listener.
Yes, if you have a listener on people collection and another listener on a document of that collection. Both the listeners will fetch updated data and you'll be charged 2 reads for that single update. You don't need another listener just for a single document though. You can read document ID from changes received and update it in your web app.
import { collection, query, where, onSnapshot } from "firebase/firestore";
const q = query(collection(db, "people"));
const unsubscribe = onSnapshot(q, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "modified") {
// a document in people collection is modified
const docId = change.doc.id;
const docData = change.doc.data();
}
});
});

Listen to Firebase Firestore data changes for current data only?

Let's say I have a Firebase firestore database table called "Articles".
I pull in a list of articles whenever I open the app, and through a FlatList component, whenever I reach the end, I pull in more articles using startAt() and endAt()
I also have the ability to like or comment on articles, so whenever I do so, the current articles in the screen get updated (like counter, comment counter gets updated).
However, I also have a separate backend that adds additional articles to the database after 6 hours interval.
If I subscribe to changes in the Articles database, the startAt() and endAt() will get messed up if new data is added when the user is scrolling through the Flatlist (right?). Instead, I only want to notify the user whenever new articles are added, and display a "New Posts Available" button at the top whenever there are any, and if user clicks on it, it just refreshes the list.
However, I still want to listen to changes in likes and comments of the article, without adding any new articles to the flatlist when the backend adds them.
How do I go about solving this?
You can check for the type of event i.e. if the document was created, modified or deleted as mentioned in documentation. In case a new document is added, the type will be "created". Then just don't append them to your Flatlist. Any updated to likes count will be a "modified" event so you can just update that in the list. Here's an example copied from the docs:
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());
}
});
});
Important: 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 the first time the user opens you app and at the same time the new documents were being added, they may end up in the list. If you still want to be strict with the time these docs were added, you can try adding a condition which says documents returned in the query should be created before a particular timestamp.
.where("addedAt", "<=", <the-timestamp>)

Categories

Resources