‹prev | 
My Chain | 
next›
To date, I have enjoyed decent success replacing the persistence layer in my 
Backbone.js calendar application.  With only a few hiccups, I have replaced the normal REST-based persistence layer with 
faye pub-sub channels.  The hope is that this will make it easier to respond to asynchronous change from other sources.
So far, I am able to fetch and populate a calendar appointment collection. I can also create new appointments.  Now, I really need to get deleting appointments working:
Thanks to  replacing 
Backbone.sync(), in my Backbone application, I already have delete requests being sent to the 
/caledars/delete faye channel:
    var faye = new Faye.Client('/faye');
    Backbone.sync = function(method, model, options) {
      faye.publish("/calendars/" + method, model);
    }
Thanks to simple client logging:
    _(['create', 'update', 'delete', 'read', 'changes']).each(function(method) {
      faye.subscribe('/calendars/' + method, function(message) {
        console.log('[/calendars/' + method + ']');
        console.log(message);
      });
    });
...I can already see that, indeed, deletes are being published as expected:
To actually delete things from my 
CouchDB backend, I have to subscribe to the 
/calendars/delete faye channel in my 
express.js server.  I already have this working for adding appointments, so I can adapt the overall structure of the 
/calendars/add listener for 
/calendars/delete:
client.subscribe('/calendars/delete', function(message) {
  // HTTP request options
  var options = {...};
  // The request object
  var req = http.request(options, function(response) {...});
  // Rudimentary connection error handling
  req.on('error', function(e) {...});
  // Send the request
  req.end();
});The HTTP options for the DELETE request are standard 
node.js HTTP request parameters:
  // HTTP request options
  var options = {
    method: 'DELETE',
    host: 'localhost',
    port: 5984,
    path: '/calendar/' + message._id,
    headers: {
      'content-type': 'application/json',
      'if-match': message._rev
    }
  };
Experience has taught me that I need to send the CouchDB revisions along with operations on existing records.  The 
if-match HTTP header ought to work.  The 
_rev attribute on the record/message sent to 
/calendars/delete holds the latest revision that the client holds.  Similarly, I can delete the correct object from CouchDB by specifying the object ID in the 
path attribute—the ID coming from the 
_id attribute of the record/message.
That should be sufficient to delete the record from CouchDB.  To tell my Backbone app to remove the deleted element from the UI, I need to send a message back on a separate faye channel.  The convention that I have been following is to send requests to the server on a channel named after a CRUD operation and to send responses back on a channel named after the Backbone collection method to be used.  In this case, I want to send back the deleted record on the 
/calendars/remove channel.
To achieve this, I do the normal node.js thing of passing an accumulator callback to 
http.request().  This accumulator callback accumulates chunks of the reply into a local 
data variable.  When all of the data has been received, the response is parsed as JSON (of course it's JSON—this is a CouchDB data store), and the JSON object is sent back to the client:
  var req = http.request(options, function(response) {
    console.log("Got response: %s %s:%d%s", response.statusCode, options.host, options.port, options.path);
    // Accumulate the response and publish when done
    var data = '';
    response.on('data', function(chunk) { data += chunk; });
    response.on('end', function() {
      var couch_response = JSON.parse(data);
      console.log(couch_response)
      client.publish('/calendars/remove', couch_response);
    });
  });
Before hooking a Backbone subscription to this 
/calendars/remove channel, I supply a simple logging tracer bullet:
    faye.subscribe('/calendars/remove', function(message) {
      console.log('[/calendars/remove]');
      console.log(message);
    });
So, if I have done this correctly, clicking the delete icon in the calendar UI should send a message on the 
/calendars/delete channel (which we have already seen working above).  The faye subscription on the server should be able to use this to remove the object from the CouchDB database.  Finally, this should result in another message being broadcast on the 
/calendars/remove channel.  This 
/calendars/remove message should be logged in the browser.  So let's see what actually happens...
Holy cow!  That actually worked.  
If I reload the web page, I see one less fake appointment on my calendar.  Of course, I would like to have the appointment removed immediately.  Could it be as simple as sending that JSON message as an argument to the 
remove() method of the collection?
    faye.subscribe('/calendars/remove', function(message) {
      console.log('[/calendars/remove]');
      console.log(message);
      calendar.appointments.remove(message);
    });
Well, no.  It is not that simple.  That has no effect on the page.  But wait...
After digging through the Backbone code a bit, it ought to work.  The 
remove() method looks up models to be deleted via 
get():
    _remove : function(model, options) {
      options || (options = {});
      model = this.getByCid(model) || this.get(model);
      // ...
    }
The 
get() method looks up models by the 
id attribute:
    // Get a model from the set by id.
    get : function(id) {
      if (id == null) return null;
      return this._byId[id.id != null ? id.id : id];
    },
The 
id attribute was set on the message/record received on the 
/calendars/remove channel:
So why is the UI not being updated by removing the appointment from the calendar?
After a little more digging, I realize that it is being removed from the collection, but the necessary events are not being generated to trigger the Backbone view to remove itself.  The events that the view currently subscribes to are:
        var Appointment = Backbone.View.extend({
          initialize: function(options) {
            // ....
            options.model.bind('destroy', this.remove, this);
            options.model.bind('error', this.deleteError, this);
            options.model.bind('change', this.render, this);
          },
          // ...
        });
It turns out that the 
destroy event is only emitted if the default 
Backbone.sync is in place.  If the XHR DELETE is successful, a 
success callback is fired that emits 
destroy.  Since I have replaced 
Backbone.sync, that event never fires.
What 
does fire is the 
remove event.  So all I need to change in order to make this work is to replace 
destroy with 
remove:
        var Appointment = Backbone.View.extend({
          initialize: function(options) {
            // ....
            options.model.bind('remove', this.remove, this);
            options.model.bind('error', this.deleteError, this);
            options.model.bind('change', this.render, this);
          },
          // ...
        });
And it works! I can now remove all of those fake appointments:
Ah.  Much better.
It took a bit of detective work, but, in the end, not much really needed to change.
I really need to cut back on my chain posts so that I can focus on writing 
Recipes with Backbone.  So I sincerely hope that lessons learned tonight will make updates easier tomorrow.  Fingers crossed.
Day #147