Monday, October 17, 2011

Backbone.js Collection Cache Invalidation

‹prev | My Chain | next›

I have been experimenting with caching Backbone.js collection caching for the past few days. The general idea being the caching collection delegates actual fetching to sub-collections. In my calendar application, the month sub-collection fetches data into the caching collection:
Last night, I got the calendar to render any cached data immediately, then re-fetch the data in the background, re-rendering if needed.

I am somewhat concerned that this exercise can quickly devolve into an exploration of caching rather than Backbone. Given that, I am unsure if I want to build a complete caching solution. But before I give up, I have to explore adding and removing from caching. That is, if the fetch-after-rendering-cached data turns up a new record, that new record should be displayed. Similarly, records deleted should be removed from the display.

For that, I add two more methods to the caching collection's refreshCache() method. One method will create newly found models in the collection. The other will remove now missing models from the collection. The two methods will be named createFound() and removeMissing():
          refreshCache: function(date) {
            console.log("[refreshCache] " + date);
            this.cached[date].fetch({
              success: _.bind(function(collection, resp) {
                this.replace(collection.models);
                this.createFound(collection);
                this.removeMissing(collection);
              }, this)
            });
          },
I leave removeMissing() an empty method for now, so that I can start on createFound(). "Found" records will be those whose IDs are in the updated sub-collection, but not in the current cache. To calculate that, I employ pluck() and difference from Underscore.js (pluck() is also in Backbone):
          createFound: function(updated_subcollection) {
            var found_ids = _.difference(
              updated_subcollection.pluck("id"),
              this.existingIds()
            );
            console.log("[createFound] " + found_ids.join(", "));
            console.log("[createFound] " + updated_subcollection.pluck("id").join(", "));
          },
To test this out, I open an incognito window to create a new appointment:
Then, in my usual window, I navigate back to October. Checking the Javascript console in Chrome, I see that I have found the newly created appointment:
To actually get that added to the collection, I need to iterate through each found ID, pull the model from the sub-collection and add it to the caching collection. Thanks to underscore, the necessary code reads almost like that sentence:
          createFound: function(updated_subcollection) {
            var found_ids = _.difference(
              updated_subcollection.pluck("id"),
              this.existingIds()
            );
            console.log("[createFound] " + found_ids.join(", "));

            _.each(found_ids, function(id) {
              var new_model = updated_subcollection.get(id);
              this.add(new_model);
            }, this);
          },
Now, if I delete the "New #1" appointment, repeat the icognito create, and navigate back to October, I see the "New #2" appointment added to the calendar:
Ah, the "magic" of Backbone. Since appointment views are already bound to the "add" event, I do not need to do anything for the appointment to show after it is added to the collection:
          initialize_appointment_views: function() {
            this.collection.
              bind('add', _.bind(this.render_appointment, this));
            this.collection.
              bind('reset', _.bind(this.render_appointment_list, this));
          }
For completeness, this is my removeMissing() method:
          removeMissing: function(updated_subcollection) {
            if (updated_subcollection.length == 0) return;

            var startDate = updated_subcollection.at(0).get("startDate"),
                month = startDate.substr(0,7),
                ids_in_month = this.
                  select(function(model) {
                    return model.get("startDate").indexOf(month) == 0;
                  }).
                  map(function(model) {
                    return model.id;
                  }),
                missing_ids = _.difference(
                  ids_in_month,
                  updated_subcollection.pluck("id")
                );
            console.log("[removeMissing] " + missing_ids.join(", "));

            this.remove(missing_ids);
          }
This is complicated by the need to first filter only this month's IDs from the cached collection. I still plow through the implementation because, what can I say? I get a kick out of mucking with iterators.

I enjoyed myself immensely playing with underscore. I did not even really need it as much as I used it, but that is just a testament to how fun Underscore is. Anyhow, that may conclude my caching experiment. Up tomorrow, I have a routing bug that can only be solved with the help of a recipe from Recipes with Backbone!


Day #177

No comments:

Post a Comment