cacheable 1.8.2 → 1.8.4

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
@@ -17,7 +17,7 @@
17
17
  * Scalable and trusted storage engine by Keyv
18
18
  * Memory Caching with LRU and Expiration `CacheableMemory`
19
19
  * Resilient to failures with try/catch and offline
20
- * Wrap / Memoization for Sync and Async Functions
20
+ * Wrap / Memoization for Sync and Async Functions with Stampede Protection
21
21
  * Hooks and Events to extend functionality
22
22
  * Shorthand for ttl in milliseconds `(1m = 60000) (1h = 3600000) (1d = 86400000)`
23
23
  * Non-blocking operations for layer 2 caching
@@ -310,6 +310,30 @@ const wrappedFunction = cache.wrap(asyncFunction, options);
310
310
  console.log(await wrappedFunction(2)); // 4
311
311
  console.log(await wrappedFunction(2)); // 4 from cache
312
312
  ```
313
+ With `Cacheable` we have also included stampede protection so that a `Promise` based call will only be called once if multiple requests of the same are executed at the same time. Here is an example of how to test for stampede protection:
314
+
315
+ ```javascript
316
+ import { Cacheable } from 'cacheable';
317
+ const asyncFunction = async (value: number) => {
318
+ return value;
319
+ };
320
+
321
+ const cache = new Cacheable();
322
+ const options = {
323
+ ttl: '1h', // 1 hour
324
+ keyPrefix: 'p1', // key prefix. This is used if you have multiple functions and need to set a unique prefix.
325
+ }
326
+
327
+ const wrappedFunction = cache.wrap(asyncFunction, options);
328
+ const promises = [];
329
+ for (let i = 0; i < 10; i++) {
330
+ promises.push(wrappedFunction(i));
331
+ }
332
+
333
+ const results = await Promise.all(promises); // all results should be the same
334
+
335
+ console.log(results); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
336
+ ```
313
337
 
314
338
  In this example we are wrapping an `async` function in a cache with a `ttl` of `1 hour`. This will cache the result of the function for `1 hour` and then expire the value. You can also wrap a `sync` function in a cache:
315
339
 
package/dist/index.cjs CHANGED
@@ -126,6 +126,61 @@ function hash(object, algorithm = "sha256") {
126
126
  return hasher.digest("hex");
127
127
  }
128
128
 
129
+ // src/coalesce-async.ts
130
+ var callbacks = /* @__PURE__ */ new Map();
131
+ function hasKey(key) {
132
+ return callbacks.has(key);
133
+ }
134
+ function addKey(key) {
135
+ callbacks.set(key, []);
136
+ }
137
+ function removeKey(key) {
138
+ callbacks.delete(key);
139
+ }
140
+ function addCallbackToKey(key, callback) {
141
+ const stash = getCallbacksByKey(key);
142
+ stash.push(callback);
143
+ callbacks.set(key, stash);
144
+ }
145
+ function getCallbacksByKey(key) {
146
+ return callbacks.get(key) ?? [];
147
+ }
148
+ async function enqueue(key) {
149
+ return new Promise((resolve, reject) => {
150
+ const callback = { resolve, reject };
151
+ addCallbackToKey(key, callback);
152
+ });
153
+ }
154
+ function dequeue(key) {
155
+ const stash = getCallbacksByKey(key);
156
+ removeKey(key);
157
+ return stash;
158
+ }
159
+ function coalesce(options) {
160
+ const { key, error, result } = options;
161
+ for (const callback of dequeue(key)) {
162
+ if (error) {
163
+ callback.reject(error);
164
+ } else {
165
+ callback.resolve(result);
166
+ }
167
+ }
168
+ }
169
+ async function coalesceAsync(key, fnc) {
170
+ if (!hasKey(key)) {
171
+ addKey(key);
172
+ try {
173
+ const result = await Promise.resolve(fnc());
174
+ coalesce({ key, result });
175
+ return result;
176
+ } catch (error) {
177
+ coalesce({ key, error });
178
+ throw error;
179
+ }
180
+ }
181
+ return enqueue(key);
182
+ }
183
+
129
184
  // src/wrap.ts
130
185
  function wrapSync(function_, options) {
131
186
  const { ttl, keyPrefix, cache } = options;
@@ -142,11 +197,18 @@ function wrapSync(function_, options) {
142
197
  function wrap(function_, options) {
143
198
  const { ttl, keyPrefix, cache } = options;
144
199
  return async function(...arguments_) {
145
- const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
146
- let value = await cache.get(cacheKey);
147
- if (value === void 0) {
148
- value = await function_(...arguments_);
149
- await cache.set(cacheKey, value, ttl);
200
+ let value;
201
+ try {
202
+ const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
203
+ value = await cache.get(cacheKey);
204
+ if (value === void 0) {
205
+ value = await coalesceAsync(cacheKey, async () => {
206
+ const result = await function_(...arguments_);
207
+ await cache.set(cacheKey, result, ttl);
208
+ return result;
209
+ });
210
+ }
211
+ } catch {
150
212
  }
151
213
  return value;
152
214
  };
@@ -713,8 +775,8 @@ var CacheableMemory = class {
713
775
  */
714
776
  wrap(function_, options) {
715
777
  const wrapOptions = {
716
- ttl: options.ttl,
717
- keyPrefix: options.keyPrefix,
778
+ ttl: options?.ttl ?? this._ttl,
779
+ keyPrefix: options?.keyPrefix,
718
780
  cache: this
719
781
  };
720
782
  return wrapSync(function_, wrapOptions);
@@ -1522,8 +1584,8 @@ var Cacheable = class extends import_hookified.Hookified {
1522
1584
  */
1523
1585
  wrap(function_, options) {
1524
1586
  const wrapOptions = {
1525
- ttl: options.ttl,
1526
- keyPrefix: options.keyPrefix,
1587
+ ttl: options?.ttl ?? this._ttl,
1588
+ keyPrefix: options?.keyPrefix,
1527
1589
  cache: this
1528
1590
  };
1529
1591
  return wrap(function_, wrapOptions);
package/dist/index.d.cts CHANGED
@@ -109,20 +109,16 @@ type CacheableStoreItem = {
109
109
  expires?: number;
110
110
  };
111
111
 
112
- type WrapOptions = {
112
+ type WrapFunctionOptions = {
113
113
  ttl?: number | string;
114
114
  keyPrefix?: string;
115
+ };
116
+ type WrapOptions = WrapFunctionOptions & {
115
117
  cache: Cacheable;
116
118
  };
117
- type WrapSyncOptions = {
118
- ttl?: number | string;
119
- keyPrefix?: string;
119
+ type WrapSyncOptions = WrapFunctionOptions & {
120
120
  cache: CacheableMemory;
121
121
  };
122
- type WrapFunctionOptions = {
123
- ttl?: number | string;
124
- keyPrefix: string;
125
- };
126
122
  type AnyFunction = (...arguments_: any[]) => any;
127
123
  declare function wrapSync<T>(function_: AnyFunction, options: WrapSyncOptions): AnyFunction;
128
124
  declare function wrap<T>(function_: AnyFunction, options: WrapOptions): AnyFunction;
@@ -369,7 +365,7 @@ declare class CacheableMemory {
369
365
  * @param {Object} [options] - The options to wrap
370
366
  * @returns {Function} - The wrapped function
371
367
  */
372
- wrap<T>(function_: (...arguments_: any[]) => T, options: WrapFunctionOptions): (...arguments_: any[]) => T;
368
+ wrap<T>(function_: (...arguments_: any[]) => T, options?: WrapFunctionOptions): (...arguments_: any[]) => T;
373
369
  private isPrimitive;
374
370
  private concatStores;
375
371
  private setTtl;
@@ -625,7 +621,7 @@ declare class Cacheable extends Hookified {
625
621
  * @param {WrapOptions} [options] The options for the wrap function
626
622
  * @returns {Function} The wrapped function
627
623
  */
628
- wrap<T>(function_: (...arguments_: any[]) => T, options: WrapFunctionOptions): (...arguments_: any[]) => T;
624
+ wrap<T>(function_: (...arguments_: any[]) => T, options?: WrapFunctionOptions): (...arguments_: any[]) => T;
629
625
  /**
630
626
  * Will hash an object using the specified algorithm. The default algorithm is 'sha256'.
631
627
  * @param {any} object the object to hash
@@ -639,4 +635,4 @@ declare class Cacheable extends Hookified {
639
635
  private setTtl;
640
636
  }
641
637
 
642
- export { Cacheable, CacheableEvents, CacheableHooks, type CacheableItem, CacheableMemory, type CacheableOptions, CacheableStats, KeyvCacheableMemory, type WrapOptions, type WrapSyncOptions, shorthandToMilliseconds, shorthandToTime, wrap, wrapSync };
638
+ export { Cacheable, CacheableEvents, CacheableHooks, type CacheableItem, CacheableMemory, type CacheableMemoryOptions, type CacheableOptions, CacheableStats, KeyvCacheableMemory, type WrapOptions, type WrapSyncOptions, shorthandToMilliseconds, shorthandToTime, wrap, wrapSync };
package/dist/index.d.ts CHANGED
@@ -109,20 +109,16 @@ type CacheableStoreItem = {
109
109
  expires?: number;
110
110
  };
111
111
 
112
- type WrapOptions = {
112
+ type WrapFunctionOptions = {
113
113
  ttl?: number | string;
114
114
  keyPrefix?: string;
115
+ };
116
+ type WrapOptions = WrapFunctionOptions & {
115
117
  cache: Cacheable;
116
118
  };
117
- type WrapSyncOptions = {
118
- ttl?: number | string;
119
- keyPrefix?: string;
119
+ type WrapSyncOptions = WrapFunctionOptions & {
120
120
  cache: CacheableMemory;
121
121
  };
122
- type WrapFunctionOptions = {
123
- ttl?: number | string;
124
- keyPrefix: string;
125
- };
126
122
  type AnyFunction = (...arguments_: any[]) => any;
127
123
  declare function wrapSync<T>(function_: AnyFunction, options: WrapSyncOptions): AnyFunction;
128
124
  declare function wrap<T>(function_: AnyFunction, options: WrapOptions): AnyFunction;
@@ -369,7 +365,7 @@ declare class CacheableMemory {
369
365
  * @param {Object} [options] - The options to wrap
370
366
  * @returns {Function} - The wrapped function
371
367
  */
372
- wrap<T>(function_: (...arguments_: any[]) => T, options: WrapFunctionOptions): (...arguments_: any[]) => T;
368
+ wrap<T>(function_: (...arguments_: any[]) => T, options?: WrapFunctionOptions): (...arguments_: any[]) => T;
373
369
  private isPrimitive;
374
370
  private concatStores;
375
371
  private setTtl;
@@ -625,7 +621,7 @@ declare class Cacheable extends Hookified {
625
621
  * @param {WrapOptions} [options] The options for the wrap function
626
622
  * @returns {Function} The wrapped function
627
623
  */
628
- wrap<T>(function_: (...arguments_: any[]) => T, options: WrapFunctionOptions): (...arguments_: any[]) => T;
624
+ wrap<T>(function_: (...arguments_: any[]) => T, options?: WrapFunctionOptions): (...arguments_: any[]) => T;
629
625
  /**
630
626
  * Will hash an object using the specified algorithm. The default algorithm is 'sha256'.
631
627
  * @param {any} object the object to hash
@@ -639,4 +635,4 @@ declare class Cacheable extends Hookified {
639
635
  private setTtl;
640
636
  }
641
637
 
642
- export { Cacheable, CacheableEvents, CacheableHooks, type CacheableItem, CacheableMemory, type CacheableOptions, CacheableStats, KeyvCacheableMemory, type WrapOptions, type WrapSyncOptions, shorthandToMilliseconds, shorthandToTime, wrap, wrapSync };
638
+ export { Cacheable, CacheableEvents, CacheableHooks, type CacheableItem, CacheableMemory, type CacheableMemoryOptions, type CacheableOptions, CacheableStats, KeyvCacheableMemory, type WrapOptions, type WrapSyncOptions, shorthandToMilliseconds, shorthandToTime, wrap, wrapSync };
package/dist/index.js CHANGED
@@ -81,6 +81,61 @@ function hash(object, algorithm = "sha256") {
81
81
  return hasher.digest("hex");
82
82
  }
83
83
 
84
+ // src/coalesce-async.ts
85
+ var callbacks = /* @__PURE__ */ new Map();
86
+ function hasKey(key) {
87
+ return callbacks.has(key);
88
+ }
89
+ function addKey(key) {
90
+ callbacks.set(key, []);
91
+ }
92
+ function removeKey(key) {
93
+ callbacks.delete(key);
94
+ }
95
+ function addCallbackToKey(key, callback) {
96
+ const stash = getCallbacksByKey(key);
97
+ stash.push(callback);
98
+ callbacks.set(key, stash);
99
+ }
100
+ function getCallbacksByKey(key) {
101
+ return callbacks.get(key) ?? [];
102
+ }
103
+ async function enqueue(key) {
104
+ return new Promise((resolve, reject) => {
105
+ const callback = { resolve, reject };
106
+ addCallbackToKey(key, callback);
107
+ });
108
+ }
109
+ function dequeue(key) {
110
+ const stash = getCallbacksByKey(key);
111
+ removeKey(key);
112
+ return stash;
113
+ }
114
+ function coalesce(options) {
115
+ const { key, error, result } = options;
116
+ for (const callback of dequeue(key)) {
117
+ if (error) {
118
+ callback.reject(error);
119
+ } else {
120
+ callback.resolve(result);
121
+ }
122
+ }
123
+ }
124
+ async function coalesceAsync(key, fnc) {
125
+ if (!hasKey(key)) {
126
+ addKey(key);
127
+ try {
128
+ const result = await Promise.resolve(fnc());
129
+ coalesce({ key, result });
130
+ return result;
131
+ } catch (error) {
132
+ coalesce({ key, error });
133
+ throw error;
134
+ }
135
+ }
136
+ return enqueue(key);
137
+ }
138
+
84
139
  // src/wrap.ts
85
140
  function wrapSync(function_, options) {
86
141
  const { ttl, keyPrefix, cache } = options;
@@ -97,11 +152,18 @@ function wrapSync(function_, options) {
97
152
  function wrap(function_, options) {
98
153
  const { ttl, keyPrefix, cache } = options;
99
154
  return async function(...arguments_) {
100
- const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
101
- let value = await cache.get(cacheKey);
102
- if (value === void 0) {
103
- value = await function_(...arguments_);
104
- await cache.set(cacheKey, value, ttl);
155
+ let value;
156
+ try {
157
+ const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
158
+ value = await cache.get(cacheKey);
159
+ if (value === void 0) {
160
+ value = await coalesceAsync(cacheKey, async () => {
161
+ const result = await function_(...arguments_);
162
+ await cache.set(cacheKey, result, ttl);
163
+ return result;
164
+ });
165
+ }
166
+ } catch {
105
167
  }
106
168
  return value;
107
169
  };
@@ -668,8 +730,8 @@ var CacheableMemory = class {
668
730
  */
669
731
  wrap(function_, options) {
670
732
  const wrapOptions = {
671
- ttl: options.ttl,
672
- keyPrefix: options.keyPrefix,
733
+ ttl: options?.ttl ?? this._ttl,
734
+ keyPrefix: options?.keyPrefix,
673
735
  cache: this
674
736
  };
675
737
  return wrapSync(function_, wrapOptions);
@@ -1480,8 +1542,8 @@ var Cacheable = class extends Hookified {
1480
1542
  */
1481
1543
  wrap(function_, options) {
1482
1544
  const wrapOptions = {
1483
- ttl: options.ttl,
1484
- keyPrefix: options.keyPrefix,
1545
+ ttl: options?.ttl ?? this._ttl,
1546
+ keyPrefix: options?.keyPrefix,
1485
1547
  cache: this
1486
1548
  };
1487
1549
  return wrap(function_, wrapOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cacheable",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Simple Caching Engine using Keyv",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,18 +22,18 @@
22
22
  "private": false,
23
23
  "devDependencies": {
24
24
  "@keyv/redis": "^3.0.1",
25
- "@types/node": "^22.8.1",
26
- "@vitest/coverage-v8": "^2.1.3",
27
- "lru-cache": "^11.0.1",
25
+ "@types/node": "^22.9.0",
26
+ "@vitest/coverage-v8": "^2.1.4",
27
+ "lru-cache": "^11.0.2",
28
28
  "rimraf": "^6.0.1",
29
29
  "tsup": "^8.3.5",
30
30
  "typescript": "^5.6.3",
31
- "vitest": "^2.1.3",
31
+ "vitest": "^2.1.4",
32
32
  "xo": "^0.59.3"
33
33
  },
34
34
  "dependencies": {
35
- "hookified": "^1.4.0",
36
- "keyv": "^5.1.2"
35
+ "hookified": "^1.5.0",
36
+ "keyv": "^5.2.1"
37
37
  },
38
38
  "keywords": [
39
39
  "cacheable",