Thursday, October 13, 2011

Pre-Fetching Backbone Collections

‹prev | My Chain | next›

Last night I was able to adapt my Backbone.js application to filter and cache appointments based on the month in the URL. If the URL was /month/2011-10 then Octobers appointments were displayed:
The caching solution that I have implemented, although probably not the best in the world, works. Only the current month's appointments are fetched from the server. Navigating to a month that has previously been fetched from the server fetches appointments from cache instead.

It works reasonably well. But... what if there is an appointment on November 1?

It does not show up in the October view even though November 1 does show (albeit grayed out).

Currently, my appointments collection looks like:
      var Collections = (function() {
        var Appointments = Backbone.Collection.extend({
          model: Models.Appointment,
          url: '/appointments',
          cached: {},
          initialize: function(models, options) {
            options || (options = {});
            this.date = options.date;
          },
          fetch: function(options) {
            options || (options = {});

            if (this.cached[this.date]) {
              return this.triggerCacheHits();
            }

            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);
          },
          // ...
        });
It occurs to me that fetch() is doing a lot of work here. It is checking for cache hits. It builds query string (data) parameters to instruct the server to return only a single month's worth of data. It adds the fetched data (rather than resetting). Finally it fetches the data.

Perhaps I can get closer to my goal of pre-fetch bordering months and clean things up a bit at the same time. Refactoring typical object-oriented code involves extracting functionality out into methods. Refactoring Backbone code normally involves refactoring out into sub-collections (or sub-models or sub-views). So I try creating a new month collection that is solely responsible for fetching a month's worth of data from the server:
        var MonthAppointments = Backbone.Collection.extend({
          model: Models.Appointment,
          url: '/appointments',
          initialize: function(models, options) {
            options || (options = {});
            this.date = options.date;
          },
          fetch: function(options) {
            options || (options = {});

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

            return Backbone.Collection.prototype.fetch.call(this, options);
          },
          parse: function(response) {
            return _(response.rows).map(function(row) { return row.value ;});
          }
        });
The parent collection now needs to tell the month collection to fetch its data. Once fetched, the parent view needs to add the fetched data to its own internal store so that attached views can render the data:
        var Appointments = Backbone.Collection.extend({
          // ....
          fetch: function(options) {
            options || (options = {});
            var date = options.date || this.date;

            if (this.cached[date]) {
              return this.triggerCacheHits();
            }

            var month =
              this.cached[date] =
              new MonthAppointments(undefined, {date: date});

            var that = this;
            month.fetch({
              success: function(collection, resp) {
                that.add(collection.models);
                that.fetch({date: Helpers.previousMonth(date)});
              }
            });
          },
          // ...
        });
And that actually works! And I would totally show you a screen shot here if my browser were not running amok requesting appointments from the 1800s right now. Really, I only want to pre-fetch one month prior, but I wind up recursively calling fetch ad infinitum. To prevent this, I add a prefetch boolean:
          fetch: function(options) {
            options || (options = {});
            var date = options.date || this.date;

            if (this.cached[date]) {
              return this.triggerCacheHits();
            }

            var month =
              this.cached[date] =
              new MonthAppointments(undefined, {date: date});

            var that = this;
            month.fetch({
              success: function(collection, resp) {
                that.add(collection.models);
                if (!options.prefetch) {
                  that.fetch({
                    date: Helpers.previousMonth(date),
                    prefetch: true
                  });
                }
              }
            });
          },
And now I can show a screenshot:

The appointment from the end of October shows up in the November calendar as desired thanks to prefetching. That will serve as a stopping point for tonight. Tomorrow I will continue to explore this solution. Or I might just skip ahead to cache invalidation. Fun.



Day #173

No comments:

Post a Comment