@isaacs/ttlcache 1.0.3 → 1.1.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 (3) hide show
  1. package/index.d.ts +174 -0
  2. package/index.js +72 -50
  3. package/package.json +31 -4
package/index.d.ts ADDED
@@ -0,0 +1,174 @@
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, defaults to 0, which means "no TTL"
108
+ */
109
+ ttl: number
110
+
111
+ /**
112
+ * Boolean flag to tell the cache to not update the TTL when
113
+ * setting a new value for an existing key (ie, when updating a value
114
+ * rather than inserting a new value). Note that the TTL value is
115
+ * _always_ set when adding a new entry into the cache.
116
+ *
117
+ * @default false
118
+ */
119
+ noUpdateTTL?: boolean
120
+ }
121
+
122
+ type Options<K, V> = {
123
+ /**
124
+ * The number of items to keep.
125
+ *
126
+ * @default Infinity
127
+ */
128
+ max?: number
129
+
130
+ /**
131
+ * Update the age of items on cache.get(), renewing their TTL
132
+ *
133
+ * @default false
134
+ */
135
+ updateAgeOnGet?: boolean
136
+
137
+ /**
138
+ * Function that is called on items when they are dropped from the cache.
139
+ * This can be handy if you want to close file descriptors or do other
140
+ * cleanup tasks when items are no longer accessible. Called with `key,
141
+ * value`. It's called before actually removing the item from the
142
+ * internal cache, so it is *NOT* safe to re-add them.
143
+ * Use `disposeAfter` if you wish to dispose items after they have been
144
+ * full removed, when it is safe to add them back to the cache.
145
+ */
146
+ dispose?: Disposer<K, V>
147
+ } & TTLOptions
148
+
149
+ type SetOptions = {
150
+ /**
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
155
+ */
156
+ noDisposeOnSet?: boolean
157
+ noUpdateTTL?: boolean
158
+ ttl?: number
159
+ }
160
+
161
+ type GetOptions = {
162
+ /**
163
+ * Update the age of items
164
+ */
165
+ updateAgeOnGet?: boolean
166
+
167
+ /**
168
+ * Set new TTL, applied only when `updateAgeOnGet` is true
169
+ */
170
+ ttl?: number
171
+ }
172
+ }
173
+
174
+ export = TTLCache
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 = (fallback) => {
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 ({ max = Infinity, ttl, updateAgeOnGet = false, noUpdateTTL = false, dispose }) {
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,8 +35,8 @@ class TTLCache {
29
35
  }
30
36
  this.ttl = ttl
31
37
  this.max = max
32
- this.updateAgeOnGet = updateAgeOnGet;
33
- this.noUpdateTTL = noUpdateTTL;
38
+ this.updateAgeOnGet = updateAgeOnGet
39
+ this.noUpdateTTL = noUpdateTTL
34
40
  if (dispose !== undefined) {
35
41
  if (typeof dispose !== 'function') {
36
42
  throw new TypeError('dispose must be function if set')
@@ -39,8 +45,9 @@ class TTLCache {
39
45
  }
40
46
  }
41
47
 
42
- clear () {
43
- const entries = this.dispose !== TTLCache.prototype.dispose ? [...this] : []
48
+ clear() {
49
+ const entries =
50
+ this.dispose !== TTLCache.prototype.dispose ? [...this] : []
44
51
  this.data.clear()
45
52
  this.expirationMap.clear()
46
53
  this.expirations = Object.create(null)
@@ -49,37 +56,20 @@ class TTLCache {
49
56
  }
50
57
  }
51
58
 
52
- set (key, val, { ttl = this.ttl, noUpdateTTL = this.noUpdateTTL, noDisposeOnSet = this.noDisposeOnSet } = {}) {
53
- if (!isPosInt(ttl)) {
54
- throw new TypeError('ttl must be positive integer')
55
- }
59
+ setTTL (key, ttl = this.ttl) {
56
60
  const current = this.expirationMap.get(key)
57
- const time = now()
58
- const oldValue = current === undefined ? undefined : this.data.get(key)
59
61
  if (current !== undefined) {
60
- // we aren't updating the ttl, so just set the data
61
- if (noUpdateTTL && current > time) {
62
- if (oldValue !== val) {
63
- this.data.set(key, val)
64
- if (!noDisposeOnSet) {
65
- this.dispose(oldValue, key, 'set')
66
- }
67
- }
68
- return this
62
+ // remove from the expirations list, so it isn't purged
63
+ const exp = this.expirations[current]
64
+ if (!exp || exp.length <= 1) {
65
+ delete this.expirations[current]
69
66
  } else {
70
- // just delete from expirations list, since we're about to
71
- // add to data and expirationsMap anyway
72
- const exp = this.expirations[current]
73
- if (!exp || exp.length <= 1) {
74
- delete this.expirations[current]
75
- } else {
76
- this.expirations[current] = exp.filter(k => k !== key)
77
- }
67
+ this.expirations[current] = exp.filter(k => k !== key)
78
68
  }
79
69
  }
80
- const expiration = Math.ceil(time + ttl)
70
+
71
+ const expiration = Math.floor(now() + ttl)
81
72
  this.expirationMap.set(key, expiration)
82
- this.data.set(key, val)
83
73
  if (!this.expirations[expiration]) {
84
74
  const t = setTimeout(() => this.purgeStale(), ttl)
85
75
  /* istanbul ignore else - affordance for non-node envs */
@@ -87,37 +77,69 @@ class TTLCache {
87
77
  this.expirations[expiration] = []
88
78
  }
89
79
  this.expirations[expiration].push(key)
80
+ }
81
+
82
+ set(
83
+ key,
84
+ val,
85
+ {
86
+ ttl = this.ttl,
87
+ noUpdateTTL = this.noUpdateTTL,
88
+ noDisposeOnSet = this.noDisposeOnSet,
89
+ } = {}
90
+ ) {
91
+ if (!isPosInt(ttl)) {
92
+ throw new TypeError('ttl must be positive integer')
93
+ }
94
+ if (this.expirationMap.has(key)) {
95
+ if (!noUpdateTTL) {
96
+ this.setTTL(key, ttl)
97
+ }
98
+ // has old value
99
+ const oldValue = this.data.get(key)
100
+ if (oldValue !== val) {
101
+ this.data.set(key, val)
102
+ if (!noDisposeOnSet) {
103
+ this.dispose(oldValue, key, 'set')
104
+ }
105
+ }
106
+ } else {
107
+ this.setTTL(key, ttl)
108
+ this.data.set(key, val)
109
+ }
90
110
 
91
111
  while (this.size > this.max) {
92
112
  this.purgeToCapacity()
93
113
  }
94
114
 
95
- if (!noDisposeOnSet && current && oldValue !== val) {
96
- this.dispose(oldValue, key, 'set')
97
- }
98
115
  return this
99
116
  }
100
117
 
101
- has (key) {
118
+ has(key) {
102
119
  return this.data.has(key)
103
120
  }
104
121
 
105
- getRemainingTTL (key) {
122
+ getRemainingTTL(key) {
106
123
  const expiration = this.expirationMap.get(key)
107
- return expiration !== undefined ? Math.max(0, expiration - now()) : 0
124
+ return expiration !== undefined
125
+ ? Math.max(0, Math.ceil(expiration - now()))
126
+ : 0
108
127
  }
109
128
 
110
- get (key, { updateAgeOnGet = this.updateAgeOnGet, ttl = this.ttl } = {}) {
129
+ get(
130
+ key,
131
+ { updateAgeOnGet = this.updateAgeOnGet, ttl = this.ttl } = {}
132
+ ) {
111
133
  const val = this.data.get(key)
112
134
  if (updateAgeOnGet) {
113
- this.set(key, val, { noUpdateTTL: false, noDisposeOnSet: true, ttl })
135
+ this.setTTL(key, ttl)
114
136
  }
115
137
  return val
116
138
  }
117
139
 
118
- dispose (value, key) {}
140
+ dispose(_, __) {}
119
141
 
120
- delete (key) {
142
+ delete(key) {
121
143
  const current = this.expirationMap.get(key)
122
144
  if (current !== undefined) {
123
145
  const value = this.data.get(key)
@@ -135,7 +157,7 @@ class TTLCache {
135
157
  return false
136
158
  }
137
159
 
138
- purgeToCapacity () {
160
+ purgeToCapacity() {
139
161
  for (const exp in this.expirations) {
140
162
  const keys = this.expirations[exp]
141
163
  if (this.size - keys.length >= this.max) {
@@ -159,12 +181,12 @@ class TTLCache {
159
181
  }
160
182
  }
161
183
 
162
- get size () {
184
+ get size() {
163
185
  return this.data.size
164
186
  }
165
187
 
166
- purgeStale () {
167
- const n = now()
188
+ purgeStale() {
189
+ const n = Math.ceil(now())
168
190
  for (const exp in this.expirations) {
169
191
  if (exp > n) {
170
192
  return
@@ -179,28 +201,28 @@ class TTLCache {
179
201
  }
180
202
  }
181
203
 
182
- *entries () {
204
+ *entries() {
183
205
  for (const exp in this.expirations) {
184
206
  for (const key of this.expirations[exp]) {
185
207
  yield [key, this.data.get(key)]
186
208
  }
187
209
  }
188
210
  }
189
- *keys () {
211
+ *keys() {
190
212
  for (const exp in this.expirations) {
191
213
  for (const key of this.expirations[exp]) {
192
214
  yield key
193
215
  }
194
216
  }
195
217
  }
196
- *values () {
218
+ *values() {
197
219
  for (const exp in this.expirations) {
198
220
  for (const key of this.expirations[exp]) {
199
221
  yield this.data.get(key)
200
222
  }
201
223
  }
202
224
  }
203
- [Symbol.iterator] () {
225
+ [Symbol.iterator]() {
204
226
  return this.entries()
205
227
  }
206
228
  }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@isaacs/ttlcache",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "files": [
5
- "index.js"
5
+ "index.js",
6
+ "index.d.ts"
6
7
  ],
7
8
  "main": "index.js",
8
9
  "exports": {
@@ -23,10 +24,36 @@
23
24
  "prepublishOnly": "git push origin --follow-tags"
24
25
  },
25
26
  "devDependencies": {
26
- "clock-mock": "^1.0.4",
27
- "tap": "^16.0.1"
27
+ "@types/node": "^17.0.42",
28
+ "@types/tap": "^15.0.7",
29
+ "clock-mock": "^1.0.6",
30
+ "prettier": "^2.7.0",
31
+ "tap": "^16.0.1",
32
+ "ts-node": "^10.8.1",
33
+ "typescript": "^4.7.3"
28
34
  },
29
35
  "engines": {
30
36
  "node": ">=12"
37
+ },
38
+ "tap": {
39
+ "nyc-arg": [
40
+ "--include=index.js"
41
+ ],
42
+ "node-arg": [
43
+ "--require",
44
+ "ts-node/register"
45
+ ],
46
+ "ts": false
47
+ },
48
+ "prettier": {
49
+ "semi": false,
50
+ "printWidth": 70,
51
+ "tabWidth": 2,
52
+ "useTabs": false,
53
+ "singleQuote": true,
54
+ "jsxSingleQuote": false,
55
+ "bracketSameLine": true,
56
+ "arrowParens": "avoid",
57
+ "endOfLine": "lf"
31
58
  }
32
59
  }