@isaacs/ttlcache 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +152 -0
- package/index.js +197 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
The ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 - Isaac Z. Schlueter and Contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
|
15
|
+
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @isaacs/ttlcache
|
|
2
|
+
|
|
3
|
+
The time-based use-recency-unaware cousin of
|
|
4
|
+
[`lru-cache`](http://npm.im/lru-cache)
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
Essentially, this is the same API as
|
|
9
|
+
[`lru-cache`](http://npm.im/lru-cache), but it does not do LRU tracking,
|
|
10
|
+
and is bound primarily by time, rather than space. Since entries are not
|
|
11
|
+
purged based on recency of use, it can save a lot of extra work managing
|
|
12
|
+
linked lists, mapping keys to pointers, and so on.
|
|
13
|
+
|
|
14
|
+
TTLs are millisecond granularity.
|
|
15
|
+
|
|
16
|
+
If a capacity limit is set, then the soonest-expiring items are purged
|
|
17
|
+
first, to bring it down to the size limit.
|
|
18
|
+
|
|
19
|
+
Iteration is in order from soonest expiring until latest expiring.
|
|
20
|
+
|
|
21
|
+
If multiple items are expiring in the same ms, then the soonest-added
|
|
22
|
+
items are considered "older" for purposes of iterating and purging down to
|
|
23
|
+
capacity.
|
|
24
|
+
|
|
25
|
+
A TTL _must_ be set for every entry, which can be defaulted in the
|
|
26
|
+
constructor.
|
|
27
|
+
|
|
28
|
+
Custom size calculation is not supported. Max capacity is simply the count
|
|
29
|
+
of items in the cache.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const TTLCache = require('ttlcache')
|
|
33
|
+
const cache = new TTLCache({ max: 10000, ttl: 1000 })
|
|
34
|
+
|
|
35
|
+
// set some value
|
|
36
|
+
cache.set(1, 2)
|
|
37
|
+
|
|
38
|
+
// 999 ms later
|
|
39
|
+
cache.has(1) // returns true
|
|
40
|
+
cache.get(1) // returns 2
|
|
41
|
+
|
|
42
|
+
// 1000 ms later
|
|
43
|
+
cache.get(1) // returns undefined
|
|
44
|
+
cache.has(1) // returns false
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `const TTLCache = require('@isaacs/ttlcache')` or `import TTLCache from '@isaacs/ttlcache'
|
|
50
|
+
|
|
51
|
+
Default export is the `TTLCache` class.
|
|
52
|
+
|
|
53
|
+
### `new TTLCache({ ttl, max = Infinty, updateAgeOnGet = false, noUpdateTTL = false })`
|
|
54
|
+
|
|
55
|
+
Create a new `TTLCache` object.
|
|
56
|
+
|
|
57
|
+
* `max` The max number of items to keep in the cache.
|
|
58
|
+
* `ttl` The max time in ms to store items. Overridable on the `set()`
|
|
59
|
+
method.
|
|
60
|
+
* `updateAgeOnGet` Should the age of an item be updated when it is
|
|
61
|
+
retrieved? Defaults to `false`. Overridable on the `get()` method.
|
|
62
|
+
* `noUpdateTTL` Should setting a new value for an existing key leave the
|
|
63
|
+
TTL unchanged? Defaults to `false`. Overridable on the `set()` method.
|
|
64
|
+
* `dispose` Method called with `(value, key, reason)` when an item is
|
|
65
|
+
removed from the cache. Called once item is fully removed from cache.
|
|
66
|
+
It is safe to re-add at this point, but note that adding when `reason` is
|
|
67
|
+
`'set'` can result in infinite recursion if `noDisponseOnSet` is not
|
|
68
|
+
specified.
|
|
69
|
+
* `noDisposeOnSet` Do not call `dispose()` method when overwriting a key
|
|
70
|
+
with a new value. Defaults to `false`. Overridable on `set()` method.
|
|
71
|
+
|
|
72
|
+
When used as an iterator, like `for (const [key, value] of cache)` or
|
|
73
|
+
`[...cache]`, the cache yields the same results as the `entries()` method.
|
|
74
|
+
|
|
75
|
+
### `cache.size`
|
|
76
|
+
|
|
77
|
+
The number of items in the cache.
|
|
78
|
+
|
|
79
|
+
### `cache.set(key, value, { ttl, noUpdateTTL, noDisposeOnSet } = {})`
|
|
80
|
+
|
|
81
|
+
Store a value in the cache for the specified time.
|
|
82
|
+
|
|
83
|
+
`ttl` and `noUpdateTTL` optionally override defaults on the constructor.
|
|
84
|
+
|
|
85
|
+
Returns the cache object.
|
|
86
|
+
|
|
87
|
+
### `cache.get(key, {updateAgeOnGet, ttl} = {})`
|
|
88
|
+
|
|
89
|
+
Get an item stored in the cache. Returns `undefined` if the item is not in
|
|
90
|
+
the cache (including if it has expired and been purged).
|
|
91
|
+
|
|
92
|
+
If `updateAgeOnGet` is `true`, then re-add the item into the cache with the
|
|
93
|
+
updated `ttl` value. Both options default to the settings on the
|
|
94
|
+
constructor.
|
|
95
|
+
|
|
96
|
+
### `cache.getRemainingTTL(key)`
|
|
97
|
+
|
|
98
|
+
Return the remaining time before an item expires. Returns `0` if the item
|
|
99
|
+
is not found in the cache or is already expired.
|
|
100
|
+
|
|
101
|
+
### `cache.has(key)`
|
|
102
|
+
|
|
103
|
+
Return true if the item is in the cache.
|
|
104
|
+
|
|
105
|
+
### `cache.delete(key)`
|
|
106
|
+
|
|
107
|
+
Remove an item from the cache.
|
|
108
|
+
|
|
109
|
+
### `cache.clear()`
|
|
110
|
+
|
|
111
|
+
Delete all items from the cache.
|
|
112
|
+
|
|
113
|
+
### `cache.entries()`
|
|
114
|
+
|
|
115
|
+
Return an iterator that walks through each `[key, value]` from soonest
|
|
116
|
+
expiring to latest expiring.
|
|
117
|
+
|
|
118
|
+
Default iteration method for the cache object.
|
|
119
|
+
|
|
120
|
+
### `cache.keys()`
|
|
121
|
+
|
|
122
|
+
Return an iterator that walks through each `key` from soonest expiring to
|
|
123
|
+
latest expiring.
|
|
124
|
+
|
|
125
|
+
### `cache.values()`
|
|
126
|
+
|
|
127
|
+
Return an iterator that walks through each `value` from soonest expiring to
|
|
128
|
+
latest expiring.
|
|
129
|
+
|
|
130
|
+
## Internal Methods
|
|
131
|
+
|
|
132
|
+
You should not ever call these, they are managed automatically.
|
|
133
|
+
|
|
134
|
+
### `purgeStale`
|
|
135
|
+
|
|
136
|
+
**Internal**
|
|
137
|
+
|
|
138
|
+
Removes items which have expired. Called automatically.
|
|
139
|
+
|
|
140
|
+
### `purgeToCapacity`
|
|
141
|
+
|
|
142
|
+
**Internal**
|
|
143
|
+
|
|
144
|
+
Removes soonest-expiring items when the capacity limit is reached. Called
|
|
145
|
+
automatically.
|
|
146
|
+
|
|
147
|
+
### `dispose`
|
|
148
|
+
|
|
149
|
+
**Internal**
|
|
150
|
+
|
|
151
|
+
Called when an item is removed from the cache and should be disposed. Set
|
|
152
|
+
this on the constructor options.
|
package/index.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// A simple TTL cache with max capacity option, ms resolution,
|
|
2
|
+
// autopurge, and reasonably optimized performance
|
|
3
|
+
// Relies on the fact that integer Object keys are kept sorted,
|
|
4
|
+
// and managed very efficiently by V8.
|
|
5
|
+
|
|
6
|
+
const maybeReqPerfHooks = (fallback) => {
|
|
7
|
+
try {
|
|
8
|
+
return require('perf_hooks').performance
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return fallback
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const {now} = maybeReqPerfHooks(Date)
|
|
14
|
+
const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n)
|
|
15
|
+
|
|
16
|
+
class TTLCache {
|
|
17
|
+
constructor ({ max = Infinity, ttl, updateAgeOnGet = false, noUpdateTTL = false, dispose }) {
|
|
18
|
+
// {[expirationTime]: [keys]}
|
|
19
|
+
this.expirations = Object.create(null)
|
|
20
|
+
// {key=>val}
|
|
21
|
+
this.data = new Map()
|
|
22
|
+
// {key=>expiration}
|
|
23
|
+
this.expirationMap = new Map()
|
|
24
|
+
if (ttl !== undefined && !isPosInt(ttl)) {
|
|
25
|
+
throw new TypeError('ttl must be positive integer if set')
|
|
26
|
+
}
|
|
27
|
+
if (!isPosInt(max) && max !== Infinity) {
|
|
28
|
+
throw new TypeError('max must be positive integer or Infinity')
|
|
29
|
+
}
|
|
30
|
+
this.ttl = ttl
|
|
31
|
+
this.max = max
|
|
32
|
+
if (dispose !== undefined) {
|
|
33
|
+
if (typeof dispose !== 'function') {
|
|
34
|
+
throw new TypeError('dispose must be function if set')
|
|
35
|
+
}
|
|
36
|
+
this.dispose = dispose
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
clear () {
|
|
41
|
+
const entries = this.dispose !== TTLCache.prototype.dispose ? [...this] : []
|
|
42
|
+
this.data.clear()
|
|
43
|
+
this.expirationMap.clear()
|
|
44
|
+
this.expirations = Object.create(null)
|
|
45
|
+
for (const [key, val] of entries) {
|
|
46
|
+
this.dispose(val, key, 'delete')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set (key, val, { ttl = this.ttl, noUpdateTTL = this.noUpdateTTL, noDisposeOnSet = this.noDisposeOnSet } = {}) {
|
|
51
|
+
if (!isPosInt(ttl)) {
|
|
52
|
+
throw new TypeError('ttl must be positive integer')
|
|
53
|
+
}
|
|
54
|
+
const current = this.expirationMap.get(key)
|
|
55
|
+
if (current !== undefined) {
|
|
56
|
+
const oldValue = this.data.get(key)
|
|
57
|
+
if (noUpdateTTL) {
|
|
58
|
+
if (oldValue !== val) {
|
|
59
|
+
this.data.set(key, val)
|
|
60
|
+
if (!noDisposeOnSet) {
|
|
61
|
+
this.dispose(oldValue, key, 'set')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return this
|
|
65
|
+
} else {
|
|
66
|
+
this.delete(key, {
|
|
67
|
+
reason: 'set',
|
|
68
|
+
noDispose: noDisposeOnSet || oldValue === val,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const expiration = Math.ceil(now() + ttl)
|
|
73
|
+
this.expirationMap.set(key, expiration)
|
|
74
|
+
this.data.set(key, val)
|
|
75
|
+
if (!this.expirations[expiration]) {
|
|
76
|
+
const t = setTimeout(() => this.purgeStale(), ttl)
|
|
77
|
+
/* istanbul ignore else - affordance for non-node envs */
|
|
78
|
+
if (t.unref) t.unref()
|
|
79
|
+
this.expirations[expiration] = []
|
|
80
|
+
}
|
|
81
|
+
this.expirations[expiration].push(key)
|
|
82
|
+
while (this.size > this.max) {
|
|
83
|
+
this.purgeToCapacity()
|
|
84
|
+
}
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
has (key) {
|
|
89
|
+
return this.data.has(key)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getRemainingTTL (key) {
|
|
93
|
+
const expiration = this.expirationMap.get(key)
|
|
94
|
+
return expiration !== undefined ? Math.max(0, expiration - now()) : 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get (key, { updateAgeOnGet = this.updateAgeOnGet, ttl = this.ttl } = {}) {
|
|
98
|
+
const val = this.data.get(key)
|
|
99
|
+
if (updateAgeOnGet) {
|
|
100
|
+
this.set(key, val, { noUpdateTTL: false, noDisposeOnSet: true, ttl })
|
|
101
|
+
}
|
|
102
|
+
return val
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
dispose (value, key) {}
|
|
106
|
+
|
|
107
|
+
delete (key, { reason = 'delete', noDispose = false } = {}) {
|
|
108
|
+
const current = this.expirationMap.get(key)
|
|
109
|
+
if (current !== undefined) {
|
|
110
|
+
const value = this.data.get(key)
|
|
111
|
+
this.data.delete(key)
|
|
112
|
+
this.expirationMap.delete(key)
|
|
113
|
+
const exp = this.expirations[current]
|
|
114
|
+
if (exp.length === 1) {
|
|
115
|
+
delete this.expirations[current]
|
|
116
|
+
} else {
|
|
117
|
+
this.expirations[current] = exp.filter(k => k !== key)
|
|
118
|
+
}
|
|
119
|
+
if (!noDispose) {
|
|
120
|
+
this.dispose(value, key, reason)
|
|
121
|
+
}
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
purgeToCapacity () {
|
|
128
|
+
for (const exp in this.expirations) {
|
|
129
|
+
const keys = this.expirations[exp]
|
|
130
|
+
if (this.size - keys.length >= this.max) {
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
const val = this.data.get(key)
|
|
133
|
+
this.data.delete(key)
|
|
134
|
+
this.expirationMap.delete(key)
|
|
135
|
+
this.dispose(val, key, 'evict')
|
|
136
|
+
}
|
|
137
|
+
delete this.expirations[exp]
|
|
138
|
+
} else {
|
|
139
|
+
const s = this.size - this.max
|
|
140
|
+
for (const key of keys.splice(0, s)) {
|
|
141
|
+
const val = this.data.get(key)
|
|
142
|
+
this.data.delete(key)
|
|
143
|
+
this.expirationMap.delete(key)
|
|
144
|
+
this.dispose(val, key, 'evict')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get size () {
|
|
152
|
+
return this.data.size
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
purgeStale () {
|
|
156
|
+
const n = now()
|
|
157
|
+
for (const exp in this.expirations) {
|
|
158
|
+
if (exp > n) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
for (const key of this.expirations[exp]) {
|
|
162
|
+
const val = this.data.get(key)
|
|
163
|
+
this.data.delete(key)
|
|
164
|
+
this.expirationMap.delete(key)
|
|
165
|
+
this.dispose(val, key, 'stale')
|
|
166
|
+
}
|
|
167
|
+
delete this.expirations[exp]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
*entries () {
|
|
172
|
+
for (const exp in this.expirations) {
|
|
173
|
+
for (const key of this.expirations[exp]) {
|
|
174
|
+
yield [key, this.data.get(key)]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
*keys () {
|
|
179
|
+
for (const exp in this.expirations) {
|
|
180
|
+
for (const key of this.expirations[exp]) {
|
|
181
|
+
yield key
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
*values () {
|
|
186
|
+
for (const exp in this.expirations) {
|
|
187
|
+
for (const key of this.expirations[exp]) {
|
|
188
|
+
yield this.data.get(key)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
[Symbol.iterator] () {
|
|
193
|
+
return this.entries()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = TTLCache
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@isaacs/ttlcache",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"index.js"
|
|
6
|
+
],
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"description": "The time-based use-recency-unaware cousin of [`lru-cache`](http://npm.im/lru-cache)",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/isaacs/ttlcache"
|
|
15
|
+
},
|
|
16
|
+
"author": "Isaac Z. Schlueter <i@izs.me> (https://izs.me)",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "tap",
|
|
20
|
+
"snap": "tap",
|
|
21
|
+
"preversion": "npm test",
|
|
22
|
+
"postversion": "npm publish",
|
|
23
|
+
"prepublishOnly": "git push origin --follow-tags"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"clock-mock": "^1.0.4",
|
|
27
|
+
"tap": "^16.0.1"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=12"
|
|
31
|
+
}
|
|
32
|
+
}
|