How can I effectively manage the amount of observers/listeners on a document in Firebase? - swift

Let's say I have a collection of documents in the Cloud Firestore. I want people to observe these documents, but I don't want more than 100 people observing the same document at any point in time. How do I effectively track this?
I was initially thinking to track the amount of observers/listeners by managing an array of user IDs in each document. Effectively, users would add their ID to this array before observing/listening (or be turned away if this array was too large) and remove their IDs from this array when they stop. The problem with this is that I can't just stop the user from leaving if the call to delete this ID from the array fails. What if they terminate the app in some way and the call to remove their ID doesn't go through?
Is there a more reasonable solution to this problem? Could this be solved with the Realtime Database or Cloud Functions? Thanks!

I'm not sure, but try to google the Firebase access rules. I think that you can manage the access in this scope.
I talk about this rules.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
Sorry for answer in that style, i do not have the needed reputation to create comments

SOLVED! Big thanks to Renaud Tarnec for his original comment referencing documentation I had not yet seen. I was missing the keyword 'presence' in my Google searches. Please upvote his comment if you find this answer helpful!
SOLUTION: Using https://firebase.google.com/docs/firestore/solutions/presence
SWIFT:
var connectionReference: DatabaseReference = Database.database().reference(withPath: ".info/connected")
var personalReference: DatabaseReference?
var connectionIssued: Bool = false
func connect(room: String) {
// Remove any existing observations to ensure only one exists.
connectionReference.removeAllObservers()
// Start observing connection.
connectionReference.observe(.value) { (isConnected) in
if isConnected.value as! Bool {
// Connected!
// Use Bool connectionIssued to ensure this is run only once.
if !self.connectionIssued {
self.connectionIssued = true
self.personalReference = Database.database().reference(withPath: "OnlineUsers/\(userID)")
// Set onDisconnect before setting value.
self.personalReference!.onDisconnectRemoveValue()
self.personalReference!.setValue(room)
// Now the user is "online" and can proceed.
// Allow user to "enter" room.
} else {
// Connection already issued.
}
} else {
// The user has either disconnected from an active connection or they were already disconnected before connect() was called.
// Stop observing connection.
self.connectionReference.removeAllObservers()
// If the user disconnects after they have entered a room, kick them out.
if self.connectionIssued {
// User lost connection.
kickUserOutOfRoom()
self.connectionIssued = false
} else {
// User cannot log in.
}
}
}
}
// Call when users leave a room when still connected.
func leaveRoomManually() {
// Remove connection observation.
connectionReference.removeAllObservers()
// Attempt to remove "online" marker in database.
personalReference?.removeValue(completionBlock: { (error, reference) in
if error != nil {
// Removal failed, but that should be okay!
// onDisconnect will still be called later!
// This failure might result in ghost users if the user proceeds to join another room before disconnecting.
// Consider writing an onUpdate() cloud function in conjunction with the following onCreate() and onDelete() cloud functions to take care of that case.
} else {
// "Online" marker removed from database!
// We can now cancel the onDisconnect()
self.personalReference?.cancelDisconnectOperations()
}
})
leaveRoom()
}
CLOUD FUNCTIONS (javascript):
The following cloud functions update the respective room's guest count in Cloud Firestore.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const firestore = admin.firestore();
exports.userCameOnline = functions.database.ref('/OnlineUsers/{userID}').onCreate(
async (snapshot, context) => {
const room = snapshot.val();
const guestCountRef = firestore.doc(`Rooms/${room}`);
return guestCountRef.update({
Guests: admin.firestore.FieldValue.increment(1)
});
});
exports.userWentOffline = functions.database.ref('/OnlineUsers/{userID}').onDelete(
async (snapshot, context) => {
const room = snapshot.val();
const guestCountRef = firestore.doc(`Rooms/${room}`);
return guestCountRef.update({
Guests: admin.firestore.FieldValue.increment(-1)
});
});

Related

creating a sub user that inherit premium status from an admin?

I have this cloud function that I pass the id of an admin (sharedId)and id of sub user (subId), then make a quarry to get the admin's premiumUntill value of the doc, and copy it to the sub user. so if admin premium goes away.. a sub-user will become obsolete as well, the issue here is I am trying to find some safe guard the problem is anyone who can get an active premium admin id can use this function to make him self premium as well.... any idea how to go about this I don't want to use the method where each time a user logs in it checks the premium in the admin doc its very recourses consuming ?
my current solution is , I have the sub user id created already in the admin and stored in an array of sub users which is an immutable doc,
what I do is check if the incoming id in data.subid will be equal to snapshopt.subuss.id in that doc will this make sure that no body can mess with the data ? this will make the id a constant to verify the incoming data against. but I still it might have an issue.
export const createSubUser = functions.https.onCall(async (data, context) => {
const snapshot = await db.collection('AdminData').doc(data.sharedId).get();
// Safe Guard !!!!!1
// if(//some guard logic)
// Current solution
// getting the data from an immutable doc
const getSubuser = snapshot.data().subusers.filter((el: any) => el.subId === data.subId);
if (getSubuser[0].subId === data.subId) {
const payload = {
user: 'sub',
verifiedEmail: false,
subId: data.subId,
sharedId: data.sharedId,
createdAt: admin.firestore.Timestamp.now(),
premiumUntill: snapshot.data()?.premiumUntill,
};
return db
.collection('SubData')
.doc(context.auth?.uid!)
.set(payload)
.catch((err: any) => {
console.log(err);
});
});

Amplify Datastore subscription cost

I am trying to understand the cost of Datastore. It seems that it subscribes to all Mutations. So if there are 50 users, then each message will be send 50 times, even if it not required.
As each real time mutation costs money, we will be paying unnecessary 49 times for this real time message mutation.
Also , it seems to me SyncExpression doesn't have any effect on this Subscription.
I am really stuck here. It will be great of someone can clarify
Amplify generates the datastore boilerplate code for you, but you still need to call it. You won't pay for every user and every mutation.
You will only subscribe to a mutation (explicitly call the code to listen for changes) on a per-user basis for things that user is interested in. e.g. if you are viewing a TODO item, you'd subscribe the user to that item and they'll immediately see if someone else modify it on another device.
UPDATE
Long story... I was triggering back-end computation via GraphQL by making a lambda resolver. The computation took too long and the GQL call would timeout. I updated the code so the GQL call called itself asynchronously (re-trigger the lambda), and returned immediately. Then when the long-running task completed in the spun-up lambda, I updated the a record in the database.
I update the record using AppSync instead of direct GQL so it would trigger mutations, and in the react client, I listen to a mutation for the specific record that will be updated. This way, there is just 1 user listening (if they've triggered the long running action) and that user is only notified about changes to the single DB record they're interested in, and not receiving other user's updates.
I don't know if all this is applicable to your situation. The code snippets below may help you, but they're somewhat out of context.
// In amplify/backend/api/projectname/schema.graphql
type Subscription {
onCouponWithIdUpdated(id: ID!): Coupon #aws_subscribe(mutations: ["updateCoupon"])
}
// In my useSendCoupon hook...
// Subscribe to coupon updates
useEffect(() => {
if (0 === couponId) {
return
}
console.log(`subscribe to coupon updates for couponId:`, couponId)
const onCouponWithIdUpdated = /* GraphQL */ `
subscription OnCouponWithIdUpdated($id: ID!) {
onCouponWithIdUpdated(id: $id) {
id
proofLink
owner
}
}
`
const subscription = API
.graphql(graphqlOperation(onCouponWithIdUpdated, { id: couponId }))
.subscribe({
next: ({ provider, value }) => {
const coupon = value.data.onCouponWithIdUpdated
//console.log(`Proof Link:`, coupon.proofLink)
setProofLinks([coupon.proofLink])
setSendCouponState(COUPON_STATE_PREVIEW_SUCCESS)
},
error: error => console.warn(error)
})
console.log('subscribed: ', subscription)
return () => {
console.log(`unsubscribe to coupon updates`)
subscription.unsubscribe()
}
}, [couponId])
// inside a lambda...
const updateCouponWithProof = async (authorization, couponId, proofLink) => {
const initializeClient = () => new AWSAppSyncClient({
url: process.env.API_XXXX_GRAPHQLAPIENDPOINTOUTPUT,
region: process.env.REGION,
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: authorization
},
disableOffline: true,
})
const executeMutation = async (mutation, operationName, variables) => {
const client = initializeClient()
try {
const response = await client.mutate({
mutation: gql(mutation),
variables,
fetchPolicy: "no-cache",
})
return response.data[operationName]
} catch (err) {
console.log("Error while trying to mutate data", err)
throw JSON.stringify(err)
}
}
const updateCoupon = /* GraphQL */ `
mutation UpdateCoupon(
$input: UpdateCouponInput!
$condition: ModelCouponConditionInput
) {
updateCoupon(input: $input, condition: $condition) {
id
proofLink
owner
}
}
`
const variables = { input: { id: couponId, proofLink } }
try {
return await executeMutation(updateCoupon, 'updateCoupon', variables)
} catch (error) {
console.log(`executeMutation error`, error)
}
}

How can I catch errors in my firebase function when setting a document fails?

I have a firebase cloud function to create a user document with user data whenever a user registers. How would I return an error when the set() fails? Since this is not an http request (an I don't want to use an http request in this case) I have no response. So how would I catch errors?
export const onUserCreated = functions.region('europe-west1').auth.user().onCreate(async user => {
const privateUserData = {
phoneNumber: user.phoneNumber
}
const publicUserData = {
name: 'Nameless'
}
try
{
await firestore.doc('users').collection('private').doc('data').set(privateUserData);
}catch(error)
{
//What do I put here?
}
try
{
await firestore.doc('users').collection('public').doc('data').set(publicUserData);
}catch(error)
{
//What do I put here?
}
});
You can't "return" an error, since the client doesn't even "know" about this function running, there is nobody to respond to.
You can make a registration collection, and in your function make a document there for the current user (using the uid as the document id). In that document, you can put any information you'd like your user to know (status, errors, etc).
So your clients would have to add a listener to this document to learn about their registration.
In your particular code, I think the error is in doc('users'). I guess you meant doc('users/'+user.uid).
Your catch -block will receive errors that occur on your set -call:
try {
await firestore.doc('users').collection('public').doc('data').set(publicUserData);
} catch (error) {
// here you have the error info.
}

Unable to create new Documents in Firestore

I recently switched to Firestore in one of my iOS apps. I am trying to store some settings in the "settings" collection.
I have the following code:
let data = FSSettings().store() // returns [String : Any]
let db = Firestore.firestore()
let document = db.collection("settings").document(uid) // I tried this
document.setData(data)
{
error in
Swift.print(error?.localizedDescription)
}
// And this..
db.collection("settings").addDocument(data: data)
{
error in
Swift.print(error?.localizedDescription)
}
According to the Firestore documentation, this would create the document (and also the collection). But the document is not being created. I have set a breakpoint at the print() line, but this is also never being called.
Here are my rules in Firestore:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid != null;
}
}
}
This should also work, because I am logged in. I also tried a different method using a transaction:
let db = Firestore.firestore()
let document = db.collection("settings").document(uid)
db.runTransaction(
{
transaction, errorPointer in
transaction.updateData(data, forDocument: document)
})
{
// Completion
object, error in
if let error = error
{
Swift.print(error.localizedDescription)
}
}
When running this statement, the following line is printed:
Transaction failed all retries.
What am I missing? My app runs on iOS 12.0 and is using the latest Firebase/Firestore version (CocoaPods).
When I create the document manually, I can change the values without any problems, it is only the creation that fails.
It looks like the Firestore initialization takes some time. The settings are created right at the start of the app, so the Firestore was not initialized yet. Running the code above with a slight delay of 2 seconds works fine.

How to Catch Error When Data is not Sent on Angularfire when adding data to firebase?

Im using angularfire to save data into my firebase. Here is a quick code.
$scope.users.$add({
Name:$scope.username,
Age:$scope.newage,
Contact: $scope.newcontact,
});
alert('Saved to firebase');
I am successful in sending these data to my firebase however how can I catch an error if these data are not saved successfully? Any ideas?
EDIT
So after implementing then() function.
$scope.users.$add({
Name:'Frank',
Age:'20',
Contact: $scope.newcontact,
}).then(function(ref) {
alert('Saved.');
}).catch(function(error) {
console.error(error); //or
console.log(error);
alert('Not Saved.');
});
When connected to the internet. The then() function is fine. It waits for those data to be saved in firebase before showing the alert.
What I want is for it to tell me that data is not saved. catch error function is not firing when i am turning off my internet connection and submitting those data.
When you call $add() it returns a promise. To detect when the data was saved, implement then(). To detect when saving failed, implement catch():
var list = $firebaseArray(ref);
list.$add({ foo: "bar" }).then(function(ref) {
var id = ref.key;
console.log("added record with id " + id);
list.$indexFor(id); // returns location in the array
}).catch(function(error) {
console.error(error);
});
See the documentation for add().
Update
To detect when the data cannot be saved due to not having a network connection is a very different problem when it comes to the Firebase Database. Not being able to save in this case is not an error, but merely a temporary condition. This condition doesn't apply just to this set() operation, but to all read/write operation. For this reason, you should handle it more globally, by detecting connection state:
var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
By listening to .info/connected your code can know that the user is not connected to the Firebase Database and handle it according to your app's needs.