From cc703784015968541d3305b837fce9cddf4a3625 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 16 Feb 2011 20:04:39 -0500 Subject: [PATCH 1/9] add $cacheFactory and $lruCacheFactory + specs --- Rakefile | 2 + src/angular-bootstrap.js | 2 + src/service/cacheFactory.js | 53 +++++++++ src/service/lruCacheFactory.js | 85 +++++++++++++++ test/service/cacheFactorySpec.js | 143 +++++++++++++++++++++++++ test/service/lruCacheFactorySpec.js | 160 ++++++++++++++++++++++++++++ 6 files changed, 445 insertions(+) create mode 100644 src/service/cacheFactory.js create mode 100644 src/service/lruCacheFactory.js create mode 100644 test/service/cacheFactorySpec.js create mode 100644 test/service/lruCacheFactorySpec.js diff --git a/Rakefile b/Rakefile index fff9ce1c53f5..63951250ad75 100644 --- a/Rakefile +++ b/Rakefile @@ -16,6 +16,7 @@ ANGULAR = [ 'src/filters.js', 'src/formatters.js', 'src/validators.js', + 'src/service/cacheFactory.js', 'src/service/cookieStore.js', 'src/service/cookies.js', 'src/service/defer.js', @@ -25,6 +26,7 @@ ANGULAR = [ 'src/service/invalidWidgets.js', 'src/service/location.js', 'src/service/log.js', + 'src/service/lruCacheFactory.js', 'src/service/resource.js', 'src/service/route.js', 'src/service/updateView.js', diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index 946968abf7ba..ce9b1b5ec4fc 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -119,6 +119,7 @@ // Extension points + 'service/cacheFactory.js', 'service/cookieStore.js', 'service/cookies.js', 'service/defer.js', @@ -128,6 +129,7 @@ 'service/invalidWidgets.js', 'service/location.js', 'service/log.js', + 'service/lruCacheFactory.js', 'service/resource.js', 'service/route.js', 'service/updateView.js', diff --git a/src/service/cacheFactory.js b/src/service/cacheFactory.js new file mode 100644 index 000000000000..6c12eea9c8df --- /dev/null +++ b/src/service/cacheFactory.js @@ -0,0 +1,53 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$cacheFactory + * + * @description + * + * + */ +angularServiceInject("$cacheFactory", function() { + var caches = {}; + + function cacheFactory(cacheId) { + if (cacheId in caches) { + throw Error('cacheId ' + cacheId + ' taken'); + } + + var stats = caches[cacheId] = {size:0}, + data = {}; + + return { + id: function() { return cacheId; }, + + size: function() { return stats.size; }, + + put: function(key, value) { + if (isUndefined(value)) return; + if (!(key in data)) stats.size++; + data[key] = value; + }, + + get: function(key) { + return data[key]; + }, + + remove: function(key) { + delete data[key]; + stats.size--; + }, + + removeAll: function(key) { + data = {}; + stats.size = 0; + } + }; + } + + cacheFactory.stats = function() { + return copy(caches); + } + + return cacheFactory; +}); diff --git a/src/service/lruCacheFactory.js b/src/service/lruCacheFactory.js new file mode 100644 index 000000000000..28e1a6c0e5b9 --- /dev/null +++ b/src/service/lruCacheFactory.js @@ -0,0 +1,85 @@ +angularServiceInject('$lruCacheFactory', function($cacheFactory) { + + return function(cacheId, capacity) { + var cache = $cacheFactory(cacheId), + lruHash = {}, + freshEnd = _null, + staleEnd = _null; + + return { + id: function() { return cache.id(); }, + + + size: function() { return cache.size(); }, + + + put: function(key, value) { + var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + + refresh(lruEntry); + cache.put(key, value); + + if (cache.size() > capacity) { + cache.remove(staleEnd.key); + delete lruHash[staleEnd.key]; + staleEnd = staleEnd.next; + staleEnd.prev = _null; + } + }, + + + get: function(key) { + var lruEntry = lruHash[key]; + + if (!lruEntry) return; + + refresh(lruEntry); + + return cache.get(key); + }, + + + remove: function(key) { + var lruEntry = lruHash[key]; + + if (lruEntry == freshEnd) freshEnd = lruEntry.prev; + if (lruEntry == staleEnd) staleEnd = lruEntry.next; + link(lruEntry.next,lruEntry.prev); + + delete lruHash[key]; + cache.remove(key); + }, + + + removeAll: function() { + cache.removeAll(); + lruHash = {}; + freshEnd = staleEnd = _null; + } + } + + + function refresh(entry) { + if (entry != freshEnd) { + if (!staleEnd) { + staleEnd = entry; + } else if (staleEnd == entry) { + staleEnd = entry.next; + } + + link(entry.next, entry.prev); + link(entry, freshEnd); + freshEnd = entry; + freshEnd.next = _null; + } + } + + + function link(nextEntry, prevEntry) { + if (nextEntry != prevEntry) { + if (nextEntry) nextEntry.prev = prevEntry; + if (prevEntry) prevEntry.next = nextEntry; + } + } + } +}, ['$cacheFactory']); diff --git a/test/service/cacheFactorySpec.js b/test/service/cacheFactorySpec.js new file mode 100644 index 000000000000..fd07db331dc3 --- /dev/null +++ b/test/service/cacheFactorySpec.js @@ -0,0 +1,143 @@ +describe('$cacheFactory', function() { + var scope, $cacheFactory; + + beforeEach(function() { + scope = angular.scope(); + $cacheFactory = scope.$service('$cacheFactory'); + }); + + + it('should be injected', function() { + expect($cacheFactory).toBeDefined(); + }); + + + it('should return a new cache whenever called', function() { + var cache1 = $cacheFactory('cache1'); + var cache2 = $cacheFactory('cache2'); + expect(cache1).not.toEqual(cache2); + }); + + + it('should complain if the cache id is being reused', function() { + $cacheFactory('cache1'); + expect(function() {$cacheFactory('cache1')}). + toThrow("cacheId cache1 taken"); + }); + + + it('should provide stats about all created caches', function() { + expect($cacheFactory.stats()).toEqual({}); + + var cache1 = $cacheFactory('cache1'); + expect($cacheFactory.stats()).toEqual({cache1: {size: 0}}); + + cache1.put('foo', 'bar'); + expect($cacheFactory.stats()).toEqual({cache1: {size: 1}}); + }); + + + + describe('cache', function() { + var cache; + + beforeEach(function() { + cache = $cacheFactory('test'); + }); + + + describe('put, get & remove', function() { + + it('should add cache entries via add and retrieve them via get', function() { + 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', function() { + cache.put(); + cache.put('key1'); + cache.put('key2', undefined); + + expect(cache.size()).toBe(0); + }); + + + it('should remove entries via remove', function() { + 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', function() { + cache.put('123', 'foo'); + cache.put(123, 'bar'); + + expect(cache.get('123')).toBe('bar'); + expect(cache.size()).toBe(1); + + cache.remove(123); + expect(cache.size()).toBe(0); + }) + }); + + + describe('size', function() { + + it('should increment with put and decrement with remove', function() { + expect(cache.size()).toBe(0); + + cache.put('foo', 'bar'); + expect(cache.size()).toBe(1); + + cache.put('baz', 'boo'); + expect(cache.size()).toBe(2); + + cache.remove('baz'); + expect(cache.size()).toBe(1); + + cache.remove('foo'); + expect(cache.size()).toBe(0); + }); + }); + + + describe('id', function() { + + it('should return cache id', function() { + expect(cache.id()).toBe('test'); + }) + }); + + + describe('removeAll', function() { + + it('should blow away all data', function() { + cache.put('id1', 1); + cache.put('id2', 2); + cache.put('id3', 3); + expect(cache.size()).toBe(3); + + cache.removeAll(); + + expect(cache.size()).toBe(0); + expect(cache.get('id1')).toBeUndefined(); + expect(cache.get('id2')).toBeUndefined(); + expect(cache.get('id3')).toBeUndefined(); + }); + }); + }); +}); diff --git a/test/service/lruCacheFactorySpec.js b/test/service/lruCacheFactorySpec.js new file mode 100644 index 000000000000..286a1438f737 --- /dev/null +++ b/test/service/lruCacheFactorySpec.js @@ -0,0 +1,160 @@ +describe('$lruCacheFactory', function() { + + var scope, $lruCacheFactory, cache; + + beforeEach(function() { + scope = angular.scope(); + $lruCacheFactory = scope.$service('$lruCacheFactory'); + }); + + + it('should create cache with defined capacity', function() { + cache = $lruCacheFactory('cache1', 5); + expect(cache.size()).toBe(0); + + for (var i=0; i<5; i++) { + cache.put('id' + i, i); + } + + expect(cache.size()).toBe(5); + + cache.put('id5', 5); + expect(cache.size()).toBe(5); + cache.put('id6', 6); + expect(cache.size()).toBe(5); + }); + + + describe('eviction', function() { + + beforeEach(function() { + cache = $lruCacheFactory('cache1', 2); + + cache.put('id0', 0); + cache.put('id1', 1); + }); + + + it('should kick out the first entry on put', function() { + 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', function() { + 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', function() { + 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', function() { + 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', function() { + 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', function() { + 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', function() { + cache.put('id0', 0); + cache.put('id1', 1); + cache.removeAll(); + + cache.put('id2', 2); + cache.put('id3', 3); + cache.put('id4', 4); + + expect(cache.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', function() { + cache = $lruCacheFactory('cache2', 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.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); + }); + }); +}); From 70f453b1d89747e29224577c907db73bdc884483 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 15 Mar 2011 17:14:22 -0700 Subject: [PATCH 2/9] merging cacheFactory and lruCacheFactory --- src/service/cacheFactory.js | 83 ++++++++++++++- src/service/lruCacheFactory.js | 85 --------------- test/service/cacheFactorySpec.js | 158 ++++++++++++++++++++++++++- test/service/lruCacheFactorySpec.js | 160 ---------------------------- 4 files changed, 234 insertions(+), 252 deletions(-) delete mode 100644 src/service/lruCacheFactory.js delete mode 100644 test/service/lruCacheFactorySpec.js diff --git a/src/service/cacheFactory.js b/src/service/cacheFactory.js index 6c12eea9c8df..a974308a5f21 100644 --- a/src/service/cacheFactory.js +++ b/src/service/cacheFactory.js @@ -4,47 +4,120 @@ * @name angular.service.$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. + * */ -angularServiceInject("$cacheFactory", function() { +angularServiceInject('$cacheFactory', function() { + var caches = {}; - function cacheFactory(cacheId) { + function cacheFactory(cacheId, options) { if (cacheId in caches) { throw Error('cacheId ' + cacheId + ' taken'); } var stats = caches[cacheId] = {size:0}, - data = {}; + data = {}, + capacity = (options && options.capacity) || Number.MAX_VALUE, + lruHash = {}, + freshEnd = _null, + staleEnd = _null; return { id: function() { return cacheId; }, + size: function() { return stats.size; }, + put: function(key, value) { + var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + + refresh(lruEntry); + if (isUndefined(value)) return; if (!(key in data)) stats.size++; data[key] = value; + + if (stats.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.prev; + if (lruEntry == staleEnd) staleEnd = lruEntry.next; + link(lruEntry.next,lruEntry.prev); + + delete lruHash[key]; delete data[key]; stats.size--; }, - removeAll: function(key) { + + removeAll: function() { data = {}; stats.size = 0; + lruHash = {}; + freshEnd = staleEnd = _null; } - }; + } + + + function refresh(entry) { + if (entry != freshEnd) { + if (!staleEnd) { + staleEnd = entry; + } else if (staleEnd == entry) { + staleEnd = entry.next; + } + + link(entry.next, entry.prev); + link(entry, freshEnd); + freshEnd = entry; + freshEnd.next = _null; + } + } + + + function link(nextEntry, prevEntry) { + if (nextEntry != prevEntry) { + if (nextEntry) nextEntry.prev = prevEntry; + if (prevEntry) prevEntry.next = nextEntry; + } + } } + cacheFactory.stats = function() { return copy(caches); } diff --git a/src/service/lruCacheFactory.js b/src/service/lruCacheFactory.js deleted file mode 100644 index 28e1a6c0e5b9..000000000000 --- a/src/service/lruCacheFactory.js +++ /dev/null @@ -1,85 +0,0 @@ -angularServiceInject('$lruCacheFactory', function($cacheFactory) { - - return function(cacheId, capacity) { - var cache = $cacheFactory(cacheId), - lruHash = {}, - freshEnd = _null, - staleEnd = _null; - - return { - id: function() { return cache.id(); }, - - - size: function() { return cache.size(); }, - - - put: function(key, value) { - var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); - - refresh(lruEntry); - cache.put(key, value); - - if (cache.size() > capacity) { - cache.remove(staleEnd.key); - delete lruHash[staleEnd.key]; - staleEnd = staleEnd.next; - staleEnd.prev = _null; - } - }, - - - get: function(key) { - var lruEntry = lruHash[key]; - - if (!lruEntry) return; - - refresh(lruEntry); - - return cache.get(key); - }, - - - remove: function(key) { - var lruEntry = lruHash[key]; - - if (lruEntry == freshEnd) freshEnd = lruEntry.prev; - if (lruEntry == staleEnd) staleEnd = lruEntry.next; - link(lruEntry.next,lruEntry.prev); - - delete lruHash[key]; - cache.remove(key); - }, - - - removeAll: function() { - cache.removeAll(); - lruHash = {}; - freshEnd = staleEnd = _null; - } - } - - - function refresh(entry) { - if (entry != freshEnd) { - if (!staleEnd) { - staleEnd = entry; - } else if (staleEnd == entry) { - staleEnd = entry.next; - } - - link(entry.next, entry.prev); - link(entry, freshEnd); - freshEnd = entry; - freshEnd.next = _null; - } - } - - - function link(nextEntry, prevEntry) { - if (nextEntry != prevEntry) { - if (nextEntry) nextEntry.prev = prevEntry; - if (prevEntry) prevEntry.next = nextEntry; - } - } - } -}, ['$cacheFactory']); diff --git a/test/service/cacheFactorySpec.js b/test/service/cacheFactorySpec.js index fd07db331dc3..39984e513952 100644 --- a/test/service/cacheFactorySpec.js +++ b/test/service/cacheFactorySpec.js @@ -1,5 +1,6 @@ describe('$cacheFactory', function() { - var scope, $cacheFactory; + + var scope, $cacheFactory, cache; beforeEach(function() { scope = angular.scope(); @@ -37,7 +38,6 @@ describe('$cacheFactory', function() { }); - describe('cache', function() { var cache; @@ -140,4 +140,158 @@ describe('$cacheFactory', function() { }); }); }); + + + describe('LRU cache', function() { + + it('should create cache with defined capacity', function() { + cache = $cacheFactory('cache1', {capacity: 5}); + expect(cache.size()).toBe(0); + + for (var i=0; i<5; i++) { + cache.put('id' + i, i); + } + + expect(cache.size()).toBe(5); + + cache.put('id5', 5); + expect(cache.size()).toBe(5); + cache.put('id6', 6); + expect(cache.size()).toBe(5); + }); + + + describe('eviction', function() { + + beforeEach(function() { + cache = $cacheFactory('cache1', {capacity: 2}); + + cache.put('id0', 0); + cache.put('id1', 1); + }); + + + it('should kick out the first entry on put', function() { + 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', function() { + 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', function() { + 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', function() { + 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', function() { + 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', function() { + 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', function() { + cache.put('id0', 0); + cache.put('id1', 1); + cache.removeAll(); + + cache.put('id2', 2); + cache.put('id3', 3); + cache.put('id4', 4); + + expect(cache.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', function() { + 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.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); + }); + }); + }); }); diff --git a/test/service/lruCacheFactorySpec.js b/test/service/lruCacheFactorySpec.js deleted file mode 100644 index 286a1438f737..000000000000 --- a/test/service/lruCacheFactorySpec.js +++ /dev/null @@ -1,160 +0,0 @@ -describe('$lruCacheFactory', function() { - - var scope, $lruCacheFactory, cache; - - beforeEach(function() { - scope = angular.scope(); - $lruCacheFactory = scope.$service('$lruCacheFactory'); - }); - - - it('should create cache with defined capacity', function() { - cache = $lruCacheFactory('cache1', 5); - expect(cache.size()).toBe(0); - - for (var i=0; i<5; i++) { - cache.put('id' + i, i); - } - - expect(cache.size()).toBe(5); - - cache.put('id5', 5); - expect(cache.size()).toBe(5); - cache.put('id6', 6); - expect(cache.size()).toBe(5); - }); - - - describe('eviction', function() { - - beforeEach(function() { - cache = $lruCacheFactory('cache1', 2); - - cache.put('id0', 0); - cache.put('id1', 1); - }); - - - it('should kick out the first entry on put', function() { - 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', function() { - 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', function() { - 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', function() { - 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', function() { - 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', function() { - 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', function() { - cache.put('id0', 0); - cache.put('id1', 1); - cache.removeAll(); - - cache.put('id2', 2); - cache.put('id3', 3); - cache.put('id4', 4); - - expect(cache.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', function() { - cache = $lruCacheFactory('cache2', 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.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); - }); - }); -}); From da2a55ee841b5c9e3ed916faff95ca796b33c59a Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 28 Feb 2011 09:46:26 -0800 Subject: [PATCH 3/9] switched from xhr.cache to dedicated cache for ng:view --- src/widgets.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/widgets.js b/src/widgets.js index 6d5ffe65267f..20955f5ef3aa 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -1059,9 +1059,12 @@ angularWidget('ng:view', function(element) { if (!element[0]['ng:compiled']) { element[0]['ng:compiled'] = true; - return injectService(['$xhr.cache', '$route'], function($xhr, $route, element){ - var parentScope = this, - childScope; + return injectService(['$xhr', '$route', '$cacheFactory', '$defer'], + function($xhr, $route, $cacheFactory, $defer, element){ + var templateCache = $cacheFactory('ng:view'), + parentScope = this, + childScope, + template; $route.onChange(function(){ var src; @@ -1072,11 +1075,19 @@ angularWidget('ng:view', function(element) { } if (src) { - //xhr's callback must be async, see commit history for more info - $xhr('GET', src, function(code, response){ - element.html(response); - compiler.compile(element)(childScope); - }); + if (template = templateCache.get(src)) { + //this must be async see 9bd2c396 for more info + $defer(function() { + element.html(template); + compiler.compile(element)(childScope); + }); + } else { + $xhr('GET', src, function(code, response){ + templateCache.put(src, response); + element.html(response); + compiler.compile(element)(childScope); + }); + } } else { element.html(''); } From bae02653dcb20b2b3f3a7ffbebd65596540dc43d Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 1 Apr 2011 17:55:54 -0700 Subject: [PATCH 4/9] _null -> null, comments, valueFn --- src/service/cacheFactory.js | 16 +++++++++++----- test/service/cacheFactorySpec.js | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/service/cacheFactory.js b/src/service/cacheFactory.js index a974308a5f21..bdd5f3f1623b 100644 --- a/src/service/cacheFactory.js +++ b/src/service/cacheFactory.js @@ -35,11 +35,11 @@ angularServiceInject('$cacheFactory', function() { data = {}, capacity = (options && options.capacity) || Number.MAX_VALUE, lruHash = {}, - freshEnd = _null, - staleEnd = _null; + freshEnd = null, + staleEnd = null; return { - id: function() { return cacheId; }, + id: valueFn(cacheId), size: function() { return stats.size; }, @@ -88,11 +88,14 @@ angularServiceInject('$cacheFactory', function() { data = {}; stats.size = 0; lruHash = {}; - freshEnd = staleEnd = _null; + freshEnd = staleEnd = null; } } + /** + * makes the `entry` the freshEnd of the LRU linked list + */ function refresh(entry) { if (entry != freshEnd) { if (!staleEnd) { @@ -104,11 +107,14 @@ angularServiceInject('$cacheFactory', function() { link(entry.next, entry.prev); link(entry, freshEnd); freshEnd = entry; - freshEnd.next = _null; + freshEnd.next = null; } } + /** + * bydirectionally links two entries of the LRU linked list + */ function link(nextEntry, prevEntry) { if (nextEntry != prevEntry) { if (nextEntry) nextEntry.prev = prevEntry; diff --git a/test/service/cacheFactorySpec.js b/test/service/cacheFactorySpec.js index 39984e513952..dfb0d5cc24be 100644 --- a/test/service/cacheFactorySpec.js +++ b/test/service/cacheFactorySpec.js @@ -23,7 +23,7 @@ describe('$cacheFactory', function() { it('should complain if the cache id is being reused', function() { $cacheFactory('cache1'); expect(function() {$cacheFactory('cache1')}). - toThrow("cacheId cache1 taken"); + toThrow('cacheId cache1 taken'); }); From b9a0df1634cf2b1235c574f7ea903c76ad7c93b1 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Apr 2011 12:02:33 -0700 Subject: [PATCH 5/9] add #get and cache#destroy --- src/service/cacheFactory.js | 26 ++++++++++++++---- test/service/cacheFactorySpec.js | 45 +++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/service/cacheFactory.js b/src/service/cacheFactory.js index bdd5f3f1623b..7e514c96f75b 100644 --- a/src/service/cacheFactory.js +++ b/src/service/cacheFactory.js @@ -24,21 +24,22 @@ */ angularServiceInject('$cacheFactory', function() { - var caches = {}; + var caches = {}, + cacheStats = {}; function cacheFactory(cacheId, options) { if (cacheId in caches) { throw Error('cacheId ' + cacheId + ' taken'); } - var stats = caches[cacheId] = {size:0}, + var stats = cacheStats[cacheId] = extend({}, options, {size:0}), data = {}, capacity = (options && options.capacity) || Number.MAX_VALUE, lruHash = {}, freshEnd = null, staleEnd = null; - return { + return caches[cacheId] = { id: valueFn(cacheId), @@ -89,6 +90,15 @@ angularServiceInject('$cacheFactory', function() { stats.size = 0; lruHash = {}; freshEnd = staleEnd = null; + }, + + + destroy: function() { + data = null; + stats = null; + lruHash = null; + delete caches[cacheId]; + delete cacheStats[cacheId]; } } @@ -124,9 +134,15 @@ angularServiceInject('$cacheFactory', function() { } - cacheFactory.stats = function() { - return copy(caches); + cacheFactory.info = function() { + return copy(cacheStats); } + + cacheFactory.get = function(cacheId) { + return caches[cacheId]; + } + + return cacheFactory; }); diff --git a/test/service/cacheFactorySpec.js b/test/service/cacheFactorySpec.js index dfb0d5cc24be..7bdbd4c0ddb1 100644 --- a/test/service/cacheFactorySpec.js +++ b/test/service/cacheFactorySpec.js @@ -1,6 +1,6 @@ describe('$cacheFactory', function() { - var scope, $cacheFactory, cache; + var scope, $cacheFactory; beforeEach(function() { scope = angular.scope(); @@ -27,17 +27,32 @@ describe('$cacheFactory', function() { }); - it('should provide stats about all created caches', function() { - expect($cacheFactory.stats()).toEqual({}); + describe('info', function() { - var cache1 = $cacheFactory('cache1'); - expect($cacheFactory.stats()).toEqual({cache1: {size: 0}}); + it('should provide info about all created caches', function() { + expect($cacheFactory.info()).toEqual({}); + + var cache1 = $cacheFactory('cache1'); + expect($cacheFactory.info()).toEqual({cache1: {size: 0}}); - cache1.put('foo', 'bar'); - expect($cacheFactory.stats()).toEqual({cache1: {size: 1}}); + cache1.put('foo', 'bar'); + expect($cacheFactory.info()).toEqual({cache1: {size: 1}}); + }); }); + describe('get', function() { + + it('should return a cache if looked up by id', function() { + 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; @@ -139,6 +154,22 @@ describe('$cacheFactory', function() { expect(cache.get('id3')).toBeUndefined(); }); }); + + + describe('destroy', function() { + + it('should make the cache unusable and remove references to it from $cacheFactory', function() { + 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({}); + }); + }); }); From 1cb6beadb52ae8827ea01ff88118e1a4d22fdb3b Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Apr 2011 16:39:06 -0700 Subject: [PATCH 6/9] remove size() and id() and replace them with info() - both size() and id() were removed - info() added that returns info object with all the stats - $cacheFactory has info() as well instead of stats() - internally size value object is used instead of stats.size for better performance http://jsperf.com/value-object-vs-object-property --- src/service/cacheFactory.js | 31 ++++++++++++---------- test/service/cacheFactorySpec.js | 45 +++++++++++++++----------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/service/cacheFactory.js b/src/service/cacheFactory.js index 7e514c96f75b..e9fbae033f01 100644 --- a/src/service/cacheFactory.js +++ b/src/service/cacheFactory.js @@ -24,15 +24,15 @@ */ angularServiceInject('$cacheFactory', function() { - var caches = {}, - cacheStats = {}; + var caches = {}; function cacheFactory(cacheId, options) { if (cacheId in caches) { throw Error('cacheId ' + cacheId + ' taken'); } - var stats = cacheStats[cacheId] = extend({}, options, {size:0}), + var size = 0, + stats = extend({}, options, {id: cacheId}), data = {}, capacity = (options && options.capacity) || Number.MAX_VALUE, lruHash = {}, @@ -40,11 +40,6 @@ angularServiceInject('$cacheFactory', function() { staleEnd = null; return caches[cacheId] = { - id: valueFn(cacheId), - - - size: function() { return stats.size; }, - put: function(key, value) { var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); @@ -52,10 +47,10 @@ angularServiceInject('$cacheFactory', function() { refresh(lruEntry); if (isUndefined(value)) return; - if (!(key in data)) stats.size++; + if (!(key in data)) size++; data[key] = value; - if (stats.size > capacity) { + if (size > capacity) { this.remove(staleEnd.key); } }, @@ -81,13 +76,13 @@ angularServiceInject('$cacheFactory', function() { delete lruHash[key]; delete data[key]; - stats.size--; + size--; }, removeAll: function() { data = {}; - stats.size = 0; + size = 0; lruHash = {}; freshEnd = staleEnd = null; }, @@ -98,7 +93,11 @@ angularServiceInject('$cacheFactory', function() { stats = null; lruHash = null; delete caches[cacheId]; - delete cacheStats[cacheId]; + }, + + + info: function() { + return extend({}, stats, {size: size}); } } @@ -135,7 +134,11 @@ angularServiceInject('$cacheFactory', function() { cacheFactory.info = function() { - return copy(cacheStats); + var info = {}; + forEach(caches, function(cache, cacheId) { + info[cacheId] = cache.info(); + }); + return info; } diff --git a/test/service/cacheFactorySpec.js b/test/service/cacheFactorySpec.js index 7bdbd4c0ddb1..f0af9b443539 100644 --- a/test/service/cacheFactorySpec.js +++ b/test/service/cacheFactorySpec.js @@ -33,10 +33,10 @@ describe('$cacheFactory', function() { expect($cacheFactory.info()).toEqual({}); var cache1 = $cacheFactory('cache1'); - expect($cacheFactory.info()).toEqual({cache1: {size: 0}}); + expect($cacheFactory.info()).toEqual({cache1: {id: 'cache1', size: 0}}); cache1.put('foo', 'bar'); - expect($cacheFactory.info()).toEqual({cache1: {size: 1}}); + expect($cacheFactory.info()).toEqual({cache1: {id: 'cache1', size: 1}}); }); }); @@ -77,7 +77,7 @@ describe('$cacheFactory', function() { cache.put('key1'); cache.put('key2', undefined); - expect(cache.size()).toBe(0); + expect(cache.info().size).toBe(0); }); @@ -102,38 +102,35 @@ describe('$cacheFactory', function() { cache.put(123, 'bar'); expect(cache.get('123')).toBe('bar'); - expect(cache.size()).toBe(1); + expect(cache.info().size).toBe(1); cache.remove(123); - expect(cache.size()).toBe(0); + expect(cache.info().size).toBe(0); }) }); - describe('size', function() { + describe('info', function() { - it('should increment with put and decrement with remove', function() { - expect(cache.size()).toBe(0); + it('should size increment with put and decrement with remove', function() { + expect(cache.info().size).toBe(0); cache.put('foo', 'bar'); - expect(cache.size()).toBe(1); + expect(cache.info().size).toBe(1); cache.put('baz', 'boo'); - expect(cache.size()).toBe(2); + expect(cache.info().size).toBe(2); cache.remove('baz'); - expect(cache.size()).toBe(1); + expect(cache.info().size).toBe(1); cache.remove('foo'); - expect(cache.size()).toBe(0); + expect(cache.info().size).toBe(0); }); - }); - - describe('id', function() { it('should return cache id', function() { - expect(cache.id()).toBe('test'); + expect(cache.info().id).toBe('test'); }) }); @@ -144,11 +141,11 @@ describe('$cacheFactory', function() { cache.put('id1', 1); cache.put('id2', 2); cache.put('id3', 3); - expect(cache.size()).toBe(3); + expect(cache.info().size).toBe(3); cache.removeAll(); - expect(cache.size()).toBe(0); + expect(cache.info().size).toBe(0); expect(cache.get('id1')).toBeUndefined(); expect(cache.get('id2')).toBeUndefined(); expect(cache.get('id3')).toBeUndefined(); @@ -177,18 +174,18 @@ describe('$cacheFactory', function() { it('should create cache with defined capacity', function() { cache = $cacheFactory('cache1', {capacity: 5}); - expect(cache.size()).toBe(0); + expect(cache.info().size).toBe(0); for (var i=0; i<5; i++) { cache.put('id' + i, i); } - expect(cache.size()).toBe(5); + expect(cache.info().size).toBe(5); cache.put('id5', 5); - expect(cache.size()).toBe(5); + expect(cache.info().size).toBe(5); cache.put('id6', 6); - expect(cache.size()).toBe(5); + expect(cache.info().size).toBe(5); }); @@ -271,7 +268,7 @@ describe('$cacheFactory', function() { cache.put('id3', 3); cache.put('id4', 4); - expect(cache.size()).toBe(2); + expect(cache.info().size).toBe(2); expect(cache.get('id0')).toBeUndefined(); expect(cache.get('id1')).toBeUndefined(); expect(cache.get('id2')).toBeUndefined(); @@ -317,7 +314,7 @@ describe('$cacheFactory', function() { cache.put('id2', 2); //2,1,0 cache.put('id3', 3); //3,2,1 - expect(cache.size()).toBe(3); + expect(cache.info().size).toBe(3); expect(cache.get('id0')).toBeUndefined(); expect(cache.get('id1')).toBe(1); expect(cache.get('id2')).toBe(2); From 49e48fed6822eb95662d1a01f02d90f4c5513621 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Apr 2011 17:33:48 -0700 Subject: [PATCH 7/9] remove lruCacheFactory.js references --- Rakefile | 1 - src/angular-bootstrap.js | 1 - 2 files changed, 2 deletions(-) diff --git a/Rakefile b/Rakefile index 63951250ad75..3fc7fd49fd7b 100644 --- a/Rakefile +++ b/Rakefile @@ -26,7 +26,6 @@ ANGULAR = [ 'src/service/invalidWidgets.js', 'src/service/location.js', 'src/service/log.js', - 'src/service/lruCacheFactory.js', 'src/service/resource.js', 'src/service/route.js', 'src/service/updateView.js', diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index ce9b1b5ec4fc..80d665204a20 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -129,7 +129,6 @@ 'service/invalidWidgets.js', 'service/location.js', 'service/log.js', - 'service/lruCacheFactory.js', 'service/resource.js', 'service/route.js', 'service/updateView.js', From 81d6edc950302827d946652d78e6b945b713fd4f Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 4 Apr 2011 17:41:51 -0700 Subject: [PATCH 8/9] rename next&prev to n&p for better minification --- src/service/cacheFactory.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/service/cacheFactory.js b/src/service/cacheFactory.js index e9fbae033f01..42d36d5f0293 100644 --- a/src/service/cacheFactory.js +++ b/src/service/cacheFactory.js @@ -70,9 +70,9 @@ angularServiceInject('$cacheFactory', function() { remove: function(key) { var lruEntry = lruHash[key]; - if (lruEntry == freshEnd) freshEnd = lruEntry.prev; - if (lruEntry == staleEnd) staleEnd = lruEntry.next; - link(lruEntry.next,lruEntry.prev); + if (lruEntry == freshEnd) freshEnd = lruEntry.p; + if (lruEntry == staleEnd) staleEnd = lruEntry.n; + link(lruEntry.n,lruEntry.p); delete lruHash[key]; delete data[key]; @@ -110,13 +110,13 @@ angularServiceInject('$cacheFactory', function() { if (!staleEnd) { staleEnd = entry; } else if (staleEnd == entry) { - staleEnd = entry.next; + staleEnd = entry.n; } - link(entry.next, entry.prev); + link(entry.n, entry.p); link(entry, freshEnd); freshEnd = entry; - freshEnd.next = null; + freshEnd.n = null; } } @@ -126,8 +126,8 @@ angularServiceInject('$cacheFactory', function() { */ function link(nextEntry, prevEntry) { if (nextEntry != prevEntry) { - if (nextEntry) nextEntry.prev = prevEntry; - if (prevEntry) prevEntry.next = nextEntry; + 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 } } } From 45de3b4c42fee39e3051687c3ce2394464e9fd66 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Thu, 7 Apr 2011 12:33:51 -0700 Subject: [PATCH 9/9] XHR WIP --- src/Browser.js | 8 ++------ src/service/xhr.js | 43 ++++++++++++++++++++++++++++++++++--------- test/BrowserSpecs.js | 7 +------ 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/Browser.js b/src/Browser.js index 554397626031..3f507075865a 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -7,11 +7,7 @@ var XHR = window.XMLHttpRequest || function () { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} throw new Error("This browser does not support XMLHttpRequest."); }; -var XHR_HEADERS = { - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json, text/plain, */*", - "X-Requested-With": "XMLHttpRequest" -}; + /** * @private @@ -103,7 +99,7 @@ function Browser(window, document, body, XHR, $log) { } else { var xhr = new XHR(); xhr.open(method, url, true); - forEach(extend(XHR_HEADERS, headers || {}), function(value, key){ + forEach(headers, function(value, key){ if (value) xhr.setRequestHeader(key, value); }); xhr.onreadystatechange = function() { diff --git a/src/service/xhr.js b/src/service/xhr.js index 18f695e6751d..42682b7b5296 100644 --- a/src/service/xhr.js +++ b/src/service/xhr.js @@ -126,7 +126,7 @@ */ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){ - return function(method, url, post, callback){ + function xhr(method, url, post, callback){ if (isFunction(post)) { callback = post; post = null; @@ -137,12 +137,6 @@ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){ $browser.xhr(method, url, post, function(code, response){ try { - if (isString(response)) { - if (response.match(/^\)\]\}',\n/)) response=response.substr(6); - if (/^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { - response = fromJson(response, true); - } - } if (200 <= code && code < 300) { callback(code, response); } else { @@ -155,8 +149,39 @@ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){ } finally { $updateView(); } - }, { + }, extend({}, xhr.defaults.request.headers, { 'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN'] - }); + })); }; + + + xhr.defaults = { + request:{ + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json, text/plain, */*", + "X-Requested-With": "XMLHttpRequest" + }, + filters: [] //xsrf + }, + + response: { + filters: [jsonSec, jsonDeser] + } + } + + + function jsonSec(response) { + if (response.match(/^\)\]\}',\n/)) return response.substr(6); + } + + function jsonDeser(response) { + if (/^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { + return fromJson(response, true); + } + } + + + return xhr; + }, ['$browser', '$xhr.error', '$log', '$updateView']); diff --git a/test/BrowserSpecs.js b/test/BrowserSpecs.js index 3b5a9ba03651..9c74021584e1 100644 --- a/test/BrowserSpecs.js +++ b/test/BrowserSpecs.js @@ -109,12 +109,7 @@ describe('browser', function(){ expect(xhr.method).toEqual('METHOD'); expect(xhr.url).toEqual('URL'); expect(xhr.post).toEqual('POST'); - expect(xhr.headers).toEqual({ - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json, text/plain, */*", - "X-Requested-With": "XMLHttpRequest", - "X-header":"value" - }); + expect(xhr.headers).toEqual({"X-header":"value"}); xhr.status = 202; xhr.responseText = 'RESPONSE';