Monday, September 16, 2013

Integrating Socket.IO into a BackboneJS Project

There's a lot of opportunities to add real-time features to an application. Since Backbone already has an excellent notification layer built into its models, it seems only natural to apply incoming changes from a Socket.IO connection to a view's bound model or collection. Instead of fetching all the data periodically, the server pushes changes down to your application and you apply them to the data.

As a simple example, suppose our server will emit a "changed" event when something important changes in the data we're displaying. The data sent in the event contains the pertinent record we have in the model/collection. In this case, the model has a few fields and contains a collection of "places" that could be added to as part of the update.

Before applying the changes, we need to setup our connection and listen to the event. One thing I like to do is funnel all the events I care about in a view through the built-in events hash. This is one of the fundamental concepts I've been tinkering with in Backbone.Fiber because the events hash makes it easy to organize all the events in the application and ensure that "this" is actually in the view's scope. So for Socket.IO, I create a namespace for all of its events under ".socket":


   events: {
     ...

     'changed.socket': function( data ) {
        this.applyUpdates( data );
     },

     ...



And, as part of my initialization, create the socket and bind the functions from the events hash with the ".socket" namespace to the socket's on method:


   initialize: function() {

      var self = this;

      this.socket = io.connect();

      _.each( this.events, function( method, event ) {

         var idx;

         if ( (idx = event.indexOf( '.socket' ) ) > -1 ) {

         method = $.isFunction( method ) ? method : self[method];
         self.socket.on( event.slice( 0, idx ), _.bind( method, self ) );

         }
      });

   }



Now that the event is wired up, I can focus on handling the data changes. I typically write a single function that all the Socket.IO events will call to push any changes into the models and collections. This allows me to handle all the scenarios that may occur in one place:


   applyUpdates: function( data ) {
      
      var places = data.places;

      if ( data.places ) {
         delete data.places;
         
         this.model.places.add( places );
      }
      
      this.model.reset();
      this.model.set( data );

   } 



Here, I'm interpreting the lack of a places array as meaning that there are no new places to add. I don't want to reset the collection with nothing just because its not in the update so I handle it separately and then remove it from the object before updating the model attributes. A problem that I encountered when trying to apply multiple updates to my model was Backbone would hold the state of the model as dirty and stop firing events. I don't need to save this data because its already persisted, however, I do want the downstream view rendering to execute because of the change. To address this issue, I added a reset function to my model which set the internal _changing flag back to false:


   Backbone.Model.extend({

      ...

      reset: function() {
         this._changing = false;
      }

      ...

   });   



This allowed the changes to be applied and the events to fire as expected. One nice feature of using set is that you don't need a complete model. The Socket.IO event can send changes to one or all attributes and they'll be merged into the current model's set of fields. As part of my initialization process, I make sure I get a full copy of the model in its current state and then know that all changes from that point forward are reflecting what's different since I last pulled the entire model. Something to consider when using the reset is that if you do need to modify the data, you have to be careful that an event doesn't come through that resets the state so it does not appear there are any changes to save. Of course, dealing with concurrent updates to the same data object is a whole other subject which I'm not going to delve into here.