@isaacs/ttlcache 1.1.0 → 1.2.1
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/README.md +43 -4
- package/index.d.ts +23 -8
- package/index.js +83 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Custom size calculation is not supported. Max capacity is simply the count
|
|
|
29
29
|
of items in the cache.
|
|
30
30
|
|
|
31
31
|
```js
|
|
32
|
-
const TTLCache = require('ttlcache')
|
|
32
|
+
const TTLCache = require('@isaacs/ttlcache')
|
|
33
33
|
const cache = new TTLCache({ max: 10000, ttl: 1000 })
|
|
34
34
|
|
|
35
35
|
// set some value
|
|
@@ -44,19 +44,37 @@ cache.get(1) // returns undefined
|
|
|
44
44
|
cache.has(1) // returns false
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Caveat Regarding Timers and Graceful Exits
|
|
48
|
+
|
|
49
|
+
On Node.js, this module uses the `Timeout.unref()` method to
|
|
50
|
+
prevent its internal `setTimeout` calls from keeping the process
|
|
51
|
+
running indefinitely. However, on other systems such as Deno,
|
|
52
|
+
where the `setTimeout` method does not return an object with an
|
|
53
|
+
`unref()` method, the process will stay open as long as any
|
|
54
|
+
unexpired entry exists in the cache.
|
|
55
|
+
|
|
56
|
+
You may delete all entries (by using `cache.clear()` or
|
|
57
|
+
`cache.delete(key)` with every key) in order to clear the
|
|
58
|
+
timeouts and allow the process to exit normally.
|
|
59
|
+
|
|
47
60
|
## API
|
|
48
61
|
|
|
49
62
|
### `const TTLCache = require('@isaacs/ttlcache')` or `import TTLCache from '@isaacs/ttlcache'`
|
|
50
63
|
|
|
51
64
|
Default export is the `TTLCache` class.
|
|
52
65
|
|
|
53
|
-
### `new TTLCache({ ttl, max = Infinty, updateAgeOnGet = false, noUpdateTTL = false })`
|
|
66
|
+
### `new TTLCache({ ttl, max = Infinty, updateAgeOnGet = false, noUpdateTTL = false, noDisposeOnSet = false })`
|
|
54
67
|
|
|
55
68
|
Create a new `TTLCache` object.
|
|
56
69
|
|
|
57
|
-
* `max` The max number of items to keep in the cache.
|
|
70
|
+
* `max` The max number of items to keep in the cache. Must be
|
|
71
|
+
positive integer or `Infinity`, defaults to `Infinity` (ie,
|
|
72
|
+
limited only by TTL, not by item count).
|
|
58
73
|
* `ttl` The max time in ms to store items. Overridable on the `set()`
|
|
59
|
-
method.
|
|
74
|
+
method. Must be a positive integer or `Infinity` (see note
|
|
75
|
+
below about immortality hazards). If `undefined` in
|
|
76
|
+
constructor, then a TTL _must_ be provided in each `set()`
|
|
77
|
+
call.
|
|
60
78
|
* `updateAgeOnGet` Should the age of an item be updated when it is
|
|
61
79
|
retrieved? Defaults to `false`. Overridable on the `get()` method.
|
|
62
80
|
* `noUpdateTTL` Should setting a new value for an existing key leave the
|
|
@@ -104,6 +122,13 @@ If `updateAgeOnGet` is `true`, then re-add the item into the cache with the
|
|
|
104
122
|
updated `ttl` value. Both options default to the settings on the
|
|
105
123
|
constructor.
|
|
106
124
|
|
|
125
|
+
Note that using `updateAgeOnGet` _can_ effectively simulate a
|
|
126
|
+
"least-recently-used" type of algorithm, by repeatedly updating
|
|
127
|
+
the TTL of items as they are used. However, if you find yourself
|
|
128
|
+
doing this, consider using
|
|
129
|
+
[`lru-cache`](http://npm.im/lru-cache), as it is much more
|
|
130
|
+
optimized for an LRU use case.
|
|
131
|
+
|
|
107
132
|
### `cache.getRemainingTTL(key)`
|
|
108
133
|
|
|
109
134
|
Return the remaining time before an item expires. Returns `0` if the item
|
|
@@ -188,3 +213,17 @@ the current time.
|
|
|
188
213
|
Thus, the `start` time doesn't need to be tracked, only the expiration
|
|
189
214
|
time. When an item age is updated (either explicitly on `get()`, or by
|
|
190
215
|
setting to a new value), it is deleted and re-inserted.
|
|
216
|
+
|
|
217
|
+
## Immortality Hazards
|
|
218
|
+
|
|
219
|
+
It is possible to set a TTL of `Infinity`, in which case an item
|
|
220
|
+
will never expire. As it does not expire, its TTL is not
|
|
221
|
+
tracked, and `getRemainingTTL()` will return `Infinity` for that
|
|
222
|
+
key.
|
|
223
|
+
|
|
224
|
+
If you do this, then the item will never be purged. Create
|
|
225
|
+
enough immortal values, and the cache will grow to consume all
|
|
226
|
+
available memory. If find yourself doing this, it's _probably_
|
|
227
|
+
better to use a different data structure, such as a `Map` or
|
|
228
|
+
plain old object to store values, as it will have better
|
|
229
|
+
performance and the hazards will be more obvious.
|
package/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// https://github.com/isaacs/node-lru-cache/blob/v7.10.1/index.d.ts
|
|
5
5
|
|
|
6
6
|
declare class TTLCache<K, V> implements Iterable<[K, V]> {
|
|
7
|
-
constructor(options
|
|
7
|
+
constructor(options?: TTLCache.Options<K, V>)
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The total number of items held in the cache at the current moment.
|
|
@@ -104,9 +104,10 @@ declare namespace TTLCache {
|
|
|
104
104
|
* by default, and MAY live in the cache, contributing to max,
|
|
105
105
|
* long after they have expired.
|
|
106
106
|
*
|
|
107
|
-
* Must be an integer number of ms,
|
|
107
|
+
* Must be an integer number of ms, or Infinity. Defaults to `undefined`,
|
|
108
|
+
* meaning that a TTL must be set explicitly for each set()
|
|
108
109
|
*/
|
|
109
|
-
ttl
|
|
110
|
+
ttl?: number
|
|
110
111
|
|
|
111
112
|
/**
|
|
112
113
|
* Boolean flag to tell the cache to not update the TTL when
|
|
@@ -134,6 +135,13 @@ declare namespace TTLCache {
|
|
|
134
135
|
*/
|
|
135
136
|
updateAgeOnGet?: boolean
|
|
136
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Do not call dispose() function when overwriting a key with a new value
|
|
140
|
+
*
|
|
141
|
+
* @default false
|
|
142
|
+
*/
|
|
143
|
+
noDisposeOnSet?: boolean
|
|
144
|
+
|
|
137
145
|
/**
|
|
138
146
|
* Function that is called on items when they are dropped from the cache.
|
|
139
147
|
* This can be handy if you want to close file descriptors or do other
|
|
@@ -148,19 +156,26 @@ declare namespace TTLCache {
|
|
|
148
156
|
|
|
149
157
|
type SetOptions = {
|
|
150
158
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* @default false
|
|
159
|
+
* Do not call dispose() function when overwriting a key with a new value
|
|
160
|
+
* Overrides the value set in the constructor.
|
|
155
161
|
*/
|
|
156
162
|
noDisposeOnSet?: boolean
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Do not update the TTL when overwriting an existing item.
|
|
166
|
+
*/
|
|
157
167
|
noUpdateTTL?: boolean
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Override the default TTL for this one set() operation.
|
|
171
|
+
* Required if a TTL was not set in the constructor options.
|
|
172
|
+
*/
|
|
158
173
|
ttl?: number
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
type GetOptions = {
|
|
162
177
|
/**
|
|
163
|
-
* Update the age of
|
|
178
|
+
* Update the age of item being retrieved.
|
|
164
179
|
*/
|
|
165
180
|
updateAgeOnGet?: boolean
|
|
166
181
|
|
package/index.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
// Relies on the fact that integer Object keys are kept sorted,
|
|
4
4
|
// and managed very efficiently by V8.
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const { now } = maybeReqPerfHooks(Date)
|
|
6
|
+
const perf =
|
|
7
|
+
typeof performance === 'object' &&
|
|
8
|
+
performance &&
|
|
9
|
+
typeof performance.now === 'function'
|
|
10
|
+
? performance
|
|
11
|
+
: Date
|
|
12
|
+
const now = () => perf.now()
|
|
14
13
|
const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n)
|
|
14
|
+
const isPosIntOrInf = n => n === Infinity || isPosInt(n)
|
|
15
15
|
|
|
16
16
|
class TTLCache {
|
|
17
17
|
constructor({
|
|
@@ -20,43 +20,62 @@ class TTLCache {
|
|
|
20
20
|
updateAgeOnGet = false,
|
|
21
21
|
noUpdateTTL = false,
|
|
22
22
|
dispose,
|
|
23
|
-
|
|
23
|
+
noDisposeOnSet = false,
|
|
24
|
+
} = {}) {
|
|
24
25
|
// {[expirationTime]: [keys]}
|
|
25
26
|
this.expirations = Object.create(null)
|
|
26
27
|
// {key=>val}
|
|
27
28
|
this.data = new Map()
|
|
28
29
|
// {key=>expiration}
|
|
29
30
|
this.expirationMap = new Map()
|
|
30
|
-
if (ttl !== undefined && !
|
|
31
|
-
throw new TypeError(
|
|
31
|
+
if (ttl !== undefined && !isPosIntOrInf(ttl)) {
|
|
32
|
+
throw new TypeError(
|
|
33
|
+
'ttl must be positive integer or Infinity if set'
|
|
34
|
+
)
|
|
32
35
|
}
|
|
33
|
-
if (!
|
|
36
|
+
if (!isPosIntOrInf(max)) {
|
|
34
37
|
throw new TypeError('max must be positive integer or Infinity')
|
|
35
38
|
}
|
|
36
39
|
this.ttl = ttl
|
|
37
40
|
this.max = max
|
|
38
41
|
this.updateAgeOnGet = updateAgeOnGet
|
|
39
42
|
this.noUpdateTTL = noUpdateTTL
|
|
43
|
+
this.noDisposeOnSet = noDisposeOnSet
|
|
40
44
|
if (dispose !== undefined) {
|
|
41
45
|
if (typeof dispose !== 'function') {
|
|
42
46
|
throw new TypeError('dispose must be function if set')
|
|
43
47
|
}
|
|
44
48
|
this.dispose = dispose
|
|
45
49
|
}
|
|
50
|
+
|
|
51
|
+
this.timers = new Set()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// hang onto the timer so we can clearTimeout if all items
|
|
55
|
+
// are deleted. Deno doesn't have Timer.unref(), so it
|
|
56
|
+
// hangs otherwise.
|
|
57
|
+
cancelTimers() {
|
|
58
|
+
for (const t of this.timers) {
|
|
59
|
+
clearTimeout(t)
|
|
60
|
+
this.timers.delete(t)
|
|
61
|
+
}
|
|
46
62
|
}
|
|
47
63
|
|
|
64
|
+
|
|
48
65
|
clear() {
|
|
49
66
|
const entries =
|
|
50
67
|
this.dispose !== TTLCache.prototype.dispose ? [...this] : []
|
|
51
68
|
this.data.clear()
|
|
52
69
|
this.expirationMap.clear()
|
|
70
|
+
// no need for any purging now
|
|
71
|
+
this.cancelTimers()
|
|
53
72
|
this.expirations = Object.create(null)
|
|
54
73
|
for (const [key, val] of entries) {
|
|
55
74
|
this.dispose(val, key, 'delete')
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
|
|
59
|
-
setTTL
|
|
78
|
+
setTTL(key, ttl = this.ttl) {
|
|
60
79
|
const current = this.expirationMap.get(key)
|
|
61
80
|
if (current !== undefined) {
|
|
62
81
|
// remove from the expirations list, so it isn't purged
|
|
@@ -68,15 +87,23 @@ class TTLCache {
|
|
|
68
87
|
}
|
|
69
88
|
}
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
if (ttl !== Infinity) {
|
|
91
|
+
const expiration = Math.floor(now() + ttl)
|
|
92
|
+
this.expirationMap.set(key, expiration)
|
|
93
|
+
if (!this.expirations[expiration]) {
|
|
94
|
+
const t = setTimeout(() => {
|
|
95
|
+
this.timers.delete(t)
|
|
96
|
+
this.purgeStale()
|
|
97
|
+
}, ttl)
|
|
98
|
+
/* istanbul ignore else - affordance for non-node envs */
|
|
99
|
+
if (t.unref) t.unref()
|
|
100
|
+
this.timers.add(t)
|
|
101
|
+
this.expirations[expiration] = []
|
|
102
|
+
}
|
|
103
|
+
this.expirations[expiration].push(key)
|
|
104
|
+
} else {
|
|
105
|
+
this.expirationMap.set(key, Infinity)
|
|
78
106
|
}
|
|
79
|
-
this.expirations[expiration].push(key)
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
set(
|
|
@@ -88,8 +115,8 @@ class TTLCache {
|
|
|
88
115
|
noDisposeOnSet = this.noDisposeOnSet,
|
|
89
116
|
} = {}
|
|
90
117
|
) {
|
|
91
|
-
if (!
|
|
92
|
-
throw new TypeError('ttl must be positive integer')
|
|
118
|
+
if (!isPosIntOrInf(ttl)) {
|
|
119
|
+
throw new TypeError('ttl must be positive integer or Infinity')
|
|
93
120
|
}
|
|
94
121
|
if (this.expirationMap.has(key)) {
|
|
95
122
|
if (!noUpdateTTL) {
|
|
@@ -121,7 +148,9 @@ class TTLCache {
|
|
|
121
148
|
|
|
122
149
|
getRemainingTTL(key) {
|
|
123
150
|
const expiration = this.expirationMap.get(key)
|
|
124
|
-
return expiration
|
|
151
|
+
return expiration === Infinity
|
|
152
|
+
? expiration
|
|
153
|
+
: expiration !== undefined
|
|
125
154
|
? Math.max(0, Math.ceil(expiration - now()))
|
|
126
155
|
: 0
|
|
127
156
|
}
|
|
@@ -146,12 +175,17 @@ class TTLCache {
|
|
|
146
175
|
this.data.delete(key)
|
|
147
176
|
this.expirationMap.delete(key)
|
|
148
177
|
const exp = this.expirations[current]
|
|
149
|
-
if (exp
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
if (exp) {
|
|
179
|
+
if (exp.length <= 1) {
|
|
180
|
+
delete this.expirations[current]
|
|
181
|
+
} else {
|
|
182
|
+
this.expirations[current] = exp.filter(k => k !== key)
|
|
183
|
+
}
|
|
153
184
|
}
|
|
154
185
|
this.dispose(value, key, 'delete')
|
|
186
|
+
if (this.size === 0) {
|
|
187
|
+
this.cancelTimers()
|
|
188
|
+
}
|
|
155
189
|
return true
|
|
156
190
|
}
|
|
157
191
|
return false
|
|
@@ -161,23 +195,29 @@ class TTLCache {
|
|
|
161
195
|
for (const exp in this.expirations) {
|
|
162
196
|
const keys = this.expirations[exp]
|
|
163
197
|
if (this.size - keys.length >= this.max) {
|
|
198
|
+
delete this.expirations[exp]
|
|
199
|
+
const entries = []
|
|
164
200
|
for (const key of keys) {
|
|
165
|
-
|
|
201
|
+
entries.push([key, this.data.get(key)])
|
|
166
202
|
this.data.delete(key)
|
|
167
203
|
this.expirationMap.delete(key)
|
|
204
|
+
}
|
|
205
|
+
for (const [key, val] of entries) {
|
|
168
206
|
this.dispose(val, key, 'evict')
|
|
169
207
|
}
|
|
170
|
-
delete this.expirations[exp]
|
|
171
208
|
} else {
|
|
172
209
|
const s = this.size - this.max
|
|
210
|
+
const entries = []
|
|
173
211
|
for (const key of keys.splice(0, s)) {
|
|
174
|
-
|
|
212
|
+
entries.push([key, this.data.get(key)])
|
|
175
213
|
this.data.delete(key)
|
|
176
214
|
this.expirationMap.delete(key)
|
|
215
|
+
}
|
|
216
|
+
for (const [key, val] of entries) {
|
|
177
217
|
this.dispose(val, key, 'evict')
|
|
178
218
|
}
|
|
219
|
+
return
|
|
179
220
|
}
|
|
180
|
-
return
|
|
181
221
|
}
|
|
182
222
|
}
|
|
183
223
|
|
|
@@ -188,16 +228,23 @@ class TTLCache {
|
|
|
188
228
|
purgeStale() {
|
|
189
229
|
const n = Math.ceil(now())
|
|
190
230
|
for (const exp in this.expirations) {
|
|
191
|
-
if (exp > n) {
|
|
231
|
+
if (exp === 'Infinity' || exp > n) {
|
|
192
232
|
return
|
|
193
233
|
}
|
|
194
|
-
|
|
195
|
-
|
|
234
|
+
const keys = [...this.expirations[exp]]
|
|
235
|
+
const entries = []
|
|
236
|
+
delete this.expirations[exp]
|
|
237
|
+
for (const key of keys) {
|
|
238
|
+
entries.push([key, this.data.get(key)])
|
|
196
239
|
this.data.delete(key)
|
|
197
240
|
this.expirationMap.delete(key)
|
|
241
|
+
}
|
|
242
|
+
for (const [key, val] of entries) {
|
|
198
243
|
this.dispose(val, key, 'stale')
|
|
199
244
|
}
|
|
200
|
-
|
|
245
|
+
}
|
|
246
|
+
if (this.size === 0) {
|
|
247
|
+
this.cancelTimers()
|
|
201
248
|
}
|
|
202
249
|
}
|
|
203
250
|
|