javascriptangularfirebasegoogle-cloud-firestore

Firestore onSnapshot Unsubscribe Not Triggered on Page Refresh


I'm experiencing an issue with Firestore's onSnapshot listener not unsubscribing when a page is refreshed or closed. I'm using Angular and the Firestore SDK, and I've implemented the unsubscribe logic within the beforeunload event. However, the unsubscribe function provided by Firestore doesn't always seem to execute, as evidenced by inconsistent request logs in the Firestore emulator and chrome's network monitoring. And even when it does I see no "ListenHandler onClose" in the emulator logs. The unsubscribe not working is also evident by the multiplying requests fire on a single change in a firestore document on each refresh. This issue is not reproduced when calling the unsubscribe upon calling unsubscribe directly (upon user logout per example)

@HostListener('window:beforeunload', ['$event'])
unloadHandler(event: Event) {
  this.unsubscribeFromFirestore();
}

ngOnDestroy() {
  this.unsubscribeFromFirestore();
}

unsubscribeFromFirestore() {
  if (this.unsubscribe) {
    console.log('Unsubscribing from Firestore');
    this.unsubscribe(); // Directly call the unsubscribe function
  }
}

  getMainTabNodes(userId: string): Observable<MenuNode[]> {
    const nodesRef = collection(this.firestore, 'nodes');
    let requestQuery: Query = query(nodesRef,
      or(
        where('access.public', '==', true),
        where('access.owner', '==', userId),
        where(`access.users.${userId}`, 'array-contains', 'read'),
      )
    );

    return new Observable<MenuNode[]>(subscriber => {
      this.unsubscribe = onSnapshot(requestQuery, {
        next: snapshot => {
          const nodes = snapshot.docs.map(doc => doc.data() as MenuNode);
          subscriber.next(nodes);
        },
        error: error => {
          subscriber.error(error);
        }
      });
      
      return () => {
        console.log('Cleaning up nodes subscription');
        this.unsubscribe();
      };
    })
  }

Question: How can I ensure that the Firestore onSnapshot unsubscribe function is reliably called during a page refresh or close? Are there any best practices or alternative approaches to handle this scenario effectively?

The error seems to be related to firebase snapshot listener create many LIST requests

My emulator logs looks exactly like that after few refreshes and one change to a document.

This is what my firebase console usage chart looks like :

enter image description here

And this is what my emulator logs looks like after 1 change in a document.

enter image description here

Edit: This is my usage chart after replicating the code almost 1 to 1 on stackblitz (the connections dies out eventually) :

enter image description here


Solution

  • According to the MDN docs:

    The beforeunload event is not reliably fired, especially on mobile platforms.

    Especially: you can't reliably make network calls in this situation anymore.

    But this whole unsubscribe logic should not be needed here anyway. When the page unloads, the listeners automatically unregister - as the server won't be able to deliver messages to them anymore.


    Let's check if the server automatically handles listeners from clients that disappear without unregistering them.

    I can't reproduce the behavior you're describing, and in fact see the database behaving as I expect it to - which means my last paragraph above is accurate: you don't need to handle the unload scenario yourself (and in fact: can't reliably do that anyway).

    I created a minimal repro consisting of two pages:

    1. A page that add a new document to a collection once per minute (link).
    2. A page that listens for the newest document in that collection (link).

    I've then let the writer page run for a while, and varied the number tabs with the listener page open.

    This is what I see in the usage charts for the past hour in the Firebase console:

    enter image description here

    The blue line shows the read count per minute, the blue number is the total number of reads in the hour. Similarly, the orange line shows the number of reads per minute, while the orange number is the total number of reads in the hour.

    The annotations in there:

    1. In the first phase I was building out the repro, so there's lot of spurious reads. You can see that the orange line (and the total number of writes) shows a pretty consistent 1 write per minute for the entire hour.
    2. In the second phase, I started with 1 listener page, then went to 3 listener pages, and finally to 5 listener pages. Each time you can see the number of document reads go up as well.
    3. In the third phase I first closed 4 listener pages in steps, leaving 1 open, and waited a few minutes. Then finally I closed that final page/tab too. The number of reads went down too as you can see. There was a few minute delay between closing the tab/page and the drop of the reads per minute, which is how long it took the server to notice that the listener was gone.

    In my repro above, Firestore works as I expected: once a client disappears the server (with a bit of lag) stops any listeners it may have had for that client, and thus stops charging reads for those listeners.

    Since the behavior you see is differently, there must be a different explanation for it than the closed tabs still keeping their listeners open on the server. For example, do you have the data panel of the Firestore console open anywhere? Keep in mind: reads by the Firestore console are also measured and charged.

    If you still can't explain the behavior you have, see if you can reproduce what I showed above (based on the minimal code samples I shared), and then from there see if you can get that minimal repro back to your situation.


    I also wrote a separate article based on this answer, where I add some (even?) more context: Do you need to unregister Firestore listeners when a web page is closed?