Sunday, October 16, 2011

Updating a Backbone.js Collection Cache

‹prev | My Chain | next›

I have myself an intriguing Backbone.js collection caching solution that I have built up over the past few days. It relies on a master collection that holds the models that are ultimately displayed in views. Actually fetching the data from the server is delegated to sub-collections.

Tonight, I hope to get things set up such that once the cached data is displayed, my application makes a call to the server and replaces the displayed cached content in my calendar:
The overall structure that accomplishes this is a nested set of fetch methods:
        var Appointments = Backbone.Collection.extend({
          model: Models.Appointment,
          url: '/appointments',
          cached: {},
          initialize: function(models, options) { /* ... */ },
          fetch: function(options) { /* ... */ },
          fetchFromCache: function(options) { /* ... */ },
          fetchFromServer: function(options) { /* ... */ },
          // ...
        });
The fetch() method calls fetchFromCache(). The fetchFromCache() method calls fetchFromServer() (if the data has not already been fetched).

The fetchFromCache() is where I will focus tonight. If the appointment data for the currently displayed month is already in the collection, it simply fires the "cached" event:
var Appointments = Backbone.Collection.extend({
          model: Models.Appointment,
          url: '/appointments',
          cached: {},
          initialize: function(models, options) { /* ... */ },
          fetch: function(options) { /* ... */ },
          fetchFromCache: function(options) {
            var success = options.success;
            options.success = _.bind(function() {
              this.triggerCacheHits(options.date)
              success();
            }, this);

            if (this.cached[options.date]) {
              options.success();
            }
            else {
              this.fetchFromServer(options);
            }
          },
          fetchFromServer: function(options) { /* ... */ },
          // ...
        });
This seems a fine place to add a refreshCache() call:
// ...
          fetchFromCache: function(options) {
            var success = options.success;
            options.success = _.bind(function() {
              this.triggerCacheHits(options.date)
              success();
            }, this);

            if (this.cached[options.date]) {
              options.success();
              this.refreshCache(options.date);
            }
            else {
              this.fetchFromServer(options);
            }
          },
//...
When the data is fetched from the server, my primary appointments collection squirrels away a reference to the sub-collection:
//...
          fetchFromServer: function(options) {
            var subcollection =
              this.cached[options.date] =
              (new MonthAppointments());
            // ...
          }
//...
So the easiest thing that could possibly work is to reset that sub-collection inside refreshCache():
          refreshCache: function(date) {
            console.log("[refreshCache] " + date);
            this.cached[date].fetch();          
          }
If I load November's appointments, then navigate to the previous month, I should see a cache refresh of October's data:
October is refreshed even though November was previously displayed because November's adjoining months (October and December) were pre-fetched. In fact November and September are pre-fetched now that I have navigated to October. This caching and pre-fetching is starting to get confusing.

Anyhow, I think I still have a handle on things. The re-fetch() of the October sub-collection is definitely going out because there is an XHR in Chrome's Javascript console.

Nothing happens in the UI, though. For that to happen, I need to actually replace the existing records. I already have a addFromSubCollection method. If I invoke that in my success callback, it ought to do something:
          refreshCache: function(date) {
            console.log("[refreshCache] " + date);
            this.cached[date].fetch({
              success: _.bind(function(collection, resp) {
                this.addFromSubCollection(collection);
              }, this)
            });
          },
But, of course, that simply adds the appointments to the calendar, duplicating the appointments already in place:
No, I need to replace each model in the collection. Unfortunately, replace() is not a method on a Backbone collection. So I have to make my own:
          replace: function(models) {
            if (_.isArray(models)) {
              for (var i = 0, l = models.length; i < l; i++) {
                this._replace(models[i]);
              }
            } else {
            this._replace(models);
            }
            return this;
          },
          _replace: function(model) {
            var existing = this.get(model);
            existing.set(model.toJSON());
            return existing;
          },
          // ...
This replace / _replace method pair mimics similar pairs in Backbone itself (e.g. add / _add). For each model in the newly fetched sub-collection, _replace gets the existing one and sets the attributes based on what was fetched from the server.

The refreshCache() method then becomes:
          refreshCache: function(date) {
            console.log("[refreshCache] " + date);
            this.cached[date].fetch({
              success: _.bind(function(collection, resp) {
                this.replace(collection.models);
              }, this)
            });
          },
And that actually works.

Of course, I am not handling deletes from cache or additions. I will pick back up there tomorrow. Ugh. Caching.


Day #176

No comments:

Post a Comment