Michael Kang
Michael Kang

Reputation: 52837

Firestore active document snapshot listener breaks sorting

I am running into a firestore issue that I hope someone can help me out with.

I have an active document snapshot listener that seems to be breaking sorting behavior, and I'm not sure why.

In the constructor of my component, I initialize the document snapshot listener once:

this.listen = this.fs.collection('users').doc('0EwcWqMVp9j0PtNXFDyO').ref.onSnapshot(t => {
  console.log(t)
})

Whenever I click the Sort button it initializes a new collection query which is sorted by first name.

  async onSort() {
    if (this.first) {
      this.first();
    }

    this.sort = this.sort === 'asc'? 'desc' : 'asc';    
    alert(this.sort)
    let arr = []
    let lastDoc = null
    let queryRef = this.fs.firestore.collection('users').orderBy('firstName', this.sort as any).limit(20);
    this.users$ = this._users$.asObservable()

    // initialize a blank list
    this._users$.next([])

    // subscribe to user list snapshot changes
    const users1: { users: any, last: any } = await new Promise((resolve, reject) => {
      this.first = queryRef.onSnapshot((doc) => {
        console.log("Current data: ", doc);
        
        const users = doc.docs.map(t => {
          return {
            id: t.id,
            ...t.data() as any
          }
        })
        console.log(users)
        resolve({
          users: users,
          last: doc.docs[doc.docs.length -1]
        })
      });
  
    })
    this._users$.next(this._users$.value.concat(users1.users))

  }

However, this is not working as expected. The sort is returning one record (there should be 20 records, based on the limit).

enter image description here

Interestingly, unsubscribing from the document snapshot, fixes the sorting. 20 records are being returned and the sorting is behaving correctly.

enter image description here

Can someone please explain what I am doing wrong?

Here is a minimal Stackblitz that reproduces the issue.

Demo of Issue

[Edit]

Getting closer to a solution and an understanding... Thanks to @ZackReam for the explanation and both @Robin and @Zack for the the canonical answers.

It seems that this._firestore.firestore.collection('users').orderBy('firstName', this.sort as any).limit(20).onSnapshot(...) is indeed emitting twice - once for the active document listener (which has 1 record in the cache), and a second time for the sorted collection (which is not in the cache since switchMap will unsubscribe everytime that the sort is executed).

We can see that briefly in the functional demo that Zack posted - it flashes with one record corresponding to the document listener, then an instant later, the list is populated with the sorted collection.

The more active listeners you have, the weirder the flashes of results will be. Is this by design? seems flawed to me...

Intuitively, I would expect snapshotChanges() or valueChanges() from the this._firestore.firestore.collection('users').orderBy('firstName', this.sort as any).limit(20) query to return 20 results (the first 20 when sorted), independent of any other document listeners. It's counter-intuitive that the first snapshot from this query is the initial active document from the document query - which should have nothing to do with the sort query.

Upvotes: 4

Views: 755

Answers (2)

Zack Ream
Zack Ream

Reputation: 3076

I played around with your sample data in a simplified environment, and I think I see what's happening. Here is my playground, just update AppModule to point at your Firebase project.

What's happening?

When you call onSort(), you are making a fresh subscription to queryRef.onSnapshot. This callback is firing twice:

  1. Immediately on subscribe, it fires with an initial set of data available within Firestore's local cache (in your case it has one record, I'll explain later).
  2. Once the actual data is fetched, it fires a second time with the full set of data you were hoping for.

In the playground, make sure Document is subscribed, then subscribe to the Sorted Collection. In the console, you'll see both times it fires:

enter image description here

Unfortunately, since you are wrapping this listener in a Promise, you are only ever getting the first fire, and thus only getting the single record that was in the cache.

Why does unsubscribing from the document snapshot fix it?

It seems that the local cache is populated based on what subscriptions to onSnapshot are currently open. So in your case, you have one listener to snapshots of a single record, thus that's the only thing in your cache.

In the playground, try hitting Log Cache with various subscription states. For instance, while only Document Subscribed is true, the cache only has the one record. While both subscriptions are active, you'll see around 20 records in the cache.

If you unsubscribe from everything and hit Log Cache, the cache is empty.

It turns out that if there is no data to return from the cache in step 1 above, it skips that step entirely.

In the playground, make sure Document is UNsubscribed, then subscribe to the Sorted Collection. In the console, you'll see it fired only once:

enter image description here

This happens to actually work with your Promise, since the first and only firing contains the data you were hoping for.

How to solve this?

This comment in the firebase-js-sdk repo describes this behavior as expected, and the reasoning why.

The Promise paired with .onSnapshot is the major thing that's messing up the data flow, since .onSnapshot is expected to fire more than once. You could switch to .get instead and it would technically work.

However, as Robin pointed out, focusing on a more reactive RxJS approach taking advantage of the @angular/fire functionality) would go a long way.

Fully-functional example.

Upvotes: 4

Robin Dijkhof
Robin Dijkhof

Reputation: 19278

I can't find the problem with your code, because you reached your firebase quota. To me, it seems your sorting is a bit too complex and introduces racing problems. I think you can make it way less complex by using RxJS. Something like this would work: https://stackblitz.com/edit/angular-ivy-cwthvf?file=src/app/app.component.ts

I couldn't really test it because you reached your quota.

Upvotes: 1

Related Questions