14

I've been using Firebase extensively and still face only one real issue: onDisconnect isn't 100% reliable in my experience.

If you close a computer without closing the window first, or kill the browser, you sometime have the "garbage collector" getting your onDisconnect executed, sometimes it doesn't.

My question is the following: I just don't use /.connected for now, I basically use a simple

userRef.set('status', 1);
userRef.onDisconnect().update({ 'status' : 0 });

Is there anything wrong with this approach? Do we agree that the update parameters are passed to the server at the time the line is executed and not before window unload ?

NB: I happen to try to keep a multi-window status, using the following approach to keep the status at 1 if another window is closed:

userRef.child('status').on('value', function(snap) {
  if (snap.val() != 1) {
    userRef.set('status', 1);
  }
});

I don't this how this could be related, but...

MY SOLUTION: In fact, I had just missed the part where you learn that onDisconnect is only triggered once. To get a persistent onDisconnect, you need to implement basic persistence.

Helpers.onConnected = function(callback) {
    var connectedRef = lm.newFirebase('.info/connected');
    var fn =  connectedRef.on('value', function(snap) {
      if (snap.val() === true) {
          if (callback) callback();
      }
    });
    var returned = {};
    returned.cancel = function() {
        connectedRef.off('value', fn);
    };
    return returned;
};       

Simple use case:

        this._onConnected = lm.helpers.onConnected(function() {
            this.firebase.onDisconnect().update({ 'tu': 0 });
        }.bind(this));

And then to cancel:

        if (this._onConnected) this._onConnected.cancel();
        this.firebase.onDisconnect().cancel();
Prisoner
  • 27,391
  • 11
  • 73
  • 102
cwehrung
  • 1,286
  • 13
  • 18

3 Answers3

6

You should always call the onDisconnect() operation BEFORE you call the set() operation. That way if the connection is lost between the two you don't end up with zombie data.

Also note that in the case where the network connection is not cleanly killed, you may have to wait for a TCP timeout before we're able to detect the user as gone and trigger disconnect cleanup. The cleanup will occur, but it may take a few minutes.

Andrew Lee
  • 10,127
  • 3
  • 46
  • 40
  • 2
    The issue is that I still get about 20% of onDisconnect which never get called unfortunately, resulting in people seeming forever connected. Do you have any idea of a way to understand what's happening as it's 100% sure there's still a connection for at least a few minutes when onDisconnect is called ? – cwehrung Jun 12 '13 at 23:06
  • 1
    How long are you waiting after the client disconnects? It could take up to 5 minutes in some extreme cases. Also - do you perhaps have security rules that are blocking the disconnect operation? A test case would be helpful as well. Do you have a link with a super-simple test case? – Andrew Lee Jun 13 '13 at 03:40
  • 4
    Andrew, I finally found the source of my issue. My mistake: I never really realized that onDisconnect was only called the first time you're disconnected. This explains all the bugs we have... I would definitely put it in red + bold on your website. – cwehrung Jun 13 '13 at 15:32
  • 1
    Could I also suggest one simple add-on ? I don't know for other projects, but in our case, we always want the onDisconnect to be called again on future onDisconnect (imagine a case where a user has a bad connection and it keeps disconnecting / reconnecting). My suggestion would be couldn't we pass as an argument to onDisconnect if we want it to be called for every future disconnection ? That would look like this: ref.onDisconnect(true).update(down); – cwehrung Jun 13 '13 at 15:34
  • 7
    This is why we provide the /.info/connected endpoint. We recommend re-establishing the disconnect handler every time the connection comes back online. See the last code snippet here: https://www.firebase.com/docs/managing-presence.html – Andrew Lee Jun 13 '13 at 22:23
  • I'm still having trouble with handling the multi - window scenario.. http://stackoverflow.com/questions/43650942/firebase-presence-system-that-works-for-multiple-devices – Vibgy Apr 27 '17 at 11:41
  • One thing i've noticed is that if I test this using 2 tabs in Chrome, sometimes it doesn't disconnect. But if I test using 1 tab in Chrome, 1 in Safari, it works reliably. – Doug Nov 16 '19 at 16:57
2

After a lot of digging around, I found this issue which was actually happening in my case and I think for most of the others also who are coming to this page for a solution.

So the problem is firebase checks it's accessToken twice when 1. the onDisconnect is enqueued and 2. when the onDsiconnect is applied. Firebase doesn't proactively refreshes tokens when a tab is not visible. If the page is inactive for more than the expiry of the accessToken and closed without focusing on that tab firebase will not allow the onDisconnect because of the expired accessToken.

Solutions to this:

  1. You can get a new accessToken by setting some sort of interval like this:

    let intervalId;
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        if (intervalId) {
          window.clearTimeout(intervalId);
          intervalId = undefined;
        }
      } else {
        firebaseApp.auth().currentUser.getIdToken(true);
    
        intervalId = setInterval(() => {
          firebaseApp.auth().currentUser.getIdToken(true);
        }, intervalDuration);
      }
    });
    
  2. Or you can disconnect the database manually firebase.database().goOffline() whenever tab visibility changes from "visible".

Suman Kundu
  • 1,702
  • 15
  • 22
1

Expanding on the comment :

This is why we provide the /.info/connected endpoint. 
We recommend re-establishing the disconnect handler every time the connection comes back online

I followed the above and got it fixed:

const userRef = Firebase.database().ref('/users/<user-id>')

Firebase.database()
  .ref('.info/connected')
  .on('value', async snap => {
    if (snap.val() === true) {
      await userRef.onDisconnect().remove();
      await userRef.update({ status : 'online' })
    } else {
      await this.userRef.remove()
    }
});

For reference, my previous code was:

const userRef = Firebase.database().ref('/users/<user-id>')
userRef.onDisconnect().remove();
await userRef.update({ status : 'online' })

The issue with this is that the onDisconnect may not work when you go offline and come back online multiple times

Fez Vrasta
  • 14,110
  • 21
  • 98
  • 160
FacePalm
  • 10,992
  • 5
  • 48
  • 50