1'use strict' 2 3// A linked list to keep track of recently-used-ness 4const Yallist = require('yallist') 5 6const MAX = Symbol('max') 7const LENGTH = Symbol('length') 8const LENGTH_CALCULATOR = Symbol('lengthCalculator') 9const ALLOW_STALE = Symbol('allowStale') 10const MAX_AGE = Symbol('maxAge') 11const DISPOSE = Symbol('dispose') 12const NO_DISPOSE_ON_SET = Symbol('noDisposeOnSet') 13const LRU_LIST = Symbol('lruList') 14const CACHE = Symbol('cache') 15const UPDATE_AGE_ON_GET = Symbol('updateAgeOnGet') 16 17const naiveLength = () => 1 18 19// lruList is a yallist where the head is the youngest 20// item, and the tail is the oldest. the list contains the Hit 21// objects as the entries. 22// Each Hit object has a reference to its Yallist.Node. This 23// never changes. 24// 25// cache is a Map (or PseudoMap) that matches the keys to 26// the Yallist.Node object. 27class LRUCache { 28 constructor (options) { 29 if (typeof options === 'number') 30 options = { max: options } 31 32 if (!options) 33 options = {} 34 35 if (options.max && (typeof options.max !== 'number' || options.max < 0)) 36 throw new TypeError('max must be a non-negative number') 37 // Kind of weird to have a default max of Infinity, but oh well. 38 const max = this[MAX] = options.max || Infinity 39 40 const lc = options.length || naiveLength 41 this[LENGTH_CALCULATOR] = (typeof lc !== 'function') ? naiveLength : lc 42 this[ALLOW_STALE] = options.stale || false 43 if (options.maxAge && typeof options.maxAge !== 'number') 44 throw new TypeError('maxAge must be a number') 45 this[MAX_AGE] = options.maxAge || 0 46 this[DISPOSE] = options.dispose 47 this[NO_DISPOSE_ON_SET] = options.noDisposeOnSet || false 48 this[UPDATE_AGE_ON_GET] = options.updateAgeOnGet || false 49 this.reset() 50 } 51 52 // resize the cache when the max changes. 53 set max (mL) { 54 if (typeof mL !== 'number' || mL < 0) 55 throw new TypeError('max must be a non-negative number') 56 57 this[MAX] = mL || Infinity 58 trim(this) 59 } 60 get max () { 61 return this[MAX] 62 } 63 64 set allowStale (allowStale) { 65 this[ALLOW_STALE] = !!allowStale 66 } 67 get allowStale () { 68 return this[ALLOW_STALE] 69 } 70 71 set maxAge (mA) { 72 if (typeof mA !== 'number') 73 throw new TypeError('maxAge must be a non-negative number') 74 75 this[MAX_AGE] = mA 76 trim(this) 77 } 78 get maxAge () { 79 return this[MAX_AGE] 80 } 81 82 // resize the cache when the lengthCalculator changes. 83 set lengthCalculator (lC) { 84 if (typeof lC !== 'function') 85 lC = naiveLength 86 87 if (lC !== this[LENGTH_CALCULATOR]) { 88 this[LENGTH_CALCULATOR] = lC 89 this[LENGTH] = 0 90 this[LRU_LIST].forEach(hit => { 91 hit.length = this[LENGTH_CALCULATOR](hit.value, hit.key) 92 this[LENGTH] += hit.length 93 }) 94 } 95 trim(this) 96 } 97 get lengthCalculator () { return this[LENGTH_CALCULATOR] } 98 99 get length () { return this[LENGTH] } 100 get itemCount () { return this[LRU_LIST].length } 101 102 rforEach (fn, thisp) { 103 thisp = thisp || this 104 for (let walker = this[LRU_LIST].tail; walker !== null;) { 105 const prev = walker.prev 106 forEachStep(this, fn, walker, thisp) 107 walker = prev 108 } 109 } 110 111 forEach (fn, thisp) { 112 thisp = thisp || this 113 for (let walker = this[LRU_LIST].head; walker !== null;) { 114 const next = walker.next 115 forEachStep(this, fn, walker, thisp) 116 walker = next 117 } 118 } 119 120 keys () { 121 return this[LRU_LIST].toArray().map(k => k.key) 122 } 123 124 values () { 125 return this[LRU_LIST].toArray().map(k => k.value) 126 } 127 128 reset () { 129 if (this[DISPOSE] && 130 this[LRU_LIST] && 131 this[LRU_LIST].length) { 132 this[LRU_LIST].forEach(hit => this[DISPOSE](hit.key, hit.value)) 133 } 134 135 this[CACHE] = new Map() // hash of items by key 136 this[LRU_LIST] = new Yallist() // list of items in order of use recency 137 this[LENGTH] = 0 // length of items in the list 138 } 139 140 dump () { 141 return this[LRU_LIST].map(hit => 142 isStale(this, hit) ? false : { 143 k: hit.key, 144 v: hit.value, 145 e: hit.now + (hit.maxAge || 0) 146 }).toArray().filter(h => h) 147 } 148 149 dumpLru () { 150 return this[LRU_LIST] 151 } 152 153 set (key, value, maxAge) { 154 maxAge = maxAge || this[MAX_AGE] 155 156 if (maxAge && typeof maxAge !== 'number') 157 throw new TypeError('maxAge must be a number') 158 159 const now = maxAge ? Date.now() : 0 160 const len = this[LENGTH_CALCULATOR](value, key) 161 162 if (this[CACHE].has(key)) { 163 if (len > this[MAX]) { 164 del(this, this[CACHE].get(key)) 165 return false 166 } 167 168 const node = this[CACHE].get(key) 169 const item = node.value 170 171 // dispose of the old one before overwriting 172 // split out into 2 ifs for better coverage tracking 173 if (this[DISPOSE]) { 174 if (!this[NO_DISPOSE_ON_SET]) 175 this[DISPOSE](key, item.value) 176 } 177 178 item.now = now 179 item.maxAge = maxAge 180 item.value = value 181 this[LENGTH] += len - item.length 182 item.length = len 183 this.get(key) 184 trim(this) 185 return true 186 } 187 188 const hit = new Entry(key, value, len, now, maxAge) 189 190 // oversized objects fall out of cache automatically. 191 if (hit.length > this[MAX]) { 192 if (this[DISPOSE]) 193 this[DISPOSE](key, value) 194 195 return false 196 } 197 198 this[LENGTH] += hit.length 199 this[LRU_LIST].unshift(hit) 200 this[CACHE].set(key, this[LRU_LIST].head) 201 trim(this) 202 return true 203 } 204 205 has (key) { 206 if (!this[CACHE].has(key)) return false 207 const hit = this[CACHE].get(key).value 208 return !isStale(this, hit) 209 } 210 211 get (key) { 212 return get(this, key, true) 213 } 214 215 peek (key) { 216 return get(this, key, false) 217 } 218 219 pop () { 220 const node = this[LRU_LIST].tail 221 if (!node) 222 return null 223 224 del(this, node) 225 return node.value 226 } 227 228 del (key) { 229 del(this, this[CACHE].get(key)) 230 } 231 232 load (arr) { 233 // reset the cache 234 this.reset() 235 236 const now = Date.now() 237 // A previous serialized cache has the most recent items first 238 for (let l = arr.length - 1; l >= 0; l--) { 239 const hit = arr[l] 240 const expiresAt = hit.e || 0 241 if (expiresAt === 0) 242 // the item was created without expiration in a non aged cache 243 this.set(hit.k, hit.v) 244 else { 245 const maxAge = expiresAt - now 246 // dont add already expired items 247 if (maxAge > 0) { 248 this.set(hit.k, hit.v, maxAge) 249 } 250 } 251 } 252 } 253 254 prune () { 255 this[CACHE].forEach((value, key) => get(this, key, false)) 256 } 257} 258 259const get = (self, key, doUse) => { 260 const node = self[CACHE].get(key) 261 if (node) { 262 const hit = node.value 263 if (isStale(self, hit)) { 264 del(self, node) 265 if (!self[ALLOW_STALE]) 266 return undefined 267 } else { 268 if (doUse) { 269 if (self[UPDATE_AGE_ON_GET]) 270 node.value.now = Date.now() 271 self[LRU_LIST].unshiftNode(node) 272 } 273 } 274 return hit.value 275 } 276} 277 278const isStale = (self, hit) => { 279 if (!hit || (!hit.maxAge && !self[MAX_AGE])) 280 return false 281 282 const diff = Date.now() - hit.now 283 return hit.maxAge ? diff > hit.maxAge 284 : self[MAX_AGE] && (diff > self[MAX_AGE]) 285} 286 287const trim = self => { 288 if (self[LENGTH] > self[MAX]) { 289 for (let walker = self[LRU_LIST].tail; 290 self[LENGTH] > self[MAX] && walker !== null;) { 291 // We know that we're about to delete this one, and also 292 // what the next least recently used key will be, so just 293 // go ahead and set it now. 294 const prev = walker.prev 295 del(self, walker) 296 walker = prev 297 } 298 } 299} 300 301const del = (self, node) => { 302 if (node) { 303 const hit = node.value 304 if (self[DISPOSE]) 305 self[DISPOSE](hit.key, hit.value) 306 307 self[LENGTH] -= hit.length 308 self[CACHE].delete(hit.key) 309 self[LRU_LIST].removeNode(node) 310 } 311} 312 313class Entry { 314 constructor (key, value, length, now, maxAge) { 315 this.key = key 316 this.value = value 317 this.length = length 318 this.now = now 319 this.maxAge = maxAge || 0 320 } 321} 322 323const forEachStep = (self, fn, node, thisp) => { 324 let hit = node.value 325 if (isStale(self, hit)) { 326 del(self, node) 327 if (!self[ALLOW_STALE]) 328 hit = undefined 329 } 330 if (hit) 331 fn.call(thisp, hit.value, hit.key, self) 332} 333 334module.exports = LRUCache 335