Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase query: Why is child_added called before the value query in the following code?

I have a schema in Firebase that looks like this:

messages/
  $groupId/
    $messageId/
      message: 'Sample Message'
      createdBy: 'userID'
      createdAt: 1513337977055

Then I have the following queries executed consecutively in my code:

// Get a specific message
ref.child('messages/$groupId/$messageId')
  .once('value')
  .then(snap => console.log('value', snap.val()))

// Start new message listener
ref.child('messages/$groupId')
  .orderByKey()
  .limitToLast(1)
  .on('child_added', snap => console.log('child_added', snap.val()))

I'm wondering why child_added gets called twice here, the first one being similar to the value returned by the once('value') query.

Here's what the console displays:

child_added { message: 'Hello', createdAt: 1513337977055, createdBy: 'userId' }
value { message: 'Hello', createdAt: 1513337977055, createdBy: 'userId' }
child_added { message: 'Another message', createdAt: 1513337977066, createdBy: 'userId2' }

Note that I'm not adding new entries to Firebase here. Just querying.

EDIT: Here is a fiddle link demonstrating the issue: https://jsfiddle.net/dspLwvc3/2/

like image 273
Jaye Renzo Montejo Avatar asked Dec 21 '17 08:12

Jaye Renzo Montejo


1 Answers

firebase here

You've uncovered a quite interesting edge case there. The behavior you're seeing is expected from how the system operates. But I think we can all agree that it's far from intuitive. :-/

It's essentially a race condition, combined with Firebase's guarantees about what it will and won't fire and when it fires events.

Essentially what happens is this:

    Client                  Server
      |                       |
   (1)|  --once('value'---->  |
      |                       |
   (2)|  -on('child_added'->  |
      |                       |
      |           .           |
      |           .           |
      |           .           |
      |                       |
      |         value         |
   (3)|  <------------------- |
      |                       |
      |         child         |
   (4)|  <------------------- |
      |                       |

There are 4 key moments in there:

  1. You attach a once('value') listener for /messages/message1. The client sends the request to the server and waits.
  2. You attach a on(-child_added listener for the last-known key of /messages. The client sends the request to the server and waits.

  3. The response to the first request comes back from the server. At this stage there are two listeners. The once('value listener is clear, so it fires and is removed. But at this point /messages/message1 is also the last-known key of /messages, so the client fires that child_added listener too.

  4. The response with /messages/message3 comes back from the server. There is only one listener left and it requested to hear about the last message, so it fires. Note that if you'd also have a listener for child_removed, it would fore for /messages/message1 at this point.

As I said, it's not very intuitive. But from the system's perspective it is the correct behavior. That means that you don't want this behavior, you will need to use the API in a different way. The simplest one with your current code would be to move attaching the child_added listener into the once('value' callback:

ref.child('messages/$groupId/$messageId')
  .once('value')
  .then(snap => {
    console.log('value', snap.val()))

  // Start new message listener
  ref.child('messages/$groupId')
    .orderByKey()
    .limitToLast(1)
    .on('child_added', snap => console.log('child_added', snap.val()))
  })

This works because, by the time the child_added listener is attached, the /messages/message1 snapshot has already been flushed from the client's cache.

Update (2018-01-07): another developer encountered this behavior and had a hard time maintaining the order of children. So I wrote up a bit more how this behavior (while unexpected) still maintains the correct order of the children. For more, see my answer here: Firebase caching ruins order of retrieved children

like image 119
Frank van Puffelen Avatar answered Nov 14 '22 21:11

Frank van Puffelen