2 backbone.fetch-cache v1.4.1
3 by Andy Appleton - https://github.com/mrappleton/backbone-fetch-cache.git
6 // AMD wrapper from https://github.com/umdjs/umd/blob/master/amdWebGlobal.js
8 (function (root, factory) {
9 if (typeof define === 'function' && define.amd) {
10 // AMD. Register as an anonymous module and set browser global
11 define(['underscore', 'backbone', 'jquery'], function (_, Backbone, $) {
12 return (root.Backbone = factory(_, Backbone, $));
14 } else if (typeof exports !== 'undefined') {
15 module.exports = factory(require('underscore'), require('backbone'), require('jquery'));
18 root.Backbone = factory(root._, root.Backbone, root.jQuery);
20 }(this, function (_, Backbone, $) {
24 modelFetch: Backbone.Model.prototype.fetch,
25 modelSync: Backbone.Model.prototype.sync,
26 collectionFetch: Backbone.Collection.prototype.fetch
28 supportLocalStorage = (function() {
29 var supported = typeof window.localStorage !== 'undefined';
32 // impossible to write on some platforms when private browsing is on and
33 // throws an exception = local storage not supported.
34 localStorage.setItem('test_support', 'test_support');
35 localStorage.removeItem('test_support');
43 Backbone.fetchCache = (Backbone.fetchCache || {});
44 Backbone.fetchCache._cache = (Backbone.fetchCache._cache || {});
45 // Global flag to enable/disable caching
46 Backbone.fetchCache.enabled = true;
48 Backbone.fetchCache.priorityFn = function(a, b) {
49 if (!a || !a.expires || !b || !b.expires) {
53 return a.expires - b.expires;
56 Backbone.fetchCache._prioritize = function() {
57 var sorted = _.values(this._cache).sort(this.priorityFn);
58 var index = _.indexOf(_.values(this._cache), sorted[0]);
59 return _.keys(this._cache)[index];
62 Backbone.fetchCache._deleteCacheWithPriority = function() {
63 Backbone.fetchCache._cache[this._prioritize()] = null;
64 delete Backbone.fetchCache._cache[this._prioritize()];
65 Backbone.fetchCache.setLocalStorage();
68 Backbone.fetchCache.getLocalStorageKey = function() {
69 return 'backboneCache';
72 if (typeof Backbone.fetchCache.localStorage === 'undefined') {
73 Backbone.fetchCache.localStorage = true;
77 function getCacheKey(instance, opts) {
80 if(opts && opts.url) {
83 url = _.isFunction(instance.url) ? instance.url() : instance.url;
86 // Need url to use as cache key so return if we can't get it
89 if(opts && opts.data) {
90 if(typeof opts.data === 'string') {
91 return url + '?' + opts.data;
93 return url + '?' + $.param(opts.data);
99 function setCache(instance, opts, attrs) {
101 var key = Backbone.fetchCache.getCacheKey(instance, opts),
104 // Need url to use as cache key so return if we can't get it
105 if (!key) { return; }
107 // Never set the cache if user has explicitly said not to
108 if (opts.cache === false) { return; }
110 // Don't set the cache unless cache: true or prefill: true option is passed
111 if (!(opts.cache || opts.prefill)) { return; }
113 if (opts.expires !== false) {
114 expires = (new Date()).getTime() + ((opts.expires || 5 * 60) * 1000);
117 Backbone.fetchCache._cache[key] = {
122 Backbone.fetchCache.setLocalStorage();
125 function clearItem(key) {
126 if (_.isFunction(key)) { key = key(); }
127 delete Backbone.fetchCache._cache[key];
128 Backbone.fetchCache.setLocalStorage();
131 function setLocalStorage() {
132 if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; }
134 localStorage.setItem(Backbone.fetchCache.getLocalStorageKey(), JSON.stringify(Backbone.fetchCache._cache));
136 var code = err.code || err.number || err.message;
138 this._deleteCacheWithPriority();
145 function getLocalStorage() {
146 if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; }
147 var json = localStorage.getItem(Backbone.fetchCache.getLocalStorageKey()) || '{}';
148 Backbone.fetchCache._cache = JSON.parse(json);
151 function nextTick(fn) {
152 return window.setTimeout(fn, 0);
156 Backbone.Model.prototype.fetch = function(opts) {
157 //Bypass caching if it's not enabled
158 if(!Backbone.fetchCache.enabled) {
159 return superMethods.modelFetch.apply(this, arguments);
161 opts = _.defaults(opts || {}, { parse: true });
162 var key = Backbone.fetchCache.getCacheKey(this, opts),
163 data = Backbone.fetchCache._cache[key],
166 deferred = new $.Deferred(),
171 attributes = self.parse(attributes, opts);
174 self.set(attributes, opts);
175 if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self, attributes, opts); }
177 // Trigger sync events
178 self.trigger('cachesync', self, attributes, opts);
179 self.trigger('sync', self, attributes, opts);
181 // Notify progress if we're still waiting for an AJAX call to happen...
182 if (opts.prefill) { deferred.notify(self); }
183 // ...finish and return if we're not
185 if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); }
186 deferred.resolve(self);
191 expired = data.expires;
192 expired = expired && data.expires < (new Date()).getTime();
193 attributes = data.value;
196 if (!expired && (opts.cache || opts.prefill) && attributes) {
197 // Ensure that cache resolution adhers to async option, defaults to true.
198 if (opts.async == null) { opts.async = true; }
211 // Delegate to the actual fetch method and store the attributes in the cache
212 var jqXHR = superMethods.modelFetch.apply(this, arguments);
213 // resolve the returned promise when the AJAX call completes
214 jqXHR.done( _.bind(deferred.resolve, this, this) )
215 // Set the new data in the cache
216 .done( _.bind(Backbone.fetchCache.setCache, null, this, opts) )
217 // Reject the promise on fail
218 .fail( _.bind(deferred.reject, this, this) );
220 deferred.abort = jqXHR.abort;
222 // return a promise which provides the same methods as a jqXHR object
226 // Override Model.prototype.sync and try to clear cache items if it looks
227 // like they are being updated.
228 Backbone.Model.prototype.sync = function(method, model, options) {
229 // Only empty the cache if we're doing a create, update, patch or delete.
230 // or caching is not enabled
231 if (method === 'read' || !Backbone.fetchCache.enabled) {
232 return superMethods.modelSync.apply(this, arguments);
235 var collection = model.collection,
239 // Build up a list of keys to delete from the cache, starting with this
240 keys.push(Backbone.fetchCache.getCacheKey(model, options));
242 // If this model has a collection, also try to delete the cache for that
244 keys.push(Backbone.fetchCache.getCacheKey(collection));
247 // Empty cache for all found keys
248 for (i = 0, len = keys.length; i < len; i++) { clearItem(keys[i]); }
250 return superMethods.modelSync.apply(this, arguments);
253 Backbone.Collection.prototype.fetch = function(opts) {
254 // Bypass caching if it's not enabled
255 if(!Backbone.fetchCache.enabled) {
256 return superMethods.collectionFetch.apply(this, arguments);
259 opts = _.defaults(opts || {}, { parse: true });
260 var key = Backbone.fetchCache.getCacheKey(this, opts),
261 data = Backbone.fetchCache._cache[key],
264 deferred = new $.Deferred(),
268 self[opts.reset ? 'reset' : 'set'](attributes, opts);
269 if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self); }
271 // Trigger sync events
272 self.trigger('cachesync', self, attributes, opts);
273 self.trigger('sync', self, attributes, opts);
275 // Notify progress if we're still waiting for an AJAX call to happen...
276 if (opts.prefill) { deferred.notify(self); }
277 // ...finish and return if we're not
279 if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); }
280 deferred.resolve(self);
285 expired = data.expires;
286 expired = expired && data.expires < (new Date()).getTime();
287 attributes = data.value;
290 if (!expired && (opts.cache || opts.prefill) && attributes) {
291 // Ensure that cache resolution adhers to async option, defaults to true.
292 if (opts.async == null) { opts.async = true; }
305 // Delegate to the actual fetch method and store the attributes in the cache
306 var jqXHR = superMethods.collectionFetch.apply(this, arguments);
307 // resolve the returned promise when the AJAX call completes
308 jqXHR.done( _.bind(deferred.resolve, this, this) )
309 // Set the new data in the cache
310 .done( _.bind(Backbone.fetchCache.setCache, null, this, opts) )
311 // Reject the promise on fail
312 .fail( _.bind(deferred.reject, this, this) );
314 deferred.abort = jqXHR.abort;
316 // return a promise which provides the same methods as a jqXHR object
320 // Prime the cache from localStorage on initialization
325 Backbone.fetchCache._superMethods = superMethods;
326 Backbone.fetchCache.setCache = setCache;
327 Backbone.fetchCache.getCacheKey = getCacheKey;
328 Backbone.fetchCache.clearItem = clearItem;
329 Backbone.fetchCache.setLocalStorage = setLocalStorage;
330 Backbone.fetchCache.getLocalStorage = getLocalStorage;