1991-2016—25 years of Art & Logic

Websockets for Backbone

Backbone + Websockets

Backbone’s had some of its thunder stolen lately by trendier frameworks like Meteor and Angular; for good reason, in most cases, as without the prosthetic functionality offered by the likes of Marionette, Backbone’s view handling (amongst a few other lacks and warts) is really just ‘roughed in’.

But the fact that a framework like marionette can be built on top of Backbone is a testament to Backbone’s flexibility – after all, as the name suggests, Backbone is really just the ‘skeleton’ of your app, and it’s willing to be fit into place however you need it to be.

For instance: Backbone’s default persistance method is via jQuery’s ajax – make a request to the server using one of the standard HTTP methods to persists changes to models in a collection to the server-side db. Works great, but maybe you need something faster/better/stronger/etc.

Like, say, WebSockets.

Let’s replace Backbone’s standard method of persistance via ajax with WebSockets! To do this, we’ll take advantage of the excellent socket.io client library (and, of course, you’ll need the appropriate server-side library for your chosen language – via npm install socket.io if you’re using Node.js, a package like Gevent Socket.io if you’re using Python, etc.).

Let’s take a look at the code, and then break it down:

So, let’s start at the top (and the very bottom).

(function(Backbone, $, _, io){
	var urlError = function(){
		throw new Error('A "url" property or function must be specified.');
	},
	eventEmit = io.EventEmitter.prototype.emit,
	ajaxSync = Backbone.sync;

	//... More code ...

	})(Backbone, jQuery, _, io);

We create a function expression, and feed it the expected globals – a reference to Backbone, jQuery, underscore and socket.io, in that order. At the top, we’re reproducing backbone’s standard urlError function, and then we’re saving a reference to io’s emit (more on that in a bit), and to the original, ajax-based sync.

/**
 * Preserve the standard, jquery ajax based persistance method as ajaxSync.
 */
Backbone.ajaxSync = function(method, model, options){
	return ajaxSync.call(this, method, model, options);
};

The comment says it all – we’re preserving the standard sync as ajaxSync, so if we want some collection or model to use the standard persistence method, we can.

We can specify ajaxSync as our preferred sync method when we extend the collection:

var collection = new (Backbone.Collection.extend({url:'/api/url', model:Backbone.Model, sync:Backbone.ajaxSync}))();
/**
 * Replace the standard sync function with our new, websocket/socket.io based solution.
 */
Backbone.sync = function(method, model, options){
	var opts = _.extend({}, options),
	defer = $.Deferred(),
	promise = defer.promise(),
	namespace,
	socket;

	opts.url = (opts.url) ? _.result(opts, 'url') : (model.url) ? _.result(model, 'url') : void 0;
	// If no url property has been specified, throw an error, as per the standard Backbone sync
	if (!opts.url) urlError();
	// Transform the url into a namespace
	namespace = Backbone.Model.prototype.namespace.call(this, opts.url);

So, we ensure options is an object (potentially empty, if no options have been passed to us), create a jQuery Deferred object and it’s promise, and declare namespace and socket for later use. Then, we attempt to determine the url, from the options first or, if not set there, from the model. Note the use of underscore’s result function – url can be either a function returning a string, or a string itself. If no url is found, we throw the backbone standard error. Otherwise, we translate the url into a namespace, using backbone conventions – more on that later.

// Determine what data we're sending, and ensure id is present if we're performing a PATCH call
if (!opts.data && model) opts.data = opts.attrs || model.toJSON(options) || {};
if ((opts.data.id === null || opts.data.id === void 0) && opts.patch === true && model){
	opts.data.id = model.id;
}
// Determine which websocket to use - set in options or on model
socket = opts.socket || model.socket;

Note that we’re deciding what socket to use in the same order as we determined url – first checking the options for the socket to have been passed in with this sync request, then checking the model. If you expect that socket may not be set on either, you may want to add a check at this point. If you intend to use a globally available socket, this is also where you’d add that as a default.

 // Add a listener for our namespaced method, and resolve or reject our deferred based on the response
socket.once(namespace+method, function(res){
	var success = (res && res.success); // Expects server json response to contain a boolean 'success' field
	if (success)
	{
		if (_.isFunction(options.success)) options.success(res);
		defer.resolve(res);
		return;
	}
	if (_.isFunction(options.error)) options.error(res);
	defer.reject(res);
});

Now, we add a listener for our namespace and request method. I’ll go into more detail on this shortly, but the general form will look like url:components:method, e.g. api:json:get or api:json:post.

We expect the server to reply with json that will include a boolean success field, and decide whether to trigger any success or error functions based on whether we’ve received a response and that response indicates success. We at this point also resolve or reject the promise we created earlier, and will return shortly.

An earlier version of the code used the callback method of emitting, that looks like:

socket.emit(namespace+method, opts.data, function(res){ // ... resolve promise ...  }

Why the change? This type of request emits a Data ACK packet, which combines the ack packet (acknowledging the request) with the data expected of the response. It seems straightforward, but the protocol dictates that communication is BLOCKED until the ack packet is received, and now you’ve mandated that the ack packet not get sent until its ready to send the data along with it. Not an issue if you don’t mind serving in order of request, and don’t expect gathering the data to return to take long, but if you want to take advantage of async, out-of-order serving of requests, you need to use the approach we’ve discussed above, which sends an EVENT packet which can be responded to with an immediate ACK, and sent data later when it’s ready.

 // Emit our namespaced method and the model+opts data
socket.emit(namespace+method, opts.data);

// Trigger the request event on the model, as per backbone spec
model.trigger('request', model, promise, opts);
// Return the promise for us to use as per usual (hanging .done blocks off, add to a .when, etc.)
return promise;

Comments say it all for this one.

/**
 * Break url apart to create namespace - every '/' save any pre/post-fixing the url will become a ':' indicating
 * namespace - so a collection that maps to /api/posts will now have its events on the namespace
 * api:posts: (ie. api:posts:create, api:posts:delete, etc.), and a model that maps to /api/posts/21
 * will have events on api:posts:21: (ie. api:posts:21:update, api:posts:21:patch, etc.)
 * @param {string=} url
 */
Backbone.Model.prototype.namespace = function(url){
	url = url || this.url();
	return _.trim(url, '/').replace('/', ':') + ":";
};

This one too, I think. Note, this isn’t a required part of implementing socket.io – it just nicely mirros the style of events that backbone uses internally.

/**
 * Override EventEmitter.emit and SocketNamespace reference for socket.io to add a catch all case for the
 * wildcard ('*') character. Now, socket.on('*') will catch any event, with the name of the caught event
 * passed to the handler as the first argument.
 */
io.EventEmitter.prototype.emit = function(name){
	var args = Array.prototype.slice.call(arguments, 1);

	eventEmit.apply(this, ['*', name].concat(args));
	eventEmit.apply(this, [name].concat(args));
};
io.SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit;

And here’s the reason why we saved the io emit function earlier – this little shim sits in front of the standard emit and allows us to add a wildcard catch-all for events on a socket.

And that’s all folks! Of course, you’ll need to support your implementation on the server-side – how exactly you go about that will depend on the language and library your using.

I do have one more goodie for you, though – here’s a version of the above tailored for those of you using Marionette:

As always, comments are appreciated.

Christopher Keefer

Christopher Keefer

Christopher Keefer is a Senior Software Engineer at Art & Logic. He generally spends his spare time on the computer too, so there isn't much hope for him.
Christopher Keefer

Latest posts by Christopher Keefer (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

3 Comments

  1. Valentin Dubois (@veacks)

    Awesome, exactly what I was looking for.
    I need to try it in my POC for the Marionette App that I’m building.
    Thanks

  2. Enrico

    Uncaught TypeError: Cannot read property ‘prototype’ of undefined

    • Christopher Keefer

      Not really enough details to say for sure, but it sounds like you might be missing one of the required libraries. Make sure you’ve included Backbone (and Marionette if you’re using it), jQuery, underscore, and Socket.io, and that there referenceable globally using whatever variable names you pass into the closure.

Contact Us