diff options
| author | Igor Minar | 2011-02-16 20:04:39 -0500 | 
|---|---|---|
| committer | Igor Minar | 2011-11-30 11:03:42 -0500 | 
| commit | 497839f583ca3dd75583fb996bb764cbd6d7c4ac (patch) | |
| tree | c05ee9e7888da61b4e6c287d9e9f6d2fc457d8d0 | |
| parent | 5487bdb3d1c905fb9453644f7e290c75dcee14c1 (diff) | |
| download | angular.js-497839f583ca3dd75583fb996bb764cbd6d7c4ac.tar.bz2 | |
feat($cacheFactory): add general purpose $cacheFactory service
| -rw-r--r-- | angularFiles.js | 1 | ||||
| -rw-r--r-- | src/AngularPublic.js | 1 | ||||
| -rw-r--r-- | src/service/cacheFactory.js | 152 | ||||
| -rw-r--r-- | test/service/cacheFactorySpec.js | 317 | 
4 files changed, 471 insertions, 0 deletions
| diff --git a/angularFiles.js b/angularFiles.js index 6bbaa448..d64630fa 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -10,6 +10,7 @@ angularFiles = {      'src/apis.js',      'src/service/autoScroll.js',      'src/service/browser.js', +    'src/service/cacheFactory.js',      'src/service/compiler.js',      'src/service/cookieStore.js',      'src/service/cookies.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 6352df33..577c29ee 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -68,6 +68,7 @@ function ngModule($provide, $injector) {    $provide.service('$autoScroll', $AutoScrollProvider);    $provide.service('$browser', $BrowserProvider); +  $provide.service('$cacheFactory', $CacheFactoryProvider);    $provide.service('$compile', $CompileProvider);    $provide.service('$cookies', $CookiesProvider);    $provide.service('$cookieStore', $CookieStoreProvider); diff --git a/src/service/cacheFactory.js b/src/service/cacheFactory.js new file mode 100644 index 00000000..ccc69313 --- /dev/null +++ b/src/service/cacheFactory.js @@ -0,0 +1,152 @@ +/** + * @ngdoc object + * @name angular.module.ng.$cacheFactory + * + * @description + * Factory that constructs cache objects. + * + * + * @param {string} cacheId Name or id of the newly created cache. + * @param {object=} options Options object that specifies the cache behavior. Properties: + * + *   - `{number=}` `capacity` — turns the cache into LRU cache. + * + * @returns {object} Newly created cache object with the following set of methods: + * + * - `{string}` `id()` — Returns id or name of the cache. + * - `{number}` `size()` — Returns number of items currently in the cache + * - `{void}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache + * - `{(*}} `get({string} key) — Returns cached value for `key` or undefined for cache miss. + * - `{void}` `remove{string} key) — Removes a key-value pair from the cache. + * - `{void}` `removeAll() — Removes all cached values. + * + */ +function $CacheFactoryProvider() { + +  this.$get = function() { +    var caches = {}; + +    function cacheFactory(cacheId, options) { +      if (cacheId in caches) { +        throw Error('cacheId ' + cacheId + ' taken'); +      } + +      var size = 0, +          stats = extend({}, options, {id: cacheId}), +          data = {}, +          capacity = (options && options.capacity) || Number.MAX_VALUE, +          lruHash = {}, +          freshEnd = null, +          staleEnd = null; + +      return caches[cacheId] = { + +        put: function(key, value) { +          var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + +          refresh(lruEntry); + +          if (isUndefined(value)) return; +          if (!(key in data)) size++; +          data[key] = value; + +          if (size > capacity) { +            this.remove(staleEnd.key); +          } +        }, + + +        get: function(key) { +          var lruEntry = lruHash[key]; + +          if (!lruEntry) return; + +          refresh(lruEntry); + +          return data[key]; +        }, + + +        remove: function(key) { +          var lruEntry = lruHash[key]; + +          if (lruEntry == freshEnd) freshEnd = lruEntry.p; +          if (lruEntry == staleEnd) staleEnd = lruEntry.n; +          link(lruEntry.n,lruEntry.p); + +          delete lruHash[key]; +          delete data[key]; +          size--; +        }, + + +        removeAll: function() { +          data = {}; +          size = 0; +          lruHash = {}; +          freshEnd = staleEnd = null; +        }, + + +        destroy: function() { +          data = null; +          stats = null; +          lruHash = null; +          delete caches[cacheId]; +        }, + + +        info: function() { +          return extend({}, stats, {size: size}); +        } +      }; + + +      /** +       * makes the `entry` the freshEnd of the LRU linked list +       */ +      function refresh(entry) { +        if (entry != freshEnd) { +          if (!staleEnd) { +            staleEnd = entry; +          } else if (staleEnd == entry) { +            staleEnd = entry.n; +          } + +          link(entry.n, entry.p); +          link(entry, freshEnd); +          freshEnd = entry; +          freshEnd.n = null; +        } +      } + + +      /** +       * bidirectionally links two entries of the LRU linked list +       */ +      function link(nextEntry, prevEntry) { +        if (nextEntry != prevEntry) { +          if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify +          if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify +        } +      } +    } + + +    cacheFactory.info = function() { +      var info = {}; +      forEach(caches, function(cache, cacheId) { +        info[cacheId] = cache.info(); +      }); +      return info; +    }; + + +    cacheFactory.get = function(cacheId) { +      return caches[cacheId]; +    }; + + +    return cacheFactory; +  }; +} diff --git a/test/service/cacheFactorySpec.js b/test/service/cacheFactorySpec.js new file mode 100644 index 00000000..dc68b63d --- /dev/null +++ b/test/service/cacheFactorySpec.js @@ -0,0 +1,317 @@ +describe('$cacheFactory', function() { + +  it('should be injected', inject(function($cacheFactory) { +    expect($cacheFactory).toBeDefined(); +  })); + + +  it('should return a new cache whenever called', inject(function($cacheFactory) { +    var cache1 = $cacheFactory('cache1'); +    var cache2 = $cacheFactory('cache2'); +    expect(cache1).not.toEqual(cache2); +  })); + + +  it('should complain if the cache id is being reused', inject(function($cacheFactory) { +    $cacheFactory('cache1'); +    expect(function() { $cacheFactory('cache1'); }). +      toThrow('cacheId cache1 taken'); +  })); + + +  describe('info', function() { + +    it('should provide info about all created caches', inject(function($cacheFactory) { +      expect($cacheFactory.info()).toEqual({}); + +      var cache1 = $cacheFactory('cache1'); +      expect($cacheFactory.info()).toEqual({cache1: {id: 'cache1', size: 0}}); + +      cache1.put('foo', 'bar'); +      expect($cacheFactory.info()).toEqual({cache1: {id: 'cache1', size: 1}}); +    })); +  }); + + +  describe('get', function() { + +    it('should return a cache if looked up by id', inject(function($cacheFactory) { +      var cache1 = $cacheFactory('cache1'), +          cache2 = $cacheFactory('cache2'); + +      expect(cache1).not.toBe(cache2); +      expect(cache1).toBe($cacheFactory.get('cache1')); +      expect(cache2).toBe($cacheFactory.get('cache2')); +    })); +  }); + +  describe('cache', function() { +    var cache; + +    beforeEach(inject(function($cacheFactory) { +      cache = $cacheFactory('test'); +    })); + + +    describe('put, get & remove', function() { + +      it('should add cache entries via add and retrieve them via get', inject(function($cacheFactory) { +        cache.put('key1', 'bar'); +        cache.put('key2', {bar:'baz'}); + +        expect(cache.get('key2')).toEqual({bar:'baz'}); +        expect(cache.get('key1')).toBe('bar'); +      })); + + +      it('should ignore put if the value is undefined', inject(function($cacheFactory) { +        cache.put(); +        cache.put('key1'); +        cache.put('key2', undefined); + +        expect(cache.info().size).toBe(0); +      })); + + +      it('should remove entries via remove', inject(function($cacheFactory) { +        cache.put('k1', 'foo'); +        cache.put('k2', 'bar'); + +        cache.remove('k2'); + +        expect(cache.get('k1')).toBe('foo'); +        expect(cache.get('k2')).toBeUndefined(); + +        cache.remove('k1'); + +        expect(cache.get('k1')).toBeUndefined(); +        expect(cache.get('k2')).toBeUndefined(); +      })); + + +      it('should stringify keys', inject(function($cacheFactory) { +        cache.put('123', 'foo'); +        cache.put(123, 'bar'); + +        expect(cache.get('123')).toBe('bar'); +        expect(cache.info().size).toBe(1); + +        cache.remove(123); +        expect(cache.info().size).toBe(0); +      })); +    }); + + +    describe('info', function() { + +      it('should size increment with put and decrement with remove', inject(function($cacheFactory) { +        expect(cache.info().size).toBe(0); + +        cache.put('foo', 'bar'); +        expect(cache.info().size).toBe(1); + +        cache.put('baz', 'boo'); +        expect(cache.info().size).toBe(2); + +        cache.remove('baz'); +        expect(cache.info().size).toBe(1); + +        cache.remove('foo'); +        expect(cache.info().size).toBe(0); +      })); + + +      it('should return cache id', inject(function($cacheFactory) { +        expect(cache.info().id).toBe('test'); +      })); +    }); + + +    describe('removeAll', function() { + +      it('should blow away all data', inject(function($cacheFactory) { +        cache.put('id1', 1); +        cache.put('id2', 2); +        cache.put('id3', 3); +        expect(cache.info().size).toBe(3); + +        cache.removeAll(); + +        expect(cache.info().size).toBe(0); +        expect(cache.get('id1')).toBeUndefined(); +        expect(cache.get('id2')).toBeUndefined(); +        expect(cache.get('id3')).toBeUndefined(); +      })); +    }); + + +    describe('destroy', function() { + +      it('should make the cache unusable and remove references to it from $cacheFactory', inject(function($cacheFactory) { +        cache.put('foo', 'bar'); +        cache.destroy(); + +        expect(function() { cache.get('foo'); } ).toThrow(); +        expect(function() { cache.get('neverexisted'); }).toThrow(); +        expect(function() { cache.put('foo', 'bar'); }).toThrow(); + +        expect($cacheFactory.get('test')).toBeUndefined(); +        expect($cacheFactory.info()).toEqual({}); +      })); +    }); +  }); + + +  describe('LRU cache', function() { + +    it('should create cache with defined capacity', inject(function($cacheFactory) { +      cache = $cacheFactory('cache1', {capacity: 5}); +      expect(cache.info().size).toBe(0); + +      for (var i=0; i<5; i++) { +        cache.put('id' + i, i); +      } + +      expect(cache.info().size).toBe(5); + +      cache.put('id5', 5); +      expect(cache.info().size).toBe(5); +      cache.put('id6', 6); +      expect(cache.info().size).toBe(5); +    })); + + +    describe('eviction', function() { + +      beforeEach(inject(function($cacheFactory) { +        cache = $cacheFactory('cache1', {capacity: 2}); + +        cache.put('id0', 0); +        cache.put('id1', 1); +      })); + + +      it('should kick out the first entry on put', inject(function($cacheFactory) { +        cache.put('id2', 2); +        expect(cache.get('id0')).toBeUndefined(); +        expect(cache.get('id1')).toBe(1); +        expect(cache.get('id2')).toBe(2); +      })); + + +      it('should refresh an entry via get', inject(function($cacheFactory) { +        cache.get('id0'); +        cache.put('id2', 2); +        expect(cache.get('id0')).toBe(0); +        expect(cache.get('id1')).toBeUndefined(); +        expect(cache.get('id2')).toBe(2); +      })); + + +      it('should refresh an entry via put', inject(function($cacheFactory) { +        cache.put('id0', '00'); +        cache.put('id2', 2); +        expect(cache.get('id0')).toBe('00'); +        expect(cache.get('id1')).toBeUndefined(); +        expect(cache.get('id2')).toBe(2); +      })); + + +      it('should not purge an entry if another one was removed', inject(function($cacheFactory) { +        cache.remove('id1'); +        cache.put('id2', 2); +        expect(cache.get('id0')).toBe(0); +        expect(cache.get('id1')).toBeUndefined(); +        expect(cache.get('id2')).toBe(2); +      })); + + +      it('should purge the next entry if the stalest one was removed', inject(function($cacheFactory) { +        cache.remove('id0'); +        cache.put('id2', 2); +        cache.put('id3', 3); +        expect(cache.get('id0')).toBeUndefined(); +        expect(cache.get('id1')).toBeUndefined(); +        expect(cache.get('id2')).toBe(2); +        expect(cache.get('id3')).toBe(3); +      })); + + +      it('should correctly recreate the linked list if all cache entries were removed', inject(function($cacheFactory) { +        cache.remove('id0'); +        cache.remove('id1'); +        cache.put('id2', 2); +        cache.put('id3', 3); +        cache.put('id4', 4); +        expect(cache.get('id0')).toBeUndefined(); +        expect(cache.get('id1')).toBeUndefined(); +        expect(cache.get('id2')).toBeUndefined(); +        expect(cache.get('id3')).toBe(3); +        expect(cache.get('id4')).toBe(4); +      })); + + +      it('should blow away the entire cache via removeAll and start evicting when full', inject(function($cacheFactory) { +        cache.put('id0', 0); +        cache.put('id1', 1); +        cache.removeAll(); + +        cache.put('id2', 2); +        cache.put('id3', 3); +        cache.put('id4', 4); + +        expect(cache.info().size).toBe(2); +        expect(cache.get('id0')).toBeUndefined(); +        expect(cache.get('id1')).toBeUndefined(); +        expect(cache.get('id2')).toBeUndefined(); +        expect(cache.get('id3')).toBe(3); +        expect(cache.get('id4')).toBe(4); +      })); + + +      it('should correctly refresh and evict items if operations are chained', inject(function($cacheFactory) { +        cache = $cacheFactory('cache2', {capacity: 3}); + +        cache.put('id0', 0); //0 +        cache.put('id1', 1); //1,0 +        cache.put('id2', 2); //2,1,0 +        cache.get('id0');    //0,2,1 +        cache.put('id3', 3); //3,0,2 +        cache.put('id0', 9); //0,3,2 +        cache.put('id4', 4); //4,0,3 + +        expect(cache.get('id3')).toBe(3); +        expect(cache.get('id0')).toBe(9); +        expect(cache.get('id4')).toBe(4); + +        cache.remove('id0'); //4,3 +        cache.remove('id3'); //4 +        cache.put('id5', 5); //5,4 +        cache.put('id6', 6); //6,5,4 +        cache.get('id4');    //4,6,5 +        cache.put('id7', 7); //7,4,6 + +        expect(cache.get('id0')).toBeUndefined(); +        expect(cache.get('id1')).toBeUndefined(); +        expect(cache.get('id2')).toBeUndefined(); +        expect(cache.get('id3')).toBeUndefined(); +        expect(cache.get('id4')).toBe(4); +        expect(cache.get('id5')).toBeUndefined(); +        expect(cache.get('id6')).toBe(6); +        expect(cache.get('id7')).toBe(7); + +        cache.removeAll(); +        cache.put('id0', 0); //0 +        cache.put('id1', 1); //1,0 +        cache.put('id2', 2); //2,1,0 +        cache.put('id3', 3); //3,2,1 + +        expect(cache.info().size).toBe(3); +        expect(cache.get('id0')).toBeUndefined(); +        expect(cache.get('id1')).toBe(1); +        expect(cache.get('id2')).toBe(2); +        expect(cache.get('id3')).toBe(3); +      })); +    }); +  }); +}); | 
