Real time event notifications for web apps - Part 2
So, in my previous post, I introduced the idea of a solution for realtime event messaging using a Pub/Sub implementation and Chromium’s Desktop Notifications API.
The architectural view presented there only covers the big picture. Now let’s get into details technically.
In summary, the solution presented here will provide a organized way of having a event server available for publishing events to active users. This is a web-based browser-only solution, as it uses WebSockets and the Socket.IO library.
Note: The notification pop-ups are implemented using Desktop Notifications in order to have the notifications appear outside the tab, even when the browser is minimized. This only works in Chrome and Safari right now. If you really need this outside Chrome, you can use an extension for Firefox, but other browsers don’t have a similar solution as of today.
In order for the communication between the event server, app and it’s subscribers to be asynchronous as mentioned, I chose to use Node.js from Joyent, due to it’s non-blocking, evented I/O model. It leverages system’s resources more efficiently when holding many concurrent connections at once, and it allows us to do concurrent network programming in a very straightforward manner. It works for me, so it’s really not because it’s fancy or cool.
We will also use the Express framework, since it simplifies the task of routing, parsing requests and sending responses.
The client and the communication
And for the core “realtime behaviour” of this scheme, we’ll want to keep a permanent connection between the event server and it’s subscribers. As they’re nothing but browser clients, the choice is made towards Socket.IO, a realtime library that provides various means to enjoy Comet-style connection between the browser and the remote server, listening for and emitting (pushing) events.
We begin by coding an
app.js in a clean working directory:
Now, we will configure the express framework, for exposing our API for publishing events.
We will use log4js to get a cleaner log output for debugging and monitoring purposes.
Declare your dependencies and project info in a
package.json file in the root:
After this setup, we can begin defining our API endpoints. We will first create a POST route that receives event data and publishes it. This can be anything you want, but for now let’s assume we only need a
url for the notification to be displayed. The data will be received by our server in a JSON string in the HTTP request body.
Note: Express also supports urlencoded and multipart for parsing the request body, when using
bodyParser()as above, but we will focus on JSON for brevity
Let’s create this route as
Notice that are not defining/validating any specific structure for the event data, but you can easily do that here, and just return a different status in case of an error.
broadcastEvent function is self-explaining - it’s responsible for broadcasting the event to all registered listeners of the event server. So let’s look at it’s code:
Now, we have a
onlineSockets variable, which is an instance of
SocketMap. This is a data structure created to hold all active sockets, but in a very specific manner, in order to solve the problem of having multiple tabs open on a page of your your app. The basic scheme is illustrated below;
The squares A, B and C demonstrate the open tabs in the browser, each of those having one open WebSocket listening to events to be published by the event server. Each socket, as soon as it’s connected, sends a
register event passing a hash which is unique across all open browsers/sessions, but shared between tabs (this can be, for instance, your session ID). The reference to the socket then gets stored in the
SocketMap, which creates “stacks” keyed by each new hash it receives.
So, when the socket registers with an existing hash:
- the new socket gets pushed to the stack of open sockets for that hash
- it becomes the one active socket for that hash
As soon as a tab is closed, it’s socket loses connection to the server, and is automatically removed from the stack, whichever position it was in. If it was the last one (the active one), it’s removed and the socket below it becomes the active socket for that hash.
In the client side, all we need to do is to insert the same script in all desired pages of the app. This script must:
- be able to access/generate a unique hash, shared between the tabs (cookie value for the session ID)
- register an socket for that tab, using said hash
- receive notifications and notify the user using Desktop Notifications API
So in order to get the hash, you can use a js library like jQuery.cookie to get the cookie that represents your session ID. In this particular case, I’ll just generate a random hash and register it as a cookie, to simulate a real app’s session ID:
To register the socket with the hash on the server, as we’re using Socket.IO, we need to bind a callback to the
connect event, which is fired right after the socket connects to the server:
And finally, use the Desktop Notifications API (Chrome/Safari only, FF via extension) to notify the user of events published by the event server:
In the code above, I added support for a
url attribute, which, if present the incoming event data, loads the specified url when the notification is clicked.
What’s briliant about this solution, is that, in order for the user not to be harassed by a lot of notifications for each open tab, which can be 10, 20 or a 100 tabs, the server only publishes events to the active sockets (represented by the continuous black line), which are basically the last sockets to register for each hash.
A demo is available on-line here.
Just enable Desktop Notifications via the switch (accept the request for permission), open multiple tabs and fire a message via the web interface. You’ll only receive one notification. As you close the tabs and fire messages, you will continue to receive only one notification, though all open tabs are listening.
You can invite some of your friends to open this same URL in their browsers and start a remote chat!
The source code for this entire solution is available in the Github repository below.