cacheable 1.8.3 → 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
  };
package/dist/index.d.cts CHANGED
@@ -635,4 +635,4 @@ declare class Cacheable extends Hookified {
635
635
  private setTtl;
636
636
  }
637
637
 
638
- 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
@@ -635,4 +635,4 @@ declare class Cacheable extends Hookified {
635
635
  private setTtl;
636
636
  }
637
637
 
638
- 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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cacheable",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "description": "Simple Caching Engine using Keyv",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,9 +22,9 @@
22
22
  "private": false,
23
23
  "devDependencies": {
24
24
  "@keyv/redis": "^3.0.1",
25
- "@types/node": "^22.8.4",
25
+ "@types/node": "^22.9.0",
26
26
  "@vitest/coverage-v8": "^2.1.4",
27
- "lru-cache": "^11.0.1",
27
+ "lru-cache": "^11.0.2",
28
28
  "rimraf": "^6.0.1",
29
29
  "tsup": "^8.3.5",
30
30
  "typescript": "^5.6.3",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "hookified": "^1.5.0",
36
- "keyv": "^5.1.2"
36
+ "keyv": "^5.2.1"
37
37
  },
38
38
  "keywords": [
39
39
  "cacheable",