This post will teach you how to build a fully working presence system using Firebase.
Imagine you have some exciting news you really want to share with a friend. You jump on your favorite social network, open the friends list, and quickly scan for your friend's name. There's a red dot next to it indicating that she's busy - no matter - this is important! You double click to open a chat window...
A lot of us do this little routine every day. Knowing if somebody is currently online or offline is called "presence" and is a pretty basic feature in most collaborative applications (such as chat). What most of us don't realize, however, is that while the feature may appear simple, building the infrastructure to support it from scratch can be very difficult.
Have no fear! Firebase makes adding presence to your application very easy.
A presence system is simply a way of sharing your status with other users. At a basic level, this status could be something simple like whether a given user is currently online or offline. Firebase is great at doing exactly that - sharing state.
The naïve approach - Online / Offline State
The first step when building a presence system is for each client to be able to detect when it is online and offline. Firebase provides a convenient way to do this via the special .info/connected path. Great!
var amOnline = new Firebase('https://<demo>.firebaseio.com/.info/connected'); var userRef = new Firebase('https://<demo>.firebaseio.com/presence/' + userid); amOnline.on('value', function(snapshot) { if (snapshot.val()) { // User is online. userRef.set(true); } else { // User is offline. // WARNING: This won't work! See an explanation below. userRef.set(false); } });
Simple enough, right? The 'value' callback is automatically triggered by Firebase whenever the connection state changes (e.g. a user loses their internet connection) and we can set a boolean value for each user as appropriate.
This won't work in practice though - can you figure out why?
When the user goes offline, our callback will be triggered, but by then it's too late to notify other clients because we aren't connected to the network anymore.
Online / Offline State Done The Right Way
So what do we do? We can solve the offline problem by utilizing a special feature in Firebase known as onDisconnect - a function that tells the Firebase server to do something when it notices a client isn't connected anymore.
This is exactly what we want - we need to instruct the Firebase server to set the user's presence boolean to false when it detects that the client went offline:
false
var amOnline = new Firebase('https://<demo>.firebaseio.com/.info/connected'); var userRef = new Firebase('https://<demo>.firebaseio.com/presence/' + userid); amOnline.on('value', function(snapshot) { if (snapshot.val()) { userRef.onDisconnect().remove(); userRef.set(true); } });
Notice how much smaller this code is. The nice thing about onDisconnect() is that Firebase will automatically handle all of the nasty corner cases for you, like unclean disconnects.
onDisconnect()
A few points to note here:
We don't need to handle an else case. We simply use onDisconnect() to setup a trigger every time the user comes online.
else
The onDisconnect() call is before the call to set() itself. This is to avoid a race condition where you set the user's presence to true and the client disconnects before the onDisconnect() operation takes effect, leaving a ghost user.
set()
The call to onDisconnect().remove() is made every time the user comes online. This is because onDisconnect() operations are performed only once.
onDisconnect().remove()
We store the presence bit for a user independently on a top-level key instead of with the user record itself. This lets us quickly obtain a list of users who are currently online or offline without having to enumerate all user records. This is an example of denormalization, an important principle we've discussed earlier.
There's no special code in the callback for when the user goes offline. This isn't necessary as onDisconnect() will ensure that the presence bit will be updated when the client disconnects.
Hello, are you there? (Idle Status)
You're telling your friend the good news and are having a great conversation. 10 minutes later, you notice your friend isn't responding anymore. Perhaps she had to leave her desk... wouldn't it be great if you were notified somehow that the user had become "idle"?
It's easy to add features like this if we store text instead of a boolean. To detect the idle state of a user, we'll make use of a library called idle.js:
<script src='https://www.firebase.com/js/libs/idle.js'></script> <script> var amOnline = new Firebase('https://<demo>.firebaseio.com/.info/connected'); var userRef = new Firebase('https://<demo>.firebaseio.com/presence/' + userid); amOnline.on('value', function(snapshot) { if (snapshot.val()) { userRef.onDisconnect().set('☆ offline'); userRef.set('★ online'); } }); document.onIdle = function () { userRef.set('☆ idle'); } document.onAway = function () { userRef.set('☄ away'); } document.onBack = function (isIdle, isAway) { userRef.set('★ online'); } </script>
Last seen at...
Let's say you want to share the news with another friend, but notice he's currently offline. Well, there's a big difference between someone being offline for the last 5 minutes and 5 hours. I almost always want to know when a friend was last seen online.
You can make use of Firebase's server-side timestamp feature to implement this. We can store the boolean value true when a particular user is online but set it to a timestamp when the user disconnects via use of onDisconnect and Firebase.ServerValue.Timestamp:
true
onDisconnect
Firebase.ServerValue.Timestamp
var amOnline = new Firebase('https://<demo>.firebaseio.com/.info/connected'); var userRef = new Firebase('https://<demo>.firebaseio.com/presence/' + userid); amOnline.on('value', function(snapshot) { if (snapshot.val()) { userRef.onDisconnect().set(Firebase.ServerValue.TIMESTAMP); userRef.set(true); } });
In your UI, you can now obtain the last time a particular user was online:
var userRef = new Firebase('https://<demo>.firebaseio.com/presence/' + userid); userRef.on('value', function(snapshot) { if (snapshot.val() === true) { // User is online, update UI. } else { // User logged off at snapshot.val() - seconds since epoch. } });
Session History
I can always tell that a friend is having a busy day if he comes online for 10 minutes at a time and keeps going offline. Wouldn't it be cool if we could keep a log of every user's session and know how long each session lasted?
Firebase makes that easy too. We can simply create a new entry in the user's presence 'log' every time they come online by using the convenient push() function and storing the current timestamp. Then, by using an onDisconnect operation, we can record the end time as well:
push()
var amOnline = new Firebase('https://<demo>.firebaseio.com/.info/connected'); var userRef = new Firebase('https://<demo>.firebaseio.com/presence' + userid); amOnline.on('value', function(snapshot) { if (snapshot.val()) { var sessionRef = userRef.push(); sessionRef.child('ended').onDisconnect().set(Firebase.ServerValue.TIMESTAMP); sessionRef.child('began').set(Firebase.ServerValue.TIMESTAMP); } });
Now, by enumerating all the children of https://<my-firebase>.firebaseio.com/presence/<userid> you can display a record of all the user's past sessions. The time at which each session began is stored at https://<my-firebase>.firebaseio.com/presence/<userid>/<sessionid>/began.
https://<my-firebase>.firebaseio.com/presence/<userid>
https://<my-firebase>.firebaseio.com/presence/<userid>/<sessionid>/began
That's It!
You now have a fully working presence system. Don't forget to check out the presence example of the code listed in this blog post. We've also updated our documentation on managing presence to include information on the server timestamps feature. Let us know via email, our Google Group, or comment on this blog if you're building something similar or have found other ways to build a presence system with Firebase.