blob: 1a449cb18aa174de2e2ccaf6db456762bafb4e5d [file] [log] [blame]
/*!
backbone.fetch-cache v1.4.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;
}
}
// Delegate to the actual fetch method and store the attributes in the cache
// to set cache : false whenever it calls ajax (addded due to Test server caching issue)
var arg = $.extend(true,[],arguments);
if((arg.length > 0) && !_.isUndefined(arg[0].cache))
arg[0].cache = false ;
superMethods.modelFetch.apply(this, arg)
// 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;
};
// 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));
// it will delete full collection cache (pagination)
_.each(Backbone.fetchCache._cache, function(url, val){
var urlStr= keys[1]+"?";
if(val.indexOf(urlStr) != -1)
keys.push(val);
});
}
// 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;
}
}
// Delegate to the actual fetch method and store the attributes in the cache
// to set cache : false whenever it calls ajax (addded due to Test server caching issue)
var arg = $.extend(true,[],arguments);
if((arg.length > 0) && !_.isUndefined(arg[0].cache))
arg[0].cache = false ;
superMethods.collectionFetch.apply(this, arg)
// 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;
};
// 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;
}));