├── .gitignore ├── .jsbeautifyrc ├── .jshintrc ├── .travis.yml ├── LICENSE.md ├── README.md ├── gulpfile.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .idea -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 2, 3 | "indent_char": " ", 4 | "indent_level": 0, 5 | "indent_with_tabs": false, 6 | "preserve_newlines": true, 7 | "max_preserve_newlines": 10, 8 | "jslint_happy": false, 9 | "brace_style": "collapse", 10 | "keep_array_indentation": false, 11 | "keep_function_indentation": false, 12 | "space_in_paren": false, 13 | "space_before_conditional": true, 14 | "break_chained_methods": false, 15 | "eval_code": false, 16 | "unescape_strings": false, 17 | "wrap_line_length": 0 18 | } 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "undef": true, 4 | "unused": true, 5 | "esnext": true, 6 | "predef": [ "require", "module" ], 7 | "globalstrict": true, 8 | "trailing": true, 9 | "-W097": true 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Paul Tarjan 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memory-cache [![Build Status](https://travis-ci.org/ptarjan/node-cache.svg?branch=master)](https://travis-ci.org/ptarjan/node-cache) 2 | 3 | A simple in-memory cache for node.js 4 | 5 | ## Installation 6 | 7 | npm install memory-cache --save 8 | 9 | ## Usage 10 | 11 | ```javascript 12 | var cache = require('memory-cache'); 13 | 14 | // now just use the cache 15 | 16 | cache.put('foo', 'bar'); 17 | console.log(cache.get('foo')); 18 | 19 | // that wasn't too interesting, here's the good part 20 | 21 | cache.put('houdini', 'disappear', 100, function(key, value) { 22 | console.log(key + ' did ' + value); 23 | }); // Time in ms 24 | 25 | console.log('Houdini will now ' + cache.get('houdini')); 26 | 27 | setTimeout(function() { 28 | console.log('Houdini is ' + cache.get('houdini')); 29 | }, 200); 30 | 31 | 32 | // create new cache instance 33 | var newCache = new cache.Cache(); 34 | 35 | newCache.put('foo', 'newbaz'); 36 | 37 | setTimeout(function() { 38 | console.log('foo in old cache is ' + cache.get('foo')); 39 | console.log('foo in new cache is ' + newCache.get('foo')); 40 | }, 200); 41 | ``` 42 | 43 | which should print 44 | 45 | bar 46 | Houdini will now disappear 47 | houdini did disappear 48 | Houdini is null 49 | foo in old cache is baz 50 | foo in new cache is newbaz 51 | 52 | ## API 53 | 54 | ### put = function(key, value, time, timeoutCallback) 55 | 56 | * Simply stores a value 57 | * If time isn't passed in, it is stored forever 58 | * Will actually remove the value in the specified time in ms (via `setTimeout`) 59 | * timeoutCallback is optional function fired after entry has expired with key and value passed (`function(key, value) {}`) 60 | * Returns the cached value 61 | 62 | ### get = function(key) 63 | 64 | * Retrieves a value for a given key 65 | * If value isn't cached, returns `null` 66 | 67 | ### del = function(key) 68 | 69 | * Deletes a key, returns a boolean specifying whether or not the key was deleted 70 | 71 | ### clear = function() 72 | 73 | * Deletes all keys 74 | 75 | ### size = function() 76 | 77 | * Returns the current number of entries in the cache 78 | 79 | ### memsize = function() 80 | 81 | * Returns the number of entries taking up space in the cache 82 | * Will usually `== size()` unless a `setTimeout` removal went wrong 83 | 84 | ### debug = function(bool) 85 | 86 | * Turns on or off debugging 87 | 88 | ### hits = function() 89 | 90 | * Returns the number of cache hits (only monitored in debug mode) 91 | 92 | ### misses = function() 93 | 94 | * Returns the number of cache misses (only monitored in debug mode) 95 | 96 | ### keys = function() 97 | 98 | * Returns all the cache keys 99 | 100 | ### exportJson = function() 101 | 102 | * Returns a JSON string representing all the cache data 103 | * Any timeoutCallbacks will be ignored 104 | 105 | ### importJson = function(json: string, options: { skipDuplicates: boolean }) 106 | 107 | * Merges all the data from a previous call to `export` into the cache 108 | * Any existing entries before an `import` will remain in the cache 109 | * Any duplicate keys will be overwritten, unless `skipDuplicates` is `true` 110 | * Any entries that would have expired since being exported will expire upon being imported (but their callbacks will not be invoked) 111 | * Available `options`: 112 | * `skipDuplicates`: If `true`, any duplicate keys will be ignored when importing them. Defaults to `false`. 113 | * Returns the new size of the cache 114 | 115 | ### Cache = function() 116 | 117 | * Cache constructor 118 | * note that `require('cache')` would return the default instance of Cache 119 | * while `require('cache').Cache` is the actual class 120 | 121 | ## Note on Patches/Pull Requests 122 | 123 | * Fork the project. 124 | * Make your feature addition or bug fix. 125 | * Send me a pull request. 126 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /**************/ 2 | /* REQUIRES */ 3 | /**************/ 4 | var gulp = require('gulp'); 5 | 6 | // File I/O 7 | var exit = require('gulp-exit'); 8 | var jshint = require('gulp-jshint'); 9 | 10 | // Testing 11 | var mocha = require('gulp-mocha'); 12 | var istanbul = require('gulp-istanbul'); 13 | 14 | 15 | /****************/ 16 | /* FILE PATHS */ 17 | /****************/ 18 | var paths = { 19 | js: [ 20 | 'index.js' 21 | ], 22 | 23 | tests: [ 24 | 'test.js' 25 | ] 26 | }; 27 | 28 | 29 | /***********/ 30 | /* TASKS */ 31 | /***********/ 32 | // Lints the JavaScript files 33 | gulp.task('lint', function() { 34 | return gulp.src(paths.js) 35 | .pipe(jshint()) 36 | .pipe(jshint.reporter('jshint-stylish')) 37 | .pipe(jshint.reporter('fail')) 38 | .on('error', function(error) { 39 | throw error; 40 | }); 41 | }); 42 | 43 | // Runs the Mocha test suite 44 | gulp.task('test', function() { 45 | return gulp.src(paths.js) 46 | .pipe(istanbul()) 47 | .pipe(istanbul.hookRequire()) 48 | .on('finish', function () { 49 | gulp.src(paths.tests) 50 | .pipe(mocha({ 51 | reporter: 'spec', 52 | timeout: 5000 53 | })) 54 | .pipe(istanbul.writeReports()) 55 | .pipe(exit()); 56 | }); 57 | }); 58 | 59 | // Re-runs the linter every time a JavaScript file changes 60 | gulp.task('watch', function() { 61 | gulp.watch(paths.js, ['lint']); 62 | }); 63 | 64 | // Default task 65 | gulp.task('default', ['lint', 'test']); 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Cache () { 4 | var _cache = Object.create(null); 5 | var _hitCount = 0; 6 | var _missCount = 0; 7 | var _size = 0; 8 | var _debug = false; 9 | 10 | this.put = function(key, value, time, timeoutCallback) { 11 | if (_debug) { 12 | console.log('caching: %s = %j (@%s)', key, value, time); 13 | } 14 | 15 | if (typeof time !== 'undefined' && (typeof time !== 'number' || isNaN(time) || time <= 0)) { 16 | throw new Error('Cache timeout must be a positive number'); 17 | } else if (typeof timeoutCallback !== 'undefined' && typeof timeoutCallback !== 'function') { 18 | throw new Error('Cache timeout callback must be a function'); 19 | } 20 | 21 | var oldRecord = _cache[key]; 22 | if (oldRecord) { 23 | clearTimeout(oldRecord.timeout); 24 | } else { 25 | _size++; 26 | } 27 | 28 | var record = { 29 | value: value, 30 | expire: time + Date.now() 31 | }; 32 | 33 | if (!isNaN(record.expire)) { 34 | record.timeout = setTimeout(function() { 35 | _del(key); 36 | if (timeoutCallback) { 37 | timeoutCallback(key, value); 38 | } 39 | }.bind(this), time); 40 | } 41 | 42 | _cache[key] = record; 43 | 44 | return value; 45 | }; 46 | 47 | this.del = function(key) { 48 | var canDelete = true; 49 | 50 | var oldRecord = _cache[key]; 51 | if (oldRecord) { 52 | clearTimeout(oldRecord.timeout); 53 | if (!isNaN(oldRecord.expire) && oldRecord.expire < Date.now()) { 54 | canDelete = false; 55 | } 56 | } else { 57 | canDelete = false; 58 | } 59 | 60 | if (canDelete) { 61 | _del(key); 62 | } 63 | 64 | return canDelete; 65 | }; 66 | 67 | function _del(key){ 68 | _size--; 69 | delete _cache[key]; 70 | } 71 | 72 | this.clear = function() { 73 | for (var key in _cache) { 74 | clearTimeout(_cache[key].timeout); 75 | } 76 | _size = 0; 77 | _cache = Object.create(null); 78 | if (_debug) { 79 | _hitCount = 0; 80 | _missCount = 0; 81 | } 82 | }; 83 | 84 | this.get = function(key) { 85 | var data = _cache[key]; 86 | if (typeof data != "undefined") { 87 | if (isNaN(data.expire) || data.expire >= Date.now()) { 88 | if (_debug) _hitCount++; 89 | return data.value; 90 | } else { 91 | // free some space 92 | if (_debug) _missCount++; 93 | _size--; 94 | delete _cache[key]; 95 | } 96 | } else if (_debug) { 97 | _missCount++; 98 | } 99 | return null; 100 | }; 101 | 102 | this.size = function() { 103 | return _size; 104 | }; 105 | 106 | this.memsize = function() { 107 | var size = 0, 108 | key; 109 | for (key in _cache) { 110 | size++; 111 | } 112 | return size; 113 | }; 114 | 115 | this.debug = function(bool) { 116 | _debug = bool; 117 | }; 118 | 119 | this.hits = function() { 120 | return _hitCount; 121 | }; 122 | 123 | this.misses = function() { 124 | return _missCount; 125 | }; 126 | 127 | this.keys = function() { 128 | return Object.keys(_cache); 129 | }; 130 | 131 | this.exportJson = function() { 132 | var plainJsCache = {}; 133 | 134 | // Discard the `timeout` property. 135 | // Note: JSON doesn't support `NaN`, so convert it to `'NaN'`. 136 | for (var key in _cache) { 137 | var record = _cache[key]; 138 | plainJsCache[key] = { 139 | value: record.value, 140 | expire: record.expire || 'NaN', 141 | }; 142 | } 143 | 144 | return JSON.stringify(plainJsCache); 145 | }; 146 | 147 | this.importJson = function(jsonToImport, options) { 148 | var cacheToImport = JSON.parse(jsonToImport); 149 | var currTime = Date.now(); 150 | 151 | var skipDuplicates = options && options.skipDuplicates; 152 | 153 | for (var key in cacheToImport) { 154 | if (cacheToImport.hasOwnProperty(key)) { 155 | if (skipDuplicates) { 156 | var existingRecord = _cache[key]; 157 | if (existingRecord) { 158 | if (_debug) { 159 | console.log('Skipping duplicate imported key \'%s\'', key); 160 | } 161 | continue; 162 | } 163 | } 164 | 165 | var record = cacheToImport[key]; 166 | 167 | // record.expire could be `'NaN'` if no expiry was set. 168 | // Try to subtract from it; a string minus a number is `NaN`, which is perfectly fine here. 169 | var remainingTime = record.expire - currTime; 170 | 171 | if (remainingTime <= 0) { 172 | // Delete any record that might exist with the same key, since this key is expired. 173 | this.del(key); 174 | continue; 175 | } 176 | 177 | // Remaining time must now be either positive or `NaN`, 178 | // but `put` will throw an error if we try to give it `NaN`. 179 | remainingTime = remainingTime > 0 ? remainingTime : undefined; 180 | 181 | this.put(key, record.value, remainingTime); 182 | } 183 | } 184 | 185 | return this.size(); 186 | }; 187 | } 188 | 189 | module.exports = new Cache(); 190 | module.exports.Cache = Cache; 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memory-cache", 3 | "description": "A simple in-memory cache. put(), get() and del()", 4 | "author": "Paul Tarjan ", 5 | "contributors": [ 6 | { 7 | "name": "Ramon Snir", 8 | "email": "ramon@dynamicyield.com" 9 | }, 10 | { 11 | "name": "Jacob Wenger", 12 | "email": "wenger.jacob@gmail.com" 13 | } 14 | ], 15 | "keywords": [ 16 | "cache", 17 | "ram", 18 | "simple", 19 | "storage" 20 | ], 21 | "main": "./index.js", 22 | "version": "0.2.0", 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/ptarjan/node-cache.git" 26 | }, 27 | "scripts": { 28 | "test": "./node_modules/.bin/gulp test" 29 | }, 30 | "license": "BSD-2-Clause", 31 | "devDependencies": { 32 | "chai": "^2.2.0", 33 | "gulp": "^3.8.11", 34 | "gulp-exit": "0.0.2", 35 | "gulp-istanbul": "^0.7.0", 36 | "gulp-jshint": "^1.10.0", 37 | "gulp-mocha": "^2.0.1", 38 | "jshint-stylish": "^1.0.1", 39 | "sinon": "^1.14.1", 40 | "sinon-chai": "^2.7.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, beforeEach, afterEach */ 2 | 'use strict'; 3 | 4 | var chai = require('chai'), 5 | expect = chai.expect, 6 | sinon = require('sinon'), 7 | sinonChai = require('sinon-chai'), 8 | Cache = require('./index').Cache, 9 | cache = new Cache(), 10 | clock; 11 | 12 | chai.use(sinonChai); 13 | 14 | 15 | describe('node-cache', function() { 16 | beforeEach(function() { 17 | clock = sinon.useFakeTimers(); 18 | 19 | cache.clear(); 20 | }); 21 | 22 | afterEach(function() { 23 | clock.restore(); 24 | }); 25 | 26 | describe('put()', function() { 27 | before(function() { 28 | cache.debug(false); 29 | }); 30 | 31 | it('should allow adding a new item to the cache', function() { 32 | expect(function() { 33 | cache.put('key', 'value'); 34 | }).to.not.throw(); 35 | }); 36 | 37 | it('should allow adding a new item to the cache with a timeout', function() { 38 | expect(function() { 39 | cache.put('key', 'value', 100); 40 | }).to.not.throw(); 41 | }); 42 | 43 | it('should allow adding a new item to the cache with a timeout callback', function() { 44 | expect(function() { 45 | cache.put('key', 'value', 100, function() {}); 46 | }).to.not.throw(); 47 | }); 48 | 49 | it('should throw an error given a non-numeric timeout', function() { 50 | expect(function() { 51 | cache.put('key', 'value', 'foo'); 52 | }).to.throw(); 53 | }); 54 | 55 | it('should throw an error given a timeout of NaN', function() { 56 | expect(function() { 57 | cache.put('key', 'value', NaN); 58 | }).to.throw(); 59 | }); 60 | 61 | it('should throw an error given a timeout of 0', function() { 62 | expect(function() { 63 | cache.put('key', 'value', 0); 64 | }).to.throw(); 65 | }); 66 | 67 | it('should throw an error given a negative timeout', function() { 68 | expect(function() { 69 | cache.put('key', 'value', -100); 70 | }).to.throw(); 71 | }); 72 | 73 | it('should throw an error given a non-function timeout callback', function() { 74 | expect(function() { 75 | cache.put('key', 'value', 100, 'foo'); 76 | }).to.throw(); 77 | }); 78 | 79 | it('should cause the timeout callback to fire once the cache item expires', function() { 80 | var spy = sinon.spy(); 81 | cache.put('key', 'value', 1000, spy); 82 | clock.tick(999); 83 | expect(spy).to.not.have.been.called; 84 | clock.tick(1); 85 | expect(spy).to.have.been.calledOnce.and.calledWith('key', 'value'); 86 | }); 87 | 88 | it('should override the timeout callback on a new put() with a different timeout callback', function() { 89 | var spy1 = sinon.spy(); 90 | var spy2 = sinon.spy(); 91 | cache.put('key', 'value', 1000, spy1); 92 | clock.tick(999); 93 | cache.put('key', 'value', 1000, spy2) 94 | clock.tick(1001); 95 | expect(spy1).to.not.have.been.called; 96 | expect(spy2).to.have.been.calledOnce.and.calledWith('key', 'value'); 97 | }); 98 | 99 | it('should cancel the timeout callback on a new put() without a timeout callback', function() { 100 | var spy = sinon.spy(); 101 | cache.put('key', 'value', 1000, spy); 102 | clock.tick(999); 103 | cache.put('key', 'value'); 104 | clock.tick(1); 105 | expect(spy).to.not.have.been.called; 106 | }); 107 | 108 | it('should return the cached value', function() { 109 | expect(cache.put('key', 'value')).to.equal('value'); 110 | }); 111 | }); 112 | 113 | describe('del()', function() { 114 | before(function() { 115 | cache.debug(false); 116 | }); 117 | 118 | it('should return false given a key for an empty cache', function() { 119 | expect(cache.del('miss')).to.be.false; 120 | }); 121 | 122 | it('should return false given a key not in a non-empty cache', function() { 123 | cache.put('key', 'value'); 124 | expect(cache.del('miss')).to.be.false; 125 | }); 126 | 127 | it('should return true given a key in the cache', function() { 128 | cache.put('key', 'value'); 129 | expect(cache.del('key')).to.be.true; 130 | }); 131 | 132 | it('should remove the provided key from the cache', function() { 133 | cache.put('key', 'value'); 134 | expect(cache.get('key')).to.equal('value'); 135 | expect(cache.del('key')).to.be.true; 136 | expect(cache.get('key')).to.be.null; 137 | }); 138 | 139 | it('should decrement the cache size by 1', function() { 140 | cache.put('key', 'value'); 141 | expect(cache.size()).to.equal(1); 142 | expect(cache.del('key')).to.be.true; 143 | expect(cache.size()).to.equal(0); 144 | }); 145 | 146 | it('should not remove other keys in the cache', function() { 147 | cache.put('key1', 'value1'); 148 | cache.put('key2', 'value2'); 149 | cache.put('key3', 'value3'); 150 | expect(cache.get('key1')).to.equal('value1'); 151 | expect(cache.get('key2')).to.equal('value2'); 152 | expect(cache.get('key3')).to.equal('value3'); 153 | cache.del('key1'); 154 | expect(cache.get('key1')).to.be.null; 155 | expect(cache.get('key2')).to.equal('value2'); 156 | expect(cache.get('key3')).to.equal('value3'); 157 | }); 158 | 159 | it('should only delete a key from the cache once even if called multiple times in a row', function() { 160 | cache.put('key1', 'value1'); 161 | cache.put('key2', 'value2'); 162 | cache.put('key3', 'value3'); 163 | expect(cache.size()).to.equal(3); 164 | cache.del('key1'); 165 | cache.del('key1'); 166 | cache.del('key1'); 167 | expect(cache.size()).to.equal(2); 168 | }); 169 | 170 | it('should handle deleting keys which were previously deleted and then re-added to the cache', function() { 171 | cache.put('key', 'value'); 172 | expect(cache.get('key')).to.equal('value'); 173 | cache.del('key'); 174 | expect(cache.get('key')).to.be.null; 175 | cache.put('key', 'value'); 176 | expect(cache.get('key')).to.equal('value'); 177 | cache.del('key'); 178 | expect(cache.get('key')).to.be.null; 179 | }); 180 | 181 | it('should return true given an non-expired key', function() { 182 | cache.put('key', 'value', 1000); 183 | clock.tick(999); 184 | expect(cache.del('key')).to.be.true; 185 | }); 186 | 187 | it('should return false given an expired key', function() { 188 | cache.put('key', 'value', 1000); 189 | clock.tick(1000); 190 | expect(cache.del('key')).to.be.false; 191 | }); 192 | 193 | it('should cancel the timeout callback for the deleted key', function() { 194 | var spy = sinon.spy(); 195 | cache.put('key', 'value', 1000, spy); 196 | cache.del('key'); 197 | clock.tick(1000); 198 | expect(spy).to.not.have.been.called; 199 | }); 200 | 201 | it('should handle deletion of many items', function(done) { 202 | clock.restore(); 203 | var num = 1000; 204 | for(var i = 0; i < num; i++){ 205 | cache.put('key' + i, i, 1000); 206 | } 207 | expect(cache.size()).to.equal(num); 208 | setTimeout(function(){ 209 | expect(cache.size()).to.equal(0); 210 | done(); 211 | }, 1000); 212 | }); 213 | }); 214 | 215 | describe('clear()', function() { 216 | before(function() { 217 | cache.debug(false); 218 | }); 219 | 220 | it('should have no effect given an empty cache', function() { 221 | expect(cache.size()).to.equal(0); 222 | cache.clear(); 223 | expect(cache.size()).to.equal(0); 224 | }); 225 | 226 | it('should remove all existing keys in the cache', function() { 227 | cache.put('key1', 'value1'); 228 | cache.put('key2', 'value2'); 229 | cache.put('key3', 'value3'); 230 | expect(cache.size()).to.equal(3); 231 | cache.clear(); 232 | expect(cache.size()).to.equal(0); 233 | }); 234 | 235 | it('should remove the keys in the cache', function() { 236 | cache.put('key1', 'value1'); 237 | cache.put('key2', 'value2'); 238 | cache.put('key3', 'value3'); 239 | expect(cache.get('key1')).to.equal('value1'); 240 | expect(cache.get('key2')).to.equal('value2'); 241 | expect(cache.get('key3')).to.equal('value3'); 242 | cache.clear(); 243 | expect(cache.get('key1')).to.be.null; 244 | expect(cache.get('key2')).to.be.null; 245 | expect(cache.get('key3')).to.be.null; 246 | }); 247 | 248 | it('should reset the cache size to 0', function() { 249 | cache.put('key1', 'value1'); 250 | cache.put('key2', 'value2'); 251 | cache.put('key3', 'value3'); 252 | expect(cache.size()).to.equal(3); 253 | cache.clear(); 254 | expect(cache.size()).to.equal(0); 255 | }); 256 | 257 | it('should reset the debug cache hits', function() { 258 | cache.debug(true); 259 | cache.put('key', 'value'); 260 | cache.get('key'); 261 | expect(cache.hits()).to.equal(1); 262 | cache.clear(); 263 | expect(cache.hits()).to.equal(0); 264 | }); 265 | 266 | it('should reset the debug cache misses', function() { 267 | cache.debug(true); 268 | cache.put('key', 'value'); 269 | cache.get('miss1'); 270 | expect(cache.misses()).to.equal(1); 271 | cache.clear(); 272 | expect(cache.misses()).to.equal(0); 273 | }); 274 | 275 | it('should cancel the timeout callbacks for all existing keys', function() { 276 | var spy1 = sinon.spy(); 277 | var spy2 = sinon.spy(); 278 | var spy3 = sinon.spy(); 279 | cache.put('key1', 'value1', 1000, spy1); 280 | cache.put('key2', 'value2', 1000, spy2); 281 | cache.put('key3', 'value3', 1000, spy3); 282 | cache.clear(); 283 | clock.tick(1000); 284 | expect(spy1).to.not.have.been.called; 285 | expect(spy2).to.not.have.been.called; 286 | expect(spy3).to.not.have.been.called; 287 | }); 288 | }); 289 | 290 | describe('get()', function() { 291 | before(function() { 292 | cache.debug(false); 293 | }); 294 | 295 | it('should return null given a key for an empty cache', function() { 296 | expect(cache.get('miss')).to.be.null; 297 | }); 298 | 299 | it('should return null given a key not in a non-empty cache', function() { 300 | cache.put('key', 'value'); 301 | expect(cache.get('miss')).to.be.null; 302 | }); 303 | 304 | it('should return the corresponding value of a key in the cache', function() { 305 | cache.put('key', 'value'); 306 | expect(cache.get('key')).to.equal('value'); 307 | }); 308 | 309 | it('should return the latest corresponding value of a key in the cache', function() { 310 | cache.put('key', 'value1'); 311 | cache.put('key', 'value2'); 312 | cache.put('key', 'value3'); 313 | expect(cache.get('key')).to.equal('value3'); 314 | }); 315 | 316 | it('should handle various types of cache keys', function() { 317 | var keys = [null, undefined, NaN, true, false, 0, 1, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '', 'a', [], {}, [1, 'a', false], {a:1,b:'a',c:false}, function() {}]; 318 | keys.forEach(function(key, index) { 319 | var value = 'value' + index; 320 | cache.put(key, value); 321 | expect(cache.get(key)).to.deep.equal(value); 322 | }); 323 | }); 324 | 325 | it('should handle various types of cache values', function() { 326 | var values = [null, undefined, NaN, true, false, 0, 1, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, '', 'a', [], {}, [1, 'a', false], {a:1,b:'a',c:false}, function() {}]; 327 | values.forEach(function(value, index) { 328 | var key = 'key' + index; 329 | cache.put(key, value); 330 | expect(cache.get(key)).to.deep.equal(value); 331 | }); 332 | }); 333 | 334 | it('should not set a timeout given no expiration time', function() { 335 | cache.put('key', 'value'); 336 | clock.tick(1000); 337 | expect(cache.get('key')).to.equal('value'); 338 | }); 339 | 340 | it('should return the corresponding value of a non-expired key in the cache', function() { 341 | cache.put('key', 'value', 1000); 342 | clock.tick(999); 343 | expect(cache.get('key')).to.equal('value'); 344 | }); 345 | 346 | it('should return null given an expired key', function() { 347 | cache.put('key', 'value', 1000); 348 | clock.tick(1000); 349 | expect(cache.get('key')).to.be.null; 350 | }); 351 | 352 | it('should return null given an expired key', function() { 353 | cache.put('key', 'value', 1000); 354 | clock.tick(1000); 355 | expect(cache.get('key')).to.be.null; 356 | }); 357 | 358 | it('should return null given a key which is a property on the Object prototype', function() { 359 | expect(cache.get('toString')).to.be.null; 360 | }); 361 | 362 | it('should allow reading the value for a key which is a property on the Object prototype', function() { 363 | cache.put('toString', 'value'); 364 | expect(cache.get('toString')).to.equal('value'); 365 | }); 366 | }); 367 | 368 | describe('size()', function() { 369 | before(function() { 370 | cache.debug(false); 371 | }); 372 | 373 | it('should return 0 given a fresh cache', function() { 374 | expect(cache.size()).to.equal(0); 375 | }); 376 | 377 | it('should return 1 after adding a single item to the cache', function() { 378 | cache.put('key', 'value'); 379 | expect(cache.size()).to.equal(1); 380 | }); 381 | 382 | it('should return 3 after adding three items to the cache', function() { 383 | cache.put('key1', 'value1'); 384 | cache.put('key2', 'value2'); 385 | cache.put('key3', 'value3'); 386 | expect(cache.size()).to.equal(3); 387 | }); 388 | 389 | it('should not multi-count duplicate items added to the cache', function() { 390 | cache.put('key', 'value1'); 391 | expect(cache.size()).to.equal(1); 392 | cache.put('key', 'value2'); 393 | expect(cache.size()).to.equal(1); 394 | }); 395 | 396 | it('should update when a key in the cache expires', function() { 397 | cache.put('key', 'value', 1000); 398 | expect(cache.size()).to.equal(1); 399 | clock.tick(999); 400 | expect(cache.size()).to.equal(1); 401 | clock.tick(1); 402 | expect(cache.size()).to.equal(0); 403 | }); 404 | }); 405 | 406 | describe('memsize()', function() { 407 | before(function() { 408 | cache.debug(false); 409 | }); 410 | 411 | it('should return 0 given a fresh cache', function() { 412 | expect(cache.memsize()).to.equal(0); 413 | }); 414 | 415 | it('should return 1 after adding a single item to the cache', function() { 416 | cache.put('key', 'value'); 417 | expect(cache.memsize()).to.equal(1); 418 | }); 419 | 420 | it('should return 3 after adding three items to the cache', function() { 421 | cache.put('key1', 'value1'); 422 | cache.put('key2', 'value2'); 423 | cache.put('key3', 'value3'); 424 | expect(cache.memsize()).to.equal(3); 425 | }); 426 | 427 | it('should not multi-count duplicate items added to the cache', function() { 428 | cache.put('key', 'value1'); 429 | expect(cache.memsize()).to.equal(1); 430 | cache.put('key', 'value2'); 431 | expect(cache.memsize()).to.equal(1); 432 | }); 433 | 434 | it('should update when a key in the cache expires', function() { 435 | cache.put('key', 'value', 1000); 436 | expect(cache.memsize()).to.equal(1); 437 | clock.tick(999); 438 | expect(cache.memsize()).to.equal(1); 439 | clock.tick(1); 440 | expect(cache.memsize()).to.equal(0); 441 | }); 442 | }); 443 | 444 | describe('debug()', function() { 445 | it('should not count cache hits when false', function() { 446 | cache.debug(false); 447 | cache.put('key', 'value'); 448 | cache.get('key'); 449 | expect(cache.hits()).to.equal(0); 450 | }); 451 | 452 | it('should not count cache misses when false', function() { 453 | cache.debug(false); 454 | cache.put('key', 'value'); 455 | cache.get('miss1'); 456 | expect(cache.misses()).to.equal(0); 457 | }); 458 | 459 | it('should count cache hits when true', function() { 460 | cache.debug(true); 461 | cache.put('key', 'value'); 462 | cache.get('key'); 463 | expect(cache.hits()).to.equal(1); 464 | }); 465 | 466 | it('should count cache misses when true', function() { 467 | cache.debug(true); 468 | cache.put('key', 'value'); 469 | cache.get('miss1'); 470 | expect(cache.misses()).to.equal(1); 471 | }); 472 | }); 473 | 474 | describe('hits()', function() { 475 | before(function() { 476 | cache.debug(true); 477 | }); 478 | 479 | it('should return 0 given an empty cache', function() { 480 | expect(cache.hits()).to.equal(0); 481 | }); 482 | 483 | it('should return 0 given a non-empty cache which has not been accessed', function() { 484 | cache.put('key', 'value'); 485 | expect(cache.hits()).to.equal(0); 486 | }); 487 | 488 | it('should return 0 given a non-empty cache which has had only misses', function() { 489 | cache.put('key', 'value'); 490 | cache.get('miss1'); 491 | cache.get('miss2'); 492 | cache.get('miss3'); 493 | expect(cache.hits()).to.equal(0); 494 | }); 495 | 496 | it('should return 1 given a non-empty cache which has had a single hit', function() { 497 | cache.put('key', 'value'); 498 | cache.get('key'); 499 | expect(cache.hits()).to.equal(1); 500 | }); 501 | 502 | it('should return 3 given a non-empty cache which has had three hits on the same key', function() { 503 | cache.put('key', 'value'); 504 | cache.get('key'); 505 | cache.get('key'); 506 | cache.get('key'); 507 | expect(cache.hits()).to.equal(3); 508 | }); 509 | 510 | it('should return 3 given a non-empty cache which has had three hits across many keys', function() { 511 | cache.put('key1', 'value1'); 512 | cache.put('key2', 'value2'); 513 | cache.put('key3', 'value3'); 514 | cache.get('key1'); 515 | cache.get('key2'); 516 | cache.get('key3'); 517 | expect(cache.hits()).to.equal(3); 518 | }); 519 | 520 | it('should return the correct value after a sequence of hits and misses', function() { 521 | cache.put('key1', 'value1'); 522 | cache.put('key2', 'value2'); 523 | cache.put('key3', 'value3'); 524 | cache.get('key1'); 525 | cache.get('miss'); 526 | cache.get('key3'); 527 | expect(cache.hits()).to.equal(2); 528 | }); 529 | 530 | it('should not count hits for expired keys', function() { 531 | cache.put('key', 'value', 1000); 532 | cache.get('key'); 533 | expect(cache.hits()).to.equal(1); 534 | clock.tick(999); 535 | cache.get('key'); 536 | expect(cache.hits()).to.equal(2); 537 | clock.tick(1); 538 | cache.get('key'); 539 | expect(cache.hits()).to.equal(2); 540 | }); 541 | }); 542 | 543 | describe('misses()', function() { 544 | before(function() { 545 | cache.debug(true); 546 | }); 547 | 548 | it('should return 0 given an empty cache', function() { 549 | expect(cache.misses()).to.equal(0); 550 | }); 551 | 552 | it('should return 0 given a non-empty cache which has not been accessed', function() { 553 | cache.put('key', 'value'); 554 | expect(cache.misses()).to.equal(0); 555 | }); 556 | 557 | it('should return 0 given a non-empty cache which has had only hits', function() { 558 | cache.put('key', 'value'); 559 | cache.get('key'); 560 | cache.get('key'); 561 | cache.get('key'); 562 | expect(cache.misses()).to.equal(0); 563 | }); 564 | 565 | it('should return 1 given a non-empty cache which has had a single miss', function() { 566 | cache.put('key', 'value'); 567 | cache.get('miss'); 568 | expect(cache.misses()).to.equal(1); 569 | }); 570 | 571 | it('should return 3 given a non-empty cache which has had three misses', function() { 572 | cache.put('key', 'value'); 573 | cache.get('miss1'); 574 | cache.get('miss2'); 575 | cache.get('miss3'); 576 | expect(cache.misses()).to.equal(3); 577 | }); 578 | 579 | it('should return the correct value after a sequence of hits and misses', function() { 580 | cache.put('key1', 'value1'); 581 | cache.put('key2', 'value2'); 582 | cache.put('key3', 'value3'); 583 | cache.get('key1'); 584 | cache.get('miss'); 585 | cache.get('key3'); 586 | expect(cache.misses()).to.equal(1); 587 | }); 588 | 589 | it('should count misses for expired keys', function() { 590 | cache.put('key', 'value', 1000); 591 | cache.get('key'); 592 | expect(cache.misses()).to.equal(0); 593 | clock.tick(999); 594 | cache.get('key'); 595 | expect(cache.misses()).to.equal(0); 596 | clock.tick(1); 597 | cache.get('key'); 598 | expect(cache.misses()).to.equal(1); 599 | }); 600 | }); 601 | 602 | describe('keys()', function() { 603 | before(function() { 604 | cache.debug(false); 605 | }); 606 | 607 | it('should return an empty array given an empty cache', function() { 608 | expect(cache.keys()).to.deep.equal([]); 609 | }); 610 | 611 | it('should return a single key after adding a single item to the cache', function() { 612 | cache.put('key', 'value'); 613 | expect(cache.keys()).to.deep.equal(['key']); 614 | }); 615 | 616 | it('should return 3 keys after adding three items to the cache', function() { 617 | cache.put('key1', 'value1'); 618 | cache.put('key2', 'value2'); 619 | cache.put('key3', 'value3'); 620 | expect(cache.keys()).to.deep.equal(['key1', 'key2', 'key3']); 621 | }); 622 | 623 | it('should not multi-count duplicate items added to the cache', function() { 624 | cache.put('key', 'value1'); 625 | expect(cache.keys()).to.deep.equal(['key']); 626 | cache.put('key', 'value2'); 627 | expect(cache.keys()).to.deep.equal(['key']); 628 | }); 629 | 630 | it('should update when a key in the cache expires', function() { 631 | cache.put('key', 'value', 1000); 632 | expect(cache.keys()).to.deep.equal(['key']); 633 | clock.tick(999); 634 | expect(cache.keys()).to.deep.equal(['key']); 635 | clock.tick(1); 636 | expect(cache.keys()).to.deep.equal([]); 637 | }); 638 | }); 639 | 640 | describe('export()', function() { 641 | var START_TIME = 10000; 642 | 643 | var BASIC_EXPORT = JSON.stringify({ 644 | key: { 645 | value: 'value', 646 | expire: START_TIME + 1000, 647 | }, 648 | }); 649 | 650 | before(function() { 651 | cache.debug(false); 652 | }); 653 | 654 | beforeEach(function() { 655 | clock.tick(START_TIME); 656 | }); 657 | 658 | it('should return an empty object given an empty cache', function() { 659 | expect(cache.exportJson()).to.equal(JSON.stringify({})); 660 | }); 661 | 662 | it('should return a single record after adding a single item to the cache', function() { 663 | cache.put('key', 'value', 1000); 664 | expect(cache.exportJson()).to.equal(BASIC_EXPORT); 665 | }); 666 | 667 | it('should return multiple records with expiry', function() { 668 | cache.put('key1', 'value1'); 669 | cache.put('key2', 'value2', 1000); 670 | expect(cache.exportJson()).to.equal(JSON.stringify({ 671 | key1: { 672 | value: 'value1', 673 | expire: 'NaN', 674 | }, 675 | key2: { 676 | value: 'value2', 677 | expire: START_TIME + 1000, 678 | }, 679 | })); 680 | }); 681 | 682 | it('should update when a key in the cache expires', function() { 683 | cache.put('key', 'value', 1000); 684 | expect(cache.exportJson()).to.equal(BASIC_EXPORT); 685 | clock.tick(999); 686 | expect(cache.exportJson()).to.equal(BASIC_EXPORT); 687 | clock.tick(1); 688 | expect(cache.exportJson()).to.equal(JSON.stringify({})); 689 | }); 690 | }); 691 | 692 | describe('import()', function() { 693 | var START_TIME = 10000; 694 | 695 | var BASIC_EXPORT = JSON.stringify({ 696 | key: { 697 | value: 'value', 698 | expire: START_TIME + 1000, 699 | }, 700 | }); 701 | 702 | before(function() { 703 | cache.debug(false); 704 | }); 705 | 706 | beforeEach(function() { 707 | clock.tick(START_TIME); 708 | }); 709 | 710 | it('should import an empty object into an empty cache', function() { 711 | var exportedJson = cache.exportJson(); 712 | 713 | cache.clear(); 714 | cache.importJson(exportedJson); 715 | 716 | expect(cache.exportJson()).to.equal(JSON.stringify({})); 717 | }); 718 | 719 | it('should import records into an empty cache', function() { 720 | cache.put('key1', 'value1'); 721 | cache.put('key2', 'value2', 1000); 722 | var exportedJson = cache.exportJson(); 723 | 724 | cache.clear(); 725 | cache.importJson(exportedJson); 726 | 727 | expect(cache.exportJson()).to.equal(JSON.stringify({ 728 | key1: { 729 | value: 'value1', 730 | expire: 'NaN', 731 | }, 732 | key2: { 733 | value: 'value2', 734 | expire: START_TIME + 1000, 735 | }, 736 | })); 737 | }); 738 | 739 | it('should import records into an already-existing cache', function() { 740 | cache.put('key1', 'value1'); 741 | cache.put('key2', 'value2', 1000); 742 | var exportedJson = cache.exportJson(); 743 | 744 | cache.put('key1', 'changed value', 5000); 745 | cache.put('key3', 'value3', 500); 746 | 747 | cache.importJson(exportedJson); 748 | 749 | expect(cache.exportJson()).to.equal(JSON.stringify({ 750 | key1: { 751 | value: 'value1', 752 | expire: 'NaN', 753 | }, 754 | key2: { 755 | value: 'value2', 756 | expire: START_TIME + 1000, 757 | }, 758 | key3: { 759 | value: 'value3', 760 | expire: START_TIME + 500, 761 | }, 762 | })); 763 | }); 764 | 765 | it('should import records into an already-existing cache and skip duplicates', function() { 766 | cache.debug(true); 767 | 768 | cache.put('key1', 'value1'); 769 | cache.put('key2', 'value2', 1000); 770 | var exportedJson = cache.exportJson(); 771 | 772 | cache.clear(); 773 | cache.put('key1', 'changed value', 5000); 774 | cache.put('key3', 'value3', 500); 775 | 776 | cache.importJson(exportedJson, { skipDuplicates: true }); 777 | 778 | expect(cache.exportJson()).to.equal(JSON.stringify({ 779 | key1: { 780 | value: 'changed value', 781 | expire: START_TIME + 5000, 782 | }, 783 | key3: { 784 | value: 'value3', 785 | expire: START_TIME + 500, 786 | }, 787 | key2: { 788 | value: 'value2', 789 | expire: START_TIME + 1000, 790 | }, 791 | })); 792 | }); 793 | 794 | it('should import with updated expire times', function() { 795 | cache.put('key1', 'value1', 500); 796 | cache.put('key2', 'value2', 1000); 797 | var exportedJson = cache.exportJson(); 798 | 799 | var tickAmount = 750; 800 | clock.tick(tickAmount); 801 | 802 | cache.importJson(exportedJson); 803 | 804 | expect(cache.exportJson()).to.equal(JSON.stringify({ 805 | key2: { 806 | value: 'value2', 807 | expire: START_TIME + tickAmount + 250, 808 | }, 809 | })); 810 | }); 811 | 812 | it('should return the new size', function() { 813 | cache.put('key1', 'value1', 500); 814 | var exportedJson = cache.exportJson(); 815 | 816 | cache.clear(); 817 | cache.put('key2', 'value2', 1000); 818 | expect(cache.size()).to.equal(1); 819 | 820 | var size = cache.importJson(exportedJson); 821 | expect(size).to.equal(2); 822 | expect(cache.size()).to.equal(2); 823 | }); 824 | }); 825 | 826 | describe('Cache()', function() { 827 | it('should return a new cache instance when called', function() { 828 | var cache1 = new Cache(), 829 | cache2 = new Cache(); 830 | cache1.put('key', 'value1'); 831 | expect(cache1.keys()).to.deep.equal(['key']); 832 | expect(cache2.keys()).to.deep.equal([]); 833 | cache2.put('key', 'value2'); 834 | expect(cache1.get('key')).to.equal('value1'); 835 | expect(cache2.get('key')).to.equal('value2'); 836 | }); 837 | }); 838 | 839 | }); 840 | --------------------------------------------------------------------------------