├── .travis.yml ├── package.json ├── LICENSE ├── .gitignore ├── src └── index.js ├── readme.md └── test.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | install: 5 | - npm install -g codecov 6 | script: 7 | - npm i 8 | - npm run test 9 | - npm run report-coverage -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lru-cache-node", 3 | "version": "1.0.1", 4 | "description": "A lighting fast cache manager for node with least-recently-used policy.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "nyc ava", 8 | "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/arbazsiddiqui/lru-cache-node.git" 13 | }, 14 | "keywords": [ 15 | "cache", 16 | "lru", 17 | "mru" 18 | ], 19 | "author": "Arbaz Siddiqui (http://arbazsiddiqui.me/)", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "ava": "1.0.0-beta.4", 23 | "nyc": "^15.0.0" 24 | }, 25 | "files": [ 26 | "index.js" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Arbaz Siddiqui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | .idea 5 | *.lcov 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | class Cache { 2 | 3 | constructor(limit, maxAge, stale) { 4 | this.size = 0; 5 | this.limit = typeof limit === 'number' ? limit : Infinity; 6 | this.maxAge = typeof maxAge === 'number' ? maxAge : Infinity; 7 | this.stale = typeof stale === 'boolean' ? stale : false; 8 | this.hashMap = {}; 9 | this.head = null; 10 | this.tail = null; 11 | } 12 | 13 | /* Three steps : 14 | 1. Make node's next to current head 15 | 2. Make head's previous to node 16 | 3. Make head as the node 17 | */ 18 | setNodeAsHead(node) { 19 | node.next = this.head; 20 | node.prev = null; 21 | if (this.head !== null) 22 | this.head.prev = node; 23 | this.head = node; 24 | if (this.tail === null) { 25 | this.tail = node; 26 | } 27 | this.size += 1; 28 | this.hashMap[node.content.key] = node 29 | } 30 | 31 | set(key, value, maxAge) { 32 | maxAge = typeof maxAge === 'number' ? maxAge : this.maxAge; 33 | const node = new Node(key, value, maxAge, Date.now() + maxAge); 34 | if (this.size >= this.limit) { 35 | delete this.hashMap[this.tail.content.key]; 36 | this.size -= 1; 37 | this.tail = this.tail.prev; 38 | this.tail.next = null; 39 | } 40 | this.setNodeAsHead(node); 41 | } 42 | 43 | remove(node) { 44 | if (node.prev !== null) { 45 | node.prev.next = node.next; 46 | } else { 47 | this.head = node.next; 48 | } 49 | if (node.next !== null) { 50 | node.next.prev = node.prev; 51 | } else { 52 | this.tail = node.prev; 53 | } 54 | delete this.hashMap[node.getKey()]; 55 | this.size -= 1; 56 | } 57 | 58 | get(key) { 59 | const oldNode = this.hashMap[key]; 60 | if (oldNode) { 61 | const value = oldNode.getValue(); 62 | const nodeMaxAge = oldNode.getMaxAge(); 63 | const maxAge = typeof nodeMaxAge === 'number' ? nodeMaxAge : this.maxAge; 64 | if (Date.now() >= oldNode.getExpiry()) { 65 | this.remove(oldNode); 66 | return this.stale ? oldNode.getValue() : null 67 | } 68 | const newNode = new Node(key, value, maxAge, Date.now() + maxAge); 69 | this.remove(oldNode); 70 | this.setNodeAsHead(newNode); 71 | return value 72 | } 73 | return null 74 | } 75 | 76 | peek(key) { 77 | return this.hashMap[key] ? this.hashMap[key].getValue() : null 78 | } 79 | 80 | reset() { 81 | this.size = 0; 82 | this.hashMap = {}; 83 | this.head = null; 84 | this.tail = null; 85 | } 86 | 87 | toArray() { 88 | const arr = []; 89 | let node = this.head; 90 | while (node) { 91 | arr.push({ 92 | key: node.getKey(), 93 | value: node.getValue() 94 | }); 95 | node = node.next; 96 | } 97 | return arr; 98 | } 99 | 100 | contains(key) { 101 | return !!this.hashMap[key] 102 | } 103 | 104 | has(key) { 105 | return this.contains(key); 106 | } 107 | 108 | forEach(callback) { 109 | let node = this.head; 110 | let i = 0; 111 | while (node) { 112 | callback.apply(this, [node.getKey(), node.getValue(), i]); 113 | i++; 114 | node = node.next; 115 | } 116 | } 117 | 118 | getSize() { 119 | return this.size 120 | } 121 | 122 | delete(key) { 123 | const node = this.hashMap[key]; 124 | this.remove(node) 125 | } 126 | } 127 | 128 | class Node { 129 | constructor(key, value, maxAge, expires) { 130 | if (key === undefined) 131 | throw new Error("Key not provided"); 132 | if (value === undefined) 133 | throw new Error("Value not provided"); 134 | this.content = {key, value}; 135 | this.prev = null; 136 | this.next = null; 137 | this.maxAge = typeof maxAge === 'number' ? maxAge : Infinity; 138 | this.expires = typeof expires === 'number' ? expires : Infinity; 139 | } 140 | 141 | getValue() { 142 | return this.content.value 143 | } 144 | 145 | getMaxAge() { 146 | return this.maxAge 147 | } 148 | 149 | getExpiry() { 150 | return this.expires 151 | } 152 | 153 | getKey() { 154 | return this.content.key 155 | } 156 | } 157 | 158 | module.exports = Cache; 159 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # lru-cache-node 2 | [![Build Status](https://travis-ci.org/arbazsiddiqui/lru-cache-node.svg?branch=master)](https://travis-ci.org/arbazsiddiqui/lru-cache-node) 3 | [![codecov](https://codecov.io/gh/arbazsiddiqui/lru-cache-node/branch/master/graph/badge.svg)](https://codecov.io/gh/arbazsiddiqui/lru-cache-node) 4 | [![npm](https://img.shields.io/npm/dt/lru-cache-node.svg)](https://npmjs.com/package/lru-cache-node) 5 | 6 | > A lighting fast cache manager for node with least-recently-used policy. 7 | 8 | A super fast cache for node with LRU policy. Cache will keep on adding values until the `maxSize` is reached. 9 | 10 | After that it will start popping out the Least recently used/accessed value from the cache in order to set the new ones. 11 | 12 | 13 | Supports expiry and stale. 14 | 15 | Implemented using doubly-linked-list and hashmap with O(1) time complexity for gets and sets. 16 | 17 | 18 | ## Install 19 | 20 | ``` 21 | $ npm install --save lru-cache-node 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 | const Cache = require('lru-cache-node'); 28 | 29 | let cache = new Cache(3); //set max size of cache as three 30 | 31 | cache.set('a', 7); //sets a value in cache with 'a' as key and 7 as value 32 | cache.set('b', 5); 33 | cache.set('c', 3); 34 | /* 35 | [ { key: 'c', value: 3 }, 36 | { key: 'b', value: 5 }, 37 | { key: 'a', value: 7 } ] 38 | */ 39 | 40 | cache.set('d', 10) // pops out a 41 | /* 42 | [ { key: 'd', value: 10 }, 43 | { key: 'c', value: 3 }, 44 | { key: 'b', value: 5 } ] 45 | */ 46 | 47 | cache.get("b") //returns 5 and makes it most recently used 48 | /* 49 | [ { key: 'b', value: 5 }, 50 | { key: 'd', value: 10 }, 51 | { key: 'c', value: 3 } ] 52 | */ 53 | 54 | cache.peek("d") //returns 10 but doesnt resets the order 55 | /* 56 | [ { key: 'b', value: 5 }, 57 | { key: 'd', value: 10 }, 58 | { key: 'c', value: 3 } ] 59 | */ 60 | 61 | let cache = new Cache(3, 10); //Initialize Cache with size 3 and expiry for keys as 10ms 62 | const sleep = ms => new Promise(r => setTimeout(r, ms)); 63 | 64 | cache.set('a', 7); //valid for 10ms 65 | cache.get('a'); //returns 7 and resets 10ms counter 66 | await sleep(15); 67 | cache.get('a'); //null 68 | 69 | cache.set('b', 5, 30); //overwrites cache's default expiry of 10ms and uses 30ms 70 | await sleep(15); 71 | cache.get('b'); //returns 5 and resets the expiry of b back to 30ms 72 | await sleep(35); 73 | cache.get('b'); //null 74 | ``` 75 | 76 | ## API 77 | ### cache(maxSize, maxAge, stale) 78 | 79 | #### `maxSize` 80 | Type: `Number`
81 | Default: `Infinity` 82 | 83 | Maximum size of the cache. 84 | 85 | #### `maxAge` 86 | Type: `Number`
87 | Default: `Infinity` 88 | 89 | Default expiry for all keys for the cache. It does not proactively deletes expired keys, but will return null when an expired key is being accessed. 90 | 91 | #### `stale` 92 | Type: `Boolean`
93 | Default: `false` 94 | 95 | If set to true, will return the value of expired key before deleting it from cache. 96 | 97 | ### set(key, value, maxAge) 98 | 99 | #### `key` 100 | 101 | Key to be set. 102 | 103 | #### `value` 104 | 105 | Value for the key. 106 | 107 | #### `maxAge` 108 | 109 | Expiry of the key. Will override cache's `maxAge` if specified. 110 | 111 | ### get(key) 112 | 113 | Returns the value for the key. If not key does not exist will return `null`. 114 | 115 | >Both set() and get() will update the "recently used"-ness and expiry of the key. 116 | 117 | ### peek(key) 118 | 119 | Returns the value for the key, without making the key most recently used. If not key does not exist will return `null`. 120 | 121 | ### delete(key) 122 | Deletes the key from the cache. 123 | 124 | ### contains(key) 125 | 126 | Returns a boolean indication if the value exists in cache or not. 127 | 128 | ### has(key) 129 | 130 | Alias for `contains` function. 131 | 132 | ### getSize() 133 | 134 | Returns the current size of cache. 135 | 136 | ### reset() 137 | 138 | Clears the whole cache and reinitialize it. 139 | 140 | ### toArray() 141 | 142 | Returns an array form of the catch. 143 | ```js 144 | let cache = new Cache(); 145 | cache.set("a", 5); 146 | cache.set("b", 4); 147 | cache.set("c", 0); 148 | cache.toArray() 149 | /* 150 | [ { key: 'c', value: 0 }, 151 | { key: 'b', value: 4 }, 152 | { key: 'a', value: 5 } ] 153 | */ 154 | ``` 155 | 156 | ### forEach(callback) 157 | 158 | Takes a function and iterates over all the keys in the cache, in order of recentness. Callback takes `key`, `value` and `index` as params. 159 | ```js 160 | let cache = new Cache(); 161 | cache.set("a", 1); 162 | cache.set("b", 2); 163 | cache.set("c", 3); 164 | cache.forEach((key, value, index) => { 165 | console.log(key, value, index) 166 | }) 167 | /* 168 | c 3 0 169 | b 2 1 170 | a 1 2 171 | */ 172 | ``` 173 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Cache = require('./src/index'); 3 | const sleep = ms => new Promise(r => setTimeout(r, ms)); 4 | 5 | test('initializes a cache correctly', t => { 6 | const cache = new Cache(3); 7 | t.is(cache.limit, 3); 8 | t.is(cache.size, 0); 9 | t.is(typeof cache, 'object') 10 | }); 11 | 12 | test('sets new keys in cache without expiry', t => { 13 | const cache = new Cache(3); 14 | 15 | cache.set("Sapiens", 5); 16 | t.is(Object.keys(cache.hashMap).length, 1); 17 | 18 | cache.set("Book Thief", 4); 19 | t.is(Object.keys(cache.hashMap).length, 2); 20 | 21 | cache.set("Catcher In The Rye", 0); 22 | t.is(Object.keys(cache.hashMap).length, 3); 23 | 24 | cache.set("Thus Spoke Zarathustra", 4); 25 | t.is(Object.keys(cache.hashMap).length, 3); 26 | t.is(cache.hashMap.Sapiens, undefined); 27 | }); 28 | 29 | test('get key without expiry', async t => { 30 | const cache = new Cache(3); 31 | 32 | cache.set("Sapiens", 5); 33 | t.is(Object.keys(cache.hashMap).length, 1); 34 | 35 | cache.set("Book Thief", 4); 36 | t.is(Object.keys(cache.hashMap).length, 2); 37 | 38 | cache.set("Catcher In The Rye", 0); 39 | t.is(Object.keys(cache.hashMap).length, 3); 40 | 41 | await sleep(10); 42 | 43 | const sapiensRating = cache.get("Sapiens"); 44 | t.is(sapiensRating, 5); 45 | 46 | cache.set("Thus Spoke Zarathustra", 4); 47 | t.is(Object.keys(cache.hashMap).length, 3); 48 | 49 | t.is(cache.hashMap.Sapiens.content.value, 5); 50 | t.is(cache.hashMap["Book Thief"], undefined); 51 | }); 52 | 53 | test('sets with expiry', t => { 54 | const cache = new Cache(3, 10); 55 | 56 | cache.set("Sapiens", 5); 57 | t.is(cache.maxAge, 10); 58 | t.is(cache.hashMap.Sapiens.maxAge, 10); 59 | 60 | cache.set("Book Thief", 4, 7); 61 | t.is(cache.hashMap["Book Thief"].maxAge, 7); 62 | 63 | cache.set("Catcher In The Rye", 0); 64 | t.is(cache.hashMap["Catcher In The Rye"].maxAge, 10); 65 | }); 66 | 67 | test('throws for undefinded key and value', t => { 68 | const cache = new Cache(3, 10); 69 | 70 | const errorKey = t.throws(() => { 71 | cache.set(undefined, 5); 72 | }); 73 | 74 | t.is(errorKey.message, 'Key not provided'); 75 | 76 | const errorValue = t.throws(() => { 77 | cache.set('Book Thief', undefined); 78 | }); 79 | 80 | t.is(errorValue.message, 'Value not provided'); 81 | }); 82 | 83 | test('gets with expiry', async t => { 84 | const cache = new Cache(3, 10); 85 | cache.set("Sapiens", 5); 86 | const sapiens = cache.get("Sapiens"); 87 | t.is(sapiens, 5); 88 | 89 | await sleep(3); 90 | const sapiensAfter3s = await cache.get("Sapiens"); 91 | t.is(sapiensAfter3s, 5); 92 | 93 | await sleep(11); 94 | const sapiensAfter11s = await cache.get("Sapiens"); 95 | t.is(sapiensAfter11s, null); 96 | t.is(cache.hashMap.Sapiens, undefined); 97 | 98 | cache.set("Book Thief", 4, 25); 99 | const bookThief = cache.get("Book Thief"); 100 | t.is(bookThief, 4); 101 | 102 | await sleep(10); 103 | const bookThiefAfter10s = await cache.get("Book Thief"); 104 | t.is(bookThiefAfter10s, 4); 105 | 106 | await sleep(10); 107 | const bookThiefAfter20s = await cache.get("Book Thief"); 108 | t.is(bookThiefAfter20s, 4); 109 | 110 | await sleep(10); 111 | const bookThiefAfter30s = await cache.get("Book Thief"); 112 | t.is(bookThiefAfter30s, 4); 113 | 114 | await sleep(30); 115 | const bookThiefAfter30sWithoutReset = await cache.get("Book Thief"); 116 | t.is(bookThiefAfter30sWithoutReset, null); 117 | }); 118 | 119 | test('stale', async t => { 120 | const cache = new Cache(3, 10, true); 121 | cache.set("Sapiens", 5); 122 | await sleep(11) 123 | t.is(cache.get("Sapiens"), 5); 124 | t.is(cache.get("Sapiens"), null); 125 | t.is(cache.hashMap.Sapiens, undefined); 126 | }); 127 | 128 | test('peek', t => { 129 | const cache = new Cache(3); 130 | 131 | cache.set("Sapiens", 5); 132 | const sapiens = cache.peek("Sapiens"); 133 | t.is(sapiens, 5); 134 | 135 | const test = cache.peek("test"); 136 | t.is(test, null); 137 | 138 | cache.set("Book Thief", 4); 139 | cache.set("Catcher In The Rye", 0); 140 | cache.peek("test"); 141 | cache.set("Thus Spoke Zarathustra", 4); 142 | t.is(cache.hashMap["Sapiens"], undefined); 143 | }); 144 | 145 | test('reset', t => { 146 | const cache = new Cache(3, 10, true); 147 | 148 | cache.set("Sapiens", 5); 149 | cache.set("Book Thief", 4); 150 | cache.set("Catcher In The Rye", 0); 151 | cache.reset(); 152 | t.is(cache.size, 0); 153 | t.is(cache.limit, 3); 154 | t.is(cache.maxAge, 10); 155 | t.is(cache.stale, true); 156 | t.deepEqual(cache.hashMap, {}); 157 | t.is(cache.head, null); 158 | t.is(cache.tail, null); 159 | }); 160 | 161 | test('toArray', t => { 162 | const cache = new Cache(3, 10, true); 163 | 164 | cache.set("Sapiens", 5); 165 | cache.set("Book Thief", 4); 166 | cache.set("Catcher In The Rye", 0); 167 | t.deepEqual(cache.toArray(), [{key: 'Catcher In The Rye', value: 0}, 168 | {key: 'Book Thief', value: 4}, 169 | {key: 'Sapiens', value: 5}]) 170 | }); 171 | 172 | test('contains', t => { 173 | const cache = new Cache(3, 10, true); 174 | 175 | cache.set("Sapiens", 5); 176 | cache.set("Book Thief", 4); 177 | cache.set("Catcher In The Rye", 0); 178 | t.is(cache.contains("Sapiens"), true); 179 | t.is(cache.contains("x"), false); 180 | t.is(cache.contains("Catcher In The Rye"), true); 181 | }); 182 | 183 | test('has', t => { 184 | const cache = new Cache(3, 10, true); 185 | 186 | cache.set("Sapiens", 5); 187 | cache.set("Book Thief", 4); 188 | cache.set("Catcher In The Rye", 0); 189 | t.is(cache.has("Sapiens"), true); 190 | t.is(cache.has("x"), false); 191 | t.is(cache.has("Catcher In The Rye"), true); 192 | }); 193 | 194 | test('forEach', t => { 195 | const cache = new Cache(3, 10, true); 196 | 197 | cache.set("Sapiens", 5); 198 | cache.set("Book Thief", 4); 199 | cache.set("Catcher In The Rye", 0); 200 | cache.forEach((key, value, index) => { 201 | if(index === 0){ 202 | t.is(key, "Catcher In The Rye"); 203 | t.is(value, 0); 204 | } 205 | if(index === 1){ 206 | t.is(key, "Book Thief"); 207 | t.is(value, 4); 208 | } 209 | if(index === 2){ 210 | t.is(key, "Sapiens"); 211 | t.is(value, 5); 212 | } 213 | }) 214 | }); 215 | 216 | test('getSize', t => { 217 | const cache = new Cache(3, 10, true); 218 | 219 | cache.set("Sapiens", 5); 220 | t.is(cache.getSize(), 1); 221 | cache.set("Book Thief", 4); 222 | t.is(cache.getSize(), 2); 223 | cache.set("Catcher In The Rye", 0); 224 | t.is(cache.getSize(), 3); 225 | }); 226 | 227 | test('delete', t => { 228 | const cache = new Cache(3, 10, true); 229 | 230 | cache.set("Sapiens", 5); 231 | cache.set("Book Thief", 4); 232 | cache.set("Catcher In The Rye", 0); 233 | cache.delete("Catcher In The Rye"); 234 | t.is(cache.size, 2); 235 | t.is(cache.hashMap["Catcher In The Rye"], undefined); 236 | t.is(cache.head.content.key, "Book Thief") 237 | }); 238 | --------------------------------------------------------------------------------