@isaacs/ttlcache 1.2.2 → 1.4.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.
Files changed (4) hide show
  1. package/README.md +32 -8
  2. package/index.d.ts +43 -1
  3. package/index.js +60 -19
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -53,9 +53,9 @@ where the `setTimeout` method does not return an object with an
53
53
  `unref()` method, the process will stay open as long as any
54
54
  unexpired entry exists in the cache.
55
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.
56
+ You may call `cache.cancelTimer()` to clear the timeout and
57
+ allow the process to exit normally. Be advised that canceling the
58
+ timer in this way will of course prevent anything from expiring.
59
59
 
60
60
  ## API
61
61
 
@@ -63,7 +63,7 @@ timeouts and allow the process to exit normally.
63
63
 
64
64
  Default export is the `TTLCache` class.
65
65
 
66
- ### `new TTLCache({ ttl, max = Infinty, updateAgeOnGet = false, noUpdateTTL = false, noDisposeOnSet = false })`
66
+ ### `new TTLCache({ ttl, max = Infinty, updateAgeOnGet = false, checkAgeOnGet = false, noUpdateTTL = false, noDisposeOnSet = false })`
67
67
 
68
68
  Create a new `TTLCache` object.
69
69
 
@@ -77,6 +77,11 @@ Create a new `TTLCache` object.
77
77
  call.
78
78
  * `updateAgeOnGet` Should the age of an item be updated when it is
79
79
  retrieved? Defaults to `false`. Overridable on the `get()` method.
80
+ * `checkAgeOnGet` Check the TTL whenever an item is retrieved
81
+ with `get()`. If the item is past its ttl, but the timer has
82
+ not yet fired, then delete it and return undefined. By default,
83
+ the cache will return a value if it has one, even if it is
84
+ technically beyond its TTL.
80
85
  * `noUpdateTTL` Should setting a new value for an existing key leave the
81
86
  TTL unchanged? Defaults to `false`. Overridable on the `set()` method.
82
87
  (Note that TTL is _always_ updated if the item is expired, since that is
@@ -113,14 +118,18 @@ Store a value in the cache for the specified time.
113
118
 
114
119
  Returns the cache object.
115
120
 
116
- ### `cache.get(key, {updateAgeOnGet, ttl} = {})`
121
+ ### `cache.get(key, {updateAgeOnGet, checkAgeOnGet, ttl} = {})`
117
122
 
118
123
  Get an item stored in the cache. Returns `undefined` if the item is not in
119
124
  the cache (including if it has expired and been purged).
120
125
 
121
- If `updateAgeOnGet` is `true`, then re-add the item into the cache with the
122
- updated `ttl` value. Both options default to the settings on the
123
- constructor.
126
+ If `updateAgeOnGet` is `true`, then re-add the item into the
127
+ cache with the updated `ttl` value. All options default to the
128
+ settings on the constructor.
129
+
130
+ If `checkAgeOnGet`, then an item will be deleted if it is found
131
+ to be beyond its TTL, which can happen if the setTimeout timer
132
+ has not yet fired to trigger its expiration.
124
133
 
125
134
  Note that using `updateAgeOnGet` _can_ effectively simulate a
126
135
  "least-recently-used" type of algorithm, by repeatedly updating
@@ -164,6 +173,14 @@ latest expiring.
164
173
  Return an iterator that walks through each `value` from soonest expiring to
165
174
  latest expiring.
166
175
 
176
+ ### `cache.cancelTimer()`
177
+
178
+ Clear the internal timer, and stop automatically expiring items
179
+ when their TTL expires.
180
+
181
+ This allows the process to exit normally on Deno and other
182
+ platforms that lack Node's `Timer.unref()` method.
183
+
167
184
  ## Internal Methods
168
185
 
169
186
  You should not ever call these, they are managed automatically.
@@ -188,6 +205,13 @@ automatically.
188
205
  Called when an item is removed from the cache and should be disposed. Set
189
206
  this on the constructor options.
190
207
 
208
+ ### `setTimer`
209
+
210
+ **Internal**
211
+
212
+ Called when an with a ttl is added. This ensures that only one timer
213
+ is setup at once. Called automatically.
214
+
191
215
  ## Algorithm
192
216
 
193
217
  The cache uses two `Map` objects. The first maps item keys to their
package/index.d.ts CHANGED
@@ -6,6 +6,13 @@
6
6
  declare class TTLCache<K, V> implements Iterable<[K, V]> {
7
7
  constructor(options?: TTLCache.Options<K, V>)
8
8
 
9
+ ttl: number
10
+ max: number
11
+ updateAgeOnGet: boolean
12
+ checkAgeOnGet: boolean
13
+ noUpdateTTL: boolean
14
+ noDisposeOnSet: boolean
15
+
9
16
  /**
10
17
  * The total number of items held in the cache at the current moment.
11
18
  */
@@ -86,6 +93,13 @@ declare class TTLCache<K, V> implements Iterable<[K, V]> {
86
93
  * `cache.entries()`
87
94
  */
88
95
  public [Symbol.iterator](): Iterator<[K, V]>
96
+
97
+ /**
98
+ * Cancel the timer and stop automatically expiring entries.
99
+ * This allows the process to gracefully exit where Timer.unref()
100
+ * is not available.
101
+ */
102
+ public cancelTimer(): void
89
103
  }
90
104
 
91
105
  declare namespace TTLCache {
@@ -135,6 +149,19 @@ declare namespace TTLCache {
135
149
  */
136
150
  updateAgeOnGet?: boolean
137
151
 
152
+ /**
153
+ * In the event that an item's expiration timer hasn't yet fired,
154
+ * and an attempt is made to get() it, then return undefined and
155
+ * delete it, rather than returning the cached value.
156
+ *
157
+ * By default, items are only expired when their timer fires, so there's
158
+ * a bit of a "best effort" expiration, and the cache will return a value
159
+ * if it has one, even if it's technically stale.
160
+ *
161
+ * @default false
162
+ */
163
+ checkAgeOnGet?: boolean
164
+
138
165
  /**
139
166
  * Do not call dispose() function when overwriting a key with a new value
140
167
  *
@@ -175,10 +202,25 @@ declare namespace TTLCache {
175
202
 
176
203
  type GetOptions = {
177
204
  /**
178
- * Update the age of item being retrieved.
205
+ * Update the age of items on cache.get(), renewing their TTL
206
+ *
207
+ * @default false
179
208
  */
180
209
  updateAgeOnGet?: boolean
181
210
 
211
+ /**
212
+ * In the event that an item's expiration timer hasn't yet fired,
213
+ * and an attempt is made to get() it, then return undefined and
214
+ * delete it, rather than returning the cached value.
215
+ *
216
+ * By default, items are only expired when their timer fires, so there's
217
+ * a bit of a "best effort" expiration, and the cache will return a value
218
+ * if it has one, even if it's technically stale.
219
+ *
220
+ * @default false
221
+ */
222
+ checkAgeOnGet?: boolean
223
+
182
224
  /**
183
225
  * Set new TTL, applied only when `updateAgeOnGet` is true
184
226
  */
package/index.js CHANGED
@@ -20,6 +20,7 @@ class TTLCache {
20
20
  max = Infinity,
21
21
  ttl,
22
22
  updateAgeOnGet = false,
23
+ checkAgeOnGet = false,
23
24
  noUpdateTTL = false,
24
25
  dispose,
25
26
  noDisposeOnSet = false,
@@ -40,9 +41,10 @@ class TTLCache {
40
41
  }
41
42
  this.ttl = ttl
42
43
  this.max = max
43
- this.updateAgeOnGet = updateAgeOnGet
44
- this.noUpdateTTL = noUpdateTTL
45
- this.noDisposeOnSet = noDisposeOnSet
44
+ this.updateAgeOnGet = !!updateAgeOnGet
45
+ this.checkAgeOnGet = !!checkAgeOnGet
46
+ this.noUpdateTTL = !!noUpdateTTL
47
+ this.noDisposeOnSet = !!noDisposeOnSet
46
48
  if (dispose !== undefined) {
47
49
  if (typeof dispose !== 'function') {
48
50
  throw new TypeError('dispose must be function if set')
@@ -50,19 +52,56 @@ class TTLCache {
50
52
  this.dispose = dispose
51
53
  }
52
54
 
53
- this.timers = new Set()
55
+ this.timer = undefined
56
+ this.timerExpiration = undefined
57
+ }
58
+
59
+ setTimer(expiration, ttl) {
60
+ if (this.timerExpiration < expiration) {
61
+ return
62
+ }
63
+
64
+ if (this.timer) {
65
+ clearTimeout(this.timer)
66
+ }
67
+
68
+ const t = setTimeout(() => {
69
+ this.timer = undefined
70
+ this.timerExpiration = undefined
71
+ this.purgeStale()
72
+ for (const exp in this.expirations) {
73
+ this.setTimer(exp, exp - now())
74
+ break
75
+ }
76
+ }, ttl)
77
+
78
+ /* istanbul ignore else - affordance for non-node envs */
79
+ if (t.unref) t.unref()
80
+
81
+ this.timerExpiration = expiration
82
+ this.timer = t
54
83
  }
55
84
 
56
85
  // hang onto the timer so we can clearTimeout if all items
57
86
  // are deleted. Deno doesn't have Timer.unref(), so it
58
87
  // hangs otherwise.
59
- cancelTimers() {
60
- for (const t of this.timers) {
61
- clearTimeout(t)
62
- this.timers.delete(t)
88
+ cancelTimer() {
89
+ if (this.timer) {
90
+ clearTimeout(this.timer)
91
+ this.timerExpiration = undefined
92
+ this.timer = undefined
63
93
  }
64
94
  }
65
95
 
96
+ /* istanbul ignore next */
97
+ cancelTimers() {
98
+ process.emitWarning(
99
+ 'TTLCache.cancelTimers has been renamed to ' +
100
+ 'TTLCache.cancelTimer (no "s"), and will be removed in the next ' +
101
+ 'major version update'
102
+ )
103
+ return this.cancelTimer()
104
+ }
66
105
 
67
106
  clear() {
68
107
  const entries =
@@ -70,7 +109,7 @@ class TTLCache {
70
109
  this.data.clear()
71
110
  this.expirationMap.clear()
72
111
  // no need for any purging now
73
- this.cancelTimers()
112
+ this.cancelTimer()
74
113
  this.expirations = Object.create(null)
75
114
  for (const [key, val] of entries) {
76
115
  this.dispose(val, key, 'delete')
@@ -93,14 +132,8 @@ class TTLCache {
93
132
  const expiration = Math.floor(now() + ttl)
94
133
  this.expirationMap.set(key, expiration)
95
134
  if (!this.expirations[expiration]) {
96
- const t = setTimeout(() => {
97
- this.timers.delete(t)
98
- this.purgeStale()
99
- }, ttl)
100
- /* istanbul ignore else - affordance for non-node envs */
101
- if (t.unref) t.unref()
102
- this.timers.add(t)
103
135
  this.expirations[expiration] = []
136
+ this.setTimer(expiration, ttl)
104
137
  }
105
138
  this.expirations[expiration].push(key)
106
139
  } else {
@@ -159,9 +192,17 @@ class TTLCache {
159
192
 
160
193
  get(
161
194
  key,
162
- { updateAgeOnGet = this.updateAgeOnGet, ttl = this.ttl } = {}
195
+ {
196
+ updateAgeOnGet = this.updateAgeOnGet,
197
+ ttl = this.ttl,
198
+ checkAgeOnGet = this.checkAgeOnGet,
199
+ } = {}
163
200
  ) {
164
201
  const val = this.data.get(key)
202
+ if (checkAgeOnGet && this.getRemainingTTL(key) === 0) {
203
+ this.delete(key)
204
+ return undefined
205
+ }
165
206
  if (updateAgeOnGet) {
166
207
  this.setTTL(key, ttl)
167
208
  }
@@ -186,7 +227,7 @@ class TTLCache {
186
227
  }
187
228
  this.dispose(value, key, 'delete')
188
229
  if (this.size === 0) {
189
- this.cancelTimers()
230
+ this.cancelTimer()
190
231
  }
191
232
  return true
192
233
  }
@@ -246,7 +287,7 @@ class TTLCache {
246
287
  }
247
288
  }
248
289
  if (this.size === 0) {
249
- this.cancelTimers()
290
+ this.cancelTimer()
250
291
  }
251
292
  }
252
293
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isaacs/ttlcache",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts"