Permalink
Browse files

Support for local storage!

  • Loading branch information...
1 parent 8f4ea23 commit c0f9038b7669d5f7af5bae987804703a4fc62844 @chrisronline committed Feb 22, 2014
Showing with 379 additions and 54 deletions.
  1. +3 −0 .gitignore
  2. +43 −4 README.md
  3. +94 −21 angular-promise-cache.js
  4. +4 −2 angular-promise-cache.min.js
  5. +153 −0 angular-promise-cache.test.js
  6. +1 −1 bower.json
  7. +71 −17 example/example.html
  8. +10 −9 package.json
View
@@ -0,0 +1,3 @@
+node_modules/
+coverage/
+.DS_Store
View
@@ -3,6 +3,10 @@ angular-promise-cache
AngularJS service that provides a generic way to cache promises and ensure all cached promises are resolved correctly.
+Latest Update
+------
+v0.0.5 is now available and comes packaged with support for local storage!
+
Huh?
------
Our goal is to allow this kind of code...
@@ -90,8 +94,8 @@ npm:
npm install angular-promise-cache --save
Manual:
-* [Development Build - 1.52KB gzipped (3.16KB uncompressed)](https://raw.github.com/chrisronline/angular-promise-cache/master/angular-promise-cache.js)
-* [Minified/Production Build - 529 bytes gzipped (969 uncompressed)](https://raw.github.com/chrisronline/angular-promise-cache/master/angular-promise-cache.min.js)
+* [Development Build - 2.21KB gzipped (5.15KB uncompressed)](https://raw.github.com/chrisronline/angular-promise-cache/master/angular-promise-cache.js)
+* [Minified/Production Build - 836 bytes gzipped (1.64KB uncompressed)](https://raw.github.com/chrisronline/angular-promise-cache/master/angular-promise-cache.min.js)
Usage
---------
@@ -137,9 +141,38 @@ promiseCache(opts)
// [v0.0.3]
// This function is called on promise failure and returning true will forcefully expire
// the cache for this promise
- expireOnFailure: function
+ expireOnFailure: function,
+
+ // [v0.0.5]
+ // If true, the response from the promise will be cached in local storage based on the ttl
+ localStorageEnabled: boolean,
+ // Determines the key that will be used to store within local storage
+ // If omitted, will default to the 'key' identifier used above
+ localStorageKey: string
+
}
+Events
+--------
+Added in v0.0.5, the following events are now supported:
+
+```js
+$scope.$on('angular-promise-cache.new', function(evt, key) {
+ // @key ${PROMISE_CACHE_CREATION_TIMESTAMP}$
+ // Fired when calling when an uncached promise
+});
+$scope.$on('angular-promise-cache.expired', function(evt, key) {
+ // @key ${PROMISE_CACHE_CREATION_TIMESTAMP}$
+ // Fired when a promise expired
+});
+$scope.$on('angular-promise-cache.active', function(evt, key, expireTimestamp) {
+ // @key ${PROMISE_CACHE_CREATION_TIMESTAMP}$
+ // @expireTimestamp {PROMISE_EXPIRATION_TIMESTAMP}
+ // Fired when a cached promise is returned
+});
+```
+For actual examples, please view the source of the example application.
+
Example
---------
Please view the detailed [demo](http://www.chrisronline.com/angular-promise-cache/example/example.html)
@@ -153,4 +186,10 @@ Requires:
To run:
karma start
- karma run
+ karma run
+
+Release Notes
+---------
+- v0.0.5 - Added local storage support
+- v0.0.4 - (skipped)
+- v0.0.3 - Added expireOnFailure functionality
View
@@ -23,12 +23,36 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
angular.module('angular-promise-cache', [])
- .factory('promiseCache', function() {
+ .factory('promiseCache', ['$q', '$rootScope', function($q, $rootScope) {
var memos = {},
DEFAULT_TTL_IN_MS = 5000,
keyDelimiter = '$',
whitespaceRegex = /\s+/g,
dateReference,
+ ls = window.localStorage,
+ store = function(key, complexValue) {
+ ls.setItem(key, JSON.stringify(complexValue));
+ },
+ remove = function(key) {
+ ls.removeItem(key);
+ },
+ fetch = function(key) {
+ var str = ls.getItem(key);
+ try {
+ str = JSON.parse(str);
+ }
+ catch (e) {
+ console.warn('Unable to parse json response from local storage', str);
+ }
+ return str;
+ },
+
+ getTimestamp = function(key) {
+ return parseInt(key.split(keyDelimiter)[1]) || dateReference;
+ },
+ formatCacheKey = function(ts) {
+ return keyDelimiter + ts + keyDelimiter;
+ },
memoize = typeof _ !== 'undefined' && hasOwnProperty.call(_, 'memoize') ? _.memoize :
function memoize(func, resolver) {
@@ -49,42 +73,88 @@ angular.module('angular-promise-cache', [])
return function(opts) {
// TODO: BETTER ERROR HANDLING
var promise = opts.promise,
- ttl = parseInt(opts.ttl),
+ ttl = parseInt(opts.ttl) || DEFAULT_TTL_IN_MS,
bustCache = !!opts.bustCache,
// v0.0.3: Adding ability to specify a callback function to forcefully expire the cache
// for a promise that returns a failure
expireOnFailure = opts.expireOnFailure,
args = opts.args,
now = new Date().getTime(),
strPromise = opts.key || promise.toString().replace(whitespaceRegex, ''),
- ret;
+
+ // v0.0.5: Local storage support
+ lsEnabled = !!opts.localStorageEnabled,
+ lsKey = opts.localStorageKey || strPromise,
+ lsObj = fetch(lsKey),
+ lsTs,
+ lsMemoCache,
+ lsDuration,
+ lsDeferred;
dateReference = dateReference || now;
+ if (lsEnabled) {
+ if (!lsObj || typeof lsObj !== 'object' || !hasOwnProperty.call(lsObj, 'resolver') || !hasOwnProperty.call(lsObj, 'response')) {
+ lsObj = {};
+ }
+ else {
+ // v0.0.5: Local Storage support
+
+ // Extract the timestamp from the local storage object
+ // This timestamp represents the last time this promise
+ // expired
+ lsTs = getTimestamp(lsObj.resolver);
+
+ // Determine how much longer it has to live
+ lsDuration = lsTs + ttl - now;
+
+ // Memoize the promise using the timestamp from the
+ // local storage object rather than dateReference
+ memos[strPromise] = memoize(promise, function() {
+ return formatCacheKey(lsTs);
+ });
+
+ // We want to fill the cache immediately but do not
+ // want to execute the promise and since the cache
+ // property is just a simple key/value object, we
+ // can create that and set it without any harm
+ lsMemoCache = memos[strPromise].cache || {};
+ lsDeferred = $q.defer();
+ lsDeferred.resolve(lsObj.response);
+ lsMemoCache[formatCacheKey(lsTs)] = lsDeferred.promise;
+ memos[strPromise].cache = lsMemoCache;
+ }
+ }
+
if (!hasOwnProperty.call(memos, strPromise)) {
memos[strPromise] = memoize(promise, function() {
- return keyDelimiter + dateReference + keyDelimiter + Array.prototype.slice.call(arguments);
+ return formatCacheKey(dateReference);
});
+ $rootScope.$broadcast('angular-promise-cache.new', formatCacheKey(dateReference));
}
else {
memos[strPromise].cache = (function() {
var updatedCache = {},
cache = memos[strPromise].cache,
forceExpiration = !!memos[strPromise].forceExpiration,
key,
- parts,
timestamp,
omit;
for (key in cache) {
- parts = key.split(keyDelimiter);
- timestamp = parseInt(parts[1]);
- omit = bustCache || forceExpiration || timestamp + (ttl || DEFAULT_TTL_IN_MS) < now;
+ timestamp = getTimestamp(key);
+ omit = bustCache || forceExpiration || timestamp + ttl < now;
if (omit) {
+ $rootScope.$broadcast('angular-promise-cache.expired', key);
dateReference = now;
+ if (lsEnabled) {
+ lsTs = dateReference;
+ remove(lsKey);
+ }
}
else {
+ $rootScope.$broadcast('angular-promise-cache.active', key, timestamp + ttl);
updatedCache[key] = cache[key];
}
}
@@ -97,18 +167,21 @@ angular.module('angular-promise-cache', [])
}());
}
- ret = memos[strPromise].apply(this, args);
- if (angular.isFunction(expireOnFailure)) {
- ret.then(
- angular.noop,
- function() {
- if (expireOnFailure.apply(this, arguments)) {
- memos[strPromise].forceExpiration = true;
- }
- return arguments;
+ return memos[strPromise].apply(this, args).then(
+ function(response) {
+ if (lsEnabled) {
+ lsObj.response = arguments[0];
+ lsObj.resolver = formatCacheKey(lsTs || dateReference);
+ store(lsKey, lsObj);
}
- );
- }
- return ret;
+ return response;
+ },
+ function(error) {
+ if (angular.isFunction(expireOnFailure)) {
+ memos[strPromise].forceExpiration = true;
+ }
+ return $q.reject(error);
+ }
+ );
}
- });
+ }]);
@@ -20,5 +20,7 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-angular.module("angular-promise-cache",[]).factory("promiseCache",function(){var b={},n=/\s+/g,f,q="undefined"!==typeof _&&hasOwnProperty.call(_,"memoize")?_.memoize:function(a,b){var f=+new Date+"",g=function(){var c=g.cache,e=b?b.apply(this,arguments):f+arguments[0];return hasOwnProperty.call(c,e)?c[e]:c[e]=a.apply(this,arguments)};g.cache={};return g};return function(a){var m=a.promise,p=parseInt(a.ttl),g=!!a.bustCache,c=a.expireOnFailure,e=a.args,l=(new Date).getTime(),d=a.key||m.toString().replace(n,
-"");f=f||l;hasOwnProperty.call(b,d)?b[d].cache=function(){var a={},c=b[d].cache,e=!!b[d].forceExpiration,k,h;for(k in c)h=k.split("$"),h=parseInt(h[1]),(h=g||e||h+(p||5E3)<l)?f=l:a[k]=c[k];b[d].forceExpiration=!1;return a}():b[d]=q(m,function(){return"$"+f+"$"+Array.prototype.slice.call(arguments)});a=b[d].apply(this,e);angular.isFunction(c)&&a.then(angular.noop,function(){c.apply(this,arguments)&&(b[d].forceExpiration=!0);return arguments});return a}});
+angular.module("angular-promise-cache",[]).factory("promiseCache",["$q","$rootScope",function(u,n){var b={},x=/\s+/g,e,p=window.localStorage,y=function(a){a=p.getItem(a);try{a=JSON.parse(a)}catch(b){console.warn("Unable to parse json response from local storage",a)}return a},w="undefined"!==typeof _&&hasOwnProperty.call(_,"memoize")?_.memoize:function(a,b){var e=+new Date+"",l=function(){var g=l.cache,f=b?b.apply(this,arguments):e+arguments[0];return hasOwnProperty.call(g,f)?g[f]:g[f]=a.apply(this,
+arguments)};l.cache={};return l};return function(a){var k=a.promise,v=parseInt(a.ttl)||5E3,l=!!a.bustCache,g=a.expireOnFailure,f=a.args,q=(new Date).getTime(),d=a.key||k.toString().replace(x,""),r=!!a.localStorageEnabled,s=a.localStorageKey||d,c=y(s),m,t;e=e||q;r&&(c&&"object"===typeof c&&hasOwnProperty.call(c,"resolver")&&hasOwnProperty.call(c,"response")?(m=parseInt(c.resolver.split("$")[1])||e,b[d]=w(k,function(){return"$"+m+"$"}),a=b[d].cache||{},t=u.defer(),t.resolve(c.response),a["$"+m+"$"]=
+t.promise,b[d].cache=a):c={});hasOwnProperty.call(b,d)?b[d].cache=function(){var a={},c=b[d].cache,g=!!b[d].forceExpiration,h,f,k;for(h in c)f=parseInt(h.split("$")[1])||e,(k=l||g||f+v<q)?(n.$broadcast("angular-promise-cache.expired",h),e=q,r&&(m=e,p.removeItem(s))):(n.$broadcast("angular-promise-cache.active",h,f+v),a[h]=c[h]);b[d].forceExpiration=!1;return a}():(b[d]=w(k,function(){return"$"+e+"$"}),n.$broadcast("angular-promise-cache.new","$"+e+"$"));return b[d].apply(this,f).then(function(a){r&&
+(c.response=a,c.resolver="$"+(m||e)+"$",p.setItem(s,JSON.stringify(c)));return a},function(a){angular.isFunction(g)&&(b[d].forceExpiration=!0);return u.reject(a)})}}]);
Oops, something went wrong.

0 comments on commit c0f9038

Please sign in to comment.