| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 | 
							- 'use strict'
 
- // A linked list to keep track of recently-used-ness
 
- const Yallist = require('yallist')
 
- const MAX = Symbol('max')
 
- const LENGTH = Symbol('length')
 
- const LENGTH_CALCULATOR = Symbol('lengthCalculator')
 
- const ALLOW_STALE = Symbol('allowStale')
 
- const MAX_AGE = Symbol('maxAge')
 
- const DISPOSE = Symbol('dispose')
 
- const NO_DISPOSE_ON_SET = Symbol('noDisposeOnSet')
 
- const LRU_LIST = Symbol('lruList')
 
- const CACHE = Symbol('cache')
 
- const UPDATE_AGE_ON_GET = Symbol('updateAgeOnGet')
 
- const naiveLength = () => 1
 
- // lruList is a yallist where the head is the youngest
 
- // item, and the tail is the oldest.  the list contains the Hit
 
- // objects as the entries.
 
- // Each Hit object has a reference to its Yallist.Node.  This
 
- // never changes.
 
- //
 
- // cache is a Map (or PseudoMap) that matches the keys to
 
- // the Yallist.Node object.
 
- class LRUCache {
 
-   constructor (options) {
 
-     if (typeof options === 'number')
 
-       options = { max: options }
 
-     if (!options)
 
-       options = {}
 
-     if (options.max && (typeof options.max !== 'number' || options.max < 0))
 
-       throw new TypeError('max must be a non-negative number')
 
-     // Kind of weird to have a default max of Infinity, but oh well.
 
-     const max = this[MAX] = options.max || Infinity
 
-     const lc = options.length || naiveLength
 
-     this[LENGTH_CALCULATOR] = (typeof lc !== 'function') ? naiveLength : lc
 
-     this[ALLOW_STALE] = options.stale || false
 
-     if (options.maxAge && typeof options.maxAge !== 'number')
 
-       throw new TypeError('maxAge must be a number')
 
-     this[MAX_AGE] = options.maxAge || 0
 
-     this[DISPOSE] = options.dispose
 
-     this[NO_DISPOSE_ON_SET] = options.noDisposeOnSet || false
 
-     this[UPDATE_AGE_ON_GET] = options.updateAgeOnGet || false
 
-     this.reset()
 
-   }
 
-   // resize the cache when the max changes.
 
-   set max (mL) {
 
-     if (typeof mL !== 'number' || mL < 0)
 
-       throw new TypeError('max must be a non-negative number')
 
-     this[MAX] = mL || Infinity
 
-     trim(this)
 
-   }
 
-   get max () {
 
-     return this[MAX]
 
-   }
 
-   set allowStale (allowStale) {
 
-     this[ALLOW_STALE] = !!allowStale
 
-   }
 
-   get allowStale () {
 
-     return this[ALLOW_STALE]
 
-   }
 
-   set maxAge (mA) {
 
-     if (typeof mA !== 'number')
 
-       throw new TypeError('maxAge must be a non-negative number')
 
-     this[MAX_AGE] = mA
 
-     trim(this)
 
-   }
 
-   get maxAge () {
 
-     return this[MAX_AGE]
 
-   }
 
-   // resize the cache when the lengthCalculator changes.
 
-   set lengthCalculator (lC) {
 
-     if (typeof lC !== 'function')
 
-       lC = naiveLength
 
-     if (lC !== this[LENGTH_CALCULATOR]) {
 
-       this[LENGTH_CALCULATOR] = lC
 
-       this[LENGTH] = 0
 
-       this[LRU_LIST].forEach(hit => {
 
-         hit.length = this[LENGTH_CALCULATOR](hit.value, hit.key)
 
-         this[LENGTH] += hit.length
 
-       })
 
-     }
 
-     trim(this)
 
-   }
 
-   get lengthCalculator () { return this[LENGTH_CALCULATOR] }
 
-   get length () { return this[LENGTH] }
 
-   get itemCount () { return this[LRU_LIST].length }
 
-   rforEach (fn, thisp) {
 
-     thisp = thisp || this
 
-     for (let walker = this[LRU_LIST].tail; walker !== null;) {
 
-       const prev = walker.prev
 
-       forEachStep(this, fn, walker, thisp)
 
-       walker = prev
 
-     }
 
-   }
 
-   forEach (fn, thisp) {
 
-     thisp = thisp || this
 
-     for (let walker = this[LRU_LIST].head; walker !== null;) {
 
-       const next = walker.next
 
-       forEachStep(this, fn, walker, thisp)
 
-       walker = next
 
-     }
 
-   }
 
-   keys () {
 
-     return this[LRU_LIST].toArray().map(k => k.key)
 
-   }
 
-   values () {
 
-     return this[LRU_LIST].toArray().map(k => k.value)
 
-   }
 
-   reset () {
 
-     if (this[DISPOSE] &&
 
-         this[LRU_LIST] &&
 
-         this[LRU_LIST].length) {
 
-       this[LRU_LIST].forEach(hit => this[DISPOSE](hit.key, hit.value))
 
-     }
 
-     this[CACHE] = new Map() // hash of items by key
 
-     this[LRU_LIST] = new Yallist() // list of items in order of use recency
 
-     this[LENGTH] = 0 // length of items in the list
 
-   }
 
-   dump () {
 
-     return this[LRU_LIST].map(hit =>
 
-       isStale(this, hit) ? false : {
 
-         k: hit.key,
 
-         v: hit.value,
 
-         e: hit.now + (hit.maxAge || 0)
 
-       }).toArray().filter(h => h)
 
-   }
 
-   dumpLru () {
 
-     return this[LRU_LIST]
 
-   }
 
-   set (key, value, maxAge) {
 
-     maxAge = maxAge || this[MAX_AGE]
 
-     if (maxAge && typeof maxAge !== 'number')
 
-       throw new TypeError('maxAge must be a number')
 
-     const now = maxAge ? Date.now() : 0
 
-     const len = this[LENGTH_CALCULATOR](value, key)
 
-     if (this[CACHE].has(key)) {
 
-       if (len > this[MAX]) {
 
-         del(this, this[CACHE].get(key))
 
-         return false
 
-       }
 
-       const node = this[CACHE].get(key)
 
-       const item = node.value
 
-       // dispose of the old one before overwriting
 
-       // split out into 2 ifs for better coverage tracking
 
-       if (this[DISPOSE]) {
 
-         if (!this[NO_DISPOSE_ON_SET])
 
-           this[DISPOSE](key, item.value)
 
-       }
 
-       item.now = now
 
-       item.maxAge = maxAge
 
-       item.value = value
 
-       this[LENGTH] += len - item.length
 
-       item.length = len
 
-       this.get(key)
 
-       trim(this)
 
-       return true
 
-     }
 
-     const hit = new Entry(key, value, len, now, maxAge)
 
-     // oversized objects fall out of cache automatically.
 
-     if (hit.length > this[MAX]) {
 
-       if (this[DISPOSE])
 
-         this[DISPOSE](key, value)
 
-       return false
 
-     }
 
-     this[LENGTH] += hit.length
 
-     this[LRU_LIST].unshift(hit)
 
-     this[CACHE].set(key, this[LRU_LIST].head)
 
-     trim(this)
 
-     return true
 
-   }
 
-   has (key) {
 
-     if (!this[CACHE].has(key)) return false
 
-     const hit = this[CACHE].get(key).value
 
-     return !isStale(this, hit)
 
-   }
 
-   get (key) {
 
-     return get(this, key, true)
 
-   }
 
-   peek (key) {
 
-     return get(this, key, false)
 
-   }
 
-   pop () {
 
-     const node = this[LRU_LIST].tail
 
-     if (!node)
 
-       return null
 
-     del(this, node)
 
-     return node.value
 
-   }
 
-   del (key) {
 
-     del(this, this[CACHE].get(key))
 
-   }
 
-   load (arr) {
 
-     // reset the cache
 
-     this.reset()
 
-     const now = Date.now()
 
-     // A previous serialized cache has the most recent items first
 
-     for (let l = arr.length - 1; l >= 0; l--) {
 
-       const hit = arr[l]
 
-       const expiresAt = hit.e || 0
 
-       if (expiresAt === 0)
 
-         // the item was created without expiration in a non aged cache
 
-         this.set(hit.k, hit.v)
 
-       else {
 
-         const maxAge = expiresAt - now
 
-         // dont add already expired items
 
-         if (maxAge > 0) {
 
-           this.set(hit.k, hit.v, maxAge)
 
-         }
 
-       }
 
-     }
 
-   }
 
-   prune () {
 
-     this[CACHE].forEach((value, key) => get(this, key, false))
 
-   }
 
- }
 
- const get = (self, key, doUse) => {
 
-   const node = self[CACHE].get(key)
 
-   if (node) {
 
-     const hit = node.value
 
-     if (isStale(self, hit)) {
 
-       del(self, node)
 
-       if (!self[ALLOW_STALE])
 
-         return undefined
 
-     } else {
 
-       if (doUse) {
 
-         if (self[UPDATE_AGE_ON_GET])
 
-           node.value.now = Date.now()
 
-         self[LRU_LIST].unshiftNode(node)
 
-       }
 
-     }
 
-     return hit.value
 
-   }
 
- }
 
- const isStale = (self, hit) => {
 
-   if (!hit || (!hit.maxAge && !self[MAX_AGE]))
 
-     return false
 
-   const diff = Date.now() - hit.now
 
-   return hit.maxAge ? diff > hit.maxAge
 
-     : self[MAX_AGE] && (diff > self[MAX_AGE])
 
- }
 
- const trim = self => {
 
-   if (self[LENGTH] > self[MAX]) {
 
-     for (let walker = self[LRU_LIST].tail;
 
-       self[LENGTH] > self[MAX] && walker !== null;) {
 
-       // We know that we're about to delete this one, and also
 
-       // what the next least recently used key will be, so just
 
-       // go ahead and set it now.
 
-       const prev = walker.prev
 
-       del(self, walker)
 
-       walker = prev
 
-     }
 
-   }
 
- }
 
- const del = (self, node) => {
 
-   if (node) {
 
-     const hit = node.value
 
-     if (self[DISPOSE])
 
-       self[DISPOSE](hit.key, hit.value)
 
-     self[LENGTH] -= hit.length
 
-     self[CACHE].delete(hit.key)
 
-     self[LRU_LIST].removeNode(node)
 
-   }
 
- }
 
- class Entry {
 
-   constructor (key, value, length, now, maxAge) {
 
-     this.key = key
 
-     this.value = value
 
-     this.length = length
 
-     this.now = now
 
-     this.maxAge = maxAge || 0
 
-   }
 
- }
 
- const forEachStep = (self, fn, node, thisp) => {
 
-   let hit = node.value
 
-   if (isStale(self, hit)) {
 
-     del(self, node)
 
-     if (!self[ALLOW_STALE])
 
-       hit = undefined
 
-   }
 
-   if (hit)
 
-     fn.call(thisp, hit.value, hit.key, self)
 
- }
 
- module.exports = LRUCache
 
 
  |