evicting-cache 2.2.1 → 2.3.1

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.
package/README.md CHANGED
@@ -1,7 +1,14 @@
1
1
  # Evicting Cache
2
- JavaScript Cache using an LRU (Least Recently Used) algorithm
3
2
 
4
- The cache is backed by a LinkedMap, which is a Map that maintains insertion order. When the cache is full, the least recently used item is evicted.
3
+ [![npm version](https://img.shields.io/npm/v/evicting-cache.svg)](https://www.npmjs.com/package/evicting-cache)
4
+ [![npm downloads](https://img.shields.io/npm/dm/evicting-cache.svg)](https://www.npmjs.com/package/evicting-cache)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
7
+ [![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/D1g1talEntr0py/evicting-cache)
8
+
9
+ A lightweight, high-performance TypeScript implementation of an LRU (Least Recently Used) cache with automatic eviction.
10
+
11
+ The cache is backed by JavaScript's native `Map`, leveraging its insertion order guarantee for efficient LRU semantics. When the cache reaches capacity, the least recently used item is automatically evicted.
5
12
 
6
13
  ## Installation
7
14
 
@@ -13,27 +20,256 @@ pnpm add evicting-cache
13
20
  npm install evicting-cache
14
21
  ```
15
22
 
23
+ ## Features
24
+
25
+ - 🚀 **High Performance** - O(1) operations for get, put, delete, and evict
26
+ - 📦 **Lightweight** - Zero dependencies, small bundle size
27
+ - 🔄 **LRU Eviction** - Automatic removal of least recently used items
28
+ - 📊 **Statistics Tracking** - Built-in hit/miss ratio monitoring
29
+ - 🔢 **Batch Operations** - Efficient multi-key operations
30
+ - 🎯 **Type Safe** - Full TypeScript support with generics
31
+ - ✅ **100% Test Coverage** - Thoroughly tested and reliable
32
+ - 🌐 **Modern ES Modules** - Native ESM support
33
+ - 🔍 **Map-like API** - Familiar interface with additional features
34
+
16
35
  ## Usage
17
- ```javascript
18
- import EvictingCache from 'evicting-cache';
19
36
 
20
- // Constructor accepts a number, which is the maximum number of items to store.
21
- // default is 100
22
- const cache = new EvictingCache(3);
37
+ ### Basic Example
38
+
39
+ ```typescript
40
+ import { EvictingCache } from 'evicting-cache';
41
+
42
+ // Create a cache with capacity of 3 items (default is 100)
43
+ const cache = new EvictingCache<string, string>(3);
23
44
 
24
- // Obviously a contrived example, but this is what you get with AI...
25
45
  cache.put('key1', 'value1');
26
46
  cache.put('key2', 'value2');
27
47
  cache.put('key3', 'value3');
28
- cache.put('key4', 'value4');
48
+ cache.put('key4', 'value4'); // 'key1' is evicted (LRU)
49
+
50
+ console.log(cache.get('key1')); // null (evicted)
51
+ console.log(cache.get('key2')); // 'value2'
52
+ console.log(cache.get('key3')); // 'value3'
53
+ console.log(cache.get('key4')); // 'value4'
54
+
55
+ cache.put('key5', 'value5'); // 'key2' is evicted
56
+
57
+ console.log(cache.get('key2')); // null (evicted)
58
+ console.log(cache.size); // 3
59
+ ```
60
+
61
+ ### LRU Behavior
62
+
63
+ ```typescript
64
+ const cache = new EvictingCache<string, number>(3);
65
+
66
+ cache.put('a', 1);
67
+ cache.put('b', 2);
68
+ cache.put('c', 3);
69
+ // Order: a, b, c (a is LRU)
70
+
71
+ cache.get('a'); // Access 'a', moves it to most recent
72
+ // Order: b, c, a (b is now LRU)
73
+
74
+ cache.put('d', 4); // Evicts 'b' (LRU)
75
+ // Order: c, a, d
76
+
77
+ console.log(cache.has('b')); // false (evicted)
78
+ ```
79
+
80
+ ### Peek Without Affecting LRU
81
+
82
+ ```typescript
83
+ const cache = new EvictingCache<string, string>(2);
84
+
85
+ cache.put('x', 'hello');
86
+ cache.put('y', 'world');
87
+
88
+ // peek() reads without updating LRU order
89
+ console.log(cache.peek('x')); // 'hello'
90
+ // 'x' remains LRU
91
+
92
+ cache.put('z', 'new'); // 'x' is evicted
93
+ console.log(cache.has('x')); // false
94
+ ```
95
+
96
+ ### Get or Compute
97
+
98
+ ```typescript
99
+ const cache = new EvictingCache<string, number>(10);
100
+
101
+ // Get existing value or compute and cache it
102
+ const value = cache.getOrPut('userId:123', () => {
103
+ // Expensive computation only happens if key is missing
104
+ return fetchUserFromDatabase('123');
105
+ });
106
+
107
+ // If producer throws, cache remains unchanged
108
+ try {
109
+ cache.getOrPut('key', () => {
110
+ throw new Error('Failed to compute');
111
+ });
112
+ } catch (error) {
113
+ // Cache state is unmodified
114
+ }
115
+ ```
116
+
117
+ ### Batch Operations
118
+
119
+ ```typescript
120
+ const cache = new EvictingCache<string, number>(100);
121
+
122
+ // Add multiple entries at once
123
+ cache.putAll([
124
+ ['a', 1],
125
+ ['b', 2],
126
+ ['c', 3]
127
+ ]);
128
+
129
+ // Can also use a Map
130
+ cache.putAll(new Map([['d', 4], ['e', 5]]));
131
+
132
+ // Get multiple values (returns Map, excludes missing keys)
133
+ const values = cache.getAll(['a', 'b', 'missing']);
134
+ console.log(values.size); // 2
135
+ console.log(values.get('a')); // 1
136
+ console.log(values.has('missing')); // false
137
+
138
+ // Delete multiple keys (returns count removed)
139
+ const removed = cache.deleteAll(['a', 'c', 'missing']);
140
+ console.log(removed); // 2
141
+ ```
142
+
143
+ ### Cache Statistics
144
+
145
+ ```typescript
146
+ const cache = new EvictingCache<string, string>(10);
147
+
148
+ cache.put('a', 'value1');
149
+ cache.get('a'); // hit
150
+ cache.get('b'); // miss
151
+ cache.get('a'); // hit
152
+
153
+ const stats = cache.getStats();
154
+ console.log(stats.hits); // 2
155
+ console.log(stats.misses); // 1
156
+ console.log(stats.hitRate); // 0.667 (66.7%)
157
+
158
+ // Reset statistics
159
+ cache.resetStats();
160
+ console.log(cache.getStats().hits); // 0
161
+ ```
162
+
163
+ ### Iteration
164
+
165
+ ```typescript
166
+ const cache = new EvictingCache<string, number>(3);
167
+ cache.put('a', 1);
168
+ cache.put('b', 2);
169
+ cache.put('c', 3);
170
+
171
+ // Iterate over entries (LRU to MRU order)
172
+ for (const [key, value] of cache) {
173
+ console.log(key, value);
174
+ }
175
+
176
+ // Or use forEach
177
+ cache.forEach((value, key, cache) => {
178
+ console.log(key, value);
179
+ });
180
+
181
+ // Get keys, values, or entries
182
+ console.log([...cache.keys()]); // ['a', 'b', 'c']
183
+ console.log([...cache.values()]); // [1, 2, 3]
184
+ console.log([...cache.entries()]); // [['a', 1], ['b', 2], ['c', 3]]
185
+ ```
186
+
187
+ ## API Reference
188
+
189
+ ### Constructor
190
+
191
+ - `new EvictingCache<K, V>(capacity?: number)` - Creates a new cache with the specified capacity (default: 100)
192
+
193
+ ### Core Methods
194
+
195
+ - `get(key: K): V | null` - Returns value and updates LRU order
196
+ - `peek(key: K): V | null` - Returns value without updating LRU order
197
+ - `put(key: K, value: V): void` - Adds or updates a key-value pair
198
+ - `delete(key: K): boolean` - Removes a key from the cache
199
+ - `has(key: K): boolean` - Checks if a key exists
200
+ - `getOrPut(key: K, producer: () => V): V` - Gets existing value or computes and stores new one
201
+ - `evict(): boolean` - Manually removes the LRU item
202
+ - `clear(): void` - Removes all items from the cache
203
+
204
+ ### Batch Operations
205
+
206
+ - `putAll(entries: Iterable<[K, V]>): void` - Adds multiple entries
207
+ - `getAll(keys: Iterable<K>): Map<K, V>` - Gets multiple values
208
+ - `deleteAll(keys: Iterable<K>): number` - Removes multiple keys
209
+
210
+ ### Statistics
211
+
212
+ - `getStats(): { hits: number, misses: number, hitRate: number }` - Returns cache statistics
213
+ - `resetStats(): void` - Resets statistics counters
214
+
215
+ ### Iteration
216
+
217
+ - `keys(): IterableIterator<K>` - Returns an iterator over keys
218
+ - `values(): IterableIterator<V>` - Returns an iterator over values
219
+ - `entries(): IterableIterator<[K, V]>` - Returns an iterator over entries
220
+ - `forEach(callback: (value: V, key: K, cache: EvictingCache<K, V>) => void, thisArg?: unknown): void` - Executes callback for each entry
221
+ - `[Symbol.iterator]()` - Makes the cache iterable
222
+
223
+ ### Properties
224
+
225
+ - `capacity: number` - Maximum number of items (read-only)
226
+ - `size: number` - Current number of items (read-only)
227
+
228
+ ## Performance
229
+
230
+ All core operations have **O(1)** time complexity:
231
+
232
+ | Operation | Complexity | Updates LRU |
233
+ |-----------|------------|-------------|
234
+ | `get(key)` | O(1) | ✅ Yes |
235
+ | `peek(key)` | O(1) | ❌ No |
236
+ | `put(key, value)` | O(1) | ✅ Yes |
237
+ | `delete(key)` | O(1) | ❌ N/A |
238
+ | `evict()` | O(1) | ❌ N/A |
239
+ | `has(key)` | O(1) | ❌ No |
240
+ | `clear()` | O(1) | ❌ N/A |
241
+
242
+ **Space complexity:** O(n) where n is the capacity
243
+
244
+ ## TypeScript Support
245
+
246
+ Full TypeScript support with generic types:
247
+
248
+ ```typescript
249
+ // Strongly typed cache
250
+ const userCache = new EvictingCache<number, User>(100);
251
+ const configCache = new EvictingCache<string, Config>(50);
252
+
253
+ // Works with any key/value types
254
+ const complexCache = new EvictingCache<{ id: number; tenant: string }, Promise<Data>>(25);
255
+ ```
256
+
257
+ ## Browser Compatibility
258
+
259
+ Compatible with all modern browsers and Node.js environments that support ES2015+ features:
260
+ - Chrome/Edge: ✅ Latest
261
+ - Firefox: ✅ Latest
262
+ - Safari: ✅ 15+
263
+ - Node.js: ✅ 14+
264
+
265
+ ## License
266
+
267
+ ISC License - See [LICENSE](LICENSE) file for details
29
268
 
30
- console.log(cache.get('key1')); // undefined
31
- console.log(cache.get('key2')); // value2
32
- console.log(cache.get('key3')); // value3
33
- console.log(cache.get('key4')); // value4
269
+ ## Contributing
34
270
 
35
- cache.put('key5', 'value5');
271
+ Contributions are welcome! Please feel free to submit a Pull Request.
36
272
 
37
- console.log(cache.get('key2')); // undefined
273
+ ## Changelog
38
274
 
39
- ```
275
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
@@ -1,7 +1,14 @@
1
+ type CacheStats = {
2
+ hits: number;
3
+ misses: number;
4
+ hitRate: number;
5
+ };
1
6
  /** JavaScript implementation of a Least Recently Used(LRU) Cache using a Map. */
2
7
  declare class EvictingCache<K, V> {
3
8
  private readonly _capacity;
4
9
  private readonly cache;
10
+ private hits;
11
+ private misses;
5
12
  /**
6
13
  * Creates a new Evicting Cache with the given capacity.
7
14
  *
@@ -31,6 +38,13 @@ declare class EvictingCache<K, V> {
31
38
  * @returns {void}
32
39
  */
33
40
  put(key: K, value: V): void;
41
+ /**
42
+ * Removes the specified key from the cache.
43
+ *
44
+ * @param {K} key The key to remove.
45
+ * @returns {boolean} True if the key was in the cache and was removed, false otherwise.
46
+ */
47
+ delete(key: K): boolean;
34
48
  /**
35
49
  * Returns the value associated with the given key from the cache without updating the LRU order.
36
50
  *
@@ -40,6 +54,8 @@ declare class EvictingCache<K, V> {
40
54
  peek(key: K): V | null;
41
55
  /**
42
56
  * Returns the value for the key if it exists in the cache. If not, put the key-value pair into the cache and return the value.
57
+ * If the producer function throws an error, the cache state is not modified.
58
+ *
43
59
  * @param {K} key The key.
44
60
  * @param {function(): V} producer The value to put if the key does not exist in the cache.
45
61
  * @returns {V} The value corresponding to the key.
@@ -47,16 +63,55 @@ declare class EvictingCache<K, V> {
47
63
  getOrPut(key: K, producer: () => V): V;
48
64
  /**
49
65
  * Removes the least recently used key-value pair from the cache.
50
- *
51
66
  * @returns {boolean} True if an item was removed, false otherwise.
52
67
  */
53
68
  evict(): boolean;
54
69
  /**
55
70
  * Clears the cache and the LRU list.
56
- *
57
71
  * @returns {void}
58
72
  */
59
73
  clear(): void;
74
+ /**
75
+ * Executes a provided function once per each key/value pair in the cache, in insertion order.
76
+ * @param {(value: V, key: K, cache: EvictingCache<K, V>) => void} callbackfn Function to execute for each element.
77
+ * @param {unknown} [thisArg] Value to use as `this` when executing callback.
78
+ * @returns {void}
79
+ */
80
+ forEach(callbackfn: (value: V, key: K, cache: EvictingCache<K, V>) => void, thisArg?: unknown): void;
81
+ /**
82
+ * Adds multiple key-value pairs to the cache.
83
+ * Each pair is added individually, following the same LRU eviction rules as put().
84
+ * @param {Iterable<[K, V]>} entries The entries to add.
85
+ * @returns {void}
86
+ */
87
+ putAll(entries: Iterable<[K, V]>): void;
88
+ /**
89
+ * Gets multiple values from the cache.
90
+ * Each get updates the LRU order for that key.
91
+ *
92
+ * @param {Iterable<K>} keys The keys to get values for.
93
+ * @returns {Map<K, V>} A map of keys to their values (excludes missing keys).
94
+ */
95
+ getAll(keys: Iterable<K>): Map<K, V>;
96
+ /**
97
+ * Removes multiple keys from the cache.
98
+ *
99
+ * @param {Iterable<K>} keys The keys to remove.
100
+ * @returns {number} The number of keys that were removed.
101
+ */
102
+ deleteAll(keys: Iterable<K>): number;
103
+ /**
104
+ * Gets cache statistics including hit/miss counts and hit rate.
105
+ *
106
+ * @returns {CacheStats} Cache statistics.
107
+ */
108
+ getStats(): CacheStats;
109
+ /**
110
+ * Resets cache statistics to zero.
111
+ *
112
+ * @returns {void}
113
+ */
114
+ resetStats(): void;
60
115
  /**
61
116
  * Gets the capacity of the cache.
62
117
  * This is the maximum number of key-value pairs the cache can hold.
@@ -123,4 +178,4 @@ declare class EvictingCache<K, V> {
123
178
  private putAndEvict;
124
179
  }
125
180
 
126
- export { EvictingCache };
181
+ export { EvictingCache };
@@ -1,2 +1,2 @@
1
- var r=class{_capacity;cache;constructor(e=100){if(e<1)throw new RangeError("capacity must be greater than 0");if(!Number.isInteger(e))throw new RangeError("capacity must be an integer");this._capacity=e,this.cache=new Map}get(e){let t=this.cache.get(e);return t===void 0?null:(this.cache.delete(e),this.cache.set(e,t),t)}has(e){return this.cache.has(e)}put(e,t){this.putAndEvict(e,t)}peek(e){return this.cache.get(e)??null}getOrPut(e,t){return this.get(e)??this.putAndEvict(e,t())}evict(){if(this.cache.size===0)return!1;let e=this.cache.keys().next().value;return this.cache.delete(e)}clear(){this.cache.clear()}get capacity(){return this._capacity}get size(){return this.cache.size}keys(){return this.cache.keys()}values(){return this.cache.values()}entries(){return this.cache.entries()}[Symbol.iterator](){return this.entries()}get[Symbol.toStringTag](){return"EvictingCache"}putAndEvict(e,t){return this.cache.has(e)?this.cache.delete(e):this._capacity<=this.cache.size&&this.evict(),this.cache.set(e,t),t}};export{r as EvictingCache};
1
+ var r=class{_capacity;cache;hits=0;misses=0;constructor(e=100){if(e<1)throw new RangeError("capacity must be greater than 0");if(!Number.isInteger(e))throw new RangeError("capacity must be an integer");this._capacity=e,this.cache=new Map}get(e){let t=this.cache.get(e);return t===void 0?(this.misses++,null):(this.hits++,this.cache.delete(e),this.cache.set(e,t),t)}has(e){return this.cache.has(e)}put(e,t){this.putAndEvict(e,t)}delete(e){return this.cache.delete(e)}peek(e){return this.cache.get(e)??null}getOrPut(e,t){let s=this.get(e);if(s!==null)return s;let i=t();return this.putAndEvict(e,i)}evict(){let e=this.cache.keys().next();return e.done?!1:this.cache.delete(e.value)}clear(){this.cache.clear()}forEach(e,t){let s=t!==void 0?e.bind(t):e;this.cache.forEach((i,a)=>s(i,a,this))}putAll(e){for(let[t,s]of e)this.put(t,s)}getAll(e){let t=new Map;for(let s of e){let i=this.get(s);i!==null&&t.set(s,i)}return t}deleteAll(e){let t=0;for(let s of e)this.cache.delete(s)&&t++;return t}getStats(){let e=this.hits+this.misses;return{hits:this.hits,misses:this.misses,hitRate:e===0?0:this.hits/e}}resetStats(){this.hits=0,this.misses=0}get capacity(){return this._capacity}get size(){return this.cache.size}keys(){return this.cache.keys()}values(){return this.cache.values()}entries(){return this.cache.entries()}[Symbol.iterator](){return this.entries()}get[Symbol.toStringTag](){return"EvictingCache"}putAndEvict(e,t){return!this.cache.delete(e)&&this._capacity<=this.cache.size&&this.evict(),this.cache.set(e,t),t}};export{r as EvictingCache};
2
2
  //# sourceMappingURL=evicting-cache.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/evicting-cache.ts"],
4
- "sourcesContent": ["/** JavaScript implementation of a Least Recently Used(LRU) Cache using a Map. */\nexport class EvictingCache<K, V> {\n\tprivate readonly _capacity: number;\n\tprivate readonly cache: Map<K, V>;\n\n\t/**\n\t * Creates a new Evicting Cache with the given capacity.\n\t *\n\t * @param {number} [capacity=100] The maximum number of key-value pairs the cache can hold.\n\t */\n\tconstructor(capacity: number = 100) {\n\t\tif (capacity < 1) { throw new RangeError('capacity must be greater than 0') }\n\t\tif (!Number.isInteger(capacity)) { throw new RangeError('capacity must be an integer') }\n\n\t\tthis._capacity = capacity;\n\t\tthis.cache = new Map();\n\t}\n\n\t/**\n\t * Returns the value associated with the given key from the cache and updates the LRU order.\n\t *\n\t * @param {K} key The key to get the value for.\n\t * @returns {V | null} The associated value if the key is in the cache, or null otherwise.\n\t */\n\tget(key: K): V | null {\n\t\tconst value = this.cache.get(key);\n\t\tif (value === undefined) { return null }\n\n\t\tthis.cache.delete(key);\n\t\t// Move the accessed item to the end (most recently used)\n\t\tthis.cache.set(key, value);\n\n\t\treturn value;\n\t}\n\n\t/**\n\t * Returns true if the given key is in the cache, false otherwise.\n\t *\n\t * @param {K} key The key to check.\n\t * @returns {boolean} True if the key is in the cache, false otherwise.\n\t */\n\thas(key: K): boolean {\n\t\treturn this.cache.has(key);\n\t}\n\n\t/**\n\t * Adds a new key-value pair to the cache and updates the LRU order.\n\t * If adding the new pair will exceed the capacity, removes the least recently used pair from the cache.\n\t *\n\t * @param {K} key The key to add.\n\t * @param {V} value The value to add.\n\t * @returns {void}\n\t */\n\tput(key: K, value: V): void {\n\t\tthis.putAndEvict(key, value);\n\t}\n\n\t/**\n\t * Returns the value associated with the given key from the cache without updating the LRU order.\n\t *\n\t * @param {K} key The key to get the value for.\n\t * @returns {V | null} The associated value if the key is in the cache, or null otherwise.\n\t */\n\tpeek(key: K): V | null {\n\t\treturn this.cache.get(key) ?? null;\n\t}\n\n\t/**\n\t * Returns the value for the key if it exists in the cache. If not, put the key-value pair into the cache and return the value.\n\t * @param {K} key The key.\n\t * @param {function(): V} producer The value to put if the key does not exist in the cache.\n\t * @returns {V} The value corresponding to the key.\n\t */\n\tgetOrPut(key: K, producer: () => V): V {\n\t\treturn this.get(key) ?? this.putAndEvict(key, producer());\n\t}\n\n\t/**\n\t * Removes the least recently used key-value pair from the cache.\n\t *\n\t * @returns {boolean} True if an item was removed, false otherwise.\n\t */\n\tevict(): boolean {\n\t\tif (this.cache.size === 0) { return false }\n\t\tconst key = this.cache.keys().next().value as K;\n\n\t\treturn this.cache.delete(key);\n\t}\n\n\t/**\n\t * Clears the cache and the LRU list.\n\t *\n\t * @returns {void}\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Gets the capacity of the cache.\n\t * This is the maximum number of key-value pairs the cache can hold.\n\t * This is not the number of key-value pairs in the cache.\n\t *\n\t * @readonly\n\t * @returns {number} The capacity of the cache.\n\t */\n\tget capacity(): number {\n\t\treturn this._capacity;\n\t}\n\n\t/**\n\t * Gets the size of the cache.\n\t * This is the number of key-value pairs in the cache.\n\t * This is not the capacity of the cache.\n\t * The capacity is the maximum number of key-value pairs the cache can hold.\n\t * The size is the number of key-value pairs currently in the cache.\n\t * The size will be less than or equal to the capacity.\n\t *\n\t * @returns {number} The size of the cache.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n\n\t/**\n\t * Returns an iterator over the keys in the cache.\n\t * The keys are returned in the order of least recently used to most recently used.\n\t *\n\t * @returns {IterableIterator<K>} An iterator over the keys in the cache.\n\t */\n\tkeys(): IterableIterator<K> {\n\t\treturn this.cache.keys();\n\t}\n\n\t/**\n\t * Returns an iterator over the values in the cache.\n\t * The values are returned in the order of least recently used to most recently used.\n\t *\n\t * @returns {IterableIterator<V>} An iterator over the values in the cache.\n\t */\n\tvalues(): IterableIterator<V> {\n\t\treturn this.cache.values();\n\t}\n\n\t/**\n\t * Returns an iterator over the entries in the cache.\n\t * The entries are returned in the order of least recently used to most recently used.\n\t *\n\t * @returns {IterableIterator<[K, V]>} An iterator over the entries in the cache.\n\t */\n\tentries(): IterableIterator<[K, V]> {\n\t\treturn this.cache.entries();\n\t}\n\n\t/**\n\t * Returns an iterator over the entries in the cache.\n\t * The entries are returned in the order of least recently used to most recently used.\n\t *\n\t * @returns {IterableIterator<[K, V]>} An iterator over the entries in the cache.\n\t */\n\t[Symbol.iterator](): IterableIterator<[K, V]> {\n\t\treturn this.entries();\n\t}\n\n\t/**\n\t * Gets the description of the object.\n\t *\n\t * @override\n\t * @returns {string} The description of the object.\n\t */\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'EvictingCache';\n\t}\n\n\t/**\n\t * Puts a key-value pair into the cache and evicts the least recently used item if necessary.\n\t * If the key already exists, the item is removed and re-added to update its position.\n\t * If the cache is full, the least recently used item is evicted and the new item is added.\n\t * @param {K} key The key to put.\n\t * @param {V} value The value to put.\n\t * @returns {V} The value that was put.\n\t */\n\tprivate putAndEvict(key: K, value: V): V {\n\t\tif (this.cache.has(key)) {\n\t\t\tthis.cache.delete(key);\n\t\t} else if (this._capacity <= this.cache.size) {\n\t\t\tthis.evict();\n\t\t}\n\n\t\tthis.cache.set(key, value);\n\n\t\treturn value;\n\t}\n}"],
5
- "mappings": "AACO,IAAMA,EAAN,KAA0B,CACf,UACA,MAOjB,YAAYC,EAAmB,IAAK,CACnC,GAAIA,EAAW,EAAK,MAAM,IAAI,WAAW,iCAAiC,EAC1E,GAAI,CAAC,OAAO,UAAUA,CAAQ,EAAK,MAAM,IAAI,WAAW,6BAA6B,EAErF,KAAK,UAAYA,EACjB,KAAK,MAAQ,IAAI,GAClB,CAQA,IAAIC,EAAkB,CACrB,IAAMC,EAAQ,KAAK,MAAM,IAAID,CAAG,EAChC,OAAIC,IAAU,OAAoB,MAElC,KAAK,MAAM,OAAOD,CAAG,EAErB,KAAK,MAAM,IAAIA,EAAKC,CAAK,EAElBA,EACR,CAQA,IAAID,EAAiB,CACpB,OAAO,KAAK,MAAM,IAAIA,CAAG,CAC1B,CAUA,IAAIA,EAAQC,EAAgB,CAC3B,KAAK,YAAYD,EAAKC,CAAK,CAC5B,CAQA,KAAKD,EAAkB,CACtB,OAAO,KAAK,MAAM,IAAIA,CAAG,GAAK,IAC/B,CAQA,SAASA,EAAQE,EAAsB,CACtC,OAAO,KAAK,IAAIF,CAAG,GAAK,KAAK,YAAYA,EAAKE,EAAS,CAAC,CACzD,CAOA,OAAiB,CAChB,GAAI,KAAK,MAAM,OAAS,EAAK,MAAO,GACpC,IAAMF,EAAM,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE,MAErC,OAAO,KAAK,MAAM,OAAOA,CAAG,CAC7B,CAOA,OAAc,CACb,KAAK,MAAM,MAAM,CAClB,CAUA,IAAI,UAAmB,CACtB,OAAO,KAAK,SACb,CAYA,IAAI,MAAe,CAClB,OAAO,KAAK,MAAM,IACnB,CAQA,MAA4B,CAC3B,OAAO,KAAK,MAAM,KAAK,CACxB,CAQA,QAA8B,CAC7B,OAAO,KAAK,MAAM,OAAO,CAC1B,CAQA,SAAoC,CACnC,OAAO,KAAK,MAAM,QAAQ,CAC3B,CAQA,CAAC,OAAO,QAAQ,GAA8B,CAC7C,OAAO,KAAK,QAAQ,CACrB,CAQA,IAAK,OAAO,WAAW,GAAY,CAClC,MAAO,eACR,CAUQ,YAAYA,EAAQC,EAAa,CACxC,OAAI,KAAK,MAAM,IAAID,CAAG,EACrB,KAAK,MAAM,OAAOA,CAAG,EACX,KAAK,WAAa,KAAK,MAAM,MACvC,KAAK,MAAM,EAGZ,KAAK,MAAM,IAAIA,EAAKC,CAAK,EAElBA,CACR,CACD",
6
- "names": ["EvictingCache", "capacity", "key", "value", "producer"]
4
+ "sourcesContent": ["type CacheStats = {\n\thits: number;\n\tmisses: number;\n\thitRate: number;\n};\n\n/** JavaScript implementation of a Least Recently Used(LRU) Cache using a Map. */\nexport class EvictingCache<K, V> {\n\tprivate readonly _capacity: number;\n\tprivate readonly cache: Map<K, V>;\n\tprivate hits = 0;\n\tprivate misses = 0;\n\n\t/**\n\t * Creates a new Evicting Cache with the given capacity.\n\t *\n\t * @param {number} [capacity=100] The maximum number of key-value pairs the cache can hold.\n\t */\n\tconstructor(capacity: number = 100) {\n\t\tif (capacity < 1) { throw new RangeError('capacity must be greater than 0') }\n\t\tif (!Number.isInteger(capacity)) { throw new RangeError('capacity must be an integer') }\n\n\t\tthis._capacity = capacity;\n\t\tthis.cache = new Map();\n\t}\n\n\t/**\n\t * Returns the value associated with the given key from the cache and updates the LRU order.\n\t *\n\t * @param {K} key The key to get the value for.\n\t * @returns {V | null} The associated value if the key is in the cache, or null otherwise.\n\t */\n\tget(key: K): V | null {\n\t\tconst value = this.cache.get(key);\n\t\tif (value === undefined) {\n\t\t\tthis.misses++;\n\t\t\treturn null;\n\t\t}\n\n\t\tthis.hits++;\n\t\tthis.cache.delete(key);\n\t\t// Move the accessed item to the end (most recently used)\n\t\tthis.cache.set(key, value);\n\n\t\treturn value;\n\t}\n\n\t/**\n\t * Returns true if the given key is in the cache, false otherwise.\n\t *\n\t * @param {K} key The key to check.\n\t * @returns {boolean} True if the key is in the cache, false otherwise.\n\t */\n\thas(key: K): boolean {\n\t\treturn this.cache.has(key);\n\t}\n\n\t/**\n\t * Adds a new key-value pair to the cache and updates the LRU order.\n\t * If adding the new pair will exceed the capacity, removes the least recently used pair from the cache.\n\t *\n\t * @param {K} key The key to add.\n\t * @param {V} value The value to add.\n\t * @returns {void}\n\t */\n\tput(key: K, value: V): void {\n\t\tthis.putAndEvict(key, value);\n\t}\n\n\t/**\n\t * Removes the specified key from the cache.\n\t *\n\t * @param {K} key The key to remove.\n\t * @returns {boolean} True if the key was in the cache and was removed, false otherwise.\n\t */\n\tdelete(key: K): boolean {\n\t\treturn this.cache.delete(key);\n\t}\n\n\t/**\n\t * Returns the value associated with the given key from the cache without updating the LRU order.\n\t *\n\t * @param {K} key The key to get the value for.\n\t * @returns {V | null} The associated value if the key is in the cache, or null otherwise.\n\t */\n\tpeek(key: K): V | null {\n\t\treturn this.cache.get(key) ?? null;\n\t}\n\n\t/**\n\t * Returns the value for the key if it exists in the cache. If not, put the key-value pair into the cache and return the value.\n\t * If the producer function throws an error, the cache state is not modified.\n\t *\n\t * @param {K} key The key.\n\t * @param {function(): V} producer The value to put if the key does not exist in the cache.\n\t * @returns {V} The value corresponding to the key.\n\t */\n\tgetOrPut(key: K, producer: () => V): V {\n\t\tconst existing = this.get(key);\n\t\tif (existing !== null) { return existing }\n\n\t\t// If producer throws, cache state remains unchanged\n\t\tconst value = producer();\n\t\treturn this.putAndEvict(key, value);\n\t}\n\n\t/**\n\t * Removes the least recently used key-value pair from the cache.\n\t * @returns {boolean} True if an item was removed, false otherwise.\n\t */\n\tevict(): boolean {\n\t\tconst firstEntry = this.cache.keys().next();\n\t\tif (firstEntry.done) { return false }\n\n\t\treturn this.cache.delete(firstEntry.value);\n\t}\n\n\t/**\n\t * Clears the cache and the LRU list.\n\t * @returns {void}\n\t */\n\tclear(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Executes a provided function once per each key/value pair in the cache, in insertion order.\n\t * @param {(value: V, key: K, cache: EvictingCache<K, V>) => void} callbackfn Function to execute for each element.\n\t * @param {unknown} [thisArg] Value to use as `this` when executing callback.\n\t * @returns {void}\n\t */\n\tforEach(callbackfn: (value: V, key: K, cache: EvictingCache<K, V>) => void, thisArg?: unknown): void {\n\t\tconst boundCallback = thisArg !== undefined ? callbackfn.bind(thisArg) : callbackfn;\n\t\tthis.cache.forEach((value, key) => boundCallback(value, key, this));\n\t}\n\n\t/**\n\t * Adds multiple key-value pairs to the cache.\n\t * Each pair is added individually, following the same LRU eviction rules as put().\n\t * @param {Iterable<[K, V]>} entries The entries to add.\n\t * @returns {void}\n\t */\n\tputAll(entries: Iterable<[K, V]>): void {\n\t\tfor (const [key, value] of entries) { this.put(key, value) }\n\t}\n\n\t/**\n\t * Gets multiple values from the cache.\n\t * Each get updates the LRU order for that key.\n\t *\n\t * @param {Iterable<K>} keys The keys to get values for.\n\t * @returns {Map<K, V>} A map of keys to their values (excludes missing keys).\n\t */\n\tgetAll(keys: Iterable<K>): Map<K, V> {\n\t\tconst result = new Map<K, V>();\n\t\tfor (const key of keys) {\n\t\t\tconst value = this.get(key);\n\t\t\tif (value !== null) { result.set(key, value) }\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Removes multiple keys from the cache.\n\t *\n\t * @param {Iterable<K>} keys The keys to remove.\n\t * @returns {number} The number of keys that were removed.\n\t */\n\tdeleteAll(keys: Iterable<K>): number {\n\t\tlet count = 0;\n\t\tfor (const key of keys) {\n\t\t\tif (this.cache.delete(key)) { count++ }\n\t\t}\n\n\t\treturn count;\n\t}\n\n\t/**\n\t * Gets cache statistics including hit/miss counts and hit rate.\n\t *\n\t * @returns {CacheStats} Cache statistics.\n\t */\n\tgetStats(): CacheStats {\n\t\tconst total = this.hits + this.misses;\n\n\t\treturn { hits: this.hits, misses: this.misses, hitRate: total === 0 ? 0 : this.hits / total };\n\t}\n\n\t/**\n\t * Resets cache statistics to zero.\n\t *\n\t * @returns {void}\n\t */\n\tresetStats(): void {\n\t\tthis.hits = 0;\n\t\tthis.misses = 0;\n\t}\n\n\t/**\n\t * Gets the capacity of the cache.\n\t * This is the maximum number of key-value pairs the cache can hold.\n\t * This is not the number of key-value pairs in the cache.\n\t *\n\t * @readonly\n\t * @returns {number} The capacity of the cache.\n\t */\n\tget capacity(): number {\n\t\treturn this._capacity;\n\t}\n\n\t/**\n\t * Gets the size of the cache.\n\t * This is the number of key-value pairs in the cache.\n\t * This is not the capacity of the cache.\n\t * The capacity is the maximum number of key-value pairs the cache can hold.\n\t * The size is the number of key-value pairs currently in the cache.\n\t * The size will be less than or equal to the capacity.\n\t *\n\t * @returns {number} The size of the cache.\n\t */\n\tget size(): number {\n\t\treturn this.cache.size;\n\t}\n\n\t/**\n\t * Returns an iterator over the keys in the cache.\n\t * The keys are returned in the order of least recently used to most recently used.\n\t *\n\t * @returns {IterableIterator<K>} An iterator over the keys in the cache.\n\t */\n\tkeys(): IterableIterator<K> {\n\t\treturn this.cache.keys();\n\t}\n\n\t/**\n\t * Returns an iterator over the values in the cache.\n\t * The values are returned in the order of least recently used to most recently used.\n\t *\n\t * @returns {IterableIterator<V>} An iterator over the values in the cache.\n\t */\n\tvalues(): IterableIterator<V> {\n\t\treturn this.cache.values();\n\t}\n\n\t/**\n\t * Returns an iterator over the entries in the cache.\n\t * The entries are returned in the order of least recently used to most recently used.\n\t *\n\t * @returns {IterableIterator<[K, V]>} An iterator over the entries in the cache.\n\t */\n\tentries(): IterableIterator<[K, V]> {\n\t\treturn this.cache.entries();\n\t}\n\n\t/**\n\t * Returns an iterator over the entries in the cache.\n\t * The entries are returned in the order of least recently used to most recently used.\n\t *\n\t * @returns {IterableIterator<[K, V]>} An iterator over the entries in the cache.\n\t */\n\t[Symbol.iterator](): IterableIterator<[K, V]> {\n\t\treturn this.entries();\n\t}\n\n\t/**\n\t * Gets the description of the object.\n\t *\n\t * @override\n\t * @returns {string} The description of the object.\n\t */\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'EvictingCache';\n\t}\n\n\t/**\n\t * Puts a key-value pair into the cache and evicts the least recently used item if necessary.\n\t * If the key already exists, the item is removed and re-added to update its position.\n\t * If the cache is full, the least recently used item is evicted and the new item is added.\n\t * @param {K} key The key to put.\n\t * @param {V} value The value to put.\n\t * @returns {V} The value that was put.\n\t */\n\tprivate putAndEvict(key: K, value: V): V {\n\t\tconst existed = this.cache.delete(key);\n\t\tif (!existed && this._capacity <= this.cache.size) { this.evict() }\n\n\t\tthis.cache.set(key, value);\n\n\t\treturn value;\n\t}\n}"],
5
+ "mappings": "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",
6
+ "names": ["EvictingCache", "capacity", "key", "value", "producer", "existing", "firstEntry", "callbackfn", "thisArg", "boundCallback", "entries", "keys", "result", "count", "total"]
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "evicting-cache",
3
3
  "author": "D1g1talEntr0py",
4
- "version": "2.2.1",
4
+ "version": "2.3.1",
5
5
  "license": "ISC",
6
6
  "description": "Cache implementation with an LRU evicting policy",
7
7
  "type": "module",
@@ -24,19 +24,19 @@
24
24
  ],
25
25
  "devDependencies": {
26
26
  "@eslint/compat": "^1.4.0",
27
- "@eslint/js": "^9.37.0",
27
+ "@eslint/js": "^9.38.0",
28
28
  "@types/eslint": "^9.6.1",
29
- "@types/node": "^24.7.1",
30
- "@typescript-eslint/eslint-plugin": "^8.46.0",
31
- "@typescript-eslint/parser": "^8.46.0",
32
- "@vitest/coverage-v8": "^3.2.4",
33
- "eslint": "^9.37.0",
29
+ "@types/node": "^24.9.1",
30
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
31
+ "@typescript-eslint/parser": "^8.46.2",
32
+ "@vitest/coverage-v8": "^4.0.4",
33
+ "eslint": "^9.38.0",
34
34
  "eslint-plugin-compat": "^6.0.2",
35
- "eslint-plugin-jsdoc": "^61.1.0",
35
+ "eslint-plugin-jsdoc": "^61.1.9",
36
36
  "globals": "^16.4.0",
37
37
  "typescript": "^5.9.3",
38
- "typescript-eslint": "^8.46.0",
39
- "vitest": "^3.2.4"
38
+ "typescript-eslint": "^8.46.2",
39
+ "vitest": "^4.0.4"
40
40
  },
41
41
  "browserslist": [
42
42
  "defaults",
@@ -46,11 +46,13 @@
46
46
  "scripts": {
47
47
  "build": "tsbuild",
48
48
  "build:watch": "tsbuild --watch",
49
- "type-check": "tsbuild --typeCheck",
49
+ "type-check": "tsbuild --type-check",
50
50
  "lint": "eslint",
51
51
  "test": "vitest run",
52
52
  "test:coverage": "vitest run --coverage",
53
53
  "test:watch": "vitest",
54
- "prepublish": "pnpm lint && pnpm test && pnpm -s build"
54
+ "prepublish": "pnpm lint && pnpm test && pnpm -s build --minify --force",
55
+ "preversion": "pnpm lint && pnpm test",
56
+ "version": "node -e \"const fs=require('fs');const d=new Date().toISOString().split('T')[0];const v=require('./package.json').version;let c=fs.readFileSync('CHANGELOG.md','utf8');c=c.replace('## [Unreleased]','## ['+v+'] - '+d);fs.writeFileSync('CHANGELOG.md',c);\" && git add CHANGELOG.md"
55
57
  }
56
58
  }
@@ -1,7 +1,15 @@
1
+ type CacheStats = {
2
+ hits: number;
3
+ misses: number;
4
+ hitRate: number;
5
+ };
6
+
1
7
  /** JavaScript implementation of a Least Recently Used(LRU) Cache using a Map. */
2
8
  export class EvictingCache<K, V> {
3
9
  private readonly _capacity: number;
4
10
  private readonly cache: Map<K, V>;
11
+ private hits = 0;
12
+ private misses = 0;
5
13
 
6
14
  /**
7
15
  * Creates a new Evicting Cache with the given capacity.
@@ -24,8 +32,12 @@ export class EvictingCache<K, V> {
24
32
  */
25
33
  get(key: K): V | null {
26
34
  const value = this.cache.get(key);
27
- if (value === undefined) { return null }
35
+ if (value === undefined) {
36
+ this.misses++;
37
+ return null;
38
+ }
28
39
 
40
+ this.hits++;
29
41
  this.cache.delete(key);
30
42
  // Move the accessed item to the end (most recently used)
31
43
  this.cache.set(key, value);
@@ -55,6 +67,16 @@ export class EvictingCache<K, V> {
55
67
  this.putAndEvict(key, value);
56
68
  }
57
69
 
70
+ /**
71
+ * Removes the specified key from the cache.
72
+ *
73
+ * @param {K} key The key to remove.
74
+ * @returns {boolean} True if the key was in the cache and was removed, false otherwise.
75
+ */
76
+ delete(key: K): boolean {
77
+ return this.cache.delete(key);
78
+ }
79
+
58
80
  /**
59
81
  * Returns the value associated with the given key from the cache without updating the LRU order.
60
82
  *
@@ -67,35 +89,114 @@ export class EvictingCache<K, V> {
67
89
 
68
90
  /**
69
91
  * Returns the value for the key if it exists in the cache. If not, put the key-value pair into the cache and return the value.
92
+ * If the producer function throws an error, the cache state is not modified.
93
+ *
70
94
  * @param {K} key The key.
71
95
  * @param {function(): V} producer The value to put if the key does not exist in the cache.
72
96
  * @returns {V} The value corresponding to the key.
73
97
  */
74
98
  getOrPut(key: K, producer: () => V): V {
75
- return this.get(key) ?? this.putAndEvict(key, producer());
99
+ const existing = this.get(key);
100
+ if (existing !== null) { return existing }
101
+
102
+ // If producer throws, cache state remains unchanged
103
+ const value = producer();
104
+ return this.putAndEvict(key, value);
76
105
  }
77
106
 
78
107
  /**
79
108
  * Removes the least recently used key-value pair from the cache.
80
- *
81
109
  * @returns {boolean} True if an item was removed, false otherwise.
82
110
  */
83
111
  evict(): boolean {
84
- if (this.cache.size === 0) { return false }
85
- const key = this.cache.keys().next().value as K;
112
+ const firstEntry = this.cache.keys().next();
113
+ if (firstEntry.done) { return false }
86
114
 
87
- return this.cache.delete(key);
115
+ return this.cache.delete(firstEntry.value);
88
116
  }
89
117
 
90
118
  /**
91
119
  * Clears the cache and the LRU list.
92
- *
93
120
  * @returns {void}
94
121
  */
95
122
  clear(): void {
96
123
  this.cache.clear();
97
124
  }
98
125
 
126
+ /**
127
+ * Executes a provided function once per each key/value pair in the cache, in insertion order.
128
+ * @param {(value: V, key: K, cache: EvictingCache<K, V>) => void} callbackfn Function to execute for each element.
129
+ * @param {unknown} [thisArg] Value to use as `this` when executing callback.
130
+ * @returns {void}
131
+ */
132
+ forEach(callbackfn: (value: V, key: K, cache: EvictingCache<K, V>) => void, thisArg?: unknown): void {
133
+ const boundCallback = thisArg !== undefined ? callbackfn.bind(thisArg) : callbackfn;
134
+ this.cache.forEach((value, key) => boundCallback(value, key, this));
135
+ }
136
+
137
+ /**
138
+ * Adds multiple key-value pairs to the cache.
139
+ * Each pair is added individually, following the same LRU eviction rules as put().
140
+ * @param {Iterable<[K, V]>} entries The entries to add.
141
+ * @returns {void}
142
+ */
143
+ putAll(entries: Iterable<[K, V]>): void {
144
+ for (const [key, value] of entries) { this.put(key, value) }
145
+ }
146
+
147
+ /**
148
+ * Gets multiple values from the cache.
149
+ * Each get updates the LRU order for that key.
150
+ *
151
+ * @param {Iterable<K>} keys The keys to get values for.
152
+ * @returns {Map<K, V>} A map of keys to their values (excludes missing keys).
153
+ */
154
+ getAll(keys: Iterable<K>): Map<K, V> {
155
+ const result = new Map<K, V>();
156
+ for (const key of keys) {
157
+ const value = this.get(key);
158
+ if (value !== null) { result.set(key, value) }
159
+ }
160
+
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Removes multiple keys from the cache.
166
+ *
167
+ * @param {Iterable<K>} keys The keys to remove.
168
+ * @returns {number} The number of keys that were removed.
169
+ */
170
+ deleteAll(keys: Iterable<K>): number {
171
+ let count = 0;
172
+ for (const key of keys) {
173
+ if (this.cache.delete(key)) { count++ }
174
+ }
175
+
176
+ return count;
177
+ }
178
+
179
+ /**
180
+ * Gets cache statistics including hit/miss counts and hit rate.
181
+ *
182
+ * @returns {CacheStats} Cache statistics.
183
+ */
184
+ getStats(): CacheStats {
185
+ const total = this.hits + this.misses;
186
+
187
+ return { hits: this.hits, misses: this.misses, hitRate: total === 0 ? 0 : this.hits / total };
188
+ }
189
+
190
+ /**
191
+ * Resets cache statistics to zero.
192
+ *
193
+ * @returns {void}
194
+ */
195
+ resetStats(): void {
196
+ this.hits = 0;
197
+ this.misses = 0;
198
+ }
199
+
99
200
  /**
100
201
  * Gets the capacity of the cache.
101
202
  * This is the maximum number of key-value pairs the cache can hold.
@@ -181,11 +282,8 @@ export class EvictingCache<K, V> {
181
282
  * @returns {V} The value that was put.
182
283
  */
183
284
  private putAndEvict(key: K, value: V): V {
184
- if (this.cache.has(key)) {
185
- this.cache.delete(key);
186
- } else if (this._capacity <= this.cache.size) {
187
- this.evict();
188
- }
285
+ const existed = this.cache.delete(key);
286
+ if (!existed && this._capacity <= this.cache.size) { this.evict() }
189
287
 
190
288
  this.cache.set(key, value);
191
289