├── .gitignore ├── LICENSE ├── README.md ├── example └── usage.js ├── index.js ├── package.json └── test └── lru-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Chris O'Hara 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lru 2 | 3 | **A simple LRU cache supporting O(1) set, get and eviction of old keys** 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ npm install lru 9 | ``` 10 | 11 | ### Example 12 | 13 | ```javascript 14 | var LRU = require('lru'); 15 | 16 | var cache = new LRU(2), 17 | evicted 18 | 19 | cache.on('evict',function(data) { evicted = data }); 20 | 21 | cache.set('foo', 'bar'); 22 | cache.get('foo'); //=> bar 23 | 24 | cache.set('foo2', 'bar2'); 25 | cache.get('foo2'); //=> bar2 26 | 27 | cache.set('foo3', 'bar3'); // => evicted = { key: 'foo', value: 'bar' } 28 | cache.get('foo3'); // => 'bar3' 29 | cache.remove('foo2') // => 'bar2' 30 | cache.remove('foo4') // => undefined 31 | cache.length // => 1 32 | cache.keys // => ['foo3'] 33 | 34 | cache.clear() // => it will NOT emit the 'evict' event 35 | cache.length // => 0 36 | cache.keys // => [] 37 | ``` 38 | 39 | ### API 40 | 41 | #### `LRU( length )` 42 | Create a new LRU cache that stores `length` elements before evicting the least recently used. 43 | Optionally you can pass an options map with additional options: 44 | 45 | ```js 46 | { 47 | max: maxElementsToStore, 48 | maxAge: maxAgeInMilliseconds 49 | } 50 | ``` 51 | 52 | If you pass `maxAge` items will be evicted if they are older than `maxAge` when you access them. 53 | 54 | **Returns**: the newly created LRU cache 55 | 56 | 57 | #### Properties 58 | ##### `.length` 59 | The number of keys currently in the cache. 60 | 61 | ##### `.keys` 62 | Array of all the keys currently in the cache. 63 | 64 | #### Methods 65 | 66 | ##### `.set( key, value )` 67 | Set the value of the key and mark the key as most recently used. 68 | 69 | **Returns**: `value` 70 | 71 | ##### `.get( key )` 72 | Query the value of the key and mark the key as most recently used. 73 | 74 | **Returns**: value of key if found; `undefined` otherwise. 75 | 76 | ##### `.peek( key )` 77 | Query the value of the key without marking the key as most recently used. 78 | 79 | **Returns**: value of key if found; `undefined` otherwise. 80 | 81 | ##### `.remove( key )` 82 | Remove the value from the cache. 83 | 84 | 85 | **Returns**: value of key if found; `undefined` otherwise. 86 | 87 | ##### `.clear()` 88 | Clear the cache. This method does **NOT** emit the `evict` event. 89 | 90 | ##### `.on( event, callback )` 91 | Respond to events. Currently only the `evict` event is implemented. When a key is evicted, the callback is executed with an associative array containing the evicted key: `{key: key, value: value}`. 92 | 93 | 94 | ### Credits 95 | 96 | A big thanks to [Dusty Leary](https://github.com/dustyleary) who 97 | finished the library. 98 | 99 | ### License 100 | 101 | MIT 102 | -------------------------------------------------------------------------------- /example/usage.js: -------------------------------------------------------------------------------- 1 | var LRU = require('../') 2 | 3 | var cache = new LRU(2) 4 | 5 | var evicted 6 | 7 | cache.on('evict', function (data) { 8 | evicted = data 9 | }) 10 | 11 | cache.set('foo', 'bar') // => 'bar' 12 | cache.get('foo') // => 'bar' 13 | 14 | cache.set('foo2', 'bar2') // => 'bar2' 15 | cache.get('foo2') // => 'bar2' 16 | 17 | cache.set('foo3', 'bar3') // => 'bar3' 18 | cache.get('foo3') // => 'bar3' 19 | 20 | console.log(cache.remove('foo2')) // => { key: 'foo2', value: 'bar2' } 21 | console.log(cache.remove('foo4')) // => undefined 22 | console.log(cache.length) // => 1 23 | console.log(evicted) // => evicted = { key: 'foo', value: 'bar' } 24 | 25 | cache.clear() // it will NOT emit the 'evict' event 26 | console.log(cache.length) // => 0 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var events = require('events') 2 | var inherits = require('inherits') 3 | 4 | module.exports = LRU 5 | 6 | function LRU (opts) { 7 | if (!(this instanceof LRU)) return new LRU(opts) 8 | if (typeof opts === 'number') opts = {max: opts} 9 | if (!opts) opts = {} 10 | events.EventEmitter.call(this) 11 | this.cache = {} 12 | this.head = this.tail = null 13 | this.length = 0 14 | this.max = opts.max || 1000 15 | this.maxAge = opts.maxAge || 0 16 | } 17 | 18 | inherits(LRU, events.EventEmitter) 19 | 20 | Object.defineProperty(LRU.prototype, 'keys', { 21 | get: function () { return Object.keys(this.cache) } 22 | }) 23 | 24 | LRU.prototype.clear = function () { 25 | this.cache = {} 26 | this.head = this.tail = null 27 | this.length = 0 28 | } 29 | 30 | LRU.prototype.remove = function (key) { 31 | if (typeof key !== 'string') key = '' + key 32 | if (!this.cache.hasOwnProperty(key)) return 33 | 34 | var element = this.cache[key] 35 | delete this.cache[key] 36 | this._unlink(key, element.prev, element.next) 37 | return element.value 38 | } 39 | 40 | LRU.prototype._unlink = function (key, prev, next) { 41 | this.length-- 42 | 43 | if (this.length === 0) { 44 | this.head = this.tail = null 45 | } else { 46 | if (this.head === key) { 47 | this.head = prev 48 | this.cache[this.head].next = null 49 | } else if (this.tail === key) { 50 | this.tail = next 51 | this.cache[this.tail].prev = null 52 | } else { 53 | this.cache[prev].next = next 54 | this.cache[next].prev = prev 55 | } 56 | } 57 | } 58 | 59 | LRU.prototype.peek = function (key) { 60 | if (!this.cache.hasOwnProperty(key)) return 61 | 62 | var element = this.cache[key] 63 | 64 | if (!this._checkAge(key, element)) return 65 | return element.value 66 | } 67 | 68 | LRU.prototype.set = function (key, value) { 69 | if (typeof key !== 'string') key = '' + key 70 | 71 | var element 72 | 73 | if (this.cache.hasOwnProperty(key)) { 74 | element = this.cache[key] 75 | element.value = value 76 | if (this.maxAge) element.modified = Date.now() 77 | 78 | // If it's already the head, there's nothing more to do: 79 | if (key === this.head) return value 80 | this._unlink(key, element.prev, element.next) 81 | } else { 82 | element = {value: value, modified: 0, next: null, prev: null} 83 | if (this.maxAge) element.modified = Date.now() 84 | this.cache[key] = element 85 | 86 | // Eviction is only possible if the key didn't already exist: 87 | if (this.length === this.max) this.evict() 88 | } 89 | 90 | this.length++ 91 | element.next = null 92 | element.prev = this.head 93 | 94 | if (this.head) this.cache[this.head].next = key 95 | this.head = key 96 | 97 | if (!this.tail) this.tail = key 98 | return value 99 | } 100 | 101 | LRU.prototype._checkAge = function (key, element) { 102 | if (this.maxAge && (Date.now() - element.modified) > this.maxAge) { 103 | this.remove(key) 104 | this.emit('evict', {key: key, value: element.value}) 105 | return false 106 | } 107 | return true 108 | } 109 | 110 | LRU.prototype.get = function (key) { 111 | if (typeof key !== 'string') key = '' + key 112 | if (!this.cache.hasOwnProperty(key)) return 113 | 114 | var element = this.cache[key] 115 | 116 | if (!this._checkAge(key, element)) return 117 | 118 | if (this.head !== key) { 119 | if (key === this.tail) { 120 | this.tail = element.next 121 | this.cache[this.tail].prev = null 122 | } else { 123 | // Set prev.next -> element.next: 124 | this.cache[element.prev].next = element.next 125 | } 126 | 127 | // Set element.next.prev -> element.prev: 128 | this.cache[element.next].prev = element.prev 129 | 130 | // Element is the new head 131 | this.cache[this.head].next = key 132 | element.prev = this.head 133 | element.next = null 134 | this.head = key 135 | } 136 | 137 | return element.value 138 | } 139 | 140 | LRU.prototype.evict = function () { 141 | if (!this.tail) return 142 | var key = this.tail 143 | var value = this.remove(this.tail) 144 | this.emit('evict', {key: key, value: value}) 145 | } 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lru", 3 | "description": "A simple O(1) LRU cache", 4 | "version": "3.1.0", 5 | "author": "Chris O'Hara ", 6 | "main": "index", 7 | "homepage": "http://github.com/chriso/lru", 8 | "repository": { 9 | "type": "git", 10 | "url": "http://github.com/chriso/lru.git" 11 | }, 12 | "bugs": { 13 | "url": "http://github.com/chriso/lru/issues" 14 | }, 15 | "engines": { 16 | "node": ">= 0.4.0" 17 | }, 18 | "license": "MIT", 19 | "devDependencies": { 20 | "standard": "^6.0.8", 21 | "vows": "^0.8.1" 22 | }, 23 | "scripts": { 24 | "test": "standard && vows test/*.js --spec" 25 | }, 26 | "dependencies": { 27 | "inherits": "^2.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/lru-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var vows = require('vows') 3 | var LRU = require('../') 4 | 5 | var suite = vows.describe('LRU') 6 | 7 | suite.addBatch({ 8 | 'clear() sets the cache to its initial state': function () { 9 | var lru = new LRU(2) 10 | 11 | var json1 = JSON.stringify(lru) 12 | 13 | lru.set('foo', 'bar') 14 | lru.clear() 15 | var json2 = JSON.stringify(lru) 16 | 17 | assert.equal(json2, json1) 18 | } 19 | }) 20 | 21 | suite.addBatch({ 22 | 'setting keys doesn\'t grow past max size': function () { 23 | var lru = new LRU(3) 24 | assert.equal(0, lru.length) 25 | lru.set('foo1', 'bar1') 26 | assert.equal(1, lru.length) 27 | lru.set('foo2', 'bar2') 28 | assert.equal(2, lru.length) 29 | lru.set('foo3', 'bar3') 30 | assert.equal(3, lru.length) 31 | 32 | lru.set('foo4', 'bar4') 33 | assert.equal(3, lru.length) 34 | } 35 | }) 36 | 37 | suite.addBatch({ 38 | 'setting keys returns the value': function () { 39 | var lru = new LRU(2) 40 | assert.equal('bar1', lru.set('foo1', 'bar1')) 41 | assert.equal('bar2', lru.set('foo2', 'bar2')) 42 | assert.equal('bar3', lru.set('foo3', 'bar3')) 43 | assert.equal('bar2', lru.get('foo2')) 44 | assert.equal(undefined, lru.get('foo1')) 45 | assert.equal('bar1', lru.set('foo1', 'bar1')) 46 | } 47 | }) 48 | 49 | suite.addBatch({ 50 | 'lru invariant is maintained for set()': function () { 51 | var lru = new LRU(2) 52 | 53 | lru.set('foo1', 'bar1') 54 | lru.set('foo2', 'bar2') 55 | lru.set('foo3', 'bar3') 56 | lru.set('foo4', 'bar4') 57 | 58 | assert.deepEqual(['foo3', 'foo4'], lru.keys) 59 | } 60 | }) 61 | 62 | suite.addBatch({ 63 | 'ovrewriting a key updates the value': function () { 64 | var lru = new LRU(2) 65 | lru.set('foo1', 'bar1') 66 | assert.equal('bar1', lru.get('foo1')) 67 | lru.set('foo1', 'bar2') 68 | assert.equal('bar2', lru.get('foo1')) 69 | } 70 | }) 71 | 72 | suite.addBatch({ 73 | 'lru invariant is maintained for get()': function () { 74 | var lru = new LRU(2) 75 | 76 | lru.set('foo1', 'bar1') 77 | lru.set('foo2', 'bar2') 78 | 79 | lru.get('foo1') // now foo2 should be deleted instead of foo1 80 | 81 | lru.set('foo3', 'bar3') 82 | 83 | assert.deepEqual(['foo1', 'foo3'], lru.keys) 84 | }, 85 | 'lru invariant is maintained after set(), get() and remove()': function () { 86 | var lru = new LRU(2) 87 | lru.set('a', 1) 88 | lru.set('b', 2) 89 | assert.deepEqual(lru.get('a'), 1) 90 | lru.remove('a') 91 | lru.set('c', 1) 92 | lru.set('d', 1) 93 | assert.deepEqual(['c', 'd'], lru.keys) 94 | } 95 | }) 96 | 97 | suite.addBatch({ 98 | 'lru invariant is maintained in the corner case size == 1': function () { 99 | var lru = new LRU(1) 100 | 101 | lru.set('foo1', 'bar1') 102 | lru.set('foo2', 'bar2') 103 | 104 | lru.get('foo2') // now foo2 should be deleted instead of foo1 105 | 106 | lru.set('foo3', 'bar3') 107 | 108 | assert.deepEqual(['foo3'], lru.keys) 109 | } 110 | }) 111 | 112 | suite.addBatch({ 113 | 'get() returns item value': function () { 114 | var lru = new LRU(2) 115 | 116 | assert.equal(lru.set('foo', 'bar'), 'bar') 117 | } 118 | }) 119 | 120 | suite.addBatch({ 121 | 'peek() returns item value without changing the order': function () { 122 | var lru = new LRU(2) 123 | lru.set('foo', 'bar') 124 | lru.set('bar', 'baz') 125 | assert.equal(lru.peek('foo'), 'bar') 126 | lru.set('baz', 'foo') 127 | assert.equal(lru.get('foo'), null) 128 | } 129 | }) 130 | 131 | suite.addBatch({ 132 | 'peek respects max age': { 133 | topic: function () { 134 | var lru = new LRU({maxAge: 5}) 135 | lru.set('foo', 'bar') 136 | assert.equal(lru.get('foo'), 'bar') 137 | var callback = this.callback 138 | setTimeout(function () { 139 | callback(null, lru) 140 | }, 100) 141 | }, 142 | 'the entry is removed if age > max_age': function (lru) { 143 | assert.equal(lru.peek('foo'), null) 144 | } 145 | } 146 | }) 147 | 148 | suite.addBatch({ 149 | 'evicting items by age': { 150 | topic: function () { 151 | var lru = new LRU({maxAge: 5}) 152 | lru.set('foo', 'bar') 153 | assert.equal(lru.get('foo'), 'bar') 154 | var callback = this.callback 155 | setTimeout(function () { 156 | callback(null, lru) 157 | }, 100) 158 | }, 159 | 'the entry is removed if age > max_age': function (lru) { 160 | assert.equal(lru.get('foo'), null) 161 | } 162 | }, 163 | 'evicting items by age (2)': { 164 | topic: function () { 165 | var lru = new LRU({maxAge: 100000}) 166 | lru.set('foo', 'bar') 167 | assert.equal(lru.get('foo'), 'bar') 168 | var callback = this.callback 169 | setTimeout(function () { 170 | callback(null, lru) 171 | }, 100) 172 | }, 173 | 'the entry is not removed if age < max_age': function (lru) { 174 | assert.equal(lru.get('foo'), 'bar') 175 | } 176 | } 177 | }) 178 | 179 | suite.addBatch({ 180 | 'idempotent changes': { 181 | 'set() and remove() on empty LRU is idempotent': function () { 182 | var lru = new LRU() 183 | var json1 = JSON.stringify(lru) 184 | 185 | lru.set('foo1', 'bar1') 186 | lru.remove('foo1') 187 | var json2 = JSON.stringify(lru) 188 | 189 | assert.deepEqual(json2, json1) 190 | }, 191 | 192 | '2 set()s and 2 remove()s on empty LRU is idempotent': function () { 193 | var lru = new LRU() 194 | var json1 = JSON.stringify(lru) 195 | 196 | lru.set('foo1', 'bar1') 197 | lru.set('foo2', 'bar2') 198 | lru.remove('foo1') 199 | lru.remove('foo2') 200 | var json2 = JSON.stringify(lru) 201 | 202 | assert.deepEqual(json2, json1) 203 | }, 204 | 205 | '2 set()s and 2 remove()s (in opposite order) on empty LRU is idempotent': function () { 206 | var lru = new LRU() 207 | var json1 = JSON.stringify(lru) 208 | 209 | lru.set('foo1', 'bar1') 210 | lru.set('foo2', 'bar2') 211 | lru.remove('foo2') 212 | lru.remove('foo1') 213 | var json2 = JSON.stringify(lru) 214 | 215 | assert.deepEqual(json2, json1) 216 | }, 217 | 218 | 'after setting one key, get() is idempotent': function () { 219 | var lru = new LRU(2) 220 | lru.set('a', 'a') 221 | var json1 = JSON.stringify(lru) 222 | 223 | lru.get('a') 224 | var json2 = JSON.stringify(lru) 225 | 226 | assert.equal(json2, json1) 227 | }, 228 | 229 | 'after setting two keys, get() on last-set key is idempotent': function () { 230 | var lru = new LRU(2) 231 | lru.set('a', 'a') 232 | lru.set('b', 'b') 233 | var json1 = JSON.stringify(lru) 234 | 235 | lru.get('b') 236 | var json2 = JSON.stringify(lru) 237 | 238 | assert.equal(json2, json1) 239 | } 240 | } 241 | }) 242 | 243 | suite.addBatch({ 244 | 'evict event': { 245 | '\'evict\' event is fired when evicting old keys': function () { 246 | var lru = new LRU(2) 247 | var events = [] 248 | lru.on('evict', function (element) { events.push(element) }) 249 | 250 | lru.set('foo1', 'bar1') 251 | lru.set('foo2', 'bar2') 252 | lru.set('foo3', 'bar3') 253 | lru.set('foo4', 'bar4') 254 | 255 | var expect = [{key: 'foo1', value: 'bar1'}, {key: 'foo2', value: 'bar2'}] 256 | assert.deepEqual(events, expect) 257 | } 258 | } 259 | }) 260 | 261 | suite.export(module) 262 | --------------------------------------------------------------------------------