blog

Websockets Plus Backbone

Websockets for Backbone

by

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:

/**
 * Copyright (c) Christopher Keefer. All Rights Reserved.
 *
 * Overrides the default transport for Backbone syncing to use websockets via socket.io.
 */
(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;
    /**
     * Preserve the standard, jquery ajax based persistance method as ajaxSync.
     */
    Backbone.ajaxSync = function(method, model, options){
        return ajaxSync.call(this, method, model, options);
    };
    /**
     * 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);
        // 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;
        // 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);
        });
        // 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;
    };
    /**
     * 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('/', ':') + ":";
    };
    /**
     * 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;
})(Backbone, jQuery, _, io);

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:

/**
 * Copyright (c) Christopher Keefer. All Rights Reserved.
 *
 * Overrides the default transport for Backbone syncing to use websockets via socket.io. Includes marionette
 * convenience code, specifically for sending socket-related events along the global event aggregator.
 */
(function(app, Backbone, Marionette, $, _, io){
    var urlError = function(){
        throw new Error('A "url" property or function must be specified.');
    },
        eventEmit = io.EventEmitter.prototype.emit,
        ajaxSync = Backbone.sync;
    /**
     * Preserve the standard, jquery ajax based persistance method as ajaxSync.
     */
    Backbone.ajaxSync = function(method, model, options){
        return ajaxSync.call(this, method, model, options);
    };
    /**
     * 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();
        namespace = Backbone.Model.prototype.namespace.call(this, opts.url);
        // 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, on model, or on global app
        socket = opts.socket || model.socket || app.socket;
        // Trigger the app event aggregator for interested listeners to know we're about to request data via websocket
        app.vent.trigger('backbone:request', namespace);
        // 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);
            // Trigger the app event aggregator to indicate we've received a return from the server, and success
            app.vent.trigger('backbone:receive', namespace, success);
            if (success)
            {
                if (_.isFunction(options.success)) options.success(res);
                defer.resolve(res);
                return;
            }
            if (_.isFunction(options.error)) options.error(res);
            defer.reject(res);
        });
        // 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;
    };
    /**
     * 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('/', ':') + ":";
    };
    /**
     * 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;
    /**
     * Create a socket io instance that will echo all events into the application event aggregator, so that
     * collections, models, etc. can listen on app.vent for their events.
     * @param {string=} url
     * @constructor
     */
    Marionette.Application.prototype.SocketIO = function(url){
        var socket = io.connect(url, {
            transports:['websocket']
        });
        /**
         * On any event from the server, trigger it on the app event aggregator. The first
         * argument will always be the name of the event.
         */
        socket.on('*', function(){
            var args = Array.prototype.slice.call(arguments, 0);
            app.vent.trigger(args[0], args.slice(1));
        });
        /**
         * On error, trigger the socket:error event on the global event aggregator for
         * interested listeners.
         */
        socket.on('error', function(err){
            app.vent.trigger('socket:error', err);
        });
        return socket;
    }
})(app /* replace with your global app object */, Backbone, Backbone.Marionette, jQuery, _, io);

As always, comments are appreciated.

+ more