Earlier this year we started using Fanout.io in Air Mozilla to enhance the experience for users awaiting content updates. Here I hope to flesh out its details a bit to inspire others to deploy a similar solution.
What It Is
First of all, Fanout.io is basically a service that handles your WebSockets. You put in some of Fanout's JavaScript into your site that handles a persistent WebSocket connection between your site and Fanout.io. And to push messages to your user you basically send them to Fanout.io from the server and they "forward" it to the WebSocket.
The HTML page looks like this:
<html>
<body>
<h1>Web Page</h1>
<script
src="https://{{ FANOUT_REALM_ID }}.fanoutcdn.com/bayeux/static/faye-browser-1.1.2-fanout1-min.js"
></script>
<script src="fanout.js"></script>
</body>
</html>
And the fanout.js
script looks like this:
window.onload = function() {
var client = new Faye.Client('https://{{ FANOUT_REALM_ID }}.fanoutcdn.com/bayeux')
client.subscribe('/mycomments', function(data) {
console.log('Incoming updated data from the server:', data);
})
};
And in server it looks something like this:
from django.conf import settings
import fanout
fanout.realm = settings.FANOUT_REALM_ID
fanout.key = settings.FANOUT_REALM_KEY
def post_comment(request):
"""A django view function that saves the posted comment"""
text = request.POST['comment']
saved_comment = Comment.objects.create(text=text, user=request.user)
fanout.publish('mycomments', {'new_comment': saved_comment.id})
return http.JsonResponse({'comment_posted': True})
Note that, in the client-side code, there's no security since there's no authentication. Any client can connect to any channel. So it's important that you don't send anything sensitive. In fact, you should think of this pattern simply as a hint that something has changed. For example, here's a slightly more fleshed out example of how you'd use the subscription.
window.onload = function() {
var client = new Faye.Client('https://{{ FANOUT_REALM_ID }}.fanoutcdn.com/bayeux')
client.subscribe('/mycomments', function(data) {
if (data.new_comment) {
$.json('/comments', function(response) {
$('#comments .comment').remove();
$.each(response.comments, function(comment) {
$('<div class="comment">')
.append($('<p>').text(comment.text))
.append($('<span>').text('By: ' + comment.user.name))
.appendTo('#comments');
});
});
}
})
};
Yes, I know jQuery isn't hip but it demonstrates the pattern well. Also, in the real world you might not want to ask the server for all comments (and re-render) but instead do an AJAX query to get all new comments since some parameter or something.
Why It's Awesome
It's awesome because you can have a simple page that updates near instantly when the server's database is updated. The alternative would be to do a setInterval
loop that frequently does an AJAX query to see if there's new content to update. This is cumbersome because it requires a lot heavier AJAX queries. You might want to make it secure so you engage sessions that need to be looked up each time. Or, since you're going to request it often you have to write a very optimized server-side endpoint that is cheap to query often.
And last but not least, if you rely on an AJAX loop interval, you have to pick a frequency that your server can cope with and it's likely to be in the range of several seconds or else it might overload the server. That means that updates are quite delayed.
But maybe most important, you don't need to worry about running a WebSocket server. It's not terribly hard to do one yourself on your laptop with a bit of Node Express or Tornado but now you have yet another server to maintain and it, internally, needs to be connected to a "pub-sub framework" like Redis or a full blown message queue.
Alternatives
Fanout.io is not the only service that offers this. The decision to use Fanout.io was taken about a year ago and one of the attractive things it offers is that it's got a freemium option which is ideal for doing local testing. The honest truth is that I can't remember the other justifications used to chose Fanout.io over its competitors but here are some alternatives that popped up on a quick search:
It seems they all (including Fanout.io) has freemium plans, supports authentication, REST APIs (for sending and for querying connected clients' stats).
There are also some more advanced feature packed solutions like Meteor, Firebase and GunDB that act more like databases that are connected via WebSockets or alike. For example, you can have a database as a "conduit" for pushing data to a client. Meaning, instead of sending the data from the server directly you save it in a database which syncs to the connected clients.
Lastly, I've heard that Heroku has a really neat solution that does something similar whereby it sets up something similar as an extension.
Let's Get Realistic
The solution sketched out above is very simplistic. There are a lot more fine-grained details that you'd probably want to zoom in to if you're going to do this properly.
Throttling
In Air Mozilla, we call fanout.publish(channel, message)
from a post_save
ORM signal. If you have a lot of saves for some reason, you might be sending too many messages to the client. A throttling solution, per channel, simply makes sure your "callback" gets called only once per channel per small time frame. Here's the solution we employed:
window.Fanout = (function() {
var _locks = {};
return {
subscribe: function subscribe(channel, callback) {
_client.subscribe(channel, function(data) {
if (_locks[channel]) {
return;
}
_locks[channel] = true;
callback(data);
setTimeout(function() {
_locks[channel] = false;
}, 500);
});
};
}
})();
Subresource Integrity
Subresource integrity is an important web security technique where you know in advance a hash of the remote JavaScript you include. That means that if someone hacks the result of loading https://cdn.example.com/somelib.js
the browser compares the hash of that with a hash mentioned in the <script>
tag and refuses to load it if the hash doesn't match.
In the example of Fanout.io it actually looks like this:
<script
src="https://{{ FANOUT_REALM_ID }}.fanoutcdn.com/bayeux/static/faye-browser-1.1.2-fanout1-min.js"
crossOrigin="anonymous"
integrity="sha384-/9uLm3UDnP3tBHstjgZiqLa7fopVRjYmFinSBjz+FPS/ibb2C4aowhIttvYIGGt9"
></script>
The SHA you get from the Fanout.io documentation. It requires, and implies, that you need to use an exact version of the library. You can't use it like this: <script src="https://cdn.example/somelib.latest.min.js" ...
.
WebSockets vs. Long-polling
Fanout.io's JavaScript client follows a pattern that makes it compatible with clients that don't support WebSockets. The first technique it uses is called long-polling. With this the server basically relys on standard HTTP techniques but the responses are long lasting instead. It means the request simply takes a very long time to respond and when it does, that's when data can be passed.
This is not a problem for modern browsers. They almost all support WebSocket but you might have an application that isn't a modern browser.
Anyway, what Fanout.io does internally is that it first creates a long-polling connection but then shortly after tries to "upgrade" to WebSockets if it's supported. However, the projects I work only need to support modern browsers and there's a trick to tell Fanout to go straight to WebSockets:
var client = new Faye.Client('https://{{ FANOUT_REALM_ID }}.fanoutcdn.com/bayeux', {
transportMode: 'fallback'
});
Fallbacks
In the case of Air Mozilla, it already had a traditional solution whereby it does a setInterval
loop that does an AJAX query frequently.
Because the networks can be flaky or because something might go wrong in the client, the way we use it is like this:
var RELOAD_INTERVAL = 5;
if (typeof window.Fanout !== 'undefined') {
Fanout.subscribe('/' + container.data('subscription-channel-comments'), function(data) {
Comments.load(container, data);
});
RELOAD_INTERVAL = 60 * 5;
}
setInterval(function() {
Comments.reload_loop(container);
}, RELOAD_INTERVAL * 1000);
Use Fanout Selectively/Progressively
In the case of Air Mozilla, there are lots of pages. Some don't ever need a WebSocket connection. For example, it might be a simple CRUD (Create Update Delete) page. So, for that I made the whole Fanout functionality "lazy" and it only gets set up if the page has some JavaScript that knows it needs it.
This also has the benefit that the Fanout resource loading etc. is slightly delayed until more pressing things have loaded and the DOM is ready.
You can see the whole solution here. And the way you use it here.
Have Many Channels
You can have as many channels as you like. Don't create a channel called comments
when you can have a channel called comments-123
where 123
is the ID of the page you're on for example.
In the case of Air Mozilla, there's a channel for every single page. If you're sitting on a page with a commenting widget, it doesn't get WebSocket messages about newly posted comments on other pages.
Conclusion
We've now used Fanout for almost a year in our little Django + jQuery app and it's been great. The management pages in Air Mozilla use AngularJS and the integration looks like this in the event manager page:
window.Fanout.subscribe('/events', function(data) {
$scope.$apply(lookForModifiedEvents);
});
Fanout.io's been great to us. Really responsive support and very reliable. But if I were to start a fresh new project that needs a solution like this I'd try to spend a little time to investigate the competitors to see if there are some neat features I'd enjoy.
UPDATE
Fanout reached out to help explain more what's great about Fanout.io
"One of Fanout's biggest differentiators is that we use and promote open technologies/standards. For example, our service supports the open Bayeux protocol, and you can connect to it with any compatible client library, such as Faye. Nearly all competing services have proprietary protocols. This "open" aspect of Fanout aligns pretty well with Mozilla's values, and in fact you'd have a hard time finding any alternative that works the same way."