Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: isaacs/node-lru-cache
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v7.4.2
Choose a base ref
...
head repository: isaacs/node-lru-cache
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v7.4.3
Choose a head ref
  • 3 commits
  • 11 files changed
  • 1 contributor

Commits on Mar 10, 2022

  1. Allow unbounded storage if maxSize or ttl set

    This also prevents setting size=0 if maxSize is set, since that is a
    recipe for disaster.
    
    At least one of max, maxSize, or ttl MUST be set, to prevent unbounded
    growth of the cache.  And really, without ttlAutopurge, it's effectively
    unsafe and unbounded in that case anyway, *especially* if allowStale is
    set.  This is potentially "unsafe at any speed" territory, so it emits a
    process warning in that case.
    
    If max is not set, then regular Array is used to track items, without
    setting an initial Array capacity.  This will often perform much worse,
    but in many cases, it's not so bad.  Bigger hazard is definitely
    unbounded memory consumption.
    
    Fix: #208
    isaacs committed Mar 10, 2022
    Copy the full SHA
    21c88f8 View commit details
  2. fix: purgeStale lockup on list reordering

    Fix: ##209
    isaacs committed Mar 10, 2022
    Copy the full SHA
    bff981e View commit details
  3. 7.4.3

    isaacs committed Mar 10, 2022
    Copy the full SHA
    00449b3 View commit details
Showing with 210 additions and 47 deletions.
  1. +88 −2 README.md
  2. +49 −16 index.js
  3. +2 −2 package-lock.json
  4. +1 −1 package.json
  5. +6 −0 tap-snapshots/test/deprecations.js.test.cjs
  6. +14 −14 tap-snapshots/test/map-like.js.test.cjs
  7. +21 −0 test/basic.js
  8. +5 −0 test/deprecations.js
  9. +1 −1 test/map-like.js
  10. +2 −7 test/size-calculation.js
  11. +21 −4 test/ttl.js
90 changes: 88 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ const options = {
// the number of most recently used items to keep.
// note that we may store fewer items than this if maxSize is hit.

max: 500, // <-- mandatory, you must give a maximum capacity
max: 500, // <-- Technically optional, but see "Storage Bounds Safety" below

// if you wish to track item size, you must provide a maxSize
// note that we still will only keep up to max *actual items*,
@@ -112,7 +112,11 @@ If you put more stuff in it, then items will fall out.
may be stored if size calculation is used, and `maxSize` is exceeded.
This must be a positive finite intger.

This option is required, and must be a positive integer.
At least one of `max`, `maxSize`, or `TTL` is required. This must be a
positive integer if set.

**It is strongly recommended to set a `max` to prevent unbounded growth
of the cache.** See "Storage Bounds Safety" below.

* `maxSize` - Set to a positive integer to track the sizes of items added
to the cache, and automatically evict items in order to stay below this
@@ -121,6 +125,13 @@ If you put more stuff in it, then items will fall out.
Optional, must be a positive integer if provided. Required if other
size tracking features are used.

At least one of `max`, `maxSize`, or `TTL` is required. This must be a
positive integer if set.

Even if size tracking is enabled, **it is strongly recommended to set a
`max` to prevent unbounded growth of the cache.** See "Storage Bounds
Safety" below.

* `sizeCalculation` - Function used to calculate the size of stored
items. If you're storing strings or buffers, then you probably want to
do something like `n => n.length`. The item is passed as the first
@@ -193,6 +204,17 @@ If you put more stuff in it, then items will fall out.

This may be overridden by passing an options object to `cache.set()`.

At least one of `max`, `maxSize`, or `TTL` is required. This must be a
positive integer if set.

Even if ttl tracking is enabled, **it is strongly recommended to set a
`max` to prevent unbounded growth of the cache.** See "Storage Bounds
Safety" below.

If ttl tracking is enabled, and `max` and `maxSize` are not set, and
`ttlAutopurge` is not set, then a warning will be emitted cautioning
about the potential for unbounded memory consumption.

Deprecated alias: `maxAge`

* `noUpdateTTL` - Boolean flag to tell the cache to not update the TTL when
@@ -449,6 +471,70 @@ ignored.
* `tail` Internal ID of most recently used item
* `free` Stack of deleted internal IDs

## Storage Bounds Safety

This implementation aims to be as flexible as possible, within the limits
of safe memory consumption and optimal performance.

At initial object creation, storage is allocated for `max` items. If `max`
is set to zero, then some performance is lost, and item count is unbounded.
Either `maxSize` or `ttl` _must_ be set if `max` is not specified.

If `maxSize` is set, then this creates a safe limit on the maximum storage
consumed, but without the performance benefits of pre-allocation. When
`maxSize` is set, every item _must_ provide a size, either via the
`sizeCalculation` method provided to the constructor, or via a `size` or
`sizeCalculation` option provided to `cache.set()`. The size of every item
_must_ be a positive integer.

If neither `max` nor `maxSize` are set, then `ttl` tracking must be
enabled. Note that, even when tracking item `ttl`, items are _not_
preemptively deleted when they become stale, unless `ttlAutopurge` is
enabled. Instead, they are only purged the next time the key is requested.
Thus, if `ttlAutopurge`, `max`, and `maxSize` are all not set, then the
cache will potentially grow unbounded.

In this case, a warning is printed to standard error. Future versions may
require the use of `ttlAutopurge` if `max` and `maxSize` are not specified.

If you truly wish to use a cache that is bound _only_ by TTL expiration,
consider using a `Map` object, and calling `setTimeout` to delete entries
when they expire. It will perform much better than an LRU cache.

Here is an implementation you may use, under the same [license](./LICENSE)
as this package:

```js
// a storage-unbounded ttl cache that is not an lru-cache
const cache = {
data: new Map(),
timers: new Map(),
set: (k, v, ttl) => {
if (cache.timers.has(k)) {
clearTimeout(cache.timers.get(k))
}
cache.timers.set(k, setTimeout(() => cache.del(k), ttl))
cache.data.set(k, v)
},
get: k => cache.data.get(k),
has: k => cache.data.has(k),
delete: k => {
if (cache.timers.has(k)) {
clearTimeout(cache.timers.get(k))
}
cache.timers.delete(k)
return cache.data.delete(k)
},
clear: () => {
cache.data.clear()
for (const v of cache.timers.values()) {
clearTimeout(v)
}
cache.timers.clear()
}
}
```

## Performance

As of January 2022, version 7 of this library is one of the most performant
65 changes: 49 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -29,7 +29,8 @@ const shouldWarn = (code) => typeof process === 'object' &&
!(process.noDeprecation || warned.has(code))
const warn = (code, what, instead, fn) => {
warned.add(code)
process.emitWarning(`The ${what} is deprecated. Please use ${instead} instead.`, 'DeprecationWarning', code, fn)
const msg = `The ${what} is deprecated. Please use ${instead} instead.`
process.emitWarning(msg, 'DeprecationWarning', code, fn)
}

const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n)
@@ -58,7 +59,7 @@ class ZeroArray extends Array {

class Stack {
constructor (max) {
const UintArray = getUintArray(max)
const UintArray = max ? getUintArray(max) : Array
this.heap = new UintArray(max)
this.length = 0
}
@@ -73,7 +74,7 @@ class Stack {
class LRUCache {
constructor (options = {}) {
const {
max,
max = 0,
ttl,
ttlResolution = 1,
ttlAutopurge,
@@ -83,7 +84,7 @@ class LRUCache {
disposeAfter,
noDisposeOnSet,
noUpdateTTL,
maxSize,
maxSize = 0,
sizeCalculation,
} = options

@@ -95,17 +96,17 @@ class LRUCache {
stale,
} = options instanceof LRUCache ? {} : options

if (!isPosInt(max)) {
throw new TypeError('max option must be an integer')
if (max !== 0 && !isPosInt(max)) {
throw new TypeError('max option must be a nonnegative integer')
}

const UintArray = getUintArray(max)
const UintArray = max ? getUintArray(max) : Array
if (!UintArray) {
throw new Error('invalid max value: ' + max)
}

this.max = max
this.maxSize = maxSize || 0
this.maxSize = maxSize
this.sizeCalculation = sizeCalculation || length
if (this.sizeCalculation) {
if (!this.maxSize) {
@@ -139,7 +140,7 @@ class LRUCache {
this.noDisposeOnSet = !!noDisposeOnSet
this.noUpdateTTL = !!noUpdateTTL

if (this.maxSize) {
if (this.maxSize !== 0) {
if (!isPosInt(this.maxSize)) {
throw new TypeError('maxSize must be a positive integer if specified')
}
@@ -159,6 +160,20 @@ class LRUCache {
this.initializeTTLTracking()
}

// do not allow completely unbounded caches
if (this.max === 0 && this.ttl === 0 && this.maxSize === 0) {
throw new TypeError('At least one of max, maxSize, or ttl is required')
}
if (!this.ttlAutopurge && !this.max && !this.maxSize) {
const code = 'LRU_CACHE_UNBOUNDED'
if (shouldWarn(code)) {
warned.add(code)
const msg = 'TTL caching without ttlAutopurge, max, or maxSize can ' +
'result in unbounded memory consumption.'
process.emitWarning(msg, 'UnboundedCacheWarning', code, LRUCache)
}
}

if (stale) {
deprecatedOption('stale', 'allowStale')
}
@@ -219,9 +234,17 @@ class LRUCache {
this.calculatedSize = 0
this.sizes = new ZeroArray(this.max)
this.removeItemSize = index => this.calculatedSize -= this.sizes[index]
this.addItemSize = (index, v, k, size, sizeCalculation) => {
const s = size || (sizeCalculation ? sizeCalculation(v, k) : 0)
this.sizes[index] = isPosInt(s) ? s : 0
this.requireSize = (k, v, size, sizeCalculation) => {
if (sizeCalculation && !size) {
size = sizeCalculation(v, k)
}
if (!isPosInt(size)) {
throw new TypeError('size must be positive integer')
}
return size
}
this.addItemSize = (index, v, k, size) => {
this.sizes[index] = size
const maxSize = this.maxSize - this.sizes[index]
while (this.calculatedSize > maxSize) {
this.evict()
@@ -239,7 +262,12 @@ class LRUCache {
}
}
removeItemSize (index) {}
addItemSize (index, v, k, size, sizeCalculation) {}
addItemSize (index, v, k, size) {}
requireSize (k, v, size, sizeCalculation) {
if (size || sizeCalculation) {
throw new TypeError('cannot set size without setting maxSize on cache')
}
}

*indexes ({ allowStale = this.allowStale } = {}) {
if (this.size) {
@@ -315,12 +343,16 @@ class LRUCache {

purgeStale () {
let deleted = false
const toDelete = []
for (const i of this.rindexes({ allowStale: true })) {
if (this.isStale(i)) {
this.delete(this.keyList[i])
toDelete.push(this.keyList[i])
deleted = true
}
}
for (const k of toDelete) {
this.delete(k)
}
return deleted
}

@@ -357,6 +389,7 @@ class LRUCache {
sizeCalculation = this.sizeCalculation,
noUpdateTTL = this.noUpdateTTL,
} = {}) {
size = this.requireSize(k, v, size, sizeCalculation)
let index = this.size === 0 ? undefined : this.keyMap.get(k)
if (index === undefined) {
// addition
@@ -368,7 +401,7 @@ class LRUCache {
this.prev[index] = this.tail
this.tail = index
this.size ++
this.addItemSize(index, v, k, size, sizeCalculation)
this.addItemSize(index, v, k, size)
noUpdateTTL = false
} else {
// update
@@ -382,7 +415,7 @@ class LRUCache {
}
this.removeItemSize(index)
this.valList[index] = v
this.addItemSize(index, v, k, size, sizeCalculation)
this.addItemSize(index, v, k, size)
}
this.moveToTail(index)
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "lru-cache",
"description": "A cache object that deletes the least-recently-used items.",
"version": "7.4.2",
"version": "7.4.3",
"author": "Isaac Z. Schlueter <i@izs.me>",
"keywords": [
"mru",
6 changes: 6 additions & 0 deletions tap-snapshots/test/deprecations.js.test.cjs
Original file line number Diff line number Diff line change
@@ -49,5 +49,11 @@ Array [
"LRU_CACHE_METHOD_del",
Function get del(),
],
Array [
"TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.",
"UnboundedCacheWarning",
"LRU_CACHE_UNBOUNDED",
Function LRUCache(classLRUCache),
],
]
`
Loading