index.js
  1  'use strict'
  2  
  3  // A linked list to keep track of recently-used-ness
  4  const Yallist = require('yallist')
  5  
  6  const MAX = Symbol('max')
  7  const LENGTH = Symbol('length')
  8  const LENGTH_CALCULATOR = Symbol('lengthCalculator')
  9  const ALLOW_STALE = Symbol('allowStale')
 10  const MAX_AGE = Symbol('maxAge')
 11  const DISPOSE = Symbol('dispose')
 12  const NO_DISPOSE_ON_SET = Symbol('noDisposeOnSet')
 13  const LRU_LIST = Symbol('lruList')
 14  const CACHE = Symbol('cache')
 15  const UPDATE_AGE_ON_GET = Symbol('updateAgeOnGet')
 16  
 17  const 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.
 27  class 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  
259  const 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  
278  const 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  
287  const 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  
301  const 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  
313  class 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  
323  const 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  
334  module.exports = LRUCache