@isaacs/ttlcache 1.0.1 → 1.0.2
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 +21 -12
- package/package.json +1 -1
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
|
@@ -52,9 +52,11 @@ class TTLCache {
|
|
|
52
52
|
throw new TypeError('ttl must be positive integer')
|
|
53
53
|
}
|
|
54
54
|
const current = this.expirationMap.get(key)
|
|
55
|
+
const time = now()
|
|
56
|
+
const oldValue = current === undefined ? undefined : this.data.get(key)
|
|
55
57
|
if (current !== undefined) {
|
|
56
|
-
|
|
57
|
-
if (noUpdateTTL) {
|
|
58
|
+
// we aren't updating the ttl, so just set the data
|
|
59
|
+
if (noUpdateTTL && current > time) {
|
|
58
60
|
if (oldValue !== val) {
|
|
59
61
|
this.data.set(key, val)
|
|
60
62
|
if (!noDisposeOnSet) {
|
|
@@ -63,13 +65,17 @@ class TTLCache {
|
|
|
63
65
|
}
|
|
64
66
|
return this
|
|
65
67
|
} else {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// just delete from expirations list, since we're about to
|
|
69
|
+
// add to data and expirationsMap anyway
|
|
70
|
+
const exp = this.expirations[current]
|
|
71
|
+
if (!exp || exp.length <= 1) {
|
|
72
|
+
delete this.expirations[current]
|
|
73
|
+
} else {
|
|
74
|
+
this.expirations[current] = exp.filter(k => k !== key)
|
|
75
|
+
}
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
|
-
const expiration = Math.ceil(
|
|
78
|
+
const expiration = Math.ceil(time + ttl)
|
|
73
79
|
this.expirationMap.set(key, expiration)
|
|
74
80
|
this.data.set(key, val)
|
|
75
81
|
if (!this.expirations[expiration]) {
|
|
@@ -79,9 +85,14 @@ class TTLCache {
|
|
|
79
85
|
this.expirations[expiration] = []
|
|
80
86
|
}
|
|
81
87
|
this.expirations[expiration].push(key)
|
|
88
|
+
|
|
82
89
|
while (this.size > this.max) {
|
|
83
90
|
this.purgeToCapacity()
|
|
84
91
|
}
|
|
92
|
+
|
|
93
|
+
if (!noDisposeOnSet && current && oldValue !== val) {
|
|
94
|
+
this.dispose(oldValue, key, 'set')
|
|
95
|
+
}
|
|
85
96
|
return this
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -104,21 +115,19 @@ class TTLCache {
|
|
|
104
115
|
|
|
105
116
|
dispose (value, key) {}
|
|
106
117
|
|
|
107
|
-
delete (key
|
|
118
|
+
delete (key) {
|
|
108
119
|
const current = this.expirationMap.get(key)
|
|
109
120
|
if (current !== undefined) {
|
|
110
121
|
const value = this.data.get(key)
|
|
111
122
|
this.data.delete(key)
|
|
112
123
|
this.expirationMap.delete(key)
|
|
113
124
|
const exp = this.expirations[current]
|
|
114
|
-
if (exp.length
|
|
125
|
+
if (exp && exp.length <= 1) {
|
|
115
126
|
delete this.expirations[current]
|
|
116
127
|
} else {
|
|
117
128
|
this.expirations[current] = exp.filter(k => k !== key)
|
|
118
129
|
}
|
|
119
|
-
|
|
120
|
-
this.dispose(value, key, reason)
|
|
121
|
-
}
|
|
130
|
+
this.dispose(value, key, 'delete')
|
|
122
131
|
return true
|
|
123
132
|
}
|
|
124
133
|
return false
|