OSDN Git Service

v07
[pettanr/pettanr.git] / app / assets / javascripts / backbone.fetch-cache.js
1 /*!
2   backbone.fetch-cache v1.4.1
3   by Andy Appleton - https://github.com/mrappleton/backbone-fetch-cache.git
4  */
5
6 // AMD wrapper from https://github.com/umdjs/umd/blob/master/amdWebGlobal.js
7
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, $));
13     });
14   } else if (typeof exports !== 'undefined' && typeof require !== 'undefined') {
15     module.exports = factory(require('underscore'), require('backbone'), require('jquery'));
16   } else {
17     // Browser globals
18     root.Backbone = factory(root._, root.Backbone, root.jQuery);
19   }
20 }(this, function (_, Backbone, $) {
21
22   // Setup
23   var superMethods = {
24     modelFetch: Backbone.Model.prototype.fetch,
25     modelSync: Backbone.Model.prototype.sync,
26     collectionFetch: Backbone.Collection.prototype.fetch
27   },
28   supportLocalStorage = (function() {
29     var supported = typeof window.localStorage !== 'undefined';
30     if (supported) {
31       try {
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');
36       } catch (e) {
37         supported = false;
38       }
39     }
40     return supported;
41   })();
42
43   Backbone.fetchCache = (Backbone.fetchCache || {});
44   Backbone.fetchCache._cache = (Backbone.fetchCache._cache || {});
45   // Global flag to enable/disable caching
46   Backbone.fetchCache.enabled = true;
47
48   Backbone.fetchCache.priorityFn = function(a, b) {
49     if (!a || !a.expires || !b || !b.expires) {
50       return a;
51     }
52
53     return a.expires - b.expires;
54   };
55
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];
60   };
61
62   Backbone.fetchCache._deleteCacheWithPriority = function() {
63     Backbone.fetchCache._cache[this._prioritize()] = null;
64     delete Backbone.fetchCache._cache[this._prioritize()];
65     Backbone.fetchCache.setLocalStorage();
66   };
67
68   Backbone.fetchCache.getLocalStorageKey = function() {
69     return 'backboneCache';
70   };
71
72   if (typeof Backbone.fetchCache.localStorage === 'undefined') {
73     Backbone.fetchCache.localStorage = true;
74   }
75
76   // Shared methods
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);
82       }
83       // else, use the URL
84       if (opts && opts.url) {
85         key = opts.url;
86       } else {
87         key = _.isFunction(key.url) ? key.url() : key.url;
88       }
89     } else if (_.isFunction(key)) {
90       return key(opts);
91     }
92     if (opts && opts.data) {
93       if(typeof opts.data === 'string') {
94         return key + '?' + opts.data;
95       } else {
96         return key + '?' + $.param(opts.data);
97       }
98     }
99     return key;
100   }
101
102   function setCache(instance, opts, attrs) {
103     opts = (opts || {});
104     var key = Backbone.fetchCache.getCacheKey(instance, opts),
105         expires = false,
106         lastSync = (opts.lastSync || (new Date()).getTime()),
107         prefillExpires = false;
108
109     // Need url to use as cache key so return if we can't get it
110     if (!key) { return; }
111
112     // Never set the cache if user has explicitly said not to
113     if (opts.cache === false) { return; }
114
115     // Don't set the cache unless cache: true or prefill: true option is passed
116     if (!(opts.cache || opts.prefill)) { return; }
117
118     if (opts.expires !== false) {
119       expires = (new Date()).getTime() + ((opts.expires || 5 * 60) * 1000);
120     }
121
122     if (opts.prefillExpires !== false) {
123       prefillExpires = (new Date()).getTime() + ((opts.prefillExpires || 5 * 60) * 1000);
124     }
125
126     Backbone.fetchCache._cache[key] = {
127       expires: expires,
128       lastSync : lastSync,
129       prefillExpires: prefillExpires,
130       value: attrs
131     };
132
133     Backbone.fetchCache.setLocalStorage();
134   }
135
136   function getCache(key, opts) {
137     if (_.isFunction(key)) {
138       key = key();
139     } else if (key && _.isObject(key)) {
140       key = getCacheKey(key, opts);
141     }
142
143     return Backbone.fetchCache._cache[key];
144   }
145
146   function getLastSync(key, opts) {
147     return getCache(key).lastSync;
148   }
149
150   function clearItem(key, opts) {
151     if (_.isFunction(key)) {
152       key = key();
153     } else if (key && _.isObject(key)) {
154       key = getCacheKey(key, opts);
155     }
156     delete Backbone.fetchCache._cache[key];
157     Backbone.fetchCache.setLocalStorage();
158   }
159   
160   function reset() {
161     // Clearing all cache items
162     Backbone.fetchCache._cache = {};
163   }
164
165   function setLocalStorage() {
166     if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; }
167     try {
168       localStorage.setItem(Backbone.fetchCache.getLocalStorageKey(), JSON.stringify(Backbone.fetchCache._cache));
169     } catch (err) {
170       var code = err.code || err.number || err.message;
171       if (code === 22 || code === 1014) {
172         this._deleteCacheWithPriority();
173       } else {
174         throw(err);
175       }
176     }
177   }
178
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);
183   }
184
185   function nextTick(fn) {
186     return window.setTimeout(fn, 0);
187   }
188
189   // Instance methods
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);
194     }
195     opts = _.defaults(opts || {}, { parse: true });
196     var key = Backbone.fetchCache.getCacheKey(this, opts),
197         data = getCache(key),
198         expired = false,
199         prefillExpired = false,
200         attributes = false,
201         deferred = new $.Deferred(),
202         self = this;
203
204     function isPrefilling() {
205       return opts.prefill && (!opts.prefillExpires || prefillExpired);
206     }
207
208     function setData() {
209       if (opts.parse) {
210         attributes = self.parse(attributes, opts);
211       }
212
213       self.set(attributes, opts);
214       if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self, attributes, opts); }
215
216       // Trigger sync events
217       self.trigger('cachesync', self, attributes, opts);
218       self.trigger('sync', self, attributes, opts);
219
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
223       else {
224         if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); }
225         deferred.resolve(self);
226       }
227     }
228
229     if (data) {
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;
235     }
236
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; }
240
241       if (opts.async) {
242         nextTick(setData);
243       } else {
244         setData();
245       }
246
247       if (!isPrefilling()) {
248         return deferred;
249       }
250     }
251
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) );
260
261     deferred.abort = jqXHR.abort;
262
263     // return a promise which provides the same methods as a jqXHR object
264     return deferred;
265   };
266
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);
274     }
275
276     var collection = model.collection,
277         keys = [],
278         i, len;
279
280     // Build up a list of keys to delete from the cache, starting with this
281     keys.push(Backbone.fetchCache.getCacheKey(model, options));
282
283     // If this model has a collection, also try to delete the cache for that
284     if (!!collection) {
285       keys.push(Backbone.fetchCache.getCacheKey(collection));
286     }
287
288     // Empty cache for all found keys
289     for (i = 0, len = keys.length; i < len; i++) { clearItem(keys[i]); }
290
291     return superMethods.modelSync.apply(this, arguments);
292   };
293
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);
298     }
299
300     opts = _.defaults(opts || {}, { parse: true });
301     var key = Backbone.fetchCache.getCacheKey(this, opts),
302         data = getCache(key),
303         expired = false,
304         prefillExpired = false,
305         attributes = false,
306         deferred = new $.Deferred(),
307         self = this;
308
309     function isPrefilling() {
310       return opts.prefill && (!opts.prefillExpires || prefillExpired);
311     }
312
313     function setData() {
314       self[opts.reset ? 'reset' : 'set'](attributes, opts);
315       if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self); }
316
317       // Trigger sync events
318       self.trigger('cachesync', self, attributes, opts);
319       self.trigger('sync', self, attributes, opts);
320
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
324       else {
325         if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); }
326         deferred.resolve(self);
327       }
328     }
329
330     if (data) {
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;
336     }
337
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; }
341
342       if (opts.async) {
343         nextTick(setData);
344       } else {
345         setData();
346       }
347
348       if (!isPrefilling()) {
349         return deferred;
350       }
351     }
352
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) );
361
362     deferred.abort = jqXHR.abort;
363
364     // return a promise which provides the same methods as a jqXHR object
365     return deferred;
366   };
367
368   // Prime the cache from localStorage on initialization
369   getLocalStorage();
370
371   // Exports
372
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;
382
383   return Backbone;
384 }));