cacheable 1.8.3 → 1.8.5

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
 
@@ -323,11 +347,24 @@ const cache = new CacheableMemory();
323
347
  const wrappedFunction = cache.wrap(syncFunction, { ttl: '1h', key: 'syncFunction' });
324
348
  console.log(wrappedFunction(2)); // 4
325
349
  console.log(wrappedFunction(2)); // 4 from cache
326
- console.log(cache.get('syncFunction')); // 4
327
350
  ```
328
351
 
329
352
  In this example we are wrapping a `sync` 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 set the `key` property in the `wrap()` options to set a custom key for the cache.
330
353
 
354
+ When an error occurs in the function it will not cache the value and will return the error. This is useful if you want to cache the results of a function but not cache the error. If you want it to cache the error you can set the `cacheError` property to `true` in the `wrap()` options. This is disabled by default.
355
+
356
+ ```javascript
357
+ import { CacheableMemory } from 'cacheable';
358
+ const syncFunction = (value: number) => {
359
+ throw new Error('error');
360
+ };
361
+
362
+ const cache = new CacheableMemory();
363
+ const wrappedFunction = cache.wrap(syncFunction, { ttl: '1h', key: 'syncFunction', cacheError: true });
364
+ console.log(wrappedFunction()); // error
365
+ console.log(wrappedFunction()); // error from cache
366
+ ```
367
+
331
368
  # Keyv Storage Adapter - KeyvCacheableMemory
332
369
 
333
370
  `cacheable` comes with a built-in storage adapter for Keyv called `KeyvCacheableMemory`. This takes `CacheableMemory` and creates a storage adapter for Keyv. This is useful if you want to use `CacheableMemory` as a storage adapter for Keyv. Here is an example of how to use `KeyvCacheableMemory`:
package/dist/index.cjs CHANGED
@@ -45,7 +45,7 @@ __export(src_exports, {
45
45
  });
46
46
  module.exports = __toCommonJS(src_exports);
47
47
  var import_keyv = require("keyv");
48
- var import_hookified = require("hookified");
48
+ var import_hookified2 = require("hookified");
49
49
 
50
50
  // src/shorthand-time.ts
51
51
  var shorthandToMilliseconds = (shorthand) => {
@@ -114,6 +114,9 @@ var shorthandToTime = (shorthand, fromDate) => {
114
114
  return fromDate.getTime() + milliseconds;
115
115
  };
116
116
 
117
+ // src/memory.ts
118
+ var import_hookified = require("hookified");
119
+
117
120
  // src/hash.ts
118
121
  var crypto = __toESM(require("crypto"), 1);
119
122
  function hash(object, algorithm = "sha256") {
@@ -126,6 +129,61 @@ function hash(object, algorithm = "sha256") {
126
129
  return hasher.digest("hex");
127
130
  }
128
131
 
132
+ // src/coalesce-async.ts
133
+ var callbacks = /* @__PURE__ */ new Map();
134
+ function hasKey(key) {
135
+ return callbacks.has(key);
136
+ }
137
+ function addKey(key) {
138
+ callbacks.set(key, []);
139
+ }
140
+ function removeKey(key) {
141
+ callbacks.delete(key);
142
+ }
143
+ function addCallbackToKey(key, callback) {
144
+ const stash = getCallbacksByKey(key);
145
+ stash.push(callback);
146
+ callbacks.set(key, stash);
147
+ }
148
+ function getCallbacksByKey(key) {
149
+ return callbacks.get(key) ?? [];
150
+ }
151
+ async function enqueue(key) {
152
+ return new Promise((resolve, reject) => {
153
+ const callback = { resolve, reject };
154
+ addCallbackToKey(key, callback);
155
+ });
156
+ }
157
+ function dequeue(key) {
158
+ const stash = getCallbacksByKey(key);
159
+ removeKey(key);
160
+ return stash;
161
+ }
162
+ function coalesce(options) {
163
+ const { key, error, result } = options;
164
+ for (const callback of dequeue(key)) {
165
+ if (error) {
166
+ callback.reject(error);
167
+ } else {
168
+ callback.resolve(result);
169
+ }
170
+ }
171
+ }
172
+ async function coalesceAsync(key, fnc) {
173
+ if (!hasKey(key)) {
174
+ addKey(key);
175
+ try {
176
+ const result = await Promise.resolve(fnc());
177
+ coalesce({ key, result });
178
+ return result;
179
+ } catch (error) {
180
+ coalesce({ key, error });
181
+ throw error;
182
+ }
183
+ }
184
+ return enqueue(key);
185
+ }
186
+
129
187
  // src/wrap.ts
130
188
  function wrapSync(function_, options) {
131
189
  const { ttl, keyPrefix, cache } = options;
@@ -133,8 +191,15 @@ function wrapSync(function_, options) {
133
191
  const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
134
192
  let value = cache.get(cacheKey);
135
193
  if (value === void 0) {
136
- value = function_(...arguments_);
137
- cache.set(cacheKey, value, ttl);
194
+ try {
195
+ value = function_(...arguments_);
196
+ cache.set(cacheKey, value, ttl);
197
+ } catch (error) {
198
+ cache.emit("error", error);
199
+ if (options.cacheErrors) {
200
+ cache.set(cacheKey, error, ttl);
201
+ }
202
+ }
138
203
  }
139
204
  return value;
140
205
  };
@@ -142,11 +207,22 @@ function wrapSync(function_, options) {
142
207
  function wrap(function_, options) {
143
208
  const { ttl, keyPrefix, cache } = options;
144
209
  return async function(...arguments_) {
210
+ let value;
145
211
  const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
146
- let value = await cache.get(cacheKey);
212
+ value = await cache.get(cacheKey);
147
213
  if (value === void 0) {
148
- value = await function_(...arguments_);
149
- await cache.set(cacheKey, value, ttl);
214
+ value = await coalesceAsync(cacheKey, async () => {
215
+ try {
216
+ const result = await function_(...arguments_);
217
+ await cache.set(cacheKey, result, ttl);
218
+ return result;
219
+ } catch (error) {
220
+ cache.emit("error", error);
221
+ if (options.cacheErrors) {
222
+ await cache.set(cacheKey, error, ttl);
223
+ }
224
+ }
225
+ });
150
226
  }
151
227
  return value;
152
228
  };
@@ -232,7 +308,7 @@ var DoublyLinkedList = class {
232
308
  };
233
309
 
234
310
  // src/memory.ts
235
- var CacheableMemory = class {
311
+ var CacheableMemory = class extends import_hookified.Hookified {
236
312
  _lru = new DoublyLinkedList();
237
313
  _hashCache = /* @__PURE__ */ new Map();
238
314
  _hash0 = /* @__PURE__ */ new Map();
@@ -260,6 +336,7 @@ var CacheableMemory = class {
260
336
  * @param {CacheableMemoryOptions} [options] - The options for the CacheableMemory
261
337
  */
262
338
  constructor(options) {
339
+ super();
263
340
  if (options?.ttl) {
264
341
  this.setTtl(options.ttl);
265
342
  }
@@ -805,6 +882,7 @@ var KeyvCacheableMemory = class {
805
882
  return this.getStore(this._namespace).has(key);
806
883
  }
807
884
  on(event, listener) {
885
+ this.getStore(this._namespace).on(event, listener);
808
886
  return this;
809
887
  }
810
888
  getStore(namespace) {
@@ -1052,7 +1130,7 @@ var CacheableEvents = /* @__PURE__ */ ((CacheableEvents2) => {
1052
1130
  CacheableEvents2["ERROR"] = "error";
1053
1131
  return CacheableEvents2;
1054
1132
  })(CacheableEvents || {});
1055
- var Cacheable = class extends import_hookified.Hookified {
1133
+ var Cacheable = class extends import_hookified2.Hookified {
1056
1134
  _primary = new import_keyv.Keyv({ store: new KeyvCacheableMemory() });
1057
1135
  _secondary;
1058
1136
  _nonBlocking = false;
@@ -1213,6 +1291,9 @@ var Cacheable = class extends import_hookified.Hookified {
1213
1291
  */
1214
1292
  setPrimary(primary) {
1215
1293
  this._primary = primary instanceof import_keyv.Keyv ? primary : new import_keyv.Keyv(primary);
1294
+ this._primary.on("error", (error) => {
1295
+ this.emit("error" /* ERROR */, error);
1296
+ });
1216
1297
  }
1217
1298
  /**
1218
1299
  * Sets the secondary store for the cacheable instance. If it is set to undefined then the secondary store is disabled.
@@ -1221,6 +1302,9 @@ var Cacheable = class extends import_hookified.Hookified {
1221
1302
  */
1222
1303
  setSecondary(secondary) {
1223
1304
  this._secondary = secondary instanceof import_keyv.Keyv ? secondary : new import_keyv.Keyv(secondary);
1305
+ this._secondary.on("error", (error) => {
1306
+ this.emit("error" /* ERROR */, error);
1307
+ });
1224
1308
  }
1225
1309
  getNameSpace() {
1226
1310
  if (typeof this._namespace === "function") {
package/dist/index.d.cts CHANGED
@@ -112,6 +112,7 @@ type CacheableStoreItem = {
112
112
  type WrapFunctionOptions = {
113
113
  ttl?: number | string;
114
114
  keyPrefix?: string;
115
+ cacheErrors?: boolean;
115
116
  };
116
117
  type WrapOptions = WrapFunctionOptions & {
117
118
  cache: Cacheable;
@@ -138,7 +139,7 @@ type CacheableMemoryOptions = {
138
139
  lruSize?: number;
139
140
  checkInterval?: number;
140
141
  };
141
- declare class CacheableMemory {
142
+ declare class CacheableMemory extends Hookified {
142
143
  private _lru;
143
144
  private readonly _hashCache;
144
145
  private readonly _hash0;
@@ -635,4 +636,4 @@ declare class Cacheable extends Hookified {
635
636
  private setTtl;
636
637
  }
637
638
 
638
- export { Cacheable, CacheableEvents, CacheableHooks, type CacheableItem, CacheableMemory, type CacheableOptions, CacheableStats, KeyvCacheableMemory, type WrapOptions, type WrapSyncOptions, shorthandToMilliseconds, shorthandToTime, wrap, wrapSync };
639
+ 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
@@ -112,6 +112,7 @@ type CacheableStoreItem = {
112
112
  type WrapFunctionOptions = {
113
113
  ttl?: number | string;
114
114
  keyPrefix?: string;
115
+ cacheErrors?: boolean;
115
116
  };
116
117
  type WrapOptions = WrapFunctionOptions & {
117
118
  cache: Cacheable;
@@ -138,7 +139,7 @@ type CacheableMemoryOptions = {
138
139
  lruSize?: number;
139
140
  checkInterval?: number;
140
141
  };
141
- declare class CacheableMemory {
142
+ declare class CacheableMemory extends Hookified {
142
143
  private _lru;
143
144
  private readonly _hashCache;
144
145
  private readonly _hash0;
@@ -635,4 +636,4 @@ declare class Cacheable extends Hookified {
635
636
  private setTtl;
636
637
  }
637
638
 
638
- export { Cacheable, CacheableEvents, CacheableHooks, type CacheableItem, CacheableMemory, type CacheableOptions, CacheableStats, KeyvCacheableMemory, type WrapOptions, type WrapSyncOptions, shorthandToMilliseconds, shorthandToTime, wrap, wrapSync };
639
+ 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
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { Keyv } from "keyv";
3
- import { Hookified } from "hookified";
3
+ import { Hookified as Hookified2 } from "hookified";
4
4
 
5
5
  // src/shorthand-time.ts
6
6
  var shorthandToMilliseconds = (shorthand) => {
@@ -69,6 +69,9 @@ var shorthandToTime = (shorthand, fromDate) => {
69
69
  return fromDate.getTime() + milliseconds;
70
70
  };
71
71
 
72
+ // src/memory.ts
73
+ import { Hookified } from "hookified";
74
+
72
75
  // src/hash.ts
73
76
  import * as crypto from "node:crypto";
74
77
  function hash(object, algorithm = "sha256") {
@@ -81,6 +84,61 @@ function hash(object, algorithm = "sha256") {
81
84
  return hasher.digest("hex");
82
85
  }
83
86
 
87
+ // src/coalesce-async.ts
88
+ var callbacks = /* @__PURE__ */ new Map();
89
+ function hasKey(key) {
90
+ return callbacks.has(key);
91
+ }
92
+ function addKey(key) {
93
+ callbacks.set(key, []);
94
+ }
95
+ function removeKey(key) {
96
+ callbacks.delete(key);
97
+ }
98
+ function addCallbackToKey(key, callback) {
99
+ const stash = getCallbacksByKey(key);
100
+ stash.push(callback);
101
+ callbacks.set(key, stash);
102
+ }
103
+ function getCallbacksByKey(key) {
104
+ return callbacks.get(key) ?? [];
105
+ }
106
+ async function enqueue(key) {
107
+ return new Promise((resolve, reject) => {
108
+ const callback = { resolve, reject };
109
+ addCallbackToKey(key, callback);
110
+ });
111
+ }
112
+ function dequeue(key) {
113
+ const stash = getCallbacksByKey(key);
114
+ removeKey(key);
115
+ return stash;
116
+ }
117
+ function coalesce(options) {
118
+ const { key, error, result } = options;
119
+ for (const callback of dequeue(key)) {
120
+ if (error) {
121
+ callback.reject(error);
122
+ } else {
123
+ callback.resolve(result);
124
+ }
125
+ }
126
+ }
127
+ async function coalesceAsync(key, fnc) {
128
+ if (!hasKey(key)) {
129
+ addKey(key);
130
+ try {
131
+ const result = await Promise.resolve(fnc());
132
+ coalesce({ key, result });
133
+ return result;
134
+ } catch (error) {
135
+ coalesce({ key, error });
136
+ throw error;
137
+ }
138
+ }
139
+ return enqueue(key);
140
+ }
141
+
84
142
  // src/wrap.ts
85
143
  function wrapSync(function_, options) {
86
144
  const { ttl, keyPrefix, cache } = options;
@@ -88,8 +146,15 @@ function wrapSync(function_, options) {
88
146
  const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
89
147
  let value = cache.get(cacheKey);
90
148
  if (value === void 0) {
91
- value = function_(...arguments_);
92
- cache.set(cacheKey, value, ttl);
149
+ try {
150
+ value = function_(...arguments_);
151
+ cache.set(cacheKey, value, ttl);
152
+ } catch (error) {
153
+ cache.emit("error", error);
154
+ if (options.cacheErrors) {
155
+ cache.set(cacheKey, error, ttl);
156
+ }
157
+ }
93
158
  }
94
159
  return value;
95
160
  };
@@ -97,11 +162,22 @@ function wrapSync(function_, options) {
97
162
  function wrap(function_, options) {
98
163
  const { ttl, keyPrefix, cache } = options;
99
164
  return async function(...arguments_) {
165
+ let value;
100
166
  const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
101
- let value = await cache.get(cacheKey);
167
+ value = await cache.get(cacheKey);
102
168
  if (value === void 0) {
103
- value = await function_(...arguments_);
104
- await cache.set(cacheKey, value, ttl);
169
+ value = await coalesceAsync(cacheKey, async () => {
170
+ try {
171
+ const result = await function_(...arguments_);
172
+ await cache.set(cacheKey, result, ttl);
173
+ return result;
174
+ } catch (error) {
175
+ cache.emit("error", error);
176
+ if (options.cacheErrors) {
177
+ await cache.set(cacheKey, error, ttl);
178
+ }
179
+ }
180
+ });
105
181
  }
106
182
  return value;
107
183
  };
@@ -187,7 +263,7 @@ var DoublyLinkedList = class {
187
263
  };
188
264
 
189
265
  // src/memory.ts
190
- var CacheableMemory = class {
266
+ var CacheableMemory = class extends Hookified {
191
267
  _lru = new DoublyLinkedList();
192
268
  _hashCache = /* @__PURE__ */ new Map();
193
269
  _hash0 = /* @__PURE__ */ new Map();
@@ -215,6 +291,7 @@ var CacheableMemory = class {
215
291
  * @param {CacheableMemoryOptions} [options] - The options for the CacheableMemory
216
292
  */
217
293
  constructor(options) {
294
+ super();
218
295
  if (options?.ttl) {
219
296
  this.setTtl(options.ttl);
220
297
  }
@@ -760,6 +837,7 @@ var KeyvCacheableMemory = class {
760
837
  return this.getStore(this._namespace).has(key);
761
838
  }
762
839
  on(event, listener) {
840
+ this.getStore(this._namespace).on(event, listener);
763
841
  return this;
764
842
  }
765
843
  getStore(namespace) {
@@ -1010,7 +1088,7 @@ var CacheableEvents = /* @__PURE__ */ ((CacheableEvents2) => {
1010
1088
  CacheableEvents2["ERROR"] = "error";
1011
1089
  return CacheableEvents2;
1012
1090
  })(CacheableEvents || {});
1013
- var Cacheable = class extends Hookified {
1091
+ var Cacheable = class extends Hookified2 {
1014
1092
  _primary = new Keyv({ store: new KeyvCacheableMemory() });
1015
1093
  _secondary;
1016
1094
  _nonBlocking = false;
@@ -1171,6 +1249,9 @@ var Cacheable = class extends Hookified {
1171
1249
  */
1172
1250
  setPrimary(primary) {
1173
1251
  this._primary = primary instanceof Keyv ? primary : new Keyv(primary);
1252
+ this._primary.on("error", (error) => {
1253
+ this.emit("error" /* ERROR */, error);
1254
+ });
1174
1255
  }
1175
1256
  /**
1176
1257
  * Sets the secondary store for the cacheable instance. If it is set to undefined then the secondary store is disabled.
@@ -1179,6 +1260,9 @@ var Cacheable = class extends Hookified {
1179
1260
  */
1180
1261
  setSecondary(secondary) {
1181
1262
  this._secondary = secondary instanceof Keyv ? secondary : new Keyv(secondary);
1263
+ this._secondary.on("error", (error) => {
1264
+ this.emit("error" /* ERROR */, error);
1265
+ });
1182
1266
  }
1183
1267
  getNameSpace() {
1184
1268
  if (typeof this._namespace === "function") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cacheable",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
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",