@isaacs/ttlcache 1.0.4 → 1.2.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/README.md +28 -2
- package/index.d.ts +184 -0
- package/index.js +58 -51
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -54,9 +54,14 @@ Default export is the `TTLCache` class.
|
|
|
54
54
|
|
|
55
55
|
Create a new `TTLCache` object.
|
|
56
56
|
|
|
57
|
-
* `max` The max number of items to keep in the cache.
|
|
57
|
+
* `max` The max number of items to keep in the cache. Must be
|
|
58
|
+
positive integer or `Infinity`, defaults to `Infinity` (ie,
|
|
59
|
+
limited only by TTL, not by item count).
|
|
58
60
|
* `ttl` The max time in ms to store items. Overridable on the `set()`
|
|
59
|
-
method.
|
|
61
|
+
method. Must be a positive integer or `Infinity` (see note
|
|
62
|
+
below about immortality hazards). If `undefined` in
|
|
63
|
+
constructor, then a TTL _must_ be provided in each `set()`
|
|
64
|
+
call.
|
|
60
65
|
* `updateAgeOnGet` Should the age of an item be updated when it is
|
|
61
66
|
retrieved? Defaults to `false`. Overridable on the `get()` method.
|
|
62
67
|
* `noUpdateTTL` Should setting a new value for an existing key leave the
|
|
@@ -104,6 +109,13 @@ If `updateAgeOnGet` is `true`, then re-add the item into the cache with the
|
|
|
104
109
|
updated `ttl` value. Both options default to the settings on the
|
|
105
110
|
constructor.
|
|
106
111
|
|
|
112
|
+
Note that using `updateAgeOnGet` _can_ effectively simulate a
|
|
113
|
+
"least-recently-used" type of algorithm, by repeatedly updating
|
|
114
|
+
the TTL of items as they are used. However, if you find yourself
|
|
115
|
+
doing this, consider using
|
|
116
|
+
[`lru-cache`](http://npm.im/lru-cache), as it is much more
|
|
117
|
+
optimized for an LRU use case.
|
|
118
|
+
|
|
107
119
|
### `cache.getRemainingTTL(key)`
|
|
108
120
|
|
|
109
121
|
Return the remaining time before an item expires. Returns `0` if the item
|
|
@@ -188,3 +200,17 @@ the current time.
|
|
|
188
200
|
Thus, the `start` time doesn't need to be tracked, only the expiration
|
|
189
201
|
time. When an item age is updated (either explicitly on `get()`, or by
|
|
190
202
|
setting to a new value), it is deleted and re-inserted.
|
|
203
|
+
|
|
204
|
+
## Immortality Hazards
|
|
205
|
+
|
|
206
|
+
It is possible to set a TTL of `Infinity`, in which case an item
|
|
207
|
+
will never expire. As it does not expire, its TTL is not
|
|
208
|
+
tracked, and `getRemainingTTL()` will return `Infinity` for that
|
|
209
|
+
key.
|
|
210
|
+
|
|
211
|
+
If you do this, then the item will never be purged. Create
|
|
212
|
+
enough immortal values, and the cache will grow to consume all
|
|
213
|
+
available memory. If find yourself doing this, it's _probably_
|
|
214
|
+
better to use a different data structure, such as a `Map` or
|
|
215
|
+
plain old object to store values, as it will have better
|
|
216
|
+
performance and the hazards will be more obvious.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Type definitions for ttlcache 1.0.0
|
|
2
|
+
// Project: https://github.com/isaacs/ttlcache
|
|
3
|
+
// Loosely based on @isaacs/lru-cache
|
|
4
|
+
// https://github.com/isaacs/node-lru-cache/blob/v7.10.1/index.d.ts
|
|
5
|
+
|
|
6
|
+
declare class TTLCache<K, V> implements Iterable<[K, V]> {
|
|
7
|
+
constructor(options?: TTLCache.Options<K, V>)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The total number of items held in the cache at the current moment.
|
|
11
|
+
*/
|
|
12
|
+
public readonly size: number
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Add a value to the cache.
|
|
16
|
+
*/
|
|
17
|
+
public set(key: K, value: V, options?: TTLCache.SetOptions): this
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Return a value from the cache.
|
|
21
|
+
* If the key is not found, `get()` will return `undefined`.
|
|
22
|
+
* This can be confusing when setting values specifically to `undefined`,
|
|
23
|
+
* as in `cache.set(key, undefined)`. Use `cache.has()` to determine
|
|
24
|
+
* whether a key is present in the cache at all.
|
|
25
|
+
*/
|
|
26
|
+
public get<T = V>(
|
|
27
|
+
key: K,
|
|
28
|
+
options?: TTLCache.GetOptions
|
|
29
|
+
): T | undefined
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a key is in the cache.
|
|
33
|
+
* Will return false if the item is stale, even though it is technically
|
|
34
|
+
* in the cache.
|
|
35
|
+
*/
|
|
36
|
+
public has(key: K): boolean
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Deletes a key out of the cache.
|
|
40
|
+
* Returns true if the key was deleted, false otherwise.
|
|
41
|
+
*/
|
|
42
|
+
public delete(key: K): boolean
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Clear the cache entirely, throwing away all values.
|
|
46
|
+
*/
|
|
47
|
+
public clear(): void
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Delete any stale entries. Returns true if anything was removed, false
|
|
51
|
+
* otherwise.
|
|
52
|
+
*/
|
|
53
|
+
public purgeStale(): boolean
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return the remaining time before an item expires.
|
|
57
|
+
* Returns 0 if the item is not found in the cache or is already expired.
|
|
58
|
+
*/
|
|
59
|
+
public getRemainingTTL(key: K): number
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set the ttl explicitly to a value, defaulting to the TTL set on the ctor
|
|
63
|
+
*/
|
|
64
|
+
public setTTL(key: K, ttl?: number): void
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Return a generator yielding `[key, value]` pairs, from soonest expiring
|
|
68
|
+
* to latest expiring. (Items expiring at the same time are walked in insertion order.)
|
|
69
|
+
*/
|
|
70
|
+
public entries(): Generator<[K, V]>
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Return a generator yielding the keys in the cache,
|
|
74
|
+
* from soonest expiring to latest expiring.
|
|
75
|
+
*/
|
|
76
|
+
public keys(): Generator<K>
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return a generator yielding the values in the cache,
|
|
80
|
+
* from soonest expiring to latest expiring.
|
|
81
|
+
*/
|
|
82
|
+
public values(): Generator<V>
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Iterating over the cache itself yields the same results as
|
|
86
|
+
* `cache.entries()`
|
|
87
|
+
*/
|
|
88
|
+
public [Symbol.iterator](): Iterator<[K, V]>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
declare namespace TTLCache {
|
|
92
|
+
type DisposeReason = 'evict' | 'set' | 'delete'
|
|
93
|
+
|
|
94
|
+
type Disposer<K, V> = (
|
|
95
|
+
value: V,
|
|
96
|
+
key: K,
|
|
97
|
+
reason: DisposeReason
|
|
98
|
+
) => void
|
|
99
|
+
|
|
100
|
+
type TTLOptions = {
|
|
101
|
+
/**
|
|
102
|
+
* Max time in milliseconds for items to live in cache before they are
|
|
103
|
+
* considered stale. Note that stale items are NOT preemptively removed
|
|
104
|
+
* by default, and MAY live in the cache, contributing to max,
|
|
105
|
+
* long after they have expired.
|
|
106
|
+
*
|
|
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()
|
|
109
|
+
*/
|
|
110
|
+
ttl?: number
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Boolean flag to tell the cache to not update the TTL when
|
|
114
|
+
* setting a new value for an existing key (ie, when updating a value
|
|
115
|
+
* rather than inserting a new value). Note that the TTL value is
|
|
116
|
+
* _always_ set when adding a new entry into the cache.
|
|
117
|
+
*
|
|
118
|
+
* @default false
|
|
119
|
+
*/
|
|
120
|
+
noUpdateTTL?: boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type Options<K, V> = {
|
|
124
|
+
/**
|
|
125
|
+
* The number of items to keep.
|
|
126
|
+
*
|
|
127
|
+
* @default Infinity
|
|
128
|
+
*/
|
|
129
|
+
max?: number
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Update the age of items on cache.get(), renewing their TTL
|
|
133
|
+
*
|
|
134
|
+
* @default false
|
|
135
|
+
*/
|
|
136
|
+
updateAgeOnGet?: boolean
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Function that is called on items when they are dropped from the cache.
|
|
140
|
+
* This can be handy if you want to close file descriptors or do other
|
|
141
|
+
* cleanup tasks when items are no longer accessible. Called with `key,
|
|
142
|
+
* value`. It's called before actually removing the item from the
|
|
143
|
+
* internal cache, so it is *NOT* safe to re-add them.
|
|
144
|
+
* Use `disposeAfter` if you wish to dispose items after they have been
|
|
145
|
+
* full removed, when it is safe to add them back to the cache.
|
|
146
|
+
*/
|
|
147
|
+
dispose?: Disposer<K, V>
|
|
148
|
+
} & TTLOptions
|
|
149
|
+
|
|
150
|
+
type SetOptions = {
|
|
151
|
+
/**
|
|
152
|
+
* Set to true to suppress calling the dispose() function if the entry
|
|
153
|
+
* key is still accessible within the cache.
|
|
154
|
+
*
|
|
155
|
+
* @default false
|
|
156
|
+
*/
|
|
157
|
+
noDisposeOnSet?: boolean
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Do not update the TTL when overwriting an existing item.
|
|
161
|
+
*/
|
|
162
|
+
noUpdateTTL?: boolean
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Override the default TTL for this one set() operation.
|
|
166
|
+
* Required if a TTL was not set in the constructor options.
|
|
167
|
+
*/
|
|
168
|
+
ttl?: number
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type GetOptions = {
|
|
172
|
+
/**
|
|
173
|
+
* Update the age of item being retrieved.
|
|
174
|
+
*/
|
|
175
|
+
updateAgeOnGet?: boolean
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Set new TTL, applied only when `updateAgeOnGet` is true
|
|
179
|
+
*/
|
|
180
|
+
ttl?: number
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export = TTLCache
|
package/index.js
CHANGED
|
@@ -10,8 +10,10 @@ const maybeReqPerfHooks = fallback => {
|
|
|
10
10
|
return fallback
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
const
|
|
13
|
+
const timeProvider = maybeReqPerfHooks(Date)
|
|
14
|
+
const now = () => timeProvider.now()
|
|
14
15
|
const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n)
|
|
16
|
+
const isPosIntOrInf = n => n === Infinity || isPosInt(n)
|
|
15
17
|
|
|
16
18
|
class TTLCache {
|
|
17
19
|
constructor({
|
|
@@ -20,17 +22,19 @@ class TTLCache {
|
|
|
20
22
|
updateAgeOnGet = false,
|
|
21
23
|
noUpdateTTL = false,
|
|
22
24
|
dispose,
|
|
23
|
-
}) {
|
|
25
|
+
} = {}) {
|
|
24
26
|
// {[expirationTime]: [keys]}
|
|
25
27
|
this.expirations = Object.create(null)
|
|
26
28
|
// {key=>val}
|
|
27
29
|
this.data = new Map()
|
|
28
30
|
// {key=>expiration}
|
|
29
31
|
this.expirationMap = new Map()
|
|
30
|
-
if (ttl !== undefined && !
|
|
31
|
-
throw new TypeError(
|
|
32
|
+
if (ttl !== undefined && !isPosIntOrInf(ttl)) {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
'ttl must be positive integer or Infinity if set'
|
|
35
|
+
)
|
|
32
36
|
}
|
|
33
|
-
if (!
|
|
37
|
+
if (!isPosIntOrInf(max)) {
|
|
34
38
|
throw new TypeError('max must be positive integer or Infinity')
|
|
35
39
|
}
|
|
36
40
|
this.ttl = ttl
|
|
@@ -56,6 +60,33 @@ class TTLCache {
|
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
setTTL(key, ttl = this.ttl) {
|
|
64
|
+
const current = this.expirationMap.get(key)
|
|
65
|
+
if (current !== undefined) {
|
|
66
|
+
// remove from the expirations list, so it isn't purged
|
|
67
|
+
const exp = this.expirations[current]
|
|
68
|
+
if (!exp || exp.length <= 1) {
|
|
69
|
+
delete this.expirations[current]
|
|
70
|
+
} else {
|
|
71
|
+
this.expirations[current] = exp.filter(k => k !== key)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (ttl !== Infinity) {
|
|
76
|
+
const expiration = Math.floor(now() + ttl)
|
|
77
|
+
this.expirationMap.set(key, expiration)
|
|
78
|
+
if (!this.expirations[expiration]) {
|
|
79
|
+
const t = setTimeout(() => this.purgeStale(), ttl)
|
|
80
|
+
/* istanbul ignore else - affordance for non-node envs */
|
|
81
|
+
if (t.unref) t.unref()
|
|
82
|
+
this.expirations[expiration] = []
|
|
83
|
+
}
|
|
84
|
+
this.expirations[expiration].push(key)
|
|
85
|
+
} else {
|
|
86
|
+
this.expirationMap.set(key, Infinity)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
set(
|
|
60
91
|
key,
|
|
61
92
|
val,
|
|
@@ -65,52 +96,30 @@ class TTLCache {
|
|
|
65
96
|
noDisposeOnSet = this.noDisposeOnSet,
|
|
66
97
|
} = {}
|
|
67
98
|
) {
|
|
68
|
-
if (!
|
|
69
|
-
throw new TypeError('ttl must be positive integer')
|
|
99
|
+
if (!isPosIntOrInf(ttl)) {
|
|
100
|
+
throw new TypeError('ttl must be positive integer or Infinity')
|
|
70
101
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.dispose(oldValue, key, 'set')
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return this
|
|
85
|
-
} else {
|
|
86
|
-
// just delete from expirations list, since we're about to
|
|
87
|
-
// add to data and expirationsMap anyway
|
|
88
|
-
const exp = this.expirations[current]
|
|
89
|
-
if (!exp || exp.length <= 1) {
|
|
90
|
-
delete this.expirations[current]
|
|
91
|
-
} else {
|
|
92
|
-
this.expirations[current] = exp.filter(k => k !== key)
|
|
102
|
+
if (this.expirationMap.has(key)) {
|
|
103
|
+
if (!noUpdateTTL) {
|
|
104
|
+
this.setTTL(key, ttl)
|
|
105
|
+
}
|
|
106
|
+
// has old value
|
|
107
|
+
const oldValue = this.data.get(key)
|
|
108
|
+
if (oldValue !== val) {
|
|
109
|
+
this.data.set(key, val)
|
|
110
|
+
if (!noDisposeOnSet) {
|
|
111
|
+
this.dispose(oldValue, key, 'set')
|
|
93
112
|
}
|
|
94
113
|
}
|
|
114
|
+
} else {
|
|
115
|
+
this.setTTL(key, ttl)
|
|
116
|
+
this.data.set(key, val)
|
|
95
117
|
}
|
|
96
|
-
const expiration = Math.ceil(time + ttl)
|
|
97
|
-
this.expirationMap.set(key, expiration)
|
|
98
|
-
this.data.set(key, val)
|
|
99
|
-
if (!this.expirations[expiration]) {
|
|
100
|
-
const t = setTimeout(() => this.purgeStale(), ttl)
|
|
101
|
-
/* istanbul ignore else - affordance for non-node envs */
|
|
102
|
-
if (t.unref) t.unref()
|
|
103
|
-
this.expirations[expiration] = []
|
|
104
|
-
}
|
|
105
|
-
this.expirations[expiration].push(key)
|
|
106
118
|
|
|
107
119
|
while (this.size > this.max) {
|
|
108
120
|
this.purgeToCapacity()
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
if (!noDisposeOnSet && current && oldValue !== val) {
|
|
112
|
-
this.dispose(oldValue, key, 'set')
|
|
113
|
-
}
|
|
114
123
|
return this
|
|
115
124
|
}
|
|
116
125
|
|
|
@@ -120,8 +129,10 @@ class TTLCache {
|
|
|
120
129
|
|
|
121
130
|
getRemainingTTL(key) {
|
|
122
131
|
const expiration = this.expirationMap.get(key)
|
|
123
|
-
return expiration
|
|
124
|
-
?
|
|
132
|
+
return expiration === Infinity
|
|
133
|
+
? expiration
|
|
134
|
+
: expiration !== undefined
|
|
135
|
+
? Math.max(0, Math.ceil(expiration - now()))
|
|
125
136
|
: 0
|
|
126
137
|
}
|
|
127
138
|
|
|
@@ -131,11 +142,7 @@ class TTLCache {
|
|
|
131
142
|
) {
|
|
132
143
|
const val = this.data.get(key)
|
|
133
144
|
if (updateAgeOnGet) {
|
|
134
|
-
this.
|
|
135
|
-
noUpdateTTL: false,
|
|
136
|
-
noDisposeOnSet: true,
|
|
137
|
-
ttl,
|
|
138
|
-
})
|
|
145
|
+
this.setTTL(key, ttl)
|
|
139
146
|
}
|
|
140
147
|
return val
|
|
141
148
|
}
|
|
@@ -189,9 +196,9 @@ class TTLCache {
|
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
purgeStale() {
|
|
192
|
-
const n = now()
|
|
199
|
+
const n = Math.ceil(now())
|
|
193
200
|
for (const exp in this.expirations) {
|
|
194
|
-
if (exp > n) {
|
|
201
|
+
if (exp === 'Infinity' || exp > n) {
|
|
195
202
|
return
|
|
196
203
|
}
|
|
197
204
|
for (const key of this.expirations[exp]) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isaacs/ttlcache",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"files": [
|
|
5
|
-
"index.js"
|
|
5
|
+
"index.js",
|
|
6
|
+
"index.d.ts"
|
|
6
7
|
],
|
|
7
8
|
"main": "index.js",
|
|
8
9
|
"exports": {
|
|
@@ -28,7 +29,8 @@
|
|
|
28
29
|
"clock-mock": "^1.0.6",
|
|
29
30
|
"prettier": "^2.7.0",
|
|
30
31
|
"tap": "^16.0.1",
|
|
31
|
-
"ts-node": "^10.8.1"
|
|
32
|
+
"ts-node": "^10.8.1",
|
|
33
|
+
"typescript": "^4.7.3"
|
|
32
34
|
},
|
|
33
35
|
"engines": {
|
|
34
36
|
"node": ">=12"
|