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' && typeof require !== '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(key, opts) {
78 if (key && _.isObject(key)) {
79 // If the model has its own, custom, cache key function, use it.
80 if (_.isFunction(key.getCacheKey)) {
81 return key.getCacheKey(opts);
84 if (opts && opts.url) {
87 key = _.isFunction(key.url) ? key.url() : key.url;
89 } else if (_.isFunction(key)) {
92 if (opts && opts.data) {
93 if(typeof opts.data === 'string') {
94 return key + '?' + opts.data;
96 return key + '?' + $.param(opts.data);
102 function setCache(instance, opts, attrs) {
104 var key = Backbone.fetchCache.getCacheKey(instance, opts),
106 lastSync = (opts.lastSync || (new Date()).getTime()),
107 prefillExpires = false;
109 // Need url to use as cache key so return if we can't get it
110 if (!key) { return; }
112 // Never set the cache if user has explicitly said not to
113 if (opts.cache === false) { return; }
115 // Don't set the cache unless cache: true or prefill: true option is passed
116 if (!(opts.cache || opts.prefill)) { return; }
118 if (opts.expires !== false) {
119 expires = (new Date()).getTime() + ((opts.expires || 5 * 60) * 1000);
122 if (opts.prefillExpires !== false) {
123 prefillExpires = (new Date()).getTime() + ((opts.prefillExpires || 5 * 60) * 1000);
126 Backbone.fetchCache._cache[key] = {
129 prefillExpires: prefillExpires,
133 Backbone.fetchCache.setLocalStorage();
136 function getCache(key, opts) {
137 if (_.isFunction(key)) {
139 } else if (key && _.isObject(key)) {
140 key = getCacheKey(key, opts);
143 return Backbone.fetchCache._cache[key];
146 function getLastSync(key, opts) {
147 return getCache(key).lastSync;
150 function clearItem(key, opts) {
151 if (_.isFunction(key)) {
153 } else if (key && _.isObject(key)) {
154 key = getCacheKey(key, opts);
156 delete Backbone.fetchCache._cache[key];
157 Backbone.fetchCache.setLocalStorage();
161 // Clearing all cache items
162 Backbone.fetchCache._cache = {};
165 function setLocalStorage() {
166 if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; }
168 localStorage.setItem(Backbone.fetchCache.getLocalStorageKey(), JSON.stringify(Backbone.fetchCache._cache));
170 var code = err.code || err.number || err.message;
171 if (code === 22 || code === 1014) {
172 this._deleteCacheWithPriority();
179 function getLocalStorage() {
180 if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; }
181 var json = localStorage.getItem(Backbone.fetchCache.getLocalStorageKey()) || '{}';
182 Backbone.fetchCache._cache = JSON.parse(json);
185 function nextTick(fn) {
186 return window.setTimeout(fn, 0);
190 Backbone.Model.prototype.fetch = function(opts) {
191 //Bypass caching if it's not enabled
192 if(!Backbone.fetchCache.enabled) {
193 return superMethods.modelFetch.apply(this, arguments);
195 opts = _.defaults(opts || {}, { parse: true });
196 var key = Backbone.fetchCache.getCacheKey(this, opts),
197 data = getCache(key),
199 prefillExpired = false,
201 deferred = new $.Deferred(),
204 function isPrefilling() {
205 return opts.prefill && (!opts.prefillExpires || prefillExpired);
210 attributes = self.parse(attributes, opts);
213 self.set(attributes, opts);
214 if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self, attributes, opts); }
216 // Trigger sync events
217 self.trigger('cachesync', self, attributes, opts);
218 self.trigger('sync', self, attributes, opts);
220 // Notify progress if we're still waiting for an AJAX call to happen...
221 if (isPrefilling()) { deferred.notify(self); }
222 // ...finish and return if we're not
224 if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); }
225 deferred.resolve(self);
230 expired = data.expires;
231 expired = expired && data.expires < (new Date()).getTime();
232 prefillExpired = data.prefillExpires;
233 prefillExpired = prefillExpired && data.prefillExpires < (new Date()).getTime();
234 attributes = data.value;
237 if (!expired && (opts.cache || opts.prefill) && attributes) {
238 // Ensure that cache resolution adhers to async option, defaults to true.
239 if (opts.async == null) { opts.async = true; }
247 if (!isPrefilling()) {
252 // Delegate to the actual fetch method and store the attributes in the cache
253 var jqXHR = superMethods.modelFetch.apply(this, arguments);
254 // resolve the returned promise when the AJAX call completes
255 jqXHR.done( _.bind(deferred.resolve, this, this) )
256 // Set the new data in the cache
257 .done( _.bind(Backbone.fetchCache.setCache, null, this, opts) )
258 // Reject the promise on fail
259 .fail( _.bind(deferred.reject, this, this) );
261 deferred.abort = jqXHR.abort;
263 // return a promise which provides the same methods as a jqXHR object
267 // Override Model.prototype.sync and try to clear cache items if it looks
268 // like they are being updated.
269 Backbone.Model.prototype.sync = function(method, model, options) {
270 // Only empty the cache if we're doing a create, update, patch or delete.
271 // or caching is not enabled
272 if (method === 'read' || !Backbone.fetchCache.enabled) {
273 return superMethods.modelSync.apply(this, arguments);
276 var collection = model.collection,
280 // Build up a list of keys to delete from the cache, starting with this
281 keys.push(Backbone.fetchCache.getCacheKey(model, options));
283 // If this model has a collection, also try to delete the cache for that
285 keys.push(Backbone.fetchCache.getCacheKey(collection));
288 // Empty cache for all found keys
289 for (i = 0, len = keys.length; i < len; i++) { clearItem(keys[i]); }
291 return superMethods.modelSync.apply(this, arguments);
294 Backbone.Collection.prototype.fetch = function(opts) {
295 // Bypass caching if it's not enabled
296 if(!Backbone.fetchCache.enabled) {
297 return superMethods.collectionFetch.apply(this, arguments);
300 opts = _.defaults(opts || {}, { parse: true });
301 var key = Backbone.fetchCache.getCacheKey(this, opts),
302 data = getCache(key),
304 prefillExpired = false,
306 deferred = new $.Deferred(),
309 function isPrefilling() {
310 return opts.prefill && (!opts.prefillExpires || prefillExpired);
314 self[opts.reset ? 'reset' : 'set'](attributes, opts);
315 if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self); }
317 // Trigger sync events
318 self.trigger('cachesync', self, attributes, opts);
319 self.trigger('sync', self, attributes, opts);
321 // Notify progress if we're still waiting for an AJAX call to happen...
322 if (isPrefilling()) { deferred.notify(self); }
323 // ...finish and return if we're not
325 if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); }
326 deferred.resolve(self);
331 expired = data.expires;
332 expired = expired && data.expires < (new Date()).getTime();
333 prefillExpired = data.prefillExpires;
334 prefillExpired = prefillExpired && data.prefillExpires < (new Date()).getTime();
335 attributes = data.value;
338 if (!expired && (opts.cache || opts.prefill) && attributes) {
339 // Ensure that cache resolution adhers to async option, defaults to true.
340 if (opts.async == null) { opts.async = true; }
348 if (!isPrefilling()) {
353 // Delegate to the actual fetch method and store the attributes in the cache
354 var jqXHR = superMethods.collectionFetch.apply(this, arguments);
355 // resolve the returned promise when the AJAX call completes
356 jqXHR.done( _.bind(deferred.resolve, this, this) )
357 // Set the new data in the cache
358 .done( _.bind(Backbone.fetchCache.setCache, null, this, opts) )
359 // Reject the promise on fail
360 .fail( _.bind(deferred.reject, this, this) );
362 deferred.abort = jqXHR.abort;
364 // return a promise which provides the same methods as a jqXHR object
368 // Prime the cache from localStorage on initialization
373 Backbone.fetchCache._superMethods = superMethods;
374 Backbone.fetchCache.setCache = setCache;
375 Backbone.fetchCache.getCache = getCache;
376 Backbone.fetchCache.getCacheKey = getCacheKey;
377 Backbone.fetchCache.getLastSync = getLastSync;
378 Backbone.fetchCache.clearItem = clearItem;
379 Backbone.fetchCache.reset = reset;
380 Backbone.fetchCache.setLocalStorage = setLocalStorage;
381 Backbone.fetchCache.getLocalStorage = getLocalStorage;