@isaacs/ttlcache 1.0.0 → 1.0.3

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/README.md +40 -2
  2. package/index.js +23 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,7 +46,7 @@ cache.has(1) // returns false
46
46
 
47
47
  ## API
48
48
 
49
- ### `const TTLCache = require('@isaacs/ttlcache')` or `import TTLCache from '@isaacs/ttlcache'
49
+ ### `const TTLCache = require('@isaacs/ttlcache')` or `import TTLCache from '@isaacs/ttlcache'`
50
50
 
51
51
  Default export is the `TTLCache` class.
52
52
 
@@ -61,11 +61,22 @@ Create a new `TTLCache` object.
61
61
  retrieved? Defaults to `false`. Overridable on the `get()` method.
62
62
  * `noUpdateTTL` Should setting a new value for an existing key leave the
63
63
  TTL unchanged? Defaults to `false`. Overridable on the `set()` method.
64
+ (Note that TTL is _always_ updated if the item is expired, since that is
65
+ treated as a new `set()` and the old item is no longer relevant.)
64
66
  * `dispose` Method called with `(value, key, reason)` when an item is
65
67
  removed from the cache. Called once item is fully removed from cache.
66
68
  It is safe to re-add at this point, but note that adding when `reason` is
67
69
  `'set'` can result in infinite recursion if `noDisponseOnSet` is not
68
70
  specified.
71
+
72
+ Disposal reasons:
73
+
74
+ * `'stale'` TTL expired.
75
+ * `'set'` Overwritten with a new different value.
76
+ * `'evict'` Removed from the cache to stay within capacity limit.
77
+ * `'delete'` Explicitly deleted with `cache.delete()` or
78
+ `cache.clear()`
79
+
69
80
  * `noDisposeOnSet` Do not call `dispose()` method when overwriting a key
70
81
  with a new value. Defaults to `false`. Overridable on `set()` method.
71
82
 
@@ -113,7 +124,8 @@ Delete all items from the cache.
113
124
  ### `cache.entries()`
114
125
 
115
126
  Return an iterator that walks through each `[key, value]` from soonest
116
- expiring to latest expiring.
127
+ expiring to latest expiring. (Items expiring at the same time are walked
128
+ in insertion order.)
117
129
 
118
130
  Default iteration method for the cache object.
119
131
 
@@ -150,3 +162,29 @@ automatically.
150
162
 
151
163
  Called when an item is removed from the cache and should be disposed. Set
152
164
  this on the constructor options.
165
+
166
+ ## Algorithm
167
+
168
+ The cache uses two `Map` objects. The first maps item keys to their
169
+ expiration time, and the second maps item keys to their values. Then, a
170
+ null-prototype object uses the expiration time as keys, with the value
171
+ being an array of all the keys expiring at that time.
172
+
173
+ This leverages a few important features of modern JavaScript engines for
174
+ fairly good performance:
175
+
176
+ - `Map` objects are highly optimized for referring to arbitrary values by
177
+ arbitrary keys.
178
+ - Objects with solely integer-numeric keys are iterated in sorted numeric
179
+ order rather than insertion order, and insertions in the middle of the
180
+ key ordering are still very fast. This is true of all modern JS engines
181
+ tested at the time of this module's creation, but most particularly V8
182
+ (the engine in Node.js).
183
+
184
+ When it is time to prune, we can always walk the null-prototype object in
185
+ iteration order, deleting items until we come to the first key greater than
186
+ the current time.
187
+
188
+ Thus, the `start` time doesn't need to be tracked, only the expiration
189
+ time. When an item age is updated (either explicitly on `get()`, or by
190
+ setting to a new value), it is deleted and re-inserted.
package/index.js CHANGED
@@ -29,6 +29,8 @@ class TTLCache {
29
29
  }
30
30
  this.ttl = ttl
31
31
  this.max = max
32
+ this.updateAgeOnGet = updateAgeOnGet;
33
+ this.noUpdateTTL = noUpdateTTL;
32
34
  if (dispose !== undefined) {
33
35
  if (typeof dispose !== 'function') {
34
36
  throw new TypeError('dispose must be function if set')
@@ -52,9 +54,11 @@ class TTLCache {
52
54
  throw new TypeError('ttl must be positive integer')
53
55
  }
54
56
  const current = this.expirationMap.get(key)
57
+ const time = now()
58
+ const oldValue = current === undefined ? undefined : this.data.get(key)
55
59
  if (current !== undefined) {
56
- const oldValue = this.data.get(key)
57
- if (noUpdateTTL) {
60
+ // we aren't updating the ttl, so just set the data
61
+ if (noUpdateTTL && current > time) {
58
62
  if (oldValue !== val) {
59
63
  this.data.set(key, val)
60
64
  if (!noDisposeOnSet) {
@@ -63,13 +67,17 @@ class TTLCache {
63
67
  }
64
68
  return this
65
69
  } else {
66
- this.delete(key, {
67
- reason: 'set',
68
- noDispose: noDisposeOnSet || oldValue === val,
69
- })
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
+ }
70
78
  }
71
79
  }
72
- const expiration = Math.ceil(now() + ttl)
80
+ const expiration = Math.ceil(time + ttl)
73
81
  this.expirationMap.set(key, expiration)
74
82
  this.data.set(key, val)
75
83
  if (!this.expirations[expiration]) {
@@ -79,9 +87,14 @@ class TTLCache {
79
87
  this.expirations[expiration] = []
80
88
  }
81
89
  this.expirations[expiration].push(key)
90
+
82
91
  while (this.size > this.max) {
83
92
  this.purgeToCapacity()
84
93
  }
94
+
95
+ if (!noDisposeOnSet && current && oldValue !== val) {
96
+ this.dispose(oldValue, key, 'set')
97
+ }
85
98
  return this
86
99
  }
87
100
 
@@ -104,21 +117,19 @@ class TTLCache {
104
117
 
105
118
  dispose (value, key) {}
106
119
 
107
- delete (key, { reason = 'delete', noDispose = false } = {}) {
120
+ delete (key) {
108
121
  const current = this.expirationMap.get(key)
109
122
  if (current !== undefined) {
110
123
  const value = this.data.get(key)
111
124
  this.data.delete(key)
112
125
  this.expirationMap.delete(key)
113
126
  const exp = this.expirations[current]
114
- if (exp.length === 1) {
127
+ if (exp && exp.length <= 1) {
115
128
  delete this.expirations[current]
116
129
  } else {
117
130
  this.expirations[current] = exp.filter(k => k !== key)
118
131
  }
119
- if (!noDispose) {
120
- this.dispose(value, key, reason)
121
- }
132
+ this.dispose(value, key, 'delete')
122
133
  return true
123
134
  }
124
135
  return false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isaacs/ttlcache",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "files": [
5
5
  "index.js"
6
6
  ],