Vintage JavaScript begone
almost 11 years ago
The Problem
These days all the cool kids are using Ember.JS or Angular or Meteor or some other single page web application. If you deploy often, like we do at Discourse, you have a problem.
How can you get everyone to run the latest version of your JavaScript and CSS bundle? Since people do not reload full pages and just navigate around accumulating small json payload there is a strong possibility people can be on old versions and experience weird and wonderful odd bugs.
The message bus
One BIG criticism I have heard of Rails and Django lately is the lack of “realtime” support. This is an issue we foresaw over a year ago at Discourse.
Traditionally, people add more component to a Rails system to support “realtime” based notifications. Be it Ruby built systems like faye, non Ruby systems like Node.JS with socket.io or outsourced systems like Pusher. For Discourse none of these were an option. We could not afford to complicate the setup process or outsource this stuff to a third party.
I built the message_bus gem to provide us with an engine for realtime updates:
At the core of it message_bus allows you a very simple API to publish and subscribe to messages on the client:
# in ruby
MessageBus.publish('/my_channel', 'hello')
<!-- client side -->
<script src="message-bus.js" type="text/javascript"></script>
<script>
MessageBus.subscribe('/my_channel', function(data){
alert(data);
});
</script>
Behind this trivial API hides a fairly huge amount of feature goodness:
- This thing scales really well, clients “pull” information from a reliable pub sub channel, minimal per-client house keeping.
- Built in security (send messages to user or groups only)
- Built on rack hijack and thin async, so we support passenger, thin, unicorn and puma.
- Uses long polling, with an event machine event loop, can easily service thousands of clients from a single web server.
- Built in multi-site support (for hosting sub domains)
We use this system at Discourse to notify you interesting things, update the topic pages live and so on.
On to the implementation
Given a message_bus, implementing a system for updating assets is fairly straight forward.
On the Rails side we calculate a digest that represents our application version.
def assets_digest
@assets_digest ||= Digest::MD5.hexdigest(
ActionView::Base.assets_manifest.assets.values.sort.join
)
end
def notify_clients_if_needed
# global channel is special, it goes to all sites
channel = "/global/asset-version"
message = MessageBus.last_message(channel)
unless message && message.data == digest
MessageBus.publish channel, digest
end
end
With every full page we deliver to the clients we include this magic digest:
Discourse.set('assetVersion','<%= Discourse.assets_digest %>');
Then on the client side we listen for version changes:
Discourse.MessageBus.subscribe("/global/asset-version", function(version){
Discourse.set("assetVersion",version);
if(Discourse.get("requiresRefresh")) {
// since we can do this transparently for people browsing the forum
// hold back the message a couple of hours
setTimeout(function() {
bootbox.confirm(I18n.lookup("assets_changed_confirm"), function(){
document.location.reload();
});
}, 1000 * 60 * 120);
}
});
Finally, we hook into the transition to new routes to force a refresh if we detected assets have changed:
routeTo: function(path) {
if(Discourse.get("requiresRefresh")){
document.location.href = path;
return;
}
//...
}
Since in the majority of spots we render “full links” and pass them through this magic method this works well. Recently Robin added a second mechanism that allows us to trap every transition, however it would require a double load which I wanted to avoid:
Eg, the following would also work
Discourse.PageTracker.current().on('change', function() {
if(Discourse.get("requiresRefresh")){
document.location.reload();
return;
}
});
Summary
I agree that every single page application needs some sort of messaging bus, you can have one today, on Rails, if you start using the message_bus.
Real-time is not holding us back with Rails, it is production ready.
Sam, thanks for publishing MessageBus.
Any clue how I might access Rails-level session data for the
user_id_lookup
?My app saves the user_id to the Rails session as in this Railscast (perhaps a bad idea security-wise, but let’s assume a toy application for the moment).
However I can’t seem to get access to the session from Rack. I’m using an initializer like the following:
But the session is always an empty hash. I can see that ad_request.cookies has my
_appname_session
string, and ad_request.cookie_jar.signed is there, but has no:user_id
.I checked to see how Discourse is using it, and they seem to be generating a (secure, random) permanent “remember me” token, if I’ve understood the source correctly.
I’m able to to access permanent cookies from Rack in my app (ad_request.cookie_jar.permanent[‘foo’]), but before I go down that route I’d like to know why the regular Rails session seems to not work. Must it necessarily be a permanent cookie for Rack to see it?
Many Thanks!