@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.
Files changed (4) hide show
  1. package/README.md +43 -4
  2. package/index.d.ts +23 -8
  3. package/index.js +83 -36
  4. 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: TTLCache.Options<K, V>)
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, defaults to 0, which means "no TTL"
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: number
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
- * Set to true to suppress calling the dispose() function if the entry
152
- * key is still accessible within the cache.
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 items
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 maybeReqPerfHooks = fallback => {
7
- try {
8
- return require('perf_hooks').performance
9
- } catch (e) {
10
- return fallback
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 && !isPosInt(ttl)) {
31
- throw new TypeError('ttl must be positive integer if set')
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 (!isPosInt(max) && max !== Infinity) {
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 (key, ttl = this.ttl) {
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
- const expiration = Math.floor(now() + ttl)
72
- this.expirationMap.set(key, expiration)
73
- if (!this.expirations[expiration]) {
74
- const t = setTimeout(() => this.purgeStale(), ttl)
75
- /* istanbul ignore else - affordance for non-node envs */
76
- if (t.unref) t.unref()
77
- this.expirations[expiration] = []
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 (!isPosInt(ttl)) {
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 !== undefined
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 && exp.length <= 1) {
150
- delete this.expirations[current]
151
- } else {
152
- this.expirations[current] = exp.filter(k => k !== key)
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
- const val = this.data.get(key)
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
- const val = this.data.get(key)
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
- for (const key of this.expirations[exp]) {
195
- const val = this.data.get(key)
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
- delete this.expirations[exp]
245
+ }
246
+ if (this.size === 0) {
247
+ this.cancelTimers()
201
248
  }
202
249
  }
203
250
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isaacs/ttlcache",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts"