Monday, October 10, 2011

Caching Backbone.js Models

‹prev | My Chain | next›

Mostly satisfied with my solution for filtering Backbone.js collections, I set out tonight to begin exploring caching. With filtering in place, only the appointments for October are retrieved from the server when displaying the month view for October:
Clicking "next" redraws the calendar, fetches the November appointments from the server and draws them on the calendar:
Now, if the user clicks on "previous", the calendar redraws and re-fetches the October appointment collection from the server. There is no need for this round-trip—the browser just had the October appointments. So let's see if I can cache appointments.

Since I am already overriding the collection's fetch() method (to retrieve per-month slices), this seems like a good place to start. Backbone collections reset by default when fetching. To add to the existing collection, I need to set the add option to true:
        var Appointments = Backbone.Collection.extend({
          // ...
          fetch: function(options) {
            options || (options = {});

            // only fetch appointments from this month
            var data = (options.data || {});
            options.data = {date: this.date};

            // Add fetched appointments to the existing list (don't reset)
            options.add = true;

            return Backbone.Collection.prototype.fetch.call(this, options);
          },
          // ...
        });
That is not quite the end of the story. If I load October, then navigate to November and back to October, Backbone will fetch October, then November and then October again:
My first attempt at caching will store an internal cache attribute. Every time a month is fetched, I stored it in the cache. If the month is already cached, I short-circuit the fetch() entirely:
        var Appointments = Backbone.Collection.extend({
          // ...
          cached: {},
          // ...
          fetch: function(options) {
            options || (options = {});

            if (this.cached[this.date]) {
              if (options.success) options.success(this, "");
              return;
            }

            this.cached[this.date] = true;

            // only fetch appointments from this month
            var data = (options.data || {});
            options.data = {date: this.date};

            // Add fetched appointments to the existing list (don't reset)
            options.add = true;

            return Backbone.Collection.prototype.fetch.call(this, options);
          },
          // ...
        });
And that works! To a point...

Indeed, previously fetched months are not re-requested from the server. The problem is that appointments are no longer drawn on the calendar:
I cannot quite figure out what is missing at here. Do I need to trigger model events? Collection events? I try a few, but am unable to get the magic combination to get the appointments to redraw. I will pick back up here tomorrow.
Update: I get this somewhat solved by triggering "change" events for models in the current month and "destroy" event for other models:
// ...
          fetch: function(options) {
            options || (options = {});

            if (this.cached[this.date]) {
              var date = this.date;
              _.each(this.models, function(m) {
                if (!m.get("startDate")) return;
                if (m.get("startDate").indexOf(date) > -1) {
                  m.trigger('change', m, this, options);
                }
                else {
                  m.trigger('destroy', m, this, options);
                }
              });
              return;
            }

            this.cached[this.date] = true;

            // only fetch appointments from this month
            var data = (options.data || {});
            options.data = {date: this.date};

            // Add fetched appointments to the existing list (don't reset)
            options.add = true;

            return Backbone.Collection.prototype.fetch.call(this, options);
          },
// ...
I have change and destroy handlers already bound in my Appointment views, which is why this works. However, because I am explicitly triggering events for which my application already listens, this is not a general purpose solution. I am also seeing some wonkiness when repeatedly moving backwards and forwards (I seem to lose one model each time). Ah well, this is a start. I will try to see the tale through to an end tomorrow.


Day #170

1 comment:

  1. Hey, thanks for the post. I'm not quite clear how you're putting data into your cache after the data is fetched, but reading your code gave me the inspiration to do it in the success call back... something like this:



    initialize: function(){
    this.cache = {};
    },
    fetch: function(options){
    var tempData = options.originalData ? options.originalData : options.data;
    // check the cache for existing entry with data.searchTerm and then RESET(itsJSON)
    if(this.cache[tempData.searchTerm]){
    this.reset(this.cache[tempData.searchTerm]);
    if(options.success){
    options.success(this, "");
    }
    }else{
    // override the success function to add the collection to the cache as JSON
    var tempSuccess = options.success,
    tempThis = this;
    options.success = function(collection, result){
    tempThis.cache[tempData.searchTerm] = collection.toJSON();
    if(tempSuccess){
    tempSuccess(collection, result);
    }
    };
    options.originalData = options.data;
    options.data = $.param(options.data);
    Backbone.Collection.prototype.fetch.call(this, options);
    }


    Hope that this may be helpful to your readers.
    Cheers,

    ReplyDelete