@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.
Files changed (4) hide show
  1. package/README.md +28 -2
  2. package/index.d.ts +184 -0
  3. package/index.js +58 -51
  4. 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 { now } = maybeReqPerfHooks(Date)
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 && !isPosInt(ttl)) {
31
- throw new TypeError('ttl must be positive integer if set')
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 (!isPosInt(max) && max !== Infinity) {
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 (!isPosInt(ttl)) {
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
- const current = this.expirationMap.get(key)
72
- const time = now()
73
- const oldValue =
74
- current === undefined ? undefined : this.data.get(key)
75
- if (current !== undefined) {
76
- // we aren't updating the ttl, so just set the data
77
- if (noUpdateTTL && current > time) {
78
- if (oldValue !== val) {
79
- this.data.set(key, val)
80
- if (!noDisposeOnSet) {
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 !== undefined
124
- ? Math.max(0, expiration - now())
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.set(key, val, {
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.4",
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"