| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 | 'use strict'// A linked list to keep track of recently-used-nessconst 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
 |