UNPKG

14 kBJavaScriptView Raw
1const perf = typeof performance === 'object' && performance &&
2 typeof performance.now === 'function' ? performance : Date
3
4const warned = new Set()
5const deprecatedOption = (opt, msg) => {
6 const code = `LRU_CACHE_OPTION_${opt}`
7 if (shouldWarn(code)) {
8 warn(code, `The ${opt} option is deprecated. ${msg}`, LRUCache)
9 }
10}
11const deprecatedMethod = (method, msg) => {
12 const code = `LRU_CACHE_METHOD_${method}`
13 if (shouldWarn(code)) {
14 const { prototype } = LRUCache
15 const { get } = Object.getOwnPropertyDescriptor(prototype, method)
16 warn(code, `The ${method} method is deprecated. ${msg}`, get)
17 }
18}
19const deprecatedProperty = (field, msg) => {
20 const code = `LRU_CACHE_PROPERTY_${field}`
21 if (shouldWarn(code)) {
22 const { prototype } = LRUCache
23 const { get } = Object.getOwnPropertyDescriptor(prototype, field)
24 warn(code, `The ${field} property is deprecated. ${msg}`, get)
25 }
26}
27const shouldWarn = (code) => !(process.noDeprecation || warned.has(code))
28const warn = (code, msg, fn) => {
29 warned.add(code)
30 process.emitWarning(msg, 'DeprecationWarning', code, fn)
31}
32
33const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n)
34
35/* istanbul ignore next - This is a little bit ridiculous, tbh.
36 * The maximum array length is 2^32-1 or thereabouts on most JS impls.
37 * And well before that point, you're caching the entire world, I mean,
38 * that's ~32GB of just integers for the next/prev links, plus whatever
39 * else to hold that many keys and values. Just filling the memory with
40 * zeroes at init time is brutal when you get that big.
41 * But why not be complete?
42 * Maybe in the future, these limits will have expanded. */
43const getUintArray = max => !isPosInt(max) ? null
44: max <= Math.pow(2, 8) ? Uint8Array
45: max <= Math.pow(2, 16) ? Uint16Array
46: max <= Math.pow(2, 32) ? Uint32Array
47: max <= Number.MAX_SAFE_INTEGER ? ZeroArray
48: null
49
50class ZeroArray extends Array {
51 constructor (size) {
52 super(size)
53 this.fill(0)
54 }
55}
56
57class Stack {
58 constructor (max) {
59 const UintArray = getUintArray(max)
60 this.heap = new UintArray(max)
61 this.length = 0
62 }
63 push (n) {
64 this.heap[this.length++] = n
65 }
66 pop () {
67 return this.heap[--this.length]
68 }
69}
70
71class LRUCache {
72 constructor (options = {}) {
73 const {
74 max,
75 ttl,
76 ttlResolution = 1,
77 ttlAutopurge,
78 updateAgeOnGet,
79 allowStale,
80 dispose,
81 noDisposeOnSet,
82 maxSize,
83 sizeCalculation,
84 } = options
85
86 // deprecated options, don't trigger a warning for getting them if
87 // the thing being passed in is another LRUCache we're copying.
88 const {
89 length,
90 maxAge,
91 stale,
92 } = options instanceof LRUCache ? {} : options
93
94 if (!isPosInt(max)) {
95 throw new TypeError('max option must be an integer')
96 }
97
98 const UintArray = getUintArray(max)
99 if (!UintArray) {
100 throw new Error('invalid max value: ' + max)
101 }
102
103 this.max = max
104 this.maxSize = maxSize || 0
105 this.sizeCalculation = sizeCalculation || length
106 if (this.sizeCalculation) {
107 if (!this.maxSize) {
108 throw new TypeError('cannot set sizeCalculation without setting maxSize')
109 }
110 if (typeof this.sizeCalculation !== 'function') {
111 throw new TypeError('sizeCalculating set to non-function')
112 }
113 }
114 this.keyMap = new Map()
115 this.keyList = new Array(max).fill(null)
116 this.valList = new Array(max).fill(null)
117 this.next = new UintArray(max)
118 this.prev = new UintArray(max)
119 this.head = 0
120 this.tail = 0
121 this.free = new Stack(max)
122 this.initialFill = 1
123 this.size = 0
124
125 if (typeof dispose === 'function') {
126 this.dispose = dispose
127 }
128 this.noDisposeOnSet = !!noDisposeOnSet
129
130 if (this.maxSize) {
131 if (!isPosInt(this.maxSize)) {
132 throw new TypeError('maxSize must be a positive integer if specified')
133 }
134 this.initializeSizeTracking()
135 }
136
137 this.allowStale = !!allowStale || !!stale
138 this.updateAgeOnGet = !!updateAgeOnGet
139 this.ttlResolution = isPosInt(ttlResolution) || ttlResolution === 0
140 ? ttlResolution : 1
141 this.ttlAutopurge = !!ttlAutopurge
142 this.ttl = ttl || maxAge || 0
143 if (this.ttl) {
144 if (!isPosInt(this.ttl)) {
145 throw new TypeError('ttl must be a positive integer if specified')
146 }
147 this.initializeTTLTracking()
148 }
149
150 if (stale) {
151 deprecatedOption('stale', 'please use options.allowStale instead')
152 }
153 if (maxAge) {
154 deprecatedOption('maxAge', 'please use options.ttl instead')
155 }
156 if (length) {
157 deprecatedOption('length', 'please use options.sizeCalculation instead')
158 }
159 }
160
161 initializeTTLTracking () {
162 this.ttls = new ZeroArray(this.max)
163 this.starts = new ZeroArray(this.max)
164 this.setItemTTL = (index, ttl) => {
165 this.starts[index] = ttl !== 0 ? perf.now() : 0
166 this.ttls[index] = ttl
167 if (ttl !== 0 && this.ttlAutopurge) {
168 const t = setTimeout(() => {
169 if (this.isStale(index)) {
170 this.delete(this.keyList[index])
171 }
172 }, ttl + 1)
173 /* istanbul ignore else - unref() not supported on all platforms */
174 if (t.unref) {
175 t.unref()
176 }
177 }
178 }
179 this.updateItemAge = (index) => {
180 this.starts[index] = this.ttls[index] !== 0 ? perf.now() : 0
181 }
182 // debounce calls to perf.now() to 1s so we're not hitting
183 // that costly call repeatedly.
184 let cachedNow = 0
185 const getNow = () => {
186 const n = perf.now()
187 if (this.ttlResolution > 0) {
188 cachedNow = n
189 const t = setTimeout(() => cachedNow = 0, this.ttlResolution)
190 /* istanbul ignore else - not available on all platforms */
191 if (t.unref) {
192 t.unref()
193 }
194 }
195 return n
196 }
197 this.isStale = (index) => {
198 return this.ttls[index] !== 0 && this.starts[index] !== 0 &&
199 ((cachedNow || getNow()) - this.starts[index] > this.ttls[index])
200 }
201 }
202 updateItemAge (index) {}
203 setItemTTL (index, ttl) {}
204 isStale (index) { return false }
205
206 initializeSizeTracking () {
207 this.calculatedSize = 0
208 this.sizes = new ZeroArray(this.max)
209 this.removeItemSize = index => this.calculatedSize -= this.sizes[index]
210 this.addItemSize = (index, v, k, size, sizeCalculation) => {
211 const s = size || (sizeCalculation ? sizeCalculation(v, k) : 0)
212 this.sizes[index] = isPosInt(s) ? s : 0
213 const maxSize = this.maxSize - this.sizes[index]
214 while (this.calculatedSize > maxSize) {
215 this.evict()
216 }
217 this.calculatedSize += this.sizes[index]
218 }
219 this.delete = k => {
220 if (this.size !== 0) {
221 const index = this.keyMap.get(k)
222 if (index !== undefined) {
223 this.calculatedSize -= this.sizes[index]
224 }
225 }
226 return LRUCache.prototype.delete.call(this, k)
227 }
228 }
229 removeItemSize (index) {}
230 addItemSize (index, v, k, size, sizeCalculation) {}
231
232 *indexes () {
233 if (this.size) {
234 for (let i = this.tail; true; i = this.prev[i]) {
235 if (!this.isStale(i)) {
236 yield i
237 }
238 if (i === this.head) {
239 break
240 }
241 }
242 }
243 }
244 *rindexes () {
245 if (this.size) {
246 for (let i = this.head; true; i = this.next[i]) {
247 if (!this.isStale(i)) {
248 yield i
249 }
250 if (i === this.tail) {
251 break
252 }
253 }
254 }
255 }
256
257 *entries () {
258 for (const i of this.indexes()) {
259 yield [this.keyList[i], this.valList[i]]
260 }
261 }
262
263 *keys () {
264 for (const i of this.indexes()) {
265 yield this.keyList[i]
266 }
267 }
268
269 *values () {
270 for (const i of this.indexes()) {
271 yield this.valList[i]
272 }
273 }
274
275 [Symbol.iterator] () {
276 return this.entries()
277 }
278
279 find (fn, getOptions = {}) {
280 for (const i of this.indexes()) {
281 if (fn(this.valList[i], this.keyList[i], this)) {
282 return this.get(this.keyList[i], getOptions)
283 }
284 }
285 }
286
287 forEach (fn, thisp = this) {
288 for (const i of this.indexes()) {
289 fn.call(thisp, this.valList[i], this.keyList[i], this)
290 }
291 }
292
293 rforEach (fn, thisp = this) {
294 for (const i of this.rindexes()) {
295 fn.call(thisp, this.valList[i], this.keyList[i], this)
296 }
297 }
298
299 get prune () {
300 deprecatedMethod('prune', 'Please use cache.purgeStale() instead.')
301 return this.purgeStale
302 }
303
304 purgeStale () {
305 let deleted = false
306 if (this.size) {
307 for (let i = this.head; true; i = this.next[i]) {
308 const b = i === this.tail
309 if (this.isStale(i)) {
310 this.delete(this.keyList[i])
311 deleted = true
312 }
313 if (b) {
314 break
315 }
316 }
317 }
318 return deleted
319 }
320
321 dump () {
322 const arr = []
323 for (const i of this.indexes()) {
324 const key = this.keyList[i]
325 const value = this.valList[i]
326 const entry = { value }
327 if (this.ttls) {
328 entry.ttl = this.ttls[i]
329 }
330 if (this.sizes) {
331 entry.size = this.sizes[i]
332 }
333 arr.unshift([key, entry])
334 }
335 return arr
336 }
337
338 load (arr) {
339 this.clear()
340 for (const [key, entry] of arr) {
341 this.set(key, entry.value, entry)
342 }
343 }
344
345 dispose (v, k, reason) {}
346
347 set (k, v, {
348 ttl = this.ttl,
349 noDisposeOnSet = this.noDisposeOnSet,
350 size = 0,
351 sizeCalculation = this.sizeCalculation,
352 } = {}) {
353 let index = this.size === 0 ? undefined : this.keyMap.get(k)
354 if (index === undefined) {
355 // addition
356 index = this.newIndex()
357 this.keyList[index] = k
358 this.valList[index] = v
359 this.keyMap.set(k, index)
360 this.next[this.tail] = index
361 this.prev[index] = this.tail
362 this.tail = index
363 this.size ++
364 this.addItemSize(index, v, k, size, sizeCalculation)
365 } else {
366 // update
367 const oldVal = this.valList[index]
368 if (v !== oldVal) {
369 if (!noDisposeOnSet) {
370 this.dispose(oldVal, k, 'set')
371 }
372 this.removeItemSize(index)
373 this.valList[index] = v
374 this.addItemSize(index, v, k, size, sizeCalculation)
375 }
376 this.moveToTail(index)
377 }
378 if (ttl !== 0 && this.ttl === 0 && !this.ttls) {
379 this.initializeTTLTracking()
380 }
381 this.setItemTTL(index, ttl)
382 }
383
384 newIndex () {
385 if (this.size === 0) {
386 return this.tail
387 }
388 if (this.size === this.max) {
389 return this.evict()
390 }
391 if (this.free.length !== 0) {
392 return this.free.pop()
393 }
394 // initial fill, just keep writing down the list
395 return this.initialFill++
396 }
397
398 pop () {
399 if (this.size) {
400 const val = this.valList[this.head]
401 this.evict()
402 return val
403 }
404 }
405
406 evict () {
407 const head = this.head
408 const k = this.keyList[head]
409 const v = this.valList[head]
410 this.dispose(v, k, 'evict')
411 this.removeItemSize(head)
412 this.head = this.next[head]
413 this.keyMap.delete(k)
414 this.size --
415 return head
416 }
417
418 has (k) {
419 return this.keyMap.has(k) && !this.isStale(this.keyMap.get(k))
420 }
421
422 // like get(), but without any LRU updating or TTL expiration
423 peek (k, { allowStale = this.allowStale } = {}) {
424 const index = this.keyMap.get(k)
425 if (index !== undefined && (allowStale || !this.isStale(index))) {
426 return this.valList[index]
427 }
428 }
429
430 get (k, {
431 allowStale = this.allowStale,
432 updateAgeOnGet = this.updateAgeOnGet,
433 } = {}) {
434 const index = this.keyMap.get(k)
435 if (index !== undefined) {
436 if (this.isStale(index)) {
437 const value = allowStale ? this.valList[index] : undefined
438 this.delete(k)
439 return value
440 } else {
441 this.moveToTail(index)
442 if (updateAgeOnGet) {
443 this.updateItemAge(index)
444 }
445 return this.valList[index]
446 }
447 }
448 }
449
450 connect (p, n) {
451 this.prev[n] = p
452 this.next[p] = n
453 }
454
455 moveToTail (index) {
456 // if tail already, nothing to do
457 // if head, move head to next[index]
458 // else
459 // move next[prev[index]] to next[index] (head has no prev)
460 // move prev[next[index]] to prev[index]
461 // prev[index] = tail
462 // next[tail] = index
463 // tail = index
464 if (index !== this.tail) {
465 if (index === this.head) {
466 this.head = this.next[index]
467 } else {
468 this.connect(this.prev[index], this.next[index])
469 }
470 this.connect(this.tail, index)
471 this.tail = index
472 }
473 }
474
475 delete (k) {
476 if (this.size !== 0) {
477 const index = this.keyMap.get(k)
478 if (index !== undefined) {
479 if (this.size === 1) {
480 this.clear()
481 } else {
482 this.removeItemSize(index)
483 this.dispose(this.valList[index], k, 'delete')
484 this.keyMap.delete(k)
485 this.keyList[index] = null
486 this.valList[index] = null
487 if (index === this.tail) {
488 this.tail = this.prev[index]
489 } else if (index === this.head) {
490 this.head = this.next[index]
491 } else {
492 this.next[this.prev[index]] = this.next[index]
493 this.prev[this.next[index]] = this.prev[index]
494 }
495 this.size --
496 this.free.push(index)
497 }
498 }
499 }
500 }
501
502 clear () {
503 if (this.dispose !== LRUCache.prototype.dispose) {
504 for (const index of this.rindexes()) {
505 this.dispose(this.valList[index], this.keyList[index], 'delete')
506 }
507 }
508 this.keyMap.clear()
509 this.valList.fill(null)
510 this.keyList.fill(null)
511 if (this.ttls) {
512 this.ttls.fill(0)
513 this.starts.fill(0)
514 }
515 if (this.sizes) {
516 this.sizes.fill(0)
517 }
518 this.head = 0
519 this.tail = 0
520 this.initialFill = 1
521 this.free.length = 0
522 this.calculatedSize = 0
523 this.size = 0
524 }
525 get reset () {
526 deprecatedMethod('reset', 'Please use cache.clear() instead.')
527 return this.clear
528 }
529
530 get length () {
531 deprecatedProperty('length', 'Please use cache.size instead.')
532 return this.size
533 }
534}
535
536module.exports = LRUCache
537
\No newline at end of file