var _ = require('underscore'); var Backbone = require('backbone'); (function () { // helper functions needed from backbone (they are not exported) var getValue = function (object, prop, method) { if (!(object && object[prop])) return null; return _.isFunction(object[prop]) ? object[prop](method) : object[prop]; }; // Throw an error when a URL is needed, and none is supplied. var urlError = function () { throw new Error('A "url" property or function must be specified'); }; // backbone.sync replacement to control url prefix Backbone.originalSync = Backbone.sync; Backbone.sync = function (method, model, options) { var url = options.url || getValue(model, 'url', method) || urlError(); // prefix if http is not present var absoluteUrl = url.indexOf('http') === 0 || url.indexOf('//') === 0; if (!absoluteUrl) { // We need to fix this // this comes from cdb.config.prefixUrl options.url = (model._configModel || model._config || model).get('base_url') + url; } else { options.url = url; } if (method !== 'read') { // remove everything related if (model.surrogateKeys) { Backbone.cachedSync.invalidateSurrogateKeys(getValue(model, 'surrogateKeys')); } } return Backbone.originalSync(method, model, options); }; Backbone.currentSync = Backbone.sync; Backbone.withCORS = function (method, model, options) { if (!options) { options = {}; } if (!options.crossDomain) { options.crossDomain = true; } if (!options.xhrFields) { options.xhrFields = { withCredentials: true }; } return Backbone.currentSync(method, model, options); }; // this method returns a cached version of backbone sync // take a look at https://github.com/teambox/backbone.memoized_sync/blob/master/backbone.memoized_sync.js // this is the same concept but implemented as a wrapper for ``Backbone.sync`` // usage: // initialize: function () { // this.sync = Backbone.cachedSync(this.user_name); // } Backbone.cachedSync = function (namespace, sync) { if (!namespace) { throw new Error('cachedSync needs a namespace as argument'); } var surrogateKey = namespace; var session = window.user_data && window.user_data.username; // no user session, no cache // there should be a session to have cache so we avoid // cache collision for someone with more than one account if (session) { namespace += '-' + session; } else { return Backbone.sync; } var namespaceKey = 'cdb-cache/' + namespace; // saves all the localstore references to the namespace // inside localstore. It allows to remove all the references // at a time var index = { // return a list of references for the namespace _keys: function () { return JSON.parse(localStorage.getItem(namespaceKey) || '{}'); }, // add a new reference for the namespace add: function (key) { var keys = this._keys(); keys[key] = +new Date(); localStorage.setItem(namespaceKey, JSON.stringify(keys)); }, // remove all the references for the namespace invalidate: function () { var keys = this._keys(); _.each(keys, function (v, k) { localStorage.removeItem(k); }); localStorage.removeItem(namespaceKey); } }; // localstore-like cache wrapper var cache = { setItem: function (key, value) { localStorage.setItem(key, value); index.add(key); return this; }, // this is async in case the data needs to be compressed getItem: function (key, callback) { var val = localStorage.getItem(key); _.defer(function () { callback(val); }); }, removeItem: function (key) { localStorage.removeItem(key); index.invalidate(); } }; var cached = function (method, model, options) { var url = options.url || getValue(model, 'url') || urlError(); var key = namespaceKey + '/' + url; if (method === 'read') { var success = options.success; var cachedValue = null; options.success = function (resp, status, xhr) { // if cached value is ok if (cachedValue && xhr.responseText === cachedValue) { return; } cache.setItem(key, xhr.responseText); success(resp, status, xhr); }; cache.getItem(key, function (val) { cachedValue = val; if (val) { success(JSON.parse(val), 'success'); } }); } else { cache.removeItem(key); } return (sync || Backbone.sync)(method, model, options); }; // create a public function to invalidate all the namespace // items cached.invalidate = function () { index.invalidate(); }; // for testing and debugging porpuposes cached.cache = cache; // have a global namespace -> sync function in order to avoid invalidation Backbone.cachedSync.surrogateKeys[surrogateKey] = cached; return cached; }; Backbone.cachedSync.surrogateKeys = {}; Backbone.cachedSync.invalidateSurrogateKeys = function (keys) { _.each(keys, function (k) { var s = Backbone.cachedSync.surrogateKeys[k]; if (s) { s.invalidate(); } else { console.error('Backbone sync options: surrogate key not found: ' + k); } }); }; Backbone.syncAbort = function () { var self = arguments[1]; if (self._xhr) { self._xhr.abort(); } self._xhr = Backbone.sync.apply(this, arguments); self._xhr.always(function () { self._xhr = null; }); return self._xhr; }; Backbone.delayedSaveSync = function (sync, delay) { var dsync = _.debounce(sync, delay); return function (method, model, options) { if (method === 'create' || method === 'update') { return dsync(method, model, options); } else { return sync(method, model, options); } }; }; Backbone.saveAbort = function () { var self = this; if (this._saving && this._xhr) { this._xhr.abort(); } this._saving = true; var xhr = Backbone.Model.prototype.save.apply(this, arguments); this._xhr = xhr; xhr.always(function () { self._saving = false; }); return xhr; }; })();