One advantage of using the MVC and Binding patterns is that making your application realtime is a piece of cake. All the server needs to do is inform client-side models of data changes, and the client's interface will be updated automatically. We can do this easily using a project I wrote called Juggernaut.

Juggernaut is basically realtime PubSub for web apps. Browsers subscribe to channels, and servers publish to them. Juggernaut will do the rest, negotiating the best type of realtime connection with the server dependent on browser support. It'll first try WebSockets, and then fallback to Comet and polling.

I've built an application demonstrating this and you can also see a live example here.

There's also a short screencast explaining how to integrate Spine with Pusher, a hosted WebSocket server.

Usage

Let's implement a Juggernaut handler. It'll subscribe to the /observer channel, and then process observer events. During processing, it tries to find the model the message is associated with, then creates, updates or destroys records as necessary.

//= CoffeeScript
#= require juggernaut

$ = jQuery

class JuggernautHandler
  constructor: (@options = {}) ->
    @jug = new Juggernaut(@options)
    @jug.subscribe '/observer', @processWithoutAjax

  process: (msg) =>
    klass = window[msg.class]
    throw 'Unknown class' unless klass
    switch msg.type
      when 'create'
        klass.create msg.record unless klass.exists(msg.record.id)
      when 'update'
        klass.update msg.id, msg.record
      when 'destroy'
        klass.destroy msg.id
      else
        throw 'Unknown type:' + type

  processWithoutAjax: =>
    args = arguments
    Spine.Ajax.disable =>
      @process(args...)

$ -> new JuggernautHandler(host: 'localhost', port: 8080)

//= JavaScript
var JuggernautHandler = Spine.Class.sub({
  init: function(options) {
    if ( !options ) options = {};
    this.jug = new Juggernaut(options);        
    this.jug.subscribe('/observer', this.proxy(this.processWithoutAjax));
  },

  process: function(msg){
    var klass;
    klass = window[msg["class"]];
    if ( !klass ) throw 'Unknown class';
    switch (msg.type) {
      case 'create':
        if (!klass.exists(msg.record.id)) {
          return klass.create(msg.record);
        }
        break;
      case 'update':
        return klass.update(msg.id, msg.record);
      case 'destroy':
        return klass.destroy(msg.id);
      default:
        throw 'Unknown type:' + type;
    }
  },

  processWithoutAjax: function(){
    var args;
    args = arguments;
    return Spine.Ajax.disable(this.proxy(function() {
      return this.process.apply(this, args);
    }));
  }
});

jQuery(function(){ new JuggernautHandler({host: 'localhost', port: 8080}); });

That's pretty straightforward. So, the messages we broadcast to Juggernaut need to look like this:

{
  "type": "create",
  "class": "Page",
  "id": "1",
  "record": {"name": "First one!"}
}

We can create this server-side, broadcasting it to Juggernaut whenever a record changes. For example, we could use Juggernaut's Ruby adapter in Rails to integrate with ActiveRecord models. Here we're using an observer to record whenever the Page model changes.

class JuggernautObserver < ActiveRecord::Observer
  observe :page

  def after_create(rec)
    publish(:create, rec)
  end

  def after_update(rec)
    publish(:update, rec)
  end

  def after_destroy(rec)
    publish(:destroy, rec)
  end

  protected
    def publish(type, rec)
      Juggernaut.publish(
        "/observer",
        { 
          type:   type, 
          id:     rec.id, 
          class:  rec.class.name, 
          record: rec
         }
      )
    end
end

Once the record has changed, the publish method is called. This method publishes a message to the /observer channel, pushing it out to all the connected clients.

Next steps

For more information on Juggernaut, and its installation, please see the project's README. I also recommend checking out the source of the example Rails & Spine integration application, as this uses Juggernaut for its realtime models.