From 718a93d5c6e12a3744376fb765b6de9438ced843 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Wed, 22 Feb 2012 20:48:36 -0300 Subject: [PATCH 01/25] First Node.js version --- README.md | 9 ++- cache.js => lib/cache.js | 108 ++++++------------------------------ lib/stores/basic.js | 64 +++++++++++++++++++++ lib/stores/local_storage.js | 76 +++++++++++++++++++++++++ package.json | 21 +++++++ test.js => test/test.js | 13 +++-- 6 files changed, 193 insertions(+), 98 deletions(-) rename cache.js => lib/cache.js (76%) create mode 100644 lib/stores/basic.js create mode 100644 lib/stores/local_storage.js create mode 100644 package.json rename test.js => test/test.js (97%) diff --git a/README.md b/README.md index ce831f3..6784145 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Just a simple LRU cache written in javascript. It is loosely based on ASP.NET's How It Works ------------ + var Cache = require('leru'); + // Create a new cache item // The constructor accepts an optional integer // parameter which places a limit on how many @@ -22,7 +24,7 @@ How It Works // the last cache access after which the item // should expire // priority: How important it is to leave this item in the cache. - // You can use the values CachePriority.LOW, .NORMAL, or + // You can use the values Cache.Priority.LOW, .NORMAL, or // .HIGH, or you can just use an integer. Note that // placing a priority on an item does not guarantee // it will remain in cache. It can still be purged if @@ -32,8 +34,8 @@ How It Works // are passed as parameters to the callback function. cache.setItem("A", "1", {expirationAbsolute: null, expirationSliding: 60, - priority: CachePriority.HIGH, - callback: function(k, v) { alert('removed ' + k); } + priority: Cache.Priority.HIGH, + callback: function(k, v) { console.log('removed ' + k); } }); // retrieve an item from the cache @@ -75,6 +77,7 @@ which is currently 5MB on Chrome/Safari. History ------- +* 2/22/2012: Forked from Monsur Hossain repo and ported to Node.js. * 11/29/2011: Thanks to Andrew Carman for tests, pluggable backends, localStorage persistance, and bug fixes. * 1/8/2011: Migrated project to GitHub. * 1/20/2010: Thanks to Andrej Arn for some syntax updates. diff --git a/cache.js b/lib/cache.js similarity index 76% rename from cache.js rename to lib/cache.js index bade229..5253590 100644 --- a/cache.js +++ b/lib/cache.js @@ -24,16 +24,6 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/** - * An easier way to refer to the priority of a cache item - * @enum {number} - */ -var CachePriority = { - 'LOW': 1, - 'NORMAL': 2, - 'HIGH': 4 -}; - /** * Creates a new Cache object. * @param {number} maxSize The maximum size of the cache (or -1 for no max). @@ -43,8 +33,14 @@ var CachePriority = { function Cache(maxSize, debug, storage) { this.maxSize_ = maxSize || -1; this.debug_ = debug || false; - this.storage_ = storage || new Cache.BasicCacheStorage(); - + this.storage_ = storage || 'basic'; + if (typeof this.storage_ == 'string') { + try { + this.storage_ = new (require('./stores/'+this.storage_.toLowerCase().trim())); + } catch(e) { + throw new Error('There is no bundled caching store named "'+this.storage_+'"'); + } + } this.fillFactor_ = .75; this.stats_ = {}; @@ -54,80 +50,14 @@ function Cache(maxSize, debug, storage) { } /** - * Basic in memory cache storage backend. - * @constructor - */ -Cache.BasicCacheStorage = function() { - this.items_ = {}; - this.count_ = 0; -} -Cache.BasicCacheStorage.prototype.get = function(key) { - return this.items_[key]; -} -Cache.BasicCacheStorage.prototype.set = function(key, value) { - if (typeof this.get(key) === "undefined") - this.count_++; - this.items_[key] = value; -} -Cache.BasicCacheStorage.prototype.size = function(key, value) { - return this.count_; -} -Cache.BasicCacheStorage.prototype.remove = function(key) { - var item = this.get(key); - if (typeof item !== "undefined") - this.count_--; - delete this.items_[key]; - return item; -} -Cache.BasicCacheStorage.prototype.keys = function() { - var ret = [], p; - for (p in this.items_) ret.push(p); - return ret; -} - -/** - * Local Storage based persistant cache storage backend. - * If a size of -1 is used, it will purge itself when localStorage - * is filled. This is 5MB on Chrome/Safari. - * WARNING: The amortized cost of this cache is very low, however, - * when a the cache fills up all of localStorage, and a purge is required, it can - * take a few seconds to fetch all the keys and values in storage. - * Since localStorage doesn't have namespacing, this means that even if this - * individual cache is small, it can take this time if there are lots of other - * other keys in localStorage. - * - * @param {string} namespace A string to namespace the items in localStorage. Defaults to 'default'. - * @constructor + * An easier way to refer to the priority of a cache item + * @enum {number} */ -Cache.LocalStorageCacheStorage = function(namespace) { - this.prefix_ = 'cache-storage.' + (namespace || 'default') + '.'; - // Regexp String Escaping from http://simonwillison.net/2006/Jan/20/escape/#p-6 - var escapedPrefix = this.prefix_.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - this.regexp_ = new RegExp('^' + escapedPrefix) -} -Cache.LocalStorageCacheStorage.prototype.get = function(key) { - var item = localStorage[this.prefix_ + key]; - if (item) return JSON.parse(item); - return null; -} -Cache.LocalStorageCacheStorage.prototype.set = function(key, value) { - localStorage[this.prefix_ + key] = JSON.stringify(value); -} -Cache.LocalStorageCacheStorage.prototype.size = function(key, value) { - return this.keys().length; -} -Cache.LocalStorageCacheStorage.prototype.remove = function(key) { - var item = this.get(key); - delete localStorage[this.prefix_ + key]; - return item; -} -Cache.LocalStorageCacheStorage.prototype.keys = function() { - var ret = [], p; - for (p in localStorage) { - if (p.match(this.regexp_)) ret.push(p.replace(this.prefix_, '')); - }; - return ret; -} +Cache.Priority = { + 'LOW': 1, + 'NORMAL': 2, + 'HIGH': 4 +}; /** * Retrieves an item from the cache. @@ -175,7 +105,7 @@ Cache._CacheItem = function(k, v, o) { o.expirationAbsolute = o.expirationAbsolute.getTime(); } if (!o.priority) { - o.priority = CachePriority.NORMAL; + o.priority = Cache.Priority.NORMAL; } this.options = o; this.lastAccessed = new Date().getTime(); @@ -193,7 +123,7 @@ Cache._CacheItem = function(k, v, o) { * the last cache access after which the item * should expire * priority: How important it is to leave this item in the cache. - * You can use the values CachePriority.LOW, .NORMAL, or + * You can use the values Cache.Priority.LOW, .NORMAL, or * .HIGH, or you can just use an integer. Note that * placing a priority on an item does not guarantee * it will remain in cache. It can still be purged if @@ -398,6 +328,4 @@ Cache.prototype.log_ = function(msg) { } }; -if (typeof module !== "undefined") { - module.exports = Cache; -} +module.exports = Cache; diff --git a/lib/stores/basic.js b/lib/stores/basic.js new file mode 100644 index 0000000..373e550 --- /dev/null +++ b/lib/stores/basic.js @@ -0,0 +1,64 @@ +/* +MIT LICENSE +Copyright (c) 2007 Monsur Hossain (http://monsur.hossai.in) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 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. +*/ + +/** + * Basic in memory cache storage backend. + * @constructor + */ +function BasicCacheStorage() { + this.items_ = {}; + this.count_ = 0; +} + +BasicCacheStorage.prototype.get = function(key) { + return this.items_[key]; +}; + +BasicCacheStorage.prototype.set = function(key, value) { + if (typeof this.get(key) === "undefined") + this.count_++; + this.items_[key] = value; +}; + +BasicCacheStorage.prototype.size = function(key, value) { + return this.count_; +}; + +BasicCacheStorage.prototype.remove = function(key) { + var item = this.get(key); + if (typeof item !== "undefined") + this.count_--; + delete this.items_[key]; + return item; +}; + +BasicCacheStorage.prototype.keys = function() { + var ret = [], p; + for (p in this.items_) ret.push(p); + return ret; +}; + +module.exports = BasicCacheStorage; diff --git a/lib/stores/local_storage.js b/lib/stores/local_storage.js new file mode 100644 index 0000000..257e3ac --- /dev/null +++ b/lib/stores/local_storage.js @@ -0,0 +1,76 @@ +/* +MIT LICENSE +Copyright (c) 2007 Monsur Hossain (http://monsur.hossai.in) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 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. +*/ + +/** + * Local Storage based persistant cache storage backend. + * If a size of -1 is used, it will purge itself when localStorage + * is filled. This is 5MB on Chrome/Safari. + * WARNING: The amortized cost of this cache is very low, however, + * when a the cache fills up all of localStorage, and a purge is required, it can + * take a few seconds to fetch all the keys and values in storage. + * Since localStorage doesn't have namespacing, this means that even if this + * individual cache is small, it can take this time if there are lots of other + * other keys in localStorage. + * + * @param {string} namespace A string to namespace the items in localStorage. Defaults to 'default'. + * @constructor + */ +function LocalStorageCacheStorage (namespace) { + this.prefix_ = 'cache-storage.' + (namespace || 'default') + '.'; + // Regexp String Escaping from http://simonwillison.net/2006/Jan/20/escape/#p-6 + var escapedPrefix = this.prefix_.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + this.regexp_ = new RegExp('^' + escapedPrefix) +} + +LocalStorageCacheStorage.prototype.get = function(key) { + var item = localStorage[this.prefix_ + key]; + if (item) return JSON.parse(item); + return null; +}; + +LocalStorageCacheStorage.prototype.set = function(key, value) { + localStorage[this.prefix_ + key] = JSON.stringify(value); +}; + +LocalStorageCacheStorage.prototype.size = function(key, value) { + return this.keys().length; +}; + +LocalStorageCacheStorage.prototype.remove = function(key) { + var item = this.get(key); + delete localStorage[this.prefix_ + key]; + return item; +}; + +LocalStorageCacheStorage.prototype.keys = function() { + var ret = [], p; + for (p in localStorage) { + if (p.match(this.regexp_)) ret.push(p.replace(this.prefix_, '')); + }; + return ret; +}; + +module.exports = LocalStorageCacheStorage; diff --git a/package.json b/package.json new file mode 100644 index 0000000..3beb4b4 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "author": "Ricardo Stuven ", + "name": "leru", + "description": "Node.js LRU Cache", + "version": "0.0.0", + "homepage": "https://github.com/rstuven/node-leru", + "repository": { + "type": "git", + "url": "git://github.com/rstuven/node-leru.git" + }, + "engines": { + "node": "~0.6.1" + }, + "directories": { + "lib": "./lib" + }, + "main": "./lib/cache", + "dependencies": {}, + "devDependencies": {}, + "optionalDependencies": {} +} diff --git a/test.js b/test/test.js similarity index 97% rename from test.js rename to test/test.js index 5b29e9f..024d65f 100644 --- a/test.js +++ b/test/test.js @@ -1,3 +1,5 @@ +var Cache = require('../'); + var TIMEOUT = 50 function assertEqual(a, b) { if (a !== b) { @@ -91,7 +93,7 @@ function testLRUExpiration(success) { function testPriorityExpiration(success) { var cache = new Cache(2); cache.setItem("foo1", "bar1", { - priority: CachePriority.HIGH + priority: Cache.Priority.HIGH }); cache.setItem("foo2", "bar2"); setTimeout(function() { @@ -233,8 +235,9 @@ runTests([ testLRUExpiration, testPriorityExpiration, testResize, - testFillFactor, - testLocalStorageCache, - testLocalStorageExisting, - testLocalStorageCacheMaxSize + testFillFactor + //, + //testLocalStorageCache, + //testLocalStorageExisting, + //testLocalStorageCacheMaxSize ]); From c34789fcb7586e706b55b8b04db95e8f2af50d88 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Wed, 22 Feb 2012 23:07:30 -0300 Subject: [PATCH 02/25] converted tests to mocha+should+sinon+coffee --- .gitignore | 1 + package.json | 12 ++- test/test.coffee | 212 +++++++++++++++++++++++++++++++++++++++++ test/test.js | 243 ----------------------------------------------- 4 files changed, 223 insertions(+), 245 deletions(-) create mode 100644 .gitignore create mode 100644 test/test.coffee delete mode 100644 test/test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/package.json b/package.json index 3beb4b4..2592f7f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,14 @@ }, "main": "./lib/cache", "dependencies": {}, - "devDependencies": {}, - "optionalDependencies": {} + "devDependencies": { + "mocha": "*", + "should": "*", + "sinon": "*", + "coffee-script": "*" + }, + "optionalDependencies": {}, + "scripts": { + "test": "mocha ./test/*" + } } diff --git a/test/test.coffee b/test/test.coffee new file mode 100644 index 0000000..e3aef16 --- /dev/null +++ b/test/test.coffee @@ -0,0 +1,212 @@ +Cache = require '../' +should = require 'should' +sinon = require 'sinon' + +TIMEOUT = 50 +timeout = (to, fn) => + setTimeout fn, TIMEOUT*to + +describe 'Cache', -> + + before -> + @clock = sinon.useFakeTimers() + + after -> + @clock.restore() + + it 'BasicCaching', -> + cache = new Cache + + cache.setItem "foo", "bar" + cache.getItem("foo").should.equal "bar" + should.not.exist cache.getItem("missing") + should.not.exist cache.removeItem("missing") + stats = cache.getStats() + stats.hits.should.equal 1 + stats.misses.should.equal 1 + cache.toHtmlString().should.equal "1 item(s) in cache
  • foo = bar
" + cache.size().should.equal 1 + + cache.setItem "foo2", "bar2" + cache.size().should.equal 2 + cache.removeItem("foo").should.equal "bar" + cache.size().should.equal 1 + cache.clear() + should.not.exist cache.getItem("foo") + cache.size().should.equal 0 + + + it 'AbsoluteExpiration', (success) -> + cache = new Cache + cache.setItem "foo", "bar", + expirationAbsolute: new Date(new Date().getTime() + TIMEOUT*2) + cache.getItem("foo").should.equal "bar" + + timeout 1, -> + cache.getItem("foo").should.equal "bar" + + timeout 3, -> + should.not.exist cache.getItem("foo") + success() + + @clock.tick TIMEOUT*3 + + it 'SlidingExpiration', (success) -> + cache = new Cache + cache.setItem "foo", "bar", + expirationSliding: TIMEOUT * 2 / 1000 + cache.getItem("foo").should.equal "bar" + + timeout 1, -> + cache.getItem("foo").should.equal "bar" + timeout 1, -> + cache.getItem("foo").should.equal "bar" + timeout 1, -> + cache.getItem("foo").should.equal "bar" + timeout 3, -> + should.not.exist cache.getItem("foo") + success() + + @clock.tick TIMEOUT*6 + + it 'LRUExpiration', (success) -> + cache = new Cache 2 + cache.setItem "foo1", "bar1" + cache.setItem "foo2", "bar2" + timeout 1, -> + # Access an item so foo1 will be the LRU + cache.getItem("foo2").should.equal "bar2" + + cache.setItem "foo3", "bar3" + cache.size().should.equal 3 + + # Allow time for cache to be purged + timeout 1, -> + should.not.exist cache.getItem("foo1") + cache.getItem("foo2").should.equal "bar2" + cache.getItem("foo3").should.equal "bar3" + cache.size().should.equal 2 + success() + + @clock.tick TIMEOUT*2 + + it 'PriorityExpiration', (success) -> + cache = new Cache 2 + cache.setItem "foo1", "bar1", + priority: Cache.Priority.HIGH + + cache.setItem "foo2", "bar2" + timeout 1, -> + # Access an item so foo1 will be the LRU + cache.getItem("foo2").should.equal "bar2" + + timeout 1, -> + cache.setItem "foo3", "bar3" + cache.size().should.equal 3 + + # Allow time for cache to be purged + timeout 1, -> + cache.getItem("foo1").should.equal "bar1" + should.not.exist cache.getItem("foo2") + cache.getItem("foo3").should.equal "bar3" + cache.size().should.equal 2 + success() + + @clock.tick TIMEOUT*3 + + it 'Resize', (success) -> + cache = new Cache + cache.setItem "foo1", "bar1" + timeout 1, -> + cache.setItem "foo2", "bar2" + timeout 1, -> + cache.setItem "foo3", "bar3" + cache.resize 2 + should.not.exist cache.getItem("foo1") + cache.getItem("foo2").should.equal "bar2" + timeout 1, -> + cache.getItem("foo3").should.equal "bar3" + cache.resize 1 + should.not.exist cache.getItem("foo1") + should.not.exist cache.getItem("foo2") + cache.getItem("foo3").should.equal "bar3" + success() + + @clock.tick TIMEOUT*3 + + it 'FillFactor', (success) -> + cache = new Cache 100 + counter = 0 + cache.setItem "foo" + i, "bar" + i for i in [1..100] + + cache.size().should.equal 100 + timeout 1, -> + cache.size().should.equal 100 + cache.setItem "purge", "do it" + timeout 1, -> + cache.size().should.equal 75 + success() + + @clock.tick TIMEOUT*2 + + +### + + it 'LocalStorageCache', (success) -> + localStorage.clear() + cache = new Cache 2, false, 'local_storage' + cache.setItem "foo1", "bar1" + cache.getItem("foo1").should.equal "bar1" + should.not.exist cache.getItem("missing") + stats = cache.getStats() + stats.hits, 1 + stats.misses, 1 + cache.toHtmlString().should.equal "1 item(s) in cache
  • foo1 = bar1
" + cache.size().should.equal 1 + localStorage.length, 1 + + timeout 1, -> + cache.setItem "foo2", "bar2" + cache.setItem "foo3", "bar3" + timeout 1, -> + cache.size().should.equal 2 + localStorage.length, 2 + + cache.clear() + should.not.exist cache.getItem("foo1") + should.not.exist cache.getItem("foo2") + should.not.exist cache.getItem("foo3") + cache.size().should.equal 0 + localStorage.length, 0 + success() + + it 'LocalStorageExisting', (success) -> + localStorage.clear() + cache = new Cache(-1, false, 'local_storage' + cache.setItem "foo", "bar" + cache2 = new Cache -1, false, 'local_storage' + cache.size().should.equal 1 + cache2.size().should.equal 1 + cache.removeItem "foo" + cache.size().should.equal 0 + cache2.size().should.equal 0 + success() + + it 'LocalStorageCacheMaxSize', (success) -> + localStorage.clear() + cache = new Cache -1, false, 'local_storage' + count = 0 + console.log 'Attempting to max out localStorage, this may take a while...' + while true + count += 1 + startSize = localStorage.length + if count % 500 == 0 + console.log ' added ' + count + ' items' + + cache.setItem "somelongerkeyhere" + count, Array(200).join("bar") + count + if localStorage.length - startSize < 0 + console.log ' added ' + count + ' items' + localStorage.clear() + return success() + +### diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 024d65f..0000000 --- a/test/test.js +++ /dev/null @@ -1,243 +0,0 @@ -var Cache = require('../'); - -var TIMEOUT = 50 -function assertEqual(a, b) { - if (a !== b) { - throw "AssertEqual Failed: " + a + " !== " + b; - } -} - -function testBasicCaching(done) { - var cache = new Cache(); - cache.setItem("foo", "bar"); - assertEqual(cache.getItem("foo"), "bar"); - assertEqual(cache.getItem("missing"), null); - assertEqual(cache.removeItem("missing"), null); - var stats = cache.getStats(); - assertEqual(stats.hits, 1); - assertEqual(stats.misses, 1); - assertEqual(cache.toHtmlString(), "1 item(s) in cache
  • foo = bar
"); - assertEqual(cache.size(), 1); - - - cache.setItem("foo2", "bar2"); - assertEqual(cache.size(), 2); - assertEqual(cache.removeItem("foo"), "bar"); - assertEqual(cache.size(), 1); - cache.clear(); - assertEqual(cache.getItem("foo"), null); - assertEqual(cache.size(), 0); - done(); -} - -function testAbsoluteExpiration(success) { - var cache = new Cache(); - cache.setItem("foo", "bar", { - expirationAbsolute: new Date(new Date().getTime() + TIMEOUT*2) - }); - assertEqual(cache.getItem("foo"), "bar"); - - setTimeout(function() { - assertEqual(cache.getItem("foo"), "bar"); - }, TIMEOUT); - setTimeout(function() { - assertEqual(cache.getItem("foo"), null); - success(); - }, TIMEOUT*3); -} - -function testSlidingExpiration(success) { - var cache = new Cache(); - cache.setItem("foo", "bar", { - expirationSliding: TIMEOUT * 2 / 1000 - }); - assertEqual(cache.getItem("foo"), "bar"); - - setTimeout(function() { - assertEqual(cache.getItem("foo"), "bar"); - setTimeout(function() { - assertEqual(cache.getItem("foo"), "bar"); - setTimeout(function() { - assertEqual(cache.getItem("foo"), "bar"); - setTimeout(function() { - assertEqual(cache.getItem("foo"), null); - success(); - }, TIMEOUT*3); - }, TIMEOUT); - }, TIMEOUT); - }, TIMEOUT); -} - -function testLRUExpiration(success) { - var cache = new Cache(2); - cache.setItem("foo1", "bar1"); - cache.setItem("foo2", "bar2"); - setTimeout(function() { - // Access an item so foo1 will be the LRU - assertEqual(cache.getItem("foo2"), "bar2"); - - cache.setItem("foo3", "bar3"); - assertEqual(cache.size(), 3); - - // Allow time for cache to be purged - setTimeout(function() { - assertEqual(cache.getItem("foo1"), null); - assertEqual(cache.getItem("foo2"), "bar2"); - assertEqual(cache.getItem("foo3"), "bar3"); - assertEqual(cache.size(), 2); - success(); - }, TIMEOUT) - }, TIMEOUT) -} - -function testPriorityExpiration(success) { - var cache = new Cache(2); - cache.setItem("foo1", "bar1", { - priority: Cache.Priority.HIGH - }); - cache.setItem("foo2", "bar2"); - setTimeout(function() { - // Access an item so foo1 will be the LRU - assertEqual(cache.getItem("foo2"), "bar2"); - - setTimeout(function() { - cache.setItem("foo3", "bar3"); - assertEqual(cache.size(), 3); - - // Allow time for cache to be purged - setTimeout(function() { - assertEqual(cache.getItem("foo1"), "bar1"); - assertEqual(cache.getItem("foo2"), null); - assertEqual(cache.getItem("foo3"), "bar3"); - assertEqual(cache.size(), 2); - success(); - }, TIMEOUT) - }, TIMEOUT) - }, TIMEOUT) -} - -function testResize(success) { - var cache = new Cache(); - cache.setItem("foo1", "bar1"); - setTimeout(function() { - cache.setItem("foo2", "bar2"); - setTimeout(function() { - cache.setItem("foo3", "bar3"); - cache.resize(2); - assertEqual(cache.getItem("foo1"), null); - assertEqual(cache.getItem("foo2"), "bar2"); - setTimeout(function() { - assertEqual(cache.getItem("foo3"), "bar3"); - cache.resize(1); - assertEqual(cache.getItem("foo1"), null); - assertEqual(cache.getItem("foo2"), null); - assertEqual(cache.getItem("foo3"), "bar3"); - success(); - }, TIMEOUT) - }, TIMEOUT) - }, TIMEOUT) -} - -function testFillFactor(success) { - var cache = new Cache(100); - var counter = 0; - for (var i = 1; i <= 100; i++) { - cache.setItem("foo" + i, "bar" + i); - } - assertEqual(cache.size(), 100); - setTimeout(function() { - assertEqual(cache.size(), 100); - cache.setItem("purge", "do it"); - setTimeout(function() { - assertEqual(cache.size(), 75); - success(); - }, TIMEOUT) - }, TIMEOUT) -} - -function testLocalStorageCache(success) { - localStorage.clear(); - var cache = new Cache(2, false, new Cache.LocalStorageCacheStorage()); - cache.setItem("foo1", "bar1"); - assertEqual(cache.getItem("foo1"), "bar1"); - assertEqual(cache.getItem("missing"), null); - var stats = cache.getStats(); - assertEqual(stats.hits, 1); - assertEqual(stats.misses, 1); - assertEqual(cache.toHtmlString(), "1 item(s) in cache
  • foo1 = bar1
"); - assertEqual(cache.size(), 1); - assertEqual(localStorage.length, 1); - - setTimeout(function() { - cache.setItem("foo2", "bar2"); - cache.setItem("foo3", "bar3"); - setTimeout(function() { - assertEqual(cache.size(), 2); - assertEqual(localStorage.length, 2); - - cache.clear(); - assertEqual(cache.getItem("foo1"), null); - assertEqual(cache.getItem("foo2"), null); - assertEqual(cache.getItem("foo3"), null); - assertEqual(cache.size(), 0); - assertEqual(localStorage.length, 0); - success(); - }, TIMEOUT) - }, TIMEOUT) -} - -function testLocalStorageExisting(success) { - localStorage.clear(); - var cache = new Cache(-1, false, new Cache.LocalStorageCacheStorage()); - cache.setItem("foo", "bar"); - var cache2 = new Cache(-1, false, new Cache.LocalStorageCacheStorage()); - assertEqual(cache.size(), 1); - assertEqual(cache2.size(), 1); - cache.removeItem("foo"); - assertEqual(cache.size(), 0); - assertEqual(cache2.size(), 0); - success(); -} - -function testLocalStorageCacheMaxSize(success) { - localStorage.clear(); - var cache = new Cache(-1, false, new Cache.LocalStorageCacheStorage()); - var count = 0; - console.log('Attempting to max out localStorage, this may take a while...') - while (true) { - count += 1; - var startSize = localStorage.length; - if (count % 500 === 0) { - console.log(' added ' + count + ' items') - } - cache.setItem("somelongerkeyhere" + count, Array(200).join("bar") + count); - if (localStorage.length - startSize < 0) { - console.log(' added ' + count + ' items') - localStorage.clear(); - return success(); - } - } -} - -function runTests(tests) { - if (tests.length === 0) return console.log("All tests passed!"); - var next = tests.shift(); - next(function() { - runTests(tests); - }) -} - -console.log("Running tests..."); -runTests([ - testBasicCaching, - testAbsoluteExpiration, - testSlidingExpiration, - testLRUExpiration, - testPriorityExpiration, - testResize, - testFillFactor - //, - //testLocalStorageCache, - //testLocalStorageExisting, - //testLocalStorageCacheMaxSize -]); From 2380fa6e8e2ae38347ca17e92dfe602a2fa8c93c Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Thu, 23 Feb 2012 00:31:15 -0200 Subject: [PATCH 03/25] changed description. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6784145..703cdd6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -JavaScript LRU Cache +LRU Cache for Node.js ==================== -Just a simple LRU cache written in javascript. It is loosely based on ASP.NET's Cache, and includes many caching options such as absolute expiration, sliding expiration, cache priority, and a callback function. It can be used to cache data locally in the user's browser, saving a server roundtrip in AJAX heavy applications. +This is a fork of Monsur Hossain's jscache for browsers, which is loosely based on ASP.NET's Cache, and includes many caching options such as absolute expiration, sliding expiration, cache priority, and a callback function on purge. How It Works ------------ From db7fba97088a43336d5c8ee955984ffd084bc6f7 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Thu, 23 Feb 2012 16:39:56 -0300 Subject: [PATCH 04/25] added onPurge test. --- lib/cache.js | 10 +++++----- package.json | 3 ++- test/test.coffee | 15 +++++++++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index 5253590..9d4b70d 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -128,9 +128,9 @@ Cache._CacheItem = function(k, v, o) { * placing a priority on an item does not guarantee * it will remain in cache. It can still be purged if * an expiration is hit, or if the cache is full. - * callback: A function that gets called when the item is purged - * from cache. The key and value of the removed item - * are passed as parameters to the callback function. + * onPurge: A function that gets called when the item is purged + * from cache. The key and value of the removed item + * are passed as parameters to the callback function. */ Cache.prototype.setItem = function(key, value, options) { @@ -279,9 +279,9 @@ Cache.prototype.removeItem = function(key) { this.log_("removed key " + key); // if there is a callback function, call it at the end of execution - if (item && item.options && item.options.callback) { + if (item && item.options && item.options.onPurge) { setTimeout(function() { - item.options.callback.call(null, item.key, item.value); + item.options.onPurge.call(null, item.key, item.value); }, 0); } return item ? item.value : null; diff --git a/package.json b/package.json index 2592f7f..228cc3d 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,9 @@ "dependencies": {}, "devDependencies": { "mocha": "*", - "should": "*", + "chai": "*", "sinon": "*", + "sinon-chai": "*", "coffee-script": "*" }, "optionalDependencies": {}, diff --git a/test/test.coffee b/test/test.coffee index e3aef16..e857e2d 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -1,6 +1,8 @@ Cache = require '../' -should = require 'should' sinon = require 'sinon' +chai = require 'chai' +chai.use require 'sinon-chai' +should = chai.should() TIMEOUT = 50 timeout = (to, fn) => @@ -35,7 +37,6 @@ describe 'Cache', -> should.not.exist cache.getItem("foo") cache.size().should.equal 0 - it 'AbsoluteExpiration', (success) -> cache = new Cache cache.setItem "foo", "bar", @@ -149,6 +150,16 @@ describe 'Cache', -> @clock.tick TIMEOUT*2 + it 'OnPurge', -> + cache = new Cache + spy = sinon.spy() + cache.setItem "foo", "bar", + onPurge: spy + cache.removeItem "foo" + + @clock.tick 1 + + spy.should.have.been.calledWith "foo", "bar" ### From fcebb16ca0b5a3b89f2ad4a85b699335c29449d7 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Thu, 23 Feb 2012 17:19:19 -0300 Subject: [PATCH 05/25] changed package test script arguments. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 228cc3d..0deaec7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,6 @@ }, "optionalDependencies": {}, "scripts": { - "test": "mocha ./test/*" + "test": "mocha -R spec" } } From 0b89a5aad72e3ee1c36dde7441fd3c89ee95dc2e Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Thu, 23 Feb 2012 17:27:58 -0300 Subject: [PATCH 06/25] changed package name. --- README.md | 20 +------------------- package.json | 8 ++++---- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 703cdd6..c543c53 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is a fork of Monsur Hossain's jscache for browsers, which is loosely based How It Works ------------ - var Cache = require('leru'); + var Cache = require('cachai'); // Create a new cache item // The constructor accepts an optional integer @@ -57,24 +57,6 @@ How It Works cache.clear(); -LocalStorage Persistance ------------------------- - -You can have the cache persist its values to localStorage on browsers that support it. -To do this simply create the cache with a different storage backend like: - - var cache = new Cache(-1, false, new Cache.LocalStorageCacheStorage()); - -All values have to be JSON stringifiable, which means the callback option to setItem won't work. - -If you want to have multiple independent caches, pass in a namespace argument, like: - - var cache = new Cache(-1, false, new Cache.LocalStorageCacheStorage('myNameSpace')); - -If -1 is used for the cache size, the cache will be limited to the size of localStorage, -which is currently 5MB on Chrome/Safari. - - History ------- * 2/22/2012: Forked from Monsur Hossain repo and ported to Node.js. diff --git a/package.json b/package.json index 0deaec7..d49d69b 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "author": "Ricardo Stuven ", - "name": "leru", - "description": "Node.js LRU Cache", + "name": "cachai", + "description": "LRU Cache for Node.js", "version": "0.0.0", - "homepage": "https://github.com/rstuven/node-leru", + "homepage": "https://github.com/rstuven/node-cachai", "repository": { "type": "git", - "url": "git://github.com/rstuven/node-leru.git" + "url": "git://github.com/rstuven/node-cachai.git" }, "engines": { "node": "~0.6.1" From 9d51deeff04551581c016616db02211b9e93ec6c Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Thu, 23 Feb 2012 17:29:14 -0300 Subject: [PATCH 07/25] fixed docs. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c543c53..8381268 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,13 @@ How It Works // placing a priority on an item does not guarantee // it will remain in cache. It can still be purged if // an expiration is hit, or if the cache is full. - // callback: A function that gets called when the item is purged - // from cache. The key and value of the removed item - // are passed as parameters to the callback function. + // onPurge: A function that gets called when the item is purged + // from cache. The key and value of the removed item + // are passed as parameters to the callback function. cache.setItem("A", "1", {expirationAbsolute: null, expirationSliding: 60, priority: Cache.Priority.HIGH, - callback: function(k, v) { console.log('removed ' + k); } + onPurge: function(k, v) { console.log('removed ' + k); } }); // retrieve an item from the cache From c03fd8c04e4d75c222ce3bc71875551e17fc672a Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Thu, 23 Feb 2012 18:37:03 -0300 Subject: [PATCH 08/25] refactored tests. --- test/test.coffee | 327 +++++++++++++++++++++++------------------------ 1 file changed, 157 insertions(+), 170 deletions(-) diff --git a/test/test.coffee b/test/test.coffee index e857e2d..aa62ae0 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -4,9 +4,9 @@ chai = require 'chai' chai.use require 'sinon-chai' should = chai.should() -TIMEOUT = 50 -timeout = (to, fn) => - setTimeout fn, TIMEOUT*to +INTERVAL = 5 +defer = (i, fn) => + setTimeout fn, INTERVAL*i describe 'Cache', -> @@ -16,141 +16,188 @@ describe 'Cache', -> after -> @clock.restore() - it 'BasicCaching', -> - cache = new Cache + describe 'basics', -> - cache.setItem "foo", "bar" - cache.getItem("foo").should.equal "bar" - should.not.exist cache.getItem("missing") - should.not.exist cache.removeItem("missing") - stats = cache.getStats() - stats.hits.should.equal 1 - stats.misses.should.equal 1 - cache.toHtmlString().should.equal "1 item(s) in cache
  • foo = bar
" - cache.size().should.equal 1 - - cache.setItem "foo2", "bar2" - cache.size().should.equal 2 - cache.removeItem("foo").should.equal "bar" - cache.size().should.equal 1 - cache.clear() - should.not.exist cache.getItem("foo") - cache.size().should.equal 0 - - it 'AbsoluteExpiration', (success) -> - cache = new Cache - cache.setItem "foo", "bar", - expirationAbsolute: new Date(new Date().getTime() + TIMEOUT*2) - cache.getItem("foo").should.equal "bar" + beforeEach -> + @cache = new Cache - timeout 1, -> - cache.getItem("foo").should.equal "bar" + it 'should set and get item', -> + @cache.setItem "foo", "bar" + @cache.getItem("foo").should.equal "bar" - timeout 3, -> - should.not.exist cache.getItem("foo") - success() + it 'should remove item', -> + should.not.exist @cache.getItem("foo") + @cache.setItem "foo", "bar" + should.exist @cache.getItem("foo") + should.exist @cache.removeItem("foo") + should.not.exist @cache.getItem("foo") - @clock.tick TIMEOUT*3 + it 'should not get missing item', -> + should.not.exist @cache.getItem("missing") - it 'SlidingExpiration', (success) -> - cache = new Cache - cache.setItem "foo", "bar", - expirationSliding: TIMEOUT * 2 / 1000 - cache.getItem("foo").should.equal "bar" + it 'should not remove missing item', -> + should.not.exist @cache.removeItem("missing") + + it 'should register stats', -> + @cache.setItem "foo", "bar" + @cache.getItem("foo") + @cache.getItem("missing") + @cache.removeItem("missing") + stats = @cache.getStats() + stats.hits.should.equal 1 + stats.misses.should.equal 1 + + it 'should generate an HTML report', -> + @cache.setItem "foo", "bar" + @cache.toHtmlString().should.equal "1 item(s) in cache
  • foo = bar
" + + it 'should register cache size', -> + @cache.setItem "foo", "bar" + @cache.size().should.equal 1 + + @cache.setItem "foo2", "bar2" + @cache.size().should.equal 2 + @cache.removeItem("foo") + @cache.size().should.equal 1 + @cache.clear() + should.not.exist @cache.getItem("foo") + should.not.exist @cache.getItem("foo2") + @cache.size().should.equal 0 + + describe 'absolute expiration', -> + + beforeEach -> + @cache = new Cache + + @cache.setItem "foo", "bar", + expirationAbsolute: new Date(new Date().getTime() + INTERVAL*2) + + @cache.getItem("foo").should.equal "bar" + + it 'should expire', (done) -> + + defer 3, => + should.not.exist @cache.getItem("foo") + done() + + @clock.tick INTERVAL*3 + + it 'should not expire', -> + + defer 1, => + @cache.getItem("foo").should.equal "bar" + + @clock.tick INTERVAL*3 + + describe 'sliding expiration', -> + + it 'should expire', (done) -> + cache = new Cache + + cache.setItem "foo", "bar", + expirationSliding: INTERVAL * 2 / 1000 - timeout 1, -> cache.getItem("foo").should.equal "bar" - timeout 1, -> + + defer 1, -> cache.getItem("foo").should.equal "bar" - timeout 1, -> + defer 1, -> cache.getItem("foo").should.equal "bar" - timeout 3, -> - should.not.exist cache.getItem("foo") - success() - - @clock.tick TIMEOUT*6 - - it 'LRUExpiration', (success) -> - cache = new Cache 2 - cache.setItem "foo1", "bar1" - cache.setItem "foo2", "bar2" - timeout 1, -> - # Access an item so foo1 will be the LRU - cache.getItem("foo2").should.equal "bar2" - - cache.setItem "foo3", "bar3" - cache.size().should.equal 3 - - # Allow time for cache to be purged - timeout 1, -> - should.not.exist cache.getItem("foo1") - cache.getItem("foo2").should.equal "bar2" - cache.getItem("foo3").should.equal "bar3" - cache.size().should.equal 2 - success() + defer 1, -> + cache.getItem("foo").should.equal "bar" + defer 3, -> + should.not.exist cache.getItem("foo") + done() - @clock.tick TIMEOUT*2 + @clock.tick INTERVAL*6 - it 'PriorityExpiration', (success) -> - cache = new Cache 2 - cache.setItem "foo1", "bar1", - priority: Cache.Priority.HIGH + describe 'LRU expiration', -> - cache.setItem "foo2", "bar2" - timeout 1, -> - # Access an item so foo1 will be the LRU - cache.getItem("foo2").should.equal "bar2" + it 'should expire', (done) -> + cache = new Cache 2 + cache.setItem "foo1", "bar1" + cache.setItem "foo2", "bar2" + defer 1, -> + # Access an item so foo1 will be the LRU + cache.getItem("foo2").should.equal "bar2" - timeout 1, -> cache.setItem "foo3", "bar3" cache.size().should.equal 3 # Allow time for cache to be purged - timeout 1, -> - cache.getItem("foo1").should.equal "bar1" - should.not.exist cache.getItem("foo2") + defer 1, -> + should.not.exist cache.getItem("foo1") + cache.getItem("foo2").should.equal "bar2" cache.getItem("foo3").should.equal "bar3" cache.size().should.equal 2 - success() + done() - @clock.tick TIMEOUT*3 + @clock.tick INTERVAL*2 + + describe 'priority expiration', -> + + it 'should expire', (done) -> + cache = new Cache 2 + cache.setItem "foo1", "bar1", + priority: Cache.Priority.HIGH - it 'Resize', (success) -> - cache = new Cache - cache.setItem "foo1", "bar1" - timeout 1, -> cache.setItem "foo2", "bar2" - timeout 1, -> - cache.setItem "foo3", "bar3" - cache.resize 2 - should.not.exist cache.getItem("foo1") + defer 1, -> + # Access an item so foo1 will be the LRU cache.getItem("foo2").should.equal "bar2" - timeout 1, -> - cache.getItem("foo3").should.equal "bar3" - cache.resize 1 - should.not.exist cache.getItem("foo1") - should.not.exist cache.getItem("foo2") - cache.getItem("foo3").should.equal "bar3" - success() - @clock.tick TIMEOUT*3 - - it 'FillFactor', (success) -> - cache = new Cache 100 - counter = 0 - cache.setItem "foo" + i, "bar" + i for i in [1..100] + defer 1, -> + cache.setItem "foo3", "bar3" + cache.size().should.equal 3 + + # Allow time for cache to be purged + defer 1, -> + cache.getItem("foo1").should.equal "bar1" + should.not.exist cache.getItem("foo2") + cache.getItem("foo3").should.equal "bar3" + cache.size().should.equal 2 + done() + + @clock.tick INTERVAL*3 + + describe 'sizing', -> + + it 'should resize', (done) -> + cache = new Cache + cache.setItem "foo1", "bar1" + defer 1, -> + cache.setItem "foo2", "bar2" + defer 1, -> + cache.setItem "foo3", "bar3" + cache.resize 2 + should.not.exist cache.getItem("foo1") + cache.getItem("foo2").should.equal "bar2" + defer 1, -> + cache.getItem("foo3").should.equal "bar3" + cache.resize 1 + should.not.exist cache.getItem("foo1") + should.not.exist cache.getItem("foo2") + cache.getItem("foo3").should.equal "bar3" + done() + + @clock.tick INTERVAL*3 + + it 'should use a fill factor', (done) -> + cache = new Cache 100 + counter = 0 + cache.setItem "foo" + i, "bar" + i for i in [1..100] - cache.size().should.equal 100 - timeout 1, -> cache.size().should.equal 100 - cache.setItem "purge", "do it" - timeout 1, -> - cache.size().should.equal 75 - success() + defer 1, -> + cache.size().should.equal 100 + cache.setItem "purge", "do it" + defer 1, -> + cache.size().should.equal 75 + done() - @clock.tick TIMEOUT*2 + @clock.tick INTERVAL*2 - it 'OnPurge', -> + it 'should callback on purge', -> cache = new Cache spy = sinon.spy() cache.setItem "foo", "bar", @@ -161,63 +208,3 @@ describe 'Cache', -> spy.should.have.been.calledWith "foo", "bar" -### - - it 'LocalStorageCache', (success) -> - localStorage.clear() - cache = new Cache 2, false, 'local_storage' - cache.setItem "foo1", "bar1" - cache.getItem("foo1").should.equal "bar1" - should.not.exist cache.getItem("missing") - stats = cache.getStats() - stats.hits, 1 - stats.misses, 1 - cache.toHtmlString().should.equal "1 item(s) in cache
  • foo1 = bar1
" - cache.size().should.equal 1 - localStorage.length, 1 - - timeout 1, -> - cache.setItem "foo2", "bar2" - cache.setItem "foo3", "bar3" - timeout 1, -> - cache.size().should.equal 2 - localStorage.length, 2 - - cache.clear() - should.not.exist cache.getItem("foo1") - should.not.exist cache.getItem("foo2") - should.not.exist cache.getItem("foo3") - cache.size().should.equal 0 - localStorage.length, 0 - success() - - it 'LocalStorageExisting', (success) -> - localStorage.clear() - cache = new Cache(-1, false, 'local_storage' - cache.setItem "foo", "bar" - cache2 = new Cache -1, false, 'local_storage' - cache.size().should.equal 1 - cache2.size().should.equal 1 - cache.removeItem "foo" - cache.size().should.equal 0 - cache2.size().should.equal 0 - success() - - it 'LocalStorageCacheMaxSize', (success) -> - localStorage.clear() - cache = new Cache -1, false, 'local_storage' - count = 0 - console.log 'Attempting to max out localStorage, this may take a while...' - while true - count += 1 - startSize = localStorage.length - if count % 500 == 0 - console.log ' added ' + count + ' items' - - cache.setItem "somelongerkeyhere" + count, Array(200).join("bar") + count - if localStorage.length - startSize < 0 - console.log ' added ' + count + ' items' - localStorage.clear() - return success() - -### From 90419e0c173d7133dd750d256d4473603f68e96b Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sat, 25 Apr 2015 11:38:08 +0200 Subject: [PATCH 09/25] Update test script for current mocha --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d49d69b..f87e1f7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,6 @@ }, "optionalDependencies": {}, "scripts": { - "test": "mocha -R spec" + "test": "mocha --compilers coffee:coffee-script/register -R spec" } } From 8167d8091f1f727ae4c1ab62c3e74c7590ed6e42 Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sat, 25 Apr 2015 11:40:23 +0200 Subject: [PATCH 10/25] Bring back basic test case for localStorage Using a simple mockup to be able to test local storage in mocha. The test cases are adapted (simplified) from the old test cases that were removed in a previous commit. --- test/test_local_storage.coffee | 74 ++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/test_local_storage.coffee diff --git a/test/test_local_storage.coffee b/test/test_local_storage.coffee new file mode 100644 index 0000000..a7d8971 --- /dev/null +++ b/test/test_local_storage.coffee @@ -0,0 +1,74 @@ +Cache = require '../' +LocalStorageCacheStorage = require '../lib/stores/local_storage' +sinon = require 'sinon' +chai = require 'chai' +chai.should() + +describe 'LocalStorageCacheStorage', -> + + class MockLocalStorage + Object.defineProperty @::, 'length', get: -> + l = 0 + for own k of this + l++ + l + + clear: -> + for own key of this + delete this[key] + + global.localStorage = new MockLocalStorage() + + before -> + @clock = sinon.useFakeTimers() + + after -> + @clock.restore() + + afterEach -> + localStorage.clear() + + it 'stores items in localStorage', -> + cache = new Cache(2, false, new LocalStorageCacheStorage()) + cache.setItem "foo1", "bar1" + + @clock.tick() + cache.size().should.equal 1 + localStorage.length.should.equal 1 + + cache.setItem "foo2", "bar2" + cache.setItem "foo3", "bar3" + + @clock.tick() + cache.size().should.equal 2 + localStorage.length.should.equal 2 + + cache.clear() + cache.size().should.equal 0 + localStorage.length.should.equal 0 + + it 'shares items between caches with the same namespace', -> + cache1 = new Cache(-1, false, new LocalStorageCacheStorage('a')) + cache1.setItem "foo", "bar" + cache2 = new Cache(-1, false, new LocalStorageCacheStorage('a')) + cache1.size().should.equal 1 + cache2.size().should.equal 1 + localStorage.length.should.equal 1 + cache2.removeItem "foo" + cache1.size().should.equal 0 + cache2.size().should.equal 0 + localStorage.length.should.equal 0 + + it 'does not share items between caches with different namespace', -> + cache1 = new Cache(-1, false, new LocalStorageCacheStorage('a')) + cache1.setItem "foo", "bar" + cache2 = new Cache(-1, false, new LocalStorageCacheStorage('b')) + cache1.size().should.equal 1 + cache2.size().should.equal 0 + localStorage.length.should.equal 1 + cache2.setItem "foo", "bar" + localStorage.length.should.equal 2 + cache1.removeItem "foo" + cache1.size().should.equal 0 + cache2.size().should.equal 1 + localStorage.length.should.equal 1 From 9ecb774b247f6be773e6903e90f6cd651794d25d Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sat, 25 Apr 2015 11:45:03 +0200 Subject: [PATCH 11/25] Remove support for specifying storage as a string Makes the default constructor work with browserify. Other storage backends need to be required and instantiated: ``` // before new Cache(maxSize, false, 'local_storage') // after CacheStorage = require('cachai/lib/storage/local_storage') new Cache(maxSize, false, new CacheStorage()) ``` --- lib/cache.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index 9d4b70d..15912bb 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -33,14 +33,7 @@ OTHER DEALINGS IN THE SOFTWARE. function Cache(maxSize, debug, storage) { this.maxSize_ = maxSize || -1; this.debug_ = debug || false; - this.storage_ = storage || 'basic'; - if (typeof this.storage_ == 'string') { - try { - this.storage_ = new (require('./stores/'+this.storage_.toLowerCase().trim())); - } catch(e) { - throw new Error('There is no bundled caching store named "'+this.storage_+'"'); - } - } + this.storage_ = storage || new (require('./stores/basic'))(); this.fillFactor_ = .75; this.stats_ = {}; From 6fce07140348276c684943c6942e77c163a42c42 Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sat, 25 Apr 2015 17:20:01 +0200 Subject: [PATCH 12/25] CoffeeLint test --- test/test.coffee | 347 +++++++++++++++++++++++------------------------ 1 file changed, 173 insertions(+), 174 deletions(-) diff --git a/test/test.coffee b/test/test.coffee index aa62ae0..96bcd99 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -5,206 +5,205 @@ chai.use require 'sinon-chai' should = chai.should() INTERVAL = 5 -defer = (i, fn) => - setTimeout fn, INTERVAL*i +defer = (i, fn) -> + setTimeout fn, INTERVAL*i describe 'Cache', -> - before -> - @clock = sinon.useFakeTimers() + before -> + @clock = sinon.useFakeTimers() - after -> - @clock.restore() + after -> + @clock.restore() - describe 'basics', -> + describe 'basics', -> - beforeEach -> - @cache = new Cache + beforeEach -> + @cache = new Cache - it 'should set and get item', -> - @cache.setItem "foo", "bar" - @cache.getItem("foo").should.equal "bar" + it 'should set and get item', -> + @cache.setItem "foo", "bar" + @cache.getItem("foo").should.equal "bar" - it 'should remove item', -> - should.not.exist @cache.getItem("foo") - @cache.setItem "foo", "bar" - should.exist @cache.getItem("foo") - should.exist @cache.removeItem("foo") - should.not.exist @cache.getItem("foo") + it 'should remove item', -> + should.not.exist @cache.getItem("foo") + @cache.setItem "foo", "bar" + should.exist @cache.getItem("foo") + should.exist @cache.removeItem("foo") + should.not.exist @cache.getItem("foo") - it 'should not get missing item', -> - should.not.exist @cache.getItem("missing") + it 'should not get missing item', -> + should.not.exist @cache.getItem("missing") - it 'should not remove missing item', -> - should.not.exist @cache.removeItem("missing") + it 'should not remove missing item', -> + should.not.exist @cache.removeItem("missing") - it 'should register stats', -> - @cache.setItem "foo", "bar" - @cache.getItem("foo") - @cache.getItem("missing") - @cache.removeItem("missing") - stats = @cache.getStats() - stats.hits.should.equal 1 - stats.misses.should.equal 1 + it 'should register stats', -> + @cache.setItem "foo", "bar" + @cache.getItem("foo") + @cache.getItem("missing") + @cache.removeItem("missing") + stats = @cache.getStats() + stats.hits.should.equal 1 + stats.misses.should.equal 1 - it 'should generate an HTML report', -> - @cache.setItem "foo", "bar" - @cache.toHtmlString().should.equal "1 item(s) in cache
  • foo = bar
" + it 'should generate an HTML report', -> + @cache.setItem "foo", "bar" + @cache.toHtmlString().should + .equal "1 item(s) in cache
  • foo = bar
" - it 'should register cache size', -> - @cache.setItem "foo", "bar" - @cache.size().should.equal 1 + it 'should register cache size', -> + @cache.setItem "foo", "bar" + @cache.size().should.equal 1 - @cache.setItem "foo2", "bar2" - @cache.size().should.equal 2 - @cache.removeItem("foo") - @cache.size().should.equal 1 - @cache.clear() - should.not.exist @cache.getItem("foo") - should.not.exist @cache.getItem("foo2") - @cache.size().should.equal 0 + @cache.setItem "foo2", "bar2" + @cache.size().should.equal 2 + @cache.removeItem("foo") + @cache.size().should.equal 1 + @cache.clear() + should.not.exist @cache.getItem("foo") + should.not.exist @cache.getItem("foo2") + @cache.size().should.equal 0 - describe 'absolute expiration', -> + describe 'absolute expiration', -> - beforeEach -> - @cache = new Cache + beforeEach -> + @cache = new Cache - @cache.setItem "foo", "bar", - expirationAbsolute: new Date(new Date().getTime() + INTERVAL*2) + @cache.setItem "foo", "bar", + expirationAbsolute: new Date(new Date().getTime() + INTERVAL*2) - @cache.getItem("foo").should.equal "bar" + @cache.getItem("foo").should.equal "bar" - it 'should expire', (done) -> + it 'should expire', (done) -> + defer 3, => + should.not.exist @cache.getItem("foo") + done() - defer 3, => - should.not.exist @cache.getItem("foo") - done() + @clock.tick INTERVAL*3 - @clock.tick INTERVAL*3 + it 'should not expire', -> + defer 1, => + @cache.getItem("foo").should.equal "bar" - it 'should not expire', -> + @clock.tick INTERVAL*3 - defer 1, => - @cache.getItem("foo").should.equal "bar" + describe 'sliding expiration', -> - @clock.tick INTERVAL*3 + it 'should expire', (done) -> + cache = new Cache - describe 'sliding expiration', -> + cache.setItem "foo", "bar", + expirationSliding: INTERVAL * 2 / 1000 - it 'should expire', (done) -> - cache = new Cache - - cache.setItem "foo", "bar", - expirationSliding: INTERVAL * 2 / 1000 + cache.getItem("foo").should.equal "bar" + defer 1, -> + cache.getItem("foo").should.equal "bar" + defer 1, -> + cache.getItem("foo").should.equal "bar" + defer 1, -> cache.getItem("foo").should.equal "bar" - - defer 1, -> - cache.getItem("foo").should.equal "bar" - defer 1, -> - cache.getItem("foo").should.equal "bar" - defer 1, -> - cache.getItem("foo").should.equal "bar" - defer 3, -> - should.not.exist cache.getItem("foo") - done() - - @clock.tick INTERVAL*6 - - describe 'LRU expiration', -> - - it 'should expire', (done) -> - cache = new Cache 2 - cache.setItem "foo1", "bar1" - cache.setItem "foo2", "bar2" - defer 1, -> - # Access an item so foo1 will be the LRU - cache.getItem("foo2").should.equal "bar2" - - cache.setItem "foo3", "bar3" - cache.size().should.equal 3 - - # Allow time for cache to be purged - defer 1, -> - should.not.exist cache.getItem("foo1") - cache.getItem("foo2").should.equal "bar2" - cache.getItem("foo3").should.equal "bar3" - cache.size().should.equal 2 - done() - - @clock.tick INTERVAL*2 - - describe 'priority expiration', -> - - it 'should expire', (done) -> - cache = new Cache 2 - cache.setItem "foo1", "bar1", - priority: Cache.Priority.HIGH - - cache.setItem "foo2", "bar2" - defer 1, -> - # Access an item so foo1 will be the LRU - cache.getItem("foo2").should.equal "bar2" - - defer 1, -> - cache.setItem "foo3", "bar3" - cache.size().should.equal 3 - - # Allow time for cache to be purged - defer 1, -> - cache.getItem("foo1").should.equal "bar1" - should.not.exist cache.getItem("foo2") - cache.getItem("foo3").should.equal "bar3" - cache.size().should.equal 2 - done() - - @clock.tick INTERVAL*3 - - describe 'sizing', -> - - it 'should resize', (done) -> - cache = new Cache - cache.setItem "foo1", "bar1" - defer 1, -> - cache.setItem "foo2", "bar2" - defer 1, -> - cache.setItem "foo3", "bar3" - cache.resize 2 - should.not.exist cache.getItem("foo1") - cache.getItem("foo2").should.equal "bar2" - defer 1, -> - cache.getItem("foo3").should.equal "bar3" - cache.resize 1 - should.not.exist cache.getItem("foo1") - should.not.exist cache.getItem("foo2") - cache.getItem("foo3").should.equal "bar3" - done() - - @clock.tick INTERVAL*3 - - it 'should use a fill factor', (done) -> - cache = new Cache 100 - counter = 0 - cache.setItem "foo" + i, "bar" + i for i in [1..100] - - cache.size().should.equal 100 - defer 1, -> - cache.size().should.equal 100 - cache.setItem "purge", "do it" - defer 1, -> - cache.size().should.equal 75 - done() - - @clock.tick INTERVAL*2 - - it 'should callback on purge', -> - cache = new Cache - spy = sinon.spy() - cache.setItem "foo", "bar", - onPurge: spy - cache.removeItem "foo" - - @clock.tick 1 - - spy.should.have.been.calledWith "foo", "bar" + defer 3, -> + should.not.exist cache.getItem("foo") + done() + + @clock.tick INTERVAL*6 + + describe 'LRU expiration', -> + + it 'should expire', (done) -> + cache = new Cache 2 + cache.setItem "foo1", "bar1" + cache.setItem "foo2", "bar2" + defer 1, -> + # Access an item so foo1 will be the LRU + cache.getItem("foo2").should.equal "bar2" + + cache.setItem "foo3", "bar3" + cache.size().should.equal 3 + + # Allow time for cache to be purged + defer 1, -> + should.not.exist cache.getItem("foo1") + cache.getItem("foo2").should.equal "bar2" + cache.getItem("foo3").should.equal "bar3" + cache.size().should.equal 2 + done() + + @clock.tick INTERVAL*2 + + describe 'priority expiration', -> + + it 'should expire', (done) -> + cache = new Cache 2 + cache.setItem "foo1", "bar1", + priority: Cache.Priority.HIGH + + cache.setItem "foo2", "bar2" + defer 1, -> + # Access an item so foo1 will be the LRU + cache.getItem("foo2").should.equal "bar2" + + defer 1, -> + cache.setItem "foo3", "bar3" + cache.size().should.equal 3 + + # Allow time for cache to be purged + defer 1, -> + cache.getItem("foo1").should.equal "bar1" + should.not.exist cache.getItem("foo2") + cache.getItem("foo3").should.equal "bar3" + cache.size().should.equal 2 + done() + + @clock.tick INTERVAL*3 + + describe 'sizing', -> + + it 'should resize', (done) -> + cache = new Cache + cache.setItem "foo1", "bar1" + defer 1, -> + cache.setItem "foo2", "bar2" + defer 1, -> + cache.setItem "foo3", "bar3" + cache.resize 2 + should.not.exist cache.getItem("foo1") + cache.getItem("foo2").should.equal "bar2" + defer 1, -> + cache.getItem("foo3").should.equal "bar3" + cache.resize 1 + should.not.exist cache.getItem("foo1") + should.not.exist cache.getItem("foo2") + cache.getItem("foo3").should.equal "bar3" + done() + + @clock.tick INTERVAL*3 + + it 'should use a fill factor', (done) -> + cache = new Cache 100 + counter = 0 + cache.setItem "foo" + i, "bar" + i for i in [1..100] + + cache.size().should.equal 100 + defer 1, -> + cache.size().should.equal 100 + cache.setItem "purge", "do it" + defer 1, -> + cache.size().should.equal 75 + done() + + @clock.tick INTERVAL*2 + + it 'should callback on purge', -> + cache = new Cache + spy = sinon.spy() + cache.setItem "foo", "bar", + onPurge: spy + cache.removeItem "foo" + + @clock.tick 1 + + spy.should.have.been.calledWith "foo", "bar" From bf814d05b133d6a465b537fed09dbe3b4d87c342 Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sat, 25 Apr 2015 18:50:14 +0200 Subject: [PATCH 13/25] Flatten out test code Instead of setting auxiliary timers to drive the tests, having to use 'done' callback to avoid silent failures. This is simpler to read, and should give better error messages when tests fail. --- test/test.coffee | 151 ++++++++++++++++++++++------------------------- 1 file changed, 70 insertions(+), 81 deletions(-) diff --git a/test/test.coffee b/test/test.coffee index 96bcd99..3354d68 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -4,9 +4,7 @@ chai = require 'chai' chai.use require 'sinon-chai' should = chai.should() -INTERVAL = 5 -defer = (i, fn) -> - setTimeout fn, INTERVAL*i +INTERVAL = 5000 describe 'Cache', -> @@ -75,22 +73,17 @@ describe 'Cache', -> @cache.getItem("foo").should.equal "bar" - it 'should expire', (done) -> - defer 3, => - should.not.exist @cache.getItem("foo") - done() - + it 'should expire', -> @clock.tick INTERVAL*3 + should.not.exist @cache.getItem("foo") it 'should not expire', -> - defer 1, => - @cache.getItem("foo").should.equal "bar" - - @clock.tick INTERVAL*3 + @clock.tick INTERVAL + @cache.getItem("foo").should.equal "bar" describe 'sliding expiration', -> - it 'should expire', (done) -> + it 'should expire', -> cache = new Cache cache.setItem "foo", "bar", @@ -98,103 +91,100 @@ describe 'Cache', -> cache.getItem("foo").should.equal "bar" - defer 1, -> - cache.getItem("foo").should.equal "bar" - defer 1, -> - cache.getItem("foo").should.equal "bar" - defer 1, -> - cache.getItem("foo").should.equal "bar" - defer 3, -> - should.not.exist cache.getItem("foo") - done() - - @clock.tick INTERVAL*6 + @clock.tick INTERVAL + cache.getItem("foo").should.equal "bar" + @clock.tick INTERVAL + cache.getItem("foo").should.equal "bar" + @clock.tick INTERVAL + cache.getItem("foo").should.equal "bar" + @clock.tick INTERVAL*3 + should.not.exist cache.getItem("foo") describe 'LRU expiration', -> - it 'should expire', (done) -> + it 'should expire', -> cache = new Cache 2 cache.setItem "foo1", "bar1" cache.setItem "foo2", "bar2" - defer 1, -> - # Access an item so foo1 will be the LRU - cache.getItem("foo2").should.equal "bar2" - cache.setItem "foo3", "bar3" - cache.size().should.equal 3 + @clock.tick INTERVAL - # Allow time for cache to be purged - defer 1, -> - should.not.exist cache.getItem("foo1") - cache.getItem("foo2").should.equal "bar2" - cache.getItem("foo3").should.equal "bar3" - cache.size().should.equal 2 - done() + # Access an item so foo1 will be the LRU + cache.getItem("foo2").should.equal "bar2" - @clock.tick INTERVAL*2 + cache.setItem "foo3", "bar3" + cache.size().should.equal 3 + + # Allow time for cache to be purged + @clock.tick INTERVAL + + should.not.exist cache.getItem("foo1") + cache.getItem("foo2").should.equal "bar2" + cache.getItem("foo3").should.equal "bar3" + cache.size().should.equal 2 describe 'priority expiration', -> - it 'should expire', (done) -> + it 'should expire', -> cache = new Cache 2 cache.setItem "foo1", "bar1", priority: Cache.Priority.HIGH cache.setItem "foo2", "bar2" - defer 1, -> - # Access an item so foo1 will be the LRU - cache.getItem("foo2").should.equal "bar2" - - defer 1, -> - cache.setItem "foo3", "bar3" - cache.size().should.equal 3 - - # Allow time for cache to be purged - defer 1, -> - cache.getItem("foo1").should.equal "bar1" - should.not.exist cache.getItem("foo2") - cache.getItem("foo3").should.equal "bar3" - cache.size().should.equal 2 - done() - @clock.tick INTERVAL*3 + @clock.tick INTERVAL + + # Access an item so foo1 will be the LRU + cache.getItem("foo2").should.equal "bar2" + + @clock.tick INTERVAL + + cache.setItem "foo3", "bar3" + cache.size().should.equal 3 + + # Allow time for cache to be purged + @clock.tick INTERVAL + + cache.getItem("foo1").should.equal "bar1" + should.not.exist cache.getItem("foo2") + cache.getItem("foo3").should.equal "bar3" + cache.size().should.equal 2 describe 'sizing', -> - it 'should resize', (done) -> + it 'should resize', -> cache = new Cache cache.setItem "foo1", "bar1" - defer 1, -> - cache.setItem "foo2", "bar2" - defer 1, -> - cache.setItem "foo3", "bar3" - cache.resize 2 - should.not.exist cache.getItem("foo1") - cache.getItem("foo2").should.equal "bar2" - defer 1, -> - cache.getItem("foo3").should.equal "bar3" - cache.resize 1 - should.not.exist cache.getItem("foo1") - should.not.exist cache.getItem("foo2") - cache.getItem("foo3").should.equal "bar3" - done() - @clock.tick INTERVAL*3 + @clock.tick INTERVAL + cache.setItem "foo2", "bar2" + + @clock.tick INTERVAL + cache.setItem "foo3", "bar3" + cache.resize 2 + should.not.exist cache.getItem("foo1") + cache.getItem("foo2").should.equal "bar2" + + @clock.tick INTERVAL + cache.getItem("foo3").should.equal "bar3" + cache.resize 1 + should.not.exist cache.getItem("foo1") + should.not.exist cache.getItem("foo2") + cache.getItem("foo3").should.equal "bar3" - it 'should use a fill factor', (done) -> + it 'should use a fill factor', -> cache = new Cache 100 counter = 0 cache.setItem "foo" + i, "bar" + i for i in [1..100] cache.size().should.equal 100 - defer 1, -> - cache.size().should.equal 100 - cache.setItem "purge", "do it" - defer 1, -> - cache.size().should.equal 75 - done() - @clock.tick INTERVAL*2 + @clock.tick INTERVAL + cache.size().should.equal 100 + cache.setItem "purge", "do it" + + @clock.tick INTERVAL + cache.size().should.equal 75 it 'should callback on purge', -> cache = new Cache @@ -203,7 +193,6 @@ describe 'Cache', -> onPurge: spy cache.removeItem "foo" - @clock.tick 1 + @clock.tick INTERVAL spy.should.have.been.calledWith "foo", "bar" - From d159f885ca1c693cd5fe6fd913e5003d8f8e2e5a Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sun, 26 Apr 2015 10:47:31 +0200 Subject: [PATCH 14/25] Simplify purge logic in resize() If the current size of the cache is larger than the new max size, we call purge. It's not really necessary to check if the new size was larger than the old size before checking the current size. The advantage is that the test is now identical to setItem(), so it can be generalized. --- lib/cache.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index 15912bb..b532fda 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -188,14 +188,11 @@ Cache.prototype.toHtmlString = function() { Cache.prototype.resize = function(newMaxSize) { this.log_('Resizing Cache from ' + this.maxSize_ + ' to ' + newMaxSize); // Set new size before purging so we know how many items to purge - var oldMaxSize = this.maxSize_ this.maxSize_ = newMaxSize; - if (newMaxSize > 0 && (oldMaxSize < 0 || newMaxSize < oldMaxSize)) { - if (this.size() > newMaxSize) { - // Cache needs to be purged as it does contain too much entries for the new size - this.purge_(); - } // else if cache isn't filled up to the new limit nothing is to do + if ((this.maxSize_ > 0) && (this.size() > this.maxSize_)) { + // Cache needs to be purged as it does contain too much entries for the new size + this.purge_(); } // else if newMaxSize >= maxSize nothing to do this.log_('Resizing done'); From f290d74be8eb2d6fae1bf2d46d28573ce2349d76 Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sun, 26 Apr 2015 11:07:45 +0200 Subject: [PATCH 15/25] Split max size check into a new private method Purging in resize() is now deferred, like in setItem() --- lib/cache.js | 28 ++++++++++++++-------------- test/test.coffee | 2 ++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index b532fda..5f2c229 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -131,16 +131,9 @@ Cache.prototype.setItem = function(key, value, options) { if (this.storage_.get(key) != null) { this.removeItem(key); } - this.addItem_(new Cache._CacheItem(key, value, options)); this.log_("Setting key " + key); - - // if the cache is full, purge it - if ((this.maxSize_ > 0) && (this.size() > this.maxSize_)) { - var that = this; - setTimeout(function() { - that.purge_.call(that); - }, 0); - } + this.addItem_(new Cache._CacheItem(key, value, options)); + this.checkMaxSize_(); }; @@ -187,16 +180,23 @@ Cache.prototype.toHtmlString = function() { */ Cache.prototype.resize = function(newMaxSize) { this.log_('Resizing Cache from ' + this.maxSize_ + ' to ' + newMaxSize); - // Set new size before purging so we know how many items to purge this.maxSize_ = newMaxSize; + this.checkMaxSize_(); +} +/** + * if the cache is full, purge it + * @private + */ +Cache.prototype.checkMaxSize_ = function() { if ((this.maxSize_ > 0) && (this.size() > this.maxSize_)) { // Cache needs to be purged as it does contain too much entries for the new size - this.purge_(); + var that = this; + setTimeout(function() { + that.purge_.call(that); + }, 0); } - // else if newMaxSize >= maxSize nothing to do - this.log_('Resizing done'); -} +}; /** * Removes expired items from the cache. diff --git a/test/test.coffee b/test/test.coffee index 3354d68..6772913 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -162,12 +162,14 @@ describe 'Cache', -> @clock.tick INTERVAL cache.setItem "foo3", "bar3" cache.resize 2 + @clock.tick INTERVAL should.not.exist cache.getItem("foo1") cache.getItem("foo2").should.equal "bar2" @clock.tick INTERVAL cache.getItem("foo3").should.equal "bar3" cache.resize 1 + @clock.tick INTERVAL should.not.exist cache.getItem("foo1") should.not.exist cache.getItem("foo2") cache.getItem("foo3").should.equal "bar3" From dc37309fc24629f56438921e6e29c80dede3758e Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sun, 26 Apr 2015 11:16:31 +0200 Subject: [PATCH 16/25] Make sure purge is scheduled only once When inserting many items in one synchronous "thread", and the cache max size is exceeded, make sure we only set the timeout to purge the cache once! --- lib/cache.js | 6 ++++++ test/test.coffee | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/cache.js b/lib/cache.js index 5f2c229..2c3b50a 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -35,6 +35,7 @@ function Cache(maxSize, debug, storage) { this.debug_ = debug || false; this.storage_ = storage || new (require('./stores/basic'))(); this.fillFactor_ = .75; + this.pendingPurge_ = false; this.stats_ = {}; this.stats_['hits'] = 0; @@ -189,9 +190,13 @@ Cache.prototype.resize = function(newMaxSize) { * @private */ Cache.prototype.checkMaxSize_ = function() { + if (this.pendingPurge_) + return; + if ((this.maxSize_ > 0) && (this.size() > this.maxSize_)) { // Cache needs to be purged as it does contain too much entries for the new size var that = this; + this.pendingPurge_ = true; setTimeout(function() { that.purge_.call(that); }, 0); @@ -234,6 +239,7 @@ Cache.prototype.purge_ = function() { this.removeItem(ritem.key); } } + this.pendingPurge_ = false; this.log_('Purged cached'); }; diff --git a/test/test.coffee b/test/test.coffee index 6772913..c199e5c 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -198,3 +198,17 @@ describe 'Cache', -> @clock.tick INTERVAL spy.should.have.been.calledWith "foo", "bar" + + describe 'when max size is exceeded', -> + it 'should purge once', -> + cache = new Cache(10) + spy = sinon.spy(cache, 'purge_') + + for k in [0...20] + cache.setItem "foo#{k}", "bar#{k}" + + @clock.tick() + + cache.size().should.be.lessThan 10 + spy.should.have.been.calledOnce + spy.restore() From 54076ba7de2cd102b55b234c5d5032da19d755aa Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Sun, 26 Apr 2015 11:19:49 +0200 Subject: [PATCH 17/25] Remove redundant .call --- lib/cache.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index 2c3b50a..889142c 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -195,10 +195,10 @@ Cache.prototype.checkMaxSize_ = function() { if ((this.maxSize_ > 0) && (this.size() > this.maxSize_)) { // Cache needs to be purged as it does contain too much entries for the new size - var that = this; + var self = this; this.pendingPurge_ = true; setTimeout(function() { - that.purge_.call(that); + self.purge_(); }, 0); } }; From ecf2c89ef58de165b51e7be237727bea62fb9be4 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Sun, 26 Apr 2015 20:25:51 -0300 Subject: [PATCH 18/25] Add Travis CI support. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2d3009c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 0.12 From 00af70ba9f878ab2688a6905b82371076a5cb77e Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Sun, 26 Apr 2015 20:26:25 -0300 Subject: [PATCH 19/25] Add status badges to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 8381268..6a53800 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ LRU Cache for Node.js ==================== +[![Build Status](https://secure.travis-ci.org/rstuven/node-cachai.png?branch=master)](http://travis-ci.org/rstuven/node-cachai) +[![Coverage Status](https://coveralls.io/repos/rstuven/node-cachai/badge.svg)](https://coveralls.io/r/rstuven/node-cachai) +[![dependencies Status](https://david-dm.org/rstuven/node-cachai.svg)](https://david-dm.org/rstuven/node-cachai#info=dependencies) +[![devDependencies Status](https://david-dm.org/rstuven/node-cachai/dev-status.svg)](https://david-dm.org/rstuven/node-cachai#info=devDependencies) + + This is a fork of Monsur Hossain's jscache for browsers, which is loosely based on ASP.NET's Cache, and includes many caching options such as absolute expiration, sliding expiration, cache priority, and a callback function on purge. How It Works From 4b26601353309ef28050fb5cb6deefb1d494ae4c Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Sun, 26 Apr 2015 20:43:30 -0300 Subject: [PATCH 20/25] Add code coverage tooling --- .gitignore | 1 + .travis.yml | 9 ++++++++- package.json | 29 ++++++++++++++++++++--------- test/mocha.opts | 1 + 4 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 test/mocha.opts diff --git a/.gitignore b/.gitignore index 3c3629e..8a5b45e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +coverage.html diff --git a/.travis.yml b/.travis.yml index 2d3009c..52c3b5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,10 @@ language: node_js node_js: - - 0.12 + - "0.12" + - "0.11" + - "0.10" + - "iojs" + +script: + - npm run test + - npm run test-coveralls diff --git a/package.json b/package.json index f87e1f7..c5494ce 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,41 @@ { - "author": "Ricardo Stuven ", "name": "cachai", "description": "LRU Cache for Node.js", - "version": "0.0.0", + "version": "1.0.0", + "author": "Ricardo Stuven ", "homepage": "https://github.com/rstuven/node-cachai", "repository": { "type": "git", "url": "git://github.com/rstuven/node-cachai.git" }, "engines": { - "node": "~0.6.1" + "node": ">=0.10.0" }, "directories": { "lib": "./lib" }, "main": "./lib/cache", - "dependencies": {}, "devDependencies": { - "mocha": "*", + "blanket": "git://github.com/alex-seville/blanket.git", "chai": "*", + "coffee-script": "*", + "coveralls": "^2.11.2", + "mocha": "*", + "mocha-lcov-reporter": "0.0.2", + "mocha-text-cov": "^0.1.0", "sinon": "*", - "sinon-chai": "*", - "coffee-script": "*" + "sinon-chai": "*" }, - "optionalDependencies": {}, "scripts": { - "test": "mocha --compilers coffee:coffee-script/register -R spec" + "test": "mocha -R spec", + "test-cov": "mocha -r blanket -R html-cov > coverage.html", + "test-cov-text": "mocha -r blanket -R mocha-text-cov", + "test-coveralls": "mocha -r blanket -R mocha-lcov-reporter | coveralls" + }, + "config": { + "blanket": { + "pattern": "lib", + "data-cover-never": "node_modules" + } } } diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..8bd4127 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--compilers coffee:coffee-script/register From 8c38e1e0195f4f0f414dba687eabd252630bcd26 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Sun, 26 Apr 2015 20:50:12 -0300 Subject: [PATCH 21/25] Update README --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6a53800..e5a474d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -LRU Cache for Node.js -==================== +cachai +====== + +> LRU Cache for Node.js [![Build Status](https://secure.travis-ci.org/rstuven/node-cachai.png?branch=master)](http://travis-ci.org/rstuven/node-cachai) [![Coverage Status](https://coveralls.io/repos/rstuven/node-cachai/badge.svg)](https://coveralls.io/r/rstuven/node-cachai) @@ -9,8 +11,14 @@ LRU Cache for Node.js This is a fork of Monsur Hossain's jscache for browsers, which is loosely based on ASP.NET's Cache, and includes many caching options such as absolute expiration, sliding expiration, cache priority, and a callback function on purge. -How It Works ------------- +Install +------- + + npm install --save cachai + + +Usage +----- var Cache = require('cachai'); From 7c7ec799ae4273f1ae118f874dc156690f903c68 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Sun, 26 Apr 2015 20:50:38 -0300 Subject: [PATCH 22/25] Increment version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5494ce..4da7ad4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cachai", "description": "LRU Cache for Node.js", - "version": "1.0.0", + "version": "1.0.1", "author": "Ricardo Stuven ", "homepage": "https://github.com/rstuven/node-cachai", "repository": { From d2534aba709de299bda2b46749a3712fa971d3d5 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Sun, 26 Apr 2015 21:12:35 -0300 Subject: [PATCH 23/25] Reformat README --- README.md | 142 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 84 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index e5a474d..5326c1a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ cachai This is a fork of Monsur Hossain's jscache for browsers, which is loosely based on ASP.NET's Cache, and includes many caching options such as absolute expiration, sliding expiration, cache priority, and a callback function on purge. + Install ------- @@ -20,61 +21,86 @@ Install Usage ----- - var Cache = require('cachai'); - - // Create a new cache item - // The constructor accepts an optional integer - // parameter which places a limit on how many - // items the cache holds - var cache = new Cache(); - - // add an item to the cache - // parameters: key - the key to refer to the object - // value - the object to cache - // options - an optional parameter described below - // the last parameter accepts an object which controls various caching options: - // expirationAbsolute: the datetime when the item should expire - // expirationSliding: an integer representing the seconds since - // the last cache access after which the item - // should expire - // priority: How important it is to leave this item in the cache. - // You can use the values Cache.Priority.LOW, .NORMAL, or - // .HIGH, or you can just use an integer. Note that - // placing a priority on an item does not guarantee - // it will remain in cache. It can still be purged if - // an expiration is hit, or if the cache is full. - // onPurge: A function that gets called when the item is purged - // from cache. The key and value of the removed item - // are passed as parameters to the callback function. - cache.setItem("A", "1", {expirationAbsolute: null, - expirationSliding: 60, - priority: Cache.Priority.HIGH, - onPurge: function(k, v) { console.log('removed ' + k); } - }); - - // retrieve an item from the cache - // takes one parameter, the key to retreive - // returns the cached item - cache.getItem("A"); - - // Remove and return an item from the cache. - // If the item doesn't exist it returns null. - cache.removeItem("A"); - - // Returns the number of items in the cache. - cache.size(); - - // Return stats about the cache, like {"hits": 1, "misses": 4} - cache.stats(); - - // clears all items from the cache - cache.clear(); - - -History -------- -* 2/22/2012: Forked from Monsur Hossain repo and ported to Node.js. -* 11/29/2011: Thanks to Andrew Carman for tests, pluggable backends, localStorage persistance, and bug fixes. -* 1/8/2011: Migrated project to GitHub. -* 1/20/2010: Thanks to Andrej Arn for some syntax updates. -* 5/30/2008: First version. +First, create a new cache object. +The constructor accepts an optional integer +parameter which places a limit on how many +items the cache holds. +Example: +``` javascript +var Cache = require('cachai'); + +var cache = new Cache(); +``` + +### Methods + +`setItem` adds an item to the cache. Arguments: +- `key`: key to refer to the object +- `value`: object to cache +- `options`: optional parameters described below + +Options available are: + +- `expirationAbsolute`: +The datetime when the item should expire + +- `expirationSliding`: +An integer representing the seconds since +the last cache access after which the item +should expire + +- `priority`: +How important it is to leave this item in the cache. +You can use the values Cache.Priority.LOW, .NORMAL, or +.HIGH, or you can just use an integer. Note that +placing a priority on an item does not guarantee +it will remain in cache. It can still be purged if +an expiration is hit, or if the cache is full. + +- `onPurge`: +A function that gets called when the item is purged +from cache. The key and value of the removed item +are passed as parameters to the callback function. + +Example: +``` javascript +cache.setItem("A", "1", { + expirationAbsolute: null, + expirationSliding: 60, + priority: Cache.Priority.HIGH, + onPurge: function(k, v) { console.log('removed', k, v); } +}); +``` + +`getItem` retrieves an item from the cache +takes one parameter, the key to retrieve +returns the cached item. +Example: +``` javascript +var item = cache.getItem("A"); +``` + +`removeItem` removes and returns an item from the cache. +If the item doesn't exist it returns null. +Example: +``` javascript +var removed = cache.removeItem("A"); +``` + +`size` returns the number of items in the cache. +Example: +``` javascript +var size = cache.size(); +``` + +`stats` returns stats about the cache, like `{"hits": 1, "misses": 4}`. +Example: +``` javascript +console.dir(cache.stats()); +``` + +`clear` removes all items from the cache. +Example: +``` javascript +cache.clear(); +``` From be10745a83e14f0da5528c468f84dd0000f53256 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Sun, 26 Apr 2015 21:19:56 -0300 Subject: [PATCH 24/25] Ignore coveralls error --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4da7ad4..7634a49 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "test": "mocha -R spec", "test-cov": "mocha -r blanket -R html-cov > coverage.html", "test-cov-text": "mocha -r blanket -R mocha-text-cov", - "test-coveralls": "mocha -r blanket -R mocha-lcov-reporter | coveralls" + "test-coveralls": "mocha -r blanket -R mocha-lcov-reporter | coveralls || true" }, "config": { "blanket": { From 5416f64e0aaa202af2c14442a94b690c5c097cf2 Mon Sep 17 00:00:00 2001 From: Ricardo Stuven Date: Sun, 26 Apr 2015 21:44:27 -0300 Subject: [PATCH 25/25] 1.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7634a49..2452d1c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cachai", "description": "LRU Cache for Node.js", - "version": "1.0.1", + "version": "1.0.2", "author": "Ricardo Stuven ", "homepage": "https://github.com/rstuven/node-cachai", "repository": {