4

I am storing some data under a node called "notifications" within my Firebase database. When I add an event observer to "notifications" and set the event type to ".childAdded" the completion handler is called every time a child is added to the "notifications" node AND every time a child is deleted from the "notifications" node. First of all, I don't understand why this is happening as nothing is being added, only deleted. Is there some way to avoid this?

If I can't avoid the .childAdded block executing every time a child is deleted, is there someway to detect that it was actually a child deleted event and not a child added event? I would like to do an early return from the function if the event was child deleted. Below is my code for reference:

//Event observer for notifications 
notificationsReference.queryLimited(toLast: 1).observe(.childAdded, with: {
        (snapshot) in

        //Do some stuff here only on .childAdded 
        //If the event was child deleted, do nothing 
})
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Josh
  • 2,547
  • 3
  • 13
  • 28
  • Where you able to solve this? I'm facing the same issue. – Luke Irvin Nov 23 '18 at 19:18
  • 2
    No, I was not. If I remember correctly, I contacted the firebase team and was informed that the product intentionally worked this way. That was a couple years ago so you may have better luck if you try and contact them today. Sorry I can't be of more help. – Josh Nov 26 '18 at 17:51
  • @LukeIrvin see my answer. – Parth Jan 29 '21 at 15:51

2 Answers2

9

A Firebase Query is like a view onto the data. In your case that view will always show the last item in the underlying collection.

Say that this is your collection:

item1
item2

With this data, your query will initially fire child_added for item2.

Now say you add an item:

item1
item2
item3

Since item3 is now the last item in the collection, the query will fire child_added for item3.

Now you remove item3:

item1
item2

Since item2 is now once again the last item in the query, it will fire child_added for item2.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thanks very much for the reply Frank. One thing I'm still not clear on, is there a way to detect that the event was in fact a child deleted event, and not a .childAdded event? I do not want to download all notifications items, I just want to track when new ones are added. The problem is that if I take out the query limited to last 1 the closure will execute for every single child under notifications, and then begin watching for new ones. The notifications could potentially contain hundreds or thousands of children, and I do not want to needlessly download data. – Josh Sep 28 '16 at 20:00
  • 1
    There is no specific event there to help you. Firebase synchronizes state, where you seem more interested in synchronizing operations. You might be better off in that case storing the actual operations in your database. For example: if somebody deletes a child and then adds it back, while your client is in a tunnel, it will not get an event when it comes back online. – Frank van Puffelen Sep 28 '16 at 20:17
  • @ParthTamane Can you explain why not? Or give an alternative answer that solves the problem you see in this one? – Frank van Puffelen Jan 29 '21 at 15:23
  • @FrankvanPuffelen yes let me add a link to one. I feel this is not the right answer as it won't solve his problem of trigger running on delete. – Parth Jan 29 '21 at 15:26
  • Added an answer, which is inspired by your answer :P – Parth Jan 29 '21 at 15:50
0

Interestingly I found the right approach to handle this from an alternate answer written by Frank for the JS Firebase SDK: child_added gets triggered after remove. I used an approach that modified his answer a bit from comments to his answer. Specifically the answer by Kato to how to discard initial data in a Firebase DB.

Essentially, in both answers, you need to have a timestamp (or some other numerically increasing value) in your child object that will help you sort your child objects. Then when you form the firebase query, you will set a condition that reads values where this property exceeds a minimum increasing value. This way, if values are deleted, they won't affect since their sort key will be less than the passed key.

The corresponding Swift code looks like this:

let databaseReference = Database.database().reference().child("messages")
let timeStampKeyInChildObject = "messageTimestamp"

databaseReference
.queryOrdered(byChild: timeStampKeyInChildObject)
.queryStarting(atValue: NSDate().timeIntervalSince1970)
.observe(.childAdded) 

{ (snapshot) in 

  ...


}

In my case, the child objects are chat messages that have a field called messageTimestamp. When I store messages in firebase, the timestamp is set using NSDate().timeIntervalSince1970. Hence I pass this value to queryStarting(atValue:).

Everytime the database is updated (in this case child delete/add/modify), this query will only be fired if the deleted/added/modified child had a timestamp greater than or equal to NSDate().timeIntervalSince1970 (whose value keeps increasing). This will happen only for the child that was currently added. Anything older (especially which was deleted) will have a smaller timestamp and won't be considered by our query. Hence solving the issue of delete causing the .childAdded trigger to run.

I chose to use NSDate().timeIntervalSince1970 instead of the JS equivalent of Firebase.ServerValue.TIMESTAMP which I think is, Firebase.ServerValue.timestamp(), since it returns [AnyHashable : Any]. And I don't know how to decode it.

Additionally, for performance add this to your Firebase RealtimeDB Rules. This will do the sorting on the database side and not on the client side (after downloading the data).

"messages": {
    ".indexOn": ["messageTimestamp"]
},
Parth
  • 2,682
  • 1
  • 20
  • 39