@isaacs/ttlcache 1.0.1 → 1.0.4
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 +39 -1
- package/index.js +67 -31
- package/package.json +28 -3
package/README.md
CHANGED
|
@@ -61,11 +61,22 @@ Create a new `TTLCache` object.
|
|
|
61
61
|
retrieved? Defaults to `false`. Overridable on the `get()` method.
|
|
62
62
|
* `noUpdateTTL` Should setting a new value for an existing key leave the
|
|
63
63
|
TTL unchanged? Defaults to `false`. Overridable on the `set()` method.
|
|
64
|
+
(Note that TTL is _always_ updated if the item is expired, since that is
|
|
65
|
+
treated as a new `set()` and the old item is no longer relevant.)
|
|
64
66
|
* `dispose` Method called with `(value, key, reason)` when an item is
|
|
65
67
|
removed from the cache. Called once item is fully removed from cache.
|
|
66
68
|
It is safe to re-add at this point, but note that adding when `reason` is
|
|
67
69
|
`'set'` can result in infinite recursion if `noDisponseOnSet` is not
|
|
68
70
|
specified.
|
|
71
|
+
|
|
72
|
+
Disposal reasons:
|
|
73
|
+
|
|
74
|
+
* `'stale'` TTL expired.
|
|
75
|
+
* `'set'` Overwritten with a new different value.
|
|
76
|
+
* `'evict'` Removed from the cache to stay within capacity limit.
|
|
77
|
+
* `'delete'` Explicitly deleted with `cache.delete()` or
|
|
78
|
+
`cache.clear()`
|
|
79
|
+
|
|
69
80
|
* `noDisposeOnSet` Do not call `dispose()` method when overwriting a key
|
|
70
81
|
with a new value. Defaults to `false`. Overridable on `set()` method.
|
|
71
82
|
|
|
@@ -113,7 +124,8 @@ Delete all items from the cache.
|
|
|
113
124
|
### `cache.entries()`
|
|
114
125
|
|
|
115
126
|
Return an iterator that walks through each `[key, value]` from soonest
|
|
116
|
-
expiring to latest expiring.
|
|
127
|
+
expiring to latest expiring. (Items expiring at the same time are walked
|
|
128
|
+
in insertion order.)
|
|
117
129
|
|
|
118
130
|
Default iteration method for the cache object.
|
|
119
131
|
|
|
@@ -150,3 +162,29 @@ automatically.
|
|
|
150
162
|
|
|
151
163
|
Called when an item is removed from the cache and should be disposed. Set
|
|
152
164
|
this on the constructor options.
|
|
165
|
+
|
|
166
|
+
## Algorithm
|
|
167
|
+
|
|
168
|
+
The cache uses two `Map` objects. The first maps item keys to their
|
|
169
|
+
expiration time, and the second maps item keys to their values. Then, a
|
|
170
|
+
null-prototype object uses the expiration time as keys, with the value
|
|
171
|
+
being an array of all the keys expiring at that time.
|
|
172
|
+
|
|
173
|
+
This leverages a few important features of modern JavaScript engines for
|
|
174
|
+
fairly good performance:
|
|
175
|
+
|
|
176
|
+
- `Map` objects are highly optimized for referring to arbitrary values by
|
|
177
|
+
arbitrary keys.
|
|
178
|
+
- Objects with solely integer-numeric keys are iterated in sorted numeric
|
|
179
|
+
order rather than insertion order, and insertions in the middle of the
|
|
180
|
+
key ordering are still very fast. This is true of all modern JS engines
|
|
181
|
+
tested at the time of this module's creation, but most particularly V8
|
|
182
|
+
(the engine in Node.js).
|
|
183
|
+
|
|
184
|
+
When it is time to prune, we can always walk the null-prototype object in
|
|
185
|
+
iteration order, deleting items until we come to the first key greater than
|
|
186
|
+
the current time.
|
|
187
|
+
|
|
188
|
+
Thus, the `start` time doesn't need to be tracked, only the expiration
|
|
189
|
+
time. When an item age is updated (either explicitly on `get()`, or by
|
|
190
|
+
setting to a new value), it is deleted and re-inserted.
|
package/index.js
CHANGED
|
@@ -3,18 +3,24 @@
|
|
|
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 maybeReqPerfHooks =
|
|
6
|
+
const maybeReqPerfHooks = fallback => {
|
|
7
7
|
try {
|
|
8
8
|
return require('perf_hooks').performance
|
|
9
9
|
} catch (e) {
|
|
10
10
|
return fallback
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
const {now} = maybeReqPerfHooks(Date)
|
|
13
|
+
const { now } = maybeReqPerfHooks(Date)
|
|
14
14
|
const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n)
|
|
15
15
|
|
|
16
16
|
class TTLCache {
|
|
17
|
-
constructor
|
|
17
|
+
constructor({
|
|
18
|
+
max = Infinity,
|
|
19
|
+
ttl,
|
|
20
|
+
updateAgeOnGet = false,
|
|
21
|
+
noUpdateTTL = false,
|
|
22
|
+
dispose,
|
|
23
|
+
}) {
|
|
18
24
|
// {[expirationTime]: [keys]}
|
|
19
25
|
this.expirations = Object.create(null)
|
|
20
26
|
// {key=>val}
|
|
@@ -29,6 +35,8 @@ class TTLCache {
|
|
|
29
35
|
}
|
|
30
36
|
this.ttl = ttl
|
|
31
37
|
this.max = max
|
|
38
|
+
this.updateAgeOnGet = updateAgeOnGet
|
|
39
|
+
this.noUpdateTTL = noUpdateTTL
|
|
32
40
|
if (dispose !== undefined) {
|
|
33
41
|
if (typeof dispose !== 'function') {
|
|
34
42
|
throw new TypeError('dispose must be function if set')
|
|
@@ -37,8 +45,9 @@ class TTLCache {
|
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
clear
|
|
41
|
-
const entries =
|
|
48
|
+
clear() {
|
|
49
|
+
const entries =
|
|
50
|
+
this.dispose !== TTLCache.prototype.dispose ? [...this] : []
|
|
42
51
|
this.data.clear()
|
|
43
52
|
this.expirationMap.clear()
|
|
44
53
|
this.expirations = Object.create(null)
|
|
@@ -47,14 +56,25 @@ class TTLCache {
|
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
set
|
|
59
|
+
set(
|
|
60
|
+
key,
|
|
61
|
+
val,
|
|
62
|
+
{
|
|
63
|
+
ttl = this.ttl,
|
|
64
|
+
noUpdateTTL = this.noUpdateTTL,
|
|
65
|
+
noDisposeOnSet = this.noDisposeOnSet,
|
|
66
|
+
} = {}
|
|
67
|
+
) {
|
|
51
68
|
if (!isPosInt(ttl)) {
|
|
52
69
|
throw new TypeError('ttl must be positive integer')
|
|
53
70
|
}
|
|
54
71
|
const current = this.expirationMap.get(key)
|
|
72
|
+
const time = now()
|
|
73
|
+
const oldValue =
|
|
74
|
+
current === undefined ? undefined : this.data.get(key)
|
|
55
75
|
if (current !== undefined) {
|
|
56
|
-
|
|
57
|
-
if (noUpdateTTL) {
|
|
76
|
+
// we aren't updating the ttl, so just set the data
|
|
77
|
+
if (noUpdateTTL && current > time) {
|
|
58
78
|
if (oldValue !== val) {
|
|
59
79
|
this.data.set(key, val)
|
|
60
80
|
if (!noDisposeOnSet) {
|
|
@@ -63,13 +83,17 @@ class TTLCache {
|
|
|
63
83
|
}
|
|
64
84
|
return this
|
|
65
85
|
} else {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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)
|
|
93
|
+
}
|
|
70
94
|
}
|
|
71
95
|
}
|
|
72
|
-
const expiration = Math.ceil(
|
|
96
|
+
const expiration = Math.ceil(time + ttl)
|
|
73
97
|
this.expirationMap.set(key, expiration)
|
|
74
98
|
this.data.set(key, val)
|
|
75
99
|
if (!this.expirations[expiration]) {
|
|
@@ -79,52 +103,64 @@ class TTLCache {
|
|
|
79
103
|
this.expirations[expiration] = []
|
|
80
104
|
}
|
|
81
105
|
this.expirations[expiration].push(key)
|
|
106
|
+
|
|
82
107
|
while (this.size > this.max) {
|
|
83
108
|
this.purgeToCapacity()
|
|
84
109
|
}
|
|
110
|
+
|
|
111
|
+
if (!noDisposeOnSet && current && oldValue !== val) {
|
|
112
|
+
this.dispose(oldValue, key, 'set')
|
|
113
|
+
}
|
|
85
114
|
return this
|
|
86
115
|
}
|
|
87
116
|
|
|
88
|
-
has
|
|
117
|
+
has(key) {
|
|
89
118
|
return this.data.has(key)
|
|
90
119
|
}
|
|
91
120
|
|
|
92
|
-
getRemainingTTL
|
|
121
|
+
getRemainingTTL(key) {
|
|
93
122
|
const expiration = this.expirationMap.get(key)
|
|
94
|
-
return expiration !== undefined
|
|
123
|
+
return expiration !== undefined
|
|
124
|
+
? Math.max(0, expiration - now())
|
|
125
|
+
: 0
|
|
95
126
|
}
|
|
96
127
|
|
|
97
|
-
get
|
|
128
|
+
get(
|
|
129
|
+
key,
|
|
130
|
+
{ updateAgeOnGet = this.updateAgeOnGet, ttl = this.ttl } = {}
|
|
131
|
+
) {
|
|
98
132
|
const val = this.data.get(key)
|
|
99
133
|
if (updateAgeOnGet) {
|
|
100
|
-
this.set(key, val, {
|
|
134
|
+
this.set(key, val, {
|
|
135
|
+
noUpdateTTL: false,
|
|
136
|
+
noDisposeOnSet: true,
|
|
137
|
+
ttl,
|
|
138
|
+
})
|
|
101
139
|
}
|
|
102
140
|
return val
|
|
103
141
|
}
|
|
104
142
|
|
|
105
|
-
dispose
|
|
143
|
+
dispose(_, __) {}
|
|
106
144
|
|
|
107
|
-
delete
|
|
145
|
+
delete(key) {
|
|
108
146
|
const current = this.expirationMap.get(key)
|
|
109
147
|
if (current !== undefined) {
|
|
110
148
|
const value = this.data.get(key)
|
|
111
149
|
this.data.delete(key)
|
|
112
150
|
this.expirationMap.delete(key)
|
|
113
151
|
const exp = this.expirations[current]
|
|
114
|
-
if (exp.length
|
|
152
|
+
if (exp && exp.length <= 1) {
|
|
115
153
|
delete this.expirations[current]
|
|
116
154
|
} else {
|
|
117
155
|
this.expirations[current] = exp.filter(k => k !== key)
|
|
118
156
|
}
|
|
119
|
-
|
|
120
|
-
this.dispose(value, key, reason)
|
|
121
|
-
}
|
|
157
|
+
this.dispose(value, key, 'delete')
|
|
122
158
|
return true
|
|
123
159
|
}
|
|
124
160
|
return false
|
|
125
161
|
}
|
|
126
162
|
|
|
127
|
-
purgeToCapacity
|
|
163
|
+
purgeToCapacity() {
|
|
128
164
|
for (const exp in this.expirations) {
|
|
129
165
|
const keys = this.expirations[exp]
|
|
130
166
|
if (this.size - keys.length >= this.max) {
|
|
@@ -148,11 +184,11 @@ class TTLCache {
|
|
|
148
184
|
}
|
|
149
185
|
}
|
|
150
186
|
|
|
151
|
-
get size
|
|
187
|
+
get size() {
|
|
152
188
|
return this.data.size
|
|
153
189
|
}
|
|
154
190
|
|
|
155
|
-
purgeStale
|
|
191
|
+
purgeStale() {
|
|
156
192
|
const n = now()
|
|
157
193
|
for (const exp in this.expirations) {
|
|
158
194
|
if (exp > n) {
|
|
@@ -168,28 +204,28 @@ class TTLCache {
|
|
|
168
204
|
}
|
|
169
205
|
}
|
|
170
206
|
|
|
171
|
-
*entries
|
|
207
|
+
*entries() {
|
|
172
208
|
for (const exp in this.expirations) {
|
|
173
209
|
for (const key of this.expirations[exp]) {
|
|
174
210
|
yield [key, this.data.get(key)]
|
|
175
211
|
}
|
|
176
212
|
}
|
|
177
213
|
}
|
|
178
|
-
*keys
|
|
214
|
+
*keys() {
|
|
179
215
|
for (const exp in this.expirations) {
|
|
180
216
|
for (const key of this.expirations[exp]) {
|
|
181
217
|
yield key
|
|
182
218
|
}
|
|
183
219
|
}
|
|
184
220
|
}
|
|
185
|
-
*values
|
|
221
|
+
*values() {
|
|
186
222
|
for (const exp in this.expirations) {
|
|
187
223
|
for (const key of this.expirations[exp]) {
|
|
188
224
|
yield this.data.get(key)
|
|
189
225
|
}
|
|
190
226
|
}
|
|
191
227
|
}
|
|
192
|
-
[Symbol.iterator]
|
|
228
|
+
[Symbol.iterator]() {
|
|
193
229
|
return this.entries()
|
|
194
230
|
}
|
|
195
231
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isaacs/ttlcache",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js"
|
|
6
6
|
],
|
|
@@ -23,10 +23,35 @@
|
|
|
23
23
|
"prepublishOnly": "git push origin --follow-tags"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"
|
|
27
|
-
"tap": "^
|
|
26
|
+
"@types/node": "^17.0.42",
|
|
27
|
+
"@types/tap": "^15.0.7",
|
|
28
|
+
"clock-mock": "^1.0.6",
|
|
29
|
+
"prettier": "^2.7.0",
|
|
30
|
+
"tap": "^16.0.1",
|
|
31
|
+
"ts-node": "^10.8.1"
|
|
28
32
|
},
|
|
29
33
|
"engines": {
|
|
30
34
|
"node": ">=12"
|
|
35
|
+
},
|
|
36
|
+
"tap": {
|
|
37
|
+
"nyc-arg": [
|
|
38
|
+
"--include=index.js"
|
|
39
|
+
],
|
|
40
|
+
"node-arg": [
|
|
41
|
+
"--require",
|
|
42
|
+
"ts-node/register"
|
|
43
|
+
],
|
|
44
|
+
"ts": false
|
|
45
|
+
},
|
|
46
|
+
"prettier": {
|
|
47
|
+
"semi": false,
|
|
48
|
+
"printWidth": 70,
|
|
49
|
+
"tabWidth": 2,
|
|
50
|
+
"useTabs": false,
|
|
51
|
+
"singleQuote": true,
|
|
52
|
+
"jsxSingleQuote": false,
|
|
53
|
+
"bracketSameLine": true,
|
|
54
|
+
"arrowParens": "avoid",
|
|
55
|
+
"endOfLine": "lf"
|
|
31
56
|
}
|
|
32
57
|
}
|