@isaacs/ttlcache 1.0.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/LICENSE +15 -0
  2. package/README.md +152 -0
  3. package/index.js +197 -0
  4. package/package.json +32 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ The ISC License
2
+
3
+ Copyright (c) 2022 - Isaac Z. Schlueter and Contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
15
+ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # @isaacs/ttlcache
2
+
3
+ The time-based use-recency-unaware cousin of
4
+ [`lru-cache`](http://npm.im/lru-cache)
5
+
6
+ ## Usage
7
+
8
+ Essentially, this is the same API as
9
+ [`lru-cache`](http://npm.im/lru-cache), but it does not do LRU tracking,
10
+ and is bound primarily by time, rather than space. Since entries are not
11
+ purged based on recency of use, it can save a lot of extra work managing
12
+ linked lists, mapping keys to pointers, and so on.
13
+
14
+ TTLs are millisecond granularity.
15
+
16
+ If a capacity limit is set, then the soonest-expiring items are purged
17
+ first, to bring it down to the size limit.
18
+
19
+ Iteration is in order from soonest expiring until latest expiring.
20
+
21
+ If multiple items are expiring in the same ms, then the soonest-added
22
+ items are considered "older" for purposes of iterating and purging down to
23
+ capacity.
24
+
25
+ A TTL _must_ be set for every entry, which can be defaulted in the
26
+ constructor.
27
+
28
+ Custom size calculation is not supported. Max capacity is simply the count
29
+ of items in the cache.
30
+
31
+ ```js
32
+ const TTLCache = require('ttlcache')
33
+ const cache = new TTLCache({ max: 10000, ttl: 1000 })
34
+
35
+ // set some value
36
+ cache.set(1, 2)
37
+
38
+ // 999 ms later
39
+ cache.has(1) // returns true
40
+ cache.get(1) // returns 2
41
+
42
+ // 1000 ms later
43
+ cache.get(1) // returns undefined
44
+ cache.has(1) // returns false
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `const TTLCache = require('@isaacs/ttlcache')` or `import TTLCache from '@isaacs/ttlcache'
50
+
51
+ Default export is the `TTLCache` class.
52
+
53
+ ### `new TTLCache({ ttl, max = Infinty, updateAgeOnGet = false, noUpdateTTL = false })`
54
+
55
+ Create a new `TTLCache` object.
56
+
57
+ * `max` The max number of items to keep in the cache.
58
+ * `ttl` The max time in ms to store items. Overridable on the `set()`
59
+ method.
60
+ * `updateAgeOnGet` Should the age of an item be updated when it is
61
+ retrieved? Defaults to `false`. Overridable on the `get()` method.
62
+ * `noUpdateTTL` Should setting a new value for an existing key leave the
63
+ TTL unchanged? Defaults to `false`. Overridable on the `set()` method.
64
+ * `dispose` Method called with `(value, key, reason)` when an item is
65
+ removed from the cache. Called once item is fully removed from cache.
66
+ It is safe to re-add at this point, but note that adding when `reason` is
67
+ `'set'` can result in infinite recursion if `noDisponseOnSet` is not
68
+ specified.
69
+ * `noDisposeOnSet` Do not call `dispose()` method when overwriting a key
70
+ with a new value. Defaults to `false`. Overridable on `set()` method.
71
+
72
+ When used as an iterator, like `for (const [key, value] of cache)` or
73
+ `[...cache]`, the cache yields the same results as the `entries()` method.
74
+
75
+ ### `cache.size`
76
+
77
+ The number of items in the cache.
78
+
79
+ ### `cache.set(key, value, { ttl, noUpdateTTL, noDisposeOnSet } = {})`
80
+
81
+ Store a value in the cache for the specified time.
82
+
83
+ `ttl` and `noUpdateTTL` optionally override defaults on the constructor.
84
+
85
+ Returns the cache object.
86
+
87
+ ### `cache.get(key, {updateAgeOnGet, ttl} = {})`
88
+
89
+ Get an item stored in the cache. Returns `undefined` if the item is not in
90
+ the cache (including if it has expired and been purged).
91
+
92
+ If `updateAgeOnGet` is `true`, then re-add the item into the cache with the
93
+ updated `ttl` value. Both options default to the settings on the
94
+ constructor.
95
+
96
+ ### `cache.getRemainingTTL(key)`
97
+
98
+ Return the remaining time before an item expires. Returns `0` if the item
99
+ is not found in the cache or is already expired.
100
+
101
+ ### `cache.has(key)`
102
+
103
+ Return true if the item is in the cache.
104
+
105
+ ### `cache.delete(key)`
106
+
107
+ Remove an item from the cache.
108
+
109
+ ### `cache.clear()`
110
+
111
+ Delete all items from the cache.
112
+
113
+ ### `cache.entries()`
114
+
115
+ Return an iterator that walks through each `[key, value]` from soonest
116
+ expiring to latest expiring.
117
+
118
+ Default iteration method for the cache object.
119
+
120
+ ### `cache.keys()`
121
+
122
+ Return an iterator that walks through each `key` from soonest expiring to
123
+ latest expiring.
124
+
125
+ ### `cache.values()`
126
+
127
+ Return an iterator that walks through each `value` from soonest expiring to
128
+ latest expiring.
129
+
130
+ ## Internal Methods
131
+
132
+ You should not ever call these, they are managed automatically.
133
+
134
+ ### `purgeStale`
135
+
136
+ **Internal**
137
+
138
+ Removes items which have expired. Called automatically.
139
+
140
+ ### `purgeToCapacity`
141
+
142
+ **Internal**
143
+
144
+ Removes soonest-expiring items when the capacity limit is reached. Called
145
+ automatically.
146
+
147
+ ### `dispose`
148
+
149
+ **Internal**
150
+
151
+ Called when an item is removed from the cache and should be disposed. Set
152
+ this on the constructor options.
package/index.js ADDED
@@ -0,0 +1,197 @@
1
+ // A simple TTL cache with max capacity option, ms resolution,
2
+ // autopurge, and reasonably optimized performance
3
+ // Relies on the fact that integer Object keys are kept sorted,
4
+ // and managed very efficiently by V8.
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)
14
+ const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n)
15
+
16
+ class TTLCache {
17
+ constructor ({ max = Infinity, ttl, updateAgeOnGet = false, noUpdateTTL = false, dispose }) {
18
+ // {[expirationTime]: [keys]}
19
+ this.expirations = Object.create(null)
20
+ // {key=>val}
21
+ this.data = new Map()
22
+ // {key=>expiration}
23
+ this.expirationMap = new Map()
24
+ if (ttl !== undefined && !isPosInt(ttl)) {
25
+ throw new TypeError('ttl must be positive integer if set')
26
+ }
27
+ if (!isPosInt(max) && max !== Infinity) {
28
+ throw new TypeError('max must be positive integer or Infinity')
29
+ }
30
+ this.ttl = ttl
31
+ this.max = max
32
+ if (dispose !== undefined) {
33
+ if (typeof dispose !== 'function') {
34
+ throw new TypeError('dispose must be function if set')
35
+ }
36
+ this.dispose = dispose
37
+ }
38
+ }
39
+
40
+ clear () {
41
+ const entries = this.dispose !== TTLCache.prototype.dispose ? [...this] : []
42
+ this.data.clear()
43
+ this.expirationMap.clear()
44
+ this.expirations = Object.create(null)
45
+ for (const [key, val] of entries) {
46
+ this.dispose(val, key, 'delete')
47
+ }
48
+ }
49
+
50
+ set (key, val, { ttl = this.ttl, noUpdateTTL = this.noUpdateTTL, noDisposeOnSet = this.noDisposeOnSet } = {}) {
51
+ if (!isPosInt(ttl)) {
52
+ throw new TypeError('ttl must be positive integer')
53
+ }
54
+ const current = this.expirationMap.get(key)
55
+ if (current !== undefined) {
56
+ const oldValue = this.data.get(key)
57
+ if (noUpdateTTL) {
58
+ if (oldValue !== val) {
59
+ this.data.set(key, val)
60
+ if (!noDisposeOnSet) {
61
+ this.dispose(oldValue, key, 'set')
62
+ }
63
+ }
64
+ return this
65
+ } else {
66
+ this.delete(key, {
67
+ reason: 'set',
68
+ noDispose: noDisposeOnSet || oldValue === val,
69
+ })
70
+ }
71
+ }
72
+ const expiration = Math.ceil(now() + ttl)
73
+ this.expirationMap.set(key, expiration)
74
+ this.data.set(key, val)
75
+ if (!this.expirations[expiration]) {
76
+ const t = setTimeout(() => this.purgeStale(), ttl)
77
+ /* istanbul ignore else - affordance for non-node envs */
78
+ if (t.unref) t.unref()
79
+ this.expirations[expiration] = []
80
+ }
81
+ this.expirations[expiration].push(key)
82
+ while (this.size > this.max) {
83
+ this.purgeToCapacity()
84
+ }
85
+ return this
86
+ }
87
+
88
+ has (key) {
89
+ return this.data.has(key)
90
+ }
91
+
92
+ getRemainingTTL (key) {
93
+ const expiration = this.expirationMap.get(key)
94
+ return expiration !== undefined ? Math.max(0, expiration - now()) : 0
95
+ }
96
+
97
+ get (key, { updateAgeOnGet = this.updateAgeOnGet, ttl = this.ttl } = {}) {
98
+ const val = this.data.get(key)
99
+ if (updateAgeOnGet) {
100
+ this.set(key, val, { noUpdateTTL: false, noDisposeOnSet: true, ttl })
101
+ }
102
+ return val
103
+ }
104
+
105
+ dispose (value, key) {}
106
+
107
+ delete (key, { reason = 'delete', noDispose = false } = {}) {
108
+ const current = this.expirationMap.get(key)
109
+ if (current !== undefined) {
110
+ const value = this.data.get(key)
111
+ this.data.delete(key)
112
+ this.expirationMap.delete(key)
113
+ const exp = this.expirations[current]
114
+ if (exp.length === 1) {
115
+ delete this.expirations[current]
116
+ } else {
117
+ this.expirations[current] = exp.filter(k => k !== key)
118
+ }
119
+ if (!noDispose) {
120
+ this.dispose(value, key, reason)
121
+ }
122
+ return true
123
+ }
124
+ return false
125
+ }
126
+
127
+ purgeToCapacity () {
128
+ for (const exp in this.expirations) {
129
+ const keys = this.expirations[exp]
130
+ if (this.size - keys.length >= this.max) {
131
+ for (const key of keys) {
132
+ const val = this.data.get(key)
133
+ this.data.delete(key)
134
+ this.expirationMap.delete(key)
135
+ this.dispose(val, key, 'evict')
136
+ }
137
+ delete this.expirations[exp]
138
+ } else {
139
+ const s = this.size - this.max
140
+ for (const key of keys.splice(0, s)) {
141
+ const val = this.data.get(key)
142
+ this.data.delete(key)
143
+ this.expirationMap.delete(key)
144
+ this.dispose(val, key, 'evict')
145
+ }
146
+ }
147
+ return
148
+ }
149
+ }
150
+
151
+ get size () {
152
+ return this.data.size
153
+ }
154
+
155
+ purgeStale () {
156
+ const n = now()
157
+ for (const exp in this.expirations) {
158
+ if (exp > n) {
159
+ return
160
+ }
161
+ for (const key of this.expirations[exp]) {
162
+ const val = this.data.get(key)
163
+ this.data.delete(key)
164
+ this.expirationMap.delete(key)
165
+ this.dispose(val, key, 'stale')
166
+ }
167
+ delete this.expirations[exp]
168
+ }
169
+ }
170
+
171
+ *entries () {
172
+ for (const exp in this.expirations) {
173
+ for (const key of this.expirations[exp]) {
174
+ yield [key, this.data.get(key)]
175
+ }
176
+ }
177
+ }
178
+ *keys () {
179
+ for (const exp in this.expirations) {
180
+ for (const key of this.expirations[exp]) {
181
+ yield key
182
+ }
183
+ }
184
+ }
185
+ *values () {
186
+ for (const exp in this.expirations) {
187
+ for (const key of this.expirations[exp]) {
188
+ yield this.data.get(key)
189
+ }
190
+ }
191
+ }
192
+ [Symbol.iterator] () {
193
+ return this.entries()
194
+ }
195
+ }
196
+
197
+ module.exports = TTLCache
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@isaacs/ttlcache",
3
+ "version": "1.0.0",
4
+ "files": [
5
+ "index.js"
6
+ ],
7
+ "main": "index.js",
8
+ "exports": {
9
+ ".": "./index.js"
10
+ },
11
+ "description": "The time-based use-recency-unaware cousin of [`lru-cache`](http://npm.im/lru-cache)",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/isaacs/ttlcache"
15
+ },
16
+ "author": "Isaac Z. Schlueter <i@izs.me> (https://izs.me)",
17
+ "license": "ISC",
18
+ "scripts": {
19
+ "test": "tap",
20
+ "snap": "tap",
21
+ "preversion": "npm test",
22
+ "postversion": "npm publish",
23
+ "prepublishOnly": "git push origin --follow-tags"
24
+ },
25
+ "devDependencies": {
26
+ "clock-mock": "^1.0.4",
27
+ "tap": "^16.0.1"
28
+ },
29
+ "engines": {
30
+ "node": ">=12"
31
+ }
32
+ }