diff options
author | Garren Smith <garren.smith@gmail.com> | 2014-02-04 16:47:54 +0200 |
---|---|---|
committer | Garren Smith <garren.smith@gmail.com> | 2014-02-04 16:47:54 +0200 |
commit | ed89f34020a10259344ded418785a4492159c23f (patch) | |
tree | a2b6caf51b8eff5426d09454db5a9a1091c9fd45 | |
parent | 34aabbfb58ca4f0ed60a20c466b6bf544b7dc28f (diff) | |
download | couchdb-ed89f34020a10259344ded418785a4492159c23f.tar.gz |
Add backbone.fetch-cache
-rw-r--r-- | src/fauxton/app/addons/databases/resources.js | 8 | ||||
-rw-r--r-- | src/fauxton/app/addons/documents/resources.js | 14 | ||||
-rw-r--r-- | src/fauxton/app/addons/documents/routes.js | 2 | ||||
-rw-r--r-- | src/fauxton/app/addons/documents/views.js | 22 | ||||
-rw-r--r-- | src/fauxton/app/config.js | 3 | ||||
-rw-r--r-- | src/fauxton/app/core/base.js | 27 | ||||
-rw-r--r-- | src/fauxton/assets/js/plugins/backbone.fetch-cache.js | 311 |
7 files changed, 365 insertions, 22 deletions
diff --git a/src/fauxton/app/addons/databases/resources.js b/src/fauxton/app/addons/databases/resources.js index 1b55f8885..5fa931b00 100644 --- a/src/fauxton/app/addons/databases/resources.js +++ b/src/fauxton/app/addons/databases/resources.js @@ -24,7 +24,7 @@ function(app, FauxtonAPI, Documents) { Databases.DocLimit = 20; - Databases.Model = Backbone.Model.extend({ + Databases.Model = FauxtonAPI.Model.extend({ initialize: function(options) { this.status = new Databases.Status({ database: this @@ -82,7 +82,7 @@ function(app, FauxtonAPI, Documents) { } }); - Databases.Changes = Backbone.Collection.extend({ + Databases.Changes = FauxtonAPI.Collection.extend({ initialize: function(options) { this.database = options.database; @@ -110,7 +110,7 @@ function(app, FauxtonAPI, Documents) { } }); - Databases.Status = Backbone.Model.extend({ + Databases.Status = FauxtonAPI.Model.extend({ url: function() { return app.host + "/" + this.database.safeID(); }, @@ -166,7 +166,7 @@ function(app, FauxtonAPI, Documents) { }); // TODO: shared databases - read from the user doc - Databases.List = Backbone.Collection.extend({ + Databases.List = FauxtonAPI.Collection.extend({ model: Databases.Model, documentation: function(){ return "all_dbs"; diff --git a/src/fauxton/app/addons/documents/resources.js b/src/fauxton/app/addons/documents/resources.js index 831d5e3d0..adfee1f16 100644 --- a/src/fauxton/app/addons/documents/resources.js +++ b/src/fauxton/app/addons/documents/resources.js @@ -18,7 +18,7 @@ define([ function(app, FauxtonAPI) { var Documents = FauxtonAPI.addon(); - Documents.Doc = Backbone.Model.extend({ + Documents.Doc = FauxtonAPI.Model.extend({ idAttribute: "_id", documentation: function(){ return "docs"; @@ -194,7 +194,7 @@ function(app, FauxtonAPI) { } }); - Documents.DdocInfo = Backbone.Model.extend({ + Documents.DdocInfo = FauxtonAPI.Model.extend({ idAttribute: "_id", documentation: function(){ return "docs"; @@ -224,7 +224,7 @@ function(app, FauxtonAPI) { }); - Documents.ViewRow = Backbone.Model.extend({ + Documents.ViewRow = FauxtonAPI.Model.extend({ // this is a hack so that backbone.collections doesn't group // these by id and reduce the number of items returned. idAttribute: "_id", @@ -272,7 +272,7 @@ function(app, FauxtonAPI) { }); - Documents.AllDocs = Backbone.Collection.extend({ + Documents.AllDocs = FauxtonAPI.Collection.extend({ model: Documents.Doc, documentation: function(){ return "docs"; @@ -397,7 +397,7 @@ function(app, FauxtonAPI) { } }); - Documents.IndexCollection = Backbone.Collection.extend({ + Documents.IndexCollection = FauxtonAPI.Collection.extend({ model: Documents.ViewRow, documentation: function(){ return "docs"; @@ -527,7 +527,7 @@ function(app, FauxtonAPI) { // we can get the request duration fetch: function () { this.startTime = new Date().getTime(); - return Backbone.Collection.prototype.fetch.call(this); + return FauxtonAPI.Collection.prototype.fetch.call(this); }, allDocs: function(){ @@ -568,7 +568,7 @@ function(app, FauxtonAPI) { }); - Documents.PouchIndexCollection = Backbone.Collection.extend({ + Documents.PouchIndexCollection = FauxtonAPI.Collection.extend({ model: Documents.ViewRow, documentation: function(){ return "docs"; diff --git a/src/fauxton/app/addons/documents/routes.js b/src/fauxton/app/addons/documents/routes.js index 1510485ee..be9ce2f7c 100644 --- a/src/fauxton/app/addons/documents/routes.js +++ b/src/fauxton/app/addons/documents/routes.js @@ -182,7 +182,7 @@ function(app, FauxtonAPI, Documents, Databases) { }, establish: function () { - return this.data.designDocs.fetch(); + return this.data.designDocs.fetchOnce(); }, allDocs: function(databaseName, options) { diff --git a/src/fauxton/app/addons/documents/views.js b/src/fauxton/app/addons/documents/views.js index 298cfb4d7..547620330 100644 --- a/src/fauxton/app/addons/documents/views.js +++ b/src/fauxton/app/addons/documents/views.js @@ -1803,11 +1803,31 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum Views.Changes = FauxtonAPI.View.extend({ template: "addons/documents/templates/changes", + initialize: function () { + var that = this; + this.listenTo( this.model.changes, 'change', function () { + console.log('render on change'); + that.render(); + }); + this.listenTo( this.model.changes, 'cachesync', function () { + console.log('render on cachesync'); + that.render(); + }); + }, + establish: function() { - return [ this.model.changes.fetch()]; + return [ this.model.changes.fetchOnce({prefill: true, + success: function () { + console.log('hi ajax success'); + }, + prefillSuccess: function () { + console.log('hi prefill success'); + } + })]; }, serialize: function () { + console.log('ss'); return { changes: this.model.changes.toJSON(), database: this.model diff --git a/src/fauxton/app/config.js b/src/fauxton/app/config.js index 98be9c6a8..a5d764f8c 100644 --- a/src/fauxton/app/config.js +++ b/src/fauxton/app/config.js @@ -30,7 +30,8 @@ require.config({ spin: "../assets/js/libs/spin.min", d3: "../assets/js/libs/d3", "nv.d3": "../assets/js/libs/nv.d3", - "ace":"../assets/js/libs/ace" + "ace":"../assets/js/libs/ace", + "backbone.fetch-cache": "../assets/js/plugins/backbone.fetch-cache" }, baseUrl: '/', diff --git a/src/fauxton/app/core/base.js b/src/fauxton/app/core/base.js index 53316bccf..6fe32618f 100644 --- a/src/fauxton/app/core/base.js +++ b/src/fauxton/app/core/base.js @@ -12,10 +12,12 @@ define([ "backbone", - "plugins/backbone.layoutmanager" + "plugins/backbone.layoutmanager", + "backbone.fetch-cache" ], -function(Backbone) { +function(Backbone, LayoutManager, BackboneCache) { + console.log(BackboneCache); var FauxtonAPI = { //add default objects router: { @@ -67,16 +69,25 @@ function(Backbone) { } }); + FauxtonAPI.Model = Backbone.Model.extend({ - fetchOnce: function (opt) { - var options = _.extend({}, opt); - if (!this._deferred || this._deferred.state() === "rejected" || options.forceFetch ) { - this._deferred = this.fetch(); - } + }); + + FauxtonAPI.Collection = Backbone.Collection.extend({ - return this._deferred; + }); + + var caching = { + fetchOnce: function (opts) { + var options = _.defaults(opts || {}, this.cache, {cache: true}); + console.log('opts', options); + return this.fetch(options); } + }; + + _.each([FauxtonAPI.Collection, FauxtonAPI.Model], function (ctor) { + _.extend(ctor.prototype, caching); }); var extensions = _.extend({}, Backbone.Events); diff --git a/src/fauxton/assets/js/plugins/backbone.fetch-cache.js b/src/fauxton/assets/js/plugins/backbone.fetch-cache.js new file mode 100644 index 000000000..c86a8b96f --- /dev/null +++ b/src/fauxton/assets/js/plugins/backbone.fetch-cache.js @@ -0,0 +1,311 @@ +/*! + backbone.fetch-cache v1.3.0 + by Andy Appleton - https://github.com/mrappleton/backbone-fetch-cache.git + */ + +// AMD wrapper from https://github.com/umdjs/umd/blob/master/amdWebGlobal.js + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module and set browser global + define(['underscore', 'backbone', 'jquery'], function (_, Backbone, $) { + return (root.Backbone = factory(_, Backbone, $)); + }); + } else { + // Browser globals + root.Backbone = factory(root._, root.Backbone, root.jQuery); + } +}(this, function (_, Backbone, $) { + + // Setup + var superMethods = { + modelFetch: Backbone.Model.prototype.fetch, + modelSync: Backbone.Model.prototype.sync, + collectionFetch: Backbone.Collection.prototype.fetch + }, + supportLocalStorage = (function() { + var supported = typeof window.localStorage !== 'undefined'; + if (supported) { + try { + // impossible to write on some platforms when private browsing is on and + // throws an exception = local storage not supported. + localStorage.setItem("test_support", "test_support"); + localStorage.removeItem("test_support"); + } catch (e) { + supported = false; + } + } + return supported; + })(); + + Backbone.fetchCache = (Backbone.fetchCache || {}); + Backbone.fetchCache._cache = (Backbone.fetchCache._cache || {}); + + Backbone.fetchCache.priorityFn = function(a, b) { + if (!a || !a.expires || !b || !b.expires) { + return a; + } + + return a.expires - b.expires; + }; + + Backbone.fetchCache._prioritize = function() { + var sorted = _.values(this._cache).sort(this.priorityFn); + var index = _.indexOf(_.values(this._cache), sorted[0]); + return _.keys(this._cache)[index]; + }; + + Backbone.fetchCache._deleteCacheWithPriority = function() { + Backbone.fetchCache._cache[this._prioritize()] = null; + delete Backbone.fetchCache._cache[this._prioritize()]; + Backbone.fetchCache.setLocalStorage(); + }; + + Backbone.fetchCache.getLocalStorageKey = function() { + return 'backboneCache'; + }; + + if (typeof Backbone.fetchCache.localStorage === 'undefined') { + Backbone.fetchCache.localStorage = true; + } + + // Shared methods + function getCacheKey(instance, opts) { + var url; + + if(opts && opts.url) { + url = opts.url; + } else { + url = _.isFunction(instance.url) ? instance.url() : instance.url; + } + + // Need url to use as cache key so return if we can't get it + if(!url) { return; } + + if(opts && opts.data) { + return url + "?" + $.param(opts.data); + } + return url; + } + + function setCache(instance, opts, attrs) { + opts = (opts || {}); + var key = Backbone.fetchCache.getCacheKey(instance, opts), + expires = false; + + // Need url to use as cache key so return if we can't get it + if (!key) { return; } + + // Never set the cache if user has explicitly said not to + if (opts.cache === false) { return; } + + // Don't set the cache unless cache: true or prefill: true option is passed + if (!(opts.cache || opts.prefill)) { return; } + + if (opts.expires !== false) { + expires = (new Date()).getTime() + ((opts.expires || 5 * 60) * 1000); + } + + Backbone.fetchCache._cache[key] = { + expires: expires, + value: attrs + }; + + Backbone.fetchCache.setLocalStorage(); + } + + function clearItem(key) { + if (_.isFunction(key)) { key = key(); } + delete Backbone.fetchCache._cache[key]; + Backbone.fetchCache.setLocalStorage(); + } + + function setLocalStorage() { + if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; } + try { + localStorage.setItem(Backbone.fetchCache.getLocalStorageKey(), JSON.stringify(Backbone.fetchCache._cache)); + } catch (err) { + var code = err.code || err.number || err.message; + if (code === 22) { + this._deleteCacheWithPriority(); + } else { + throw(err); + } + } + } + + function getLocalStorage() { + if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; } + var json = localStorage.getItem(Backbone.fetchCache.getLocalStorageKey()) || '{}'; + Backbone.fetchCache._cache = JSON.parse(json); + } + + function nextTick(fn) { + return window.setTimeout(fn, 0); + } + + // Instance methods + Backbone.Model.prototype.fetch = function(opts) { + opts = _.defaults(opts || {}, { parse: true }); + var key = Backbone.fetchCache.getCacheKey(this, opts), + data = Backbone.fetchCache._cache[key], + expired = false, + attributes = false, + deferred = new $.Deferred(), + self = this; + + function setData() { + if (opts.parse) { + attributes = self.parse(attributes, opts); + } + + self.set(attributes, opts); + if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self, attributes, opts); } + + // Trigger sync events + self.trigger('cachesync', self, attributes, opts); + self.trigger('sync', self, attributes, opts); + + // Notify progress if we're still waiting for an AJAX call to happen... + if (opts.prefill) { deferred.notify(self); } + // ...finish and return if we're not + else { + if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); } + deferred.resolve(self); + } + } + + if (data) { + expired = data.expires; + expired = expired && data.expires < (new Date()).getTime(); + attributes = data.value; + } + + if (!expired && (opts.cache || opts.prefill) && attributes) { + // Ensure that cache resolution adhers to async option, defaults to true. + if (opts.async == null) { opts.async = true; } + + if (opts.async) { + nextTick(setData); + } else { + setData(); + } + + if (!opts.prefill) { + return deferred.promise(); + } + } + + // Delegate to the actual fetch method and store the attributes in the cache + superMethods.modelFetch.apply(this, arguments) + // resolve the returned promise when the AJAX call completes + .done( _.bind(deferred.resolve, this, this) ) + // Set the new data in the cache + .done( _.bind(Backbone.fetchCache.setCache, null, this, opts) ) + // Reject the promise on fail + .fail( _.bind(deferred.reject, this, this) ); + + // return a promise which provides the same methods as a jqXHR object + return deferred.promise(); + }; + + // Override Model.prototype.sync and try to clear cache items if it looks + // like they are being updated. + Backbone.Model.prototype.sync = function(method, model, options) { + // Only empty the cache if we're doing a create, update, patch or delete. + if (method === 'read') { + return superMethods.modelSync.apply(this, arguments); + } + + var collection = model.collection, + keys = [], + i, len; + + // Build up a list of keys to delete from the cache, starting with this + keys.push(Backbone.fetchCache.getCacheKey(model, options)); + + // If this model has a collection, also try to delete the cache for that + if (!!collection) { + keys.push(Backbone.fetchCache.getCacheKey(collection)); + } + + // Empty cache for all found keys + for (i = 0, len = keys.length; i < len; i++) { clearItem(keys[i]); } + + return superMethods.modelSync.apply(this, arguments); + }; + + Backbone.Collection.prototype.fetch = function(opts) { + opts = _.defaults(opts || {}, { parse: true }); + var key = Backbone.fetchCache.getCacheKey(this, opts), + data = Backbone.fetchCache._cache[key], + expired = false, + attributes = false, + deferred = new $.Deferred(), + self = this; + + function setData() { + self[opts.reset ? 'reset' : 'set'](attributes, opts); + if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self); } + + // Trigger sync events + self.trigger('cachesync', self, attributes, opts); + self.trigger('sync', self, attributes, opts); + + // Notify progress if we're still waiting for an AJAX call to happen... + if (opts.prefill) { deferred.notify(self); } + // ...finish and return if we're not + else { + if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); } + deferred.resolve(self); + } + } + + if (data) { + expired = data.expires; + expired = expired && data.expires < (new Date()).getTime(); + attributes = data.value; + } + + if (!expired && (opts.cache || opts.prefill) && attributes) { + // Ensure that cache resolution adhers to async option, defaults to true. + if (opts.async == null) { opts.async = true; } + + if (opts.async) { + nextTick(setData); + } else { + setData(); + } + + if (!opts.prefill) { + return deferred.promise(); + } + } + + // Delegate to the actual fetch method and store the attributes in the cache + superMethods.collectionFetch.apply(this, arguments) + // resolve the returned promise when the AJAX call completes + .done( _.bind(deferred.resolve, this, this) ) + // Set the new data in the cache + .done( _.bind(Backbone.fetchCache.setCache, null, this, opts) ) + // Reject the promise on fail + .fail( _.bind(deferred.reject, this, this) ); + + // return a promise which provides the same methods as a jqXHR object + return deferred.promise(); + }; + + // Prime the cache from localStorage on initialization + getLocalStorage(); + + // Exports + + Backbone.fetchCache._superMethods = superMethods; + Backbone.fetchCache.setCache = setCache; + Backbone.fetchCache.getCacheKey = getCacheKey; + Backbone.fetchCache.clearItem = clearItem; + Backbone.fetchCache.setLocalStorage = setLocalStorage; + Backbone.fetchCache.getLocalStorage = getLocalStorage; + + return Backbone; +})); |