cacheable 1.8.9 → 1.9.0

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
@@ -29,6 +29,7 @@
29
29
  * [Basic Usage](#basic-usage)
30
30
  * [Hooks and Events](#hooks-and-events)
31
31
  * [Storage Tiering and Caching](#storage-tiering-and-caching)
32
+ * [TTL Propagation and Storage Tiering](#ttl-propagation-and-storage-tiering)
32
33
  * [Shorthand for Time to Live (ttl)](#shorthand-for-time-to-live-ttl)
33
34
  * [Non-Blocking Operations](#non-blocking-operations)
34
35
  * [CacheSync - Distributed Updates](#cachesync---distributed-updates)
@@ -38,8 +39,8 @@
38
39
  * [CacheableMemory - In-Memory Cache](#cacheablememory---in-memory-cache)
39
40
  * [CacheableMemory Options](#cacheablememory-options)
40
41
  * [CacheableMemory - API](#cacheablememory---api)
41
- * [Wrap / Memoization for Sync and Async Functions](#wrap--memoization-for-sync-and-async-functions)
42
42
  * [Keyv Storage Adapter - KeyvCacheableMemory](#keyv-storage-adapter---keyvcacheablememory)
43
+ * [Wrap / Memoization for Sync and Async Functions](#wrap--memoization-for-sync-and-async-functions)
43
44
  * [How to Contribute](#how-to-contribute)
44
45
  * [License and Copyright](#license-and-copyright)
45
46
 
@@ -98,6 +99,7 @@ The following hooks are available for you to extend the functionality of `cachea
98
99
  * `AFTER_GET`: This is called after the `get()` method is called.
99
100
  * `BEFORE_GET_MANY`: This is called before the `getMany()` method is called.
100
101
  * `AFTER_GET_MANY`: This is called after the `getMany()` method is called.
102
+ * `BEFORE_SECONDARY_SETS_PRIMARY`: This is called when the secondary store sets the value in the primary store.
101
103
 
102
104
  An example of how to use these hooks:
103
105
 
@@ -110,6 +112,19 @@ cacheable.onHook(CacheableHooks.BEFORE_SET, (data) => {
110
112
  });
111
113
  ```
112
114
 
115
+ Here is an example of how to use `BEFORE_SECONDARY_SETS_PRIMARY` hook:
116
+
117
+ ```javascript
118
+ import { Cacheable, CacheableHooks } from 'cacheable';
119
+ import KeyvRedis from '@keyv/redis';
120
+ const secondary = new KeyvRedis('redis://user:pass@localhost:6379');
121
+ const cache = new Cacheable({secondary});
122
+ cache.onHook(CacheableHooks.BEFORE_SECONDARY_SETS_PRIMARY, (data) => {
123
+ console.log(`before secondary sets primary: ${data.key} ${data.value} ${data.ttl}`);
124
+ });
125
+ ```
126
+ This is called when the secondary store sets the value in the primary store. This is useful if you want to do something before the value is set in the primary store such as manipulating the ttl or the value.
127
+
113
128
  # Storage Tiering and Caching
114
129
 
115
130
  `cacheable` is built as a layer 1 and layer 2 caching engine by default. The purpose is to have your layer 1 be fast and your layer 2 be more persistent. The primary store is the layer 1 cache and the secondary store is the layer 2 cache. By adding the secondary store you are enabling layer 2 caching. By default the operations are blocking but fault tolerant:
@@ -119,6 +134,48 @@ cacheable.onHook(CacheableHooks.BEFORE_SET, (data) => {
119
134
  * `Deleting Data`: Deletes the value from the primary store and secondary store at the same time waiting for both to respond.
120
135
  * `Clearing Data`: Clears the primary store and secondary store at the same time waiting for both to respond.
121
136
 
137
+ When `Getting Data` if the value does not exist in the primary store it will try to get it from the secondary store. If the secondary store returns the value it will set it in the primary store. Because we use [TTL Propagation](#ttl-propagation-and-storage-tiering) the value will be set in the primary store with the TTL of the secondary store unless the time to live (TTL) is greater than the primary store which will then use the TTL of the primary store. An example of this is:
138
+
139
+ ```javascript
140
+ import { Cacheable } from 'cacheable';
141
+ import KeyvRedis from '@keyv/redis';
142
+ const secondary = new KeyvRedis('redis://user:pass@localhost:6379', { ttl: 1000 });
143
+ const cache = new Cacheable({secondary, ttl: 100});
144
+
145
+ await cache.set('key', 'value'); // sets the value in the primary store with a ttl of 100 ms and secondary store with a ttl of 1000 ms
146
+
147
+ await sleep(500); // wait for .5 seconds
148
+
149
+ const value = await cache.get('key'); // gets the value from the secondary store and now sets the value in the primary store with a ttl of 500 ms which is what is left from the secondary store
150
+ ```
151
+
152
+ In this example the primary store has a ttl of `100 ms` and the secondary store has a ttl of `1000 ms`. Because the ttl is greater in the secondary store it will default to setting ttl value in the primary store.
153
+
154
+ ```javascript
155
+ import { Cacheable } from 'cacheable';
156
+ import {Keyv} from 'keyv';
157
+ import KeyvRedis from '@keyv/redis';
158
+ const primary = new Keyv({ ttl: 200 });
159
+ const secondary = new KeyvRedis('redis://user:pass@localhost:6379', { ttl: 1000 });
160
+ const cache = new Cacheable({primary, secondary});
161
+
162
+ await cache.set('key', 'value'); // sets the value in the primary store with a ttl of 100 ms and secondary store with a ttl of 1000 ms
163
+
164
+ await sleep(200); // wait for .2 seconds
165
+
166
+ const value = await cache.get('key'); // gets the value from the secondary store and now sets the value in the primary store with a ttl of 200 ms which is what the primary store is set with
167
+ ```
168
+
169
+ # TTL Propagation and Storage Tiering
170
+
171
+ Cacheable TTL propagation is a feature that allows you to set a time to live (TTL) for the cache. By default the TTL is set in the following order:
172
+
173
+ ```
174
+ ttl = set at the function ?? storage adapter ttl ?? cacheable ttl
175
+ ```
176
+
177
+ This means that if you set a TTL at the function level it will override the storage adapter TTL and the cacheable TTL. If you do not set a TTL at the function level it will use the storage adapter TTL and then the cacheable TTL. If you do not set a TTL at all it will use the default TTL of `undefined` which is disabled.
178
+
122
179
  # Shorthand for Time to Live (ttl)
123
180
 
124
181
  By default `Cacheable` and `CacheableMemory` the `ttl` is in milliseconds but you can use shorthand for the time to live. Here are the following shorthand values:
@@ -157,6 +214,40 @@ cache.ttl = -1; // sets the default ttl to 0 which is disabled
157
214
  console.log(cache.ttl); // undefined
158
215
  ```
159
216
 
217
+ ## Retrieving raw cache entries
218
+
219
+ The `get` and `getMany` methods support a `raw` option, which returns the full stored metadata (`StoredDataRaw<T>`) instead of just the value:
220
+
221
+ ```typescript
222
+ import { Cacheable } from 'cacheable';
223
+
224
+ const cache = new Cacheable();
225
+
226
+ // store a value
227
+ await cache.set('user:1', { name: 'Alice' });
228
+
229
+ // default: only the value
230
+ const user = await cache.get<{ name: string }>('user:1');
231
+ console.log(user); // { name: 'Alice' }
232
+
233
+ // with raw: full record including expiration
234
+ const raw = await cache.get<{ name: string }>('user:1', { raw: true });
235
+ console.log(raw.value); // { name: 'Alice' }
236
+ console.log(raw.expires); // e.g. 1677628495000 or null
237
+ ```
238
+
239
+ ```typescript
240
+ // getMany with raw option
241
+ await cache.set('a', 1);
242
+ await cache.set('b', 2);
243
+
244
+ const raws = await cache.getMany<number>(['a', 'b'], { raw: true });
245
+ raws.forEach((entry, idx) => {
246
+ console.log(`key=${['a','b'][idx]}, value=${entry?.value}, expires=${entry?.expires}`);
247
+ });
248
+ ```
249
+
250
+
160
251
  # Non-Blocking Operations
161
252
 
162
253
  If you want your layer 2 (secondary) store to be non-blocking you can set the `nonBlocking` property to `true` in the options. This will make the secondary store non-blocking and will not wait for the secondary store to respond on `setting data`, `deleting data`, or `clearing data`. This is useful if you want to have a faster response time and not wait for the secondary store to respond.
@@ -215,7 +306,9 @@ _This does not enable statistics for your layer 2 cache as that is a distributed
215
306
  * `set(key, value, ttl?)`: Sets a value in the cache.
216
307
  * `setMany([{key, value, ttl?}])`: Sets multiple values in the cache.
217
308
  * `get(key)`: Gets a value from the cache.
309
+ * `get(key, { raw: true })`: Gets a raw value from the cache.
218
310
  * `getMany([keys])`: Gets multiple values from the cache.
311
+ * `getMany([keys], { raw: true })`: Gets multiple raw values from the cache.
219
312
  * `has(key)`: Checks if a value exists in the cache.
220
313
  * `hasMany([keys])`: Checks if multiple values exist in the cache.
221
314
  * `take(key)`: Takes a value from the cache and deletes it.
@@ -289,6 +382,20 @@ By default we use lazy expiration deletion which means on `get` and `getMany` ty
289
382
  * `stopIntervalCheck()`: Stops the interval check for expired keys.
290
383
  * `hash(object: any, algorithm = 'sha256'): string`: Hashes an object with the algorithm. Default is `sha256`.
291
384
 
385
+ # Keyv Storage Adapter - KeyvCacheableMemory
386
+
387
+ `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`:
388
+
389
+ ```javascript
390
+ import { Keyv } from 'keyv';
391
+ import { KeyvCacheableMemory } from 'cacheable';
392
+
393
+ const keyv = new Keyv({ store: new KeyvCacheableMemory() });
394
+ await keyv.set('foo', 'bar');
395
+ const value = await keyv.get('foo');
396
+ console.log(value); // bar
397
+ ```
398
+
292
399
  # Wrap / Memoization for Sync and Async Functions
293
400
 
294
401
  `Cacheable` and `CacheableMemory` has a feature called `wrap` that allows you to wrap a function in a cache. This is useful for memoization and caching the results of a function. You can wrap a `sync` or `async` function in a cache. Here is an example of how to use the `wrap` function:
@@ -363,20 +470,6 @@ console.log(wrappedFunction()); // error
363
470
  console.log(wrappedFunction()); // error from cache
364
471
  ```
365
472
 
366
- # Keyv Storage Adapter - KeyvCacheableMemory
367
-
368
- `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`:
369
-
370
- ```javascript
371
- import { Keyv } from 'keyv';
372
- import { KeyvCacheableMemory } from 'cacheable';
373
-
374
- const keyv = new Keyv({ store: new KeyvCacheableMemory() });
375
- await keyv.set('foo', 'bar');
376
- const value = await keyv.get('foo');
377
- console.log(value); // bar
378
- ```
379
-
380
473
  # How to Contribute
381
474
 
382
475
  You can contribute by forking the repo and submitting a pull request. Please make sure to add tests and update the documentation. To learn more about how to contribute go to our main README [https://github.com/jaredwray/cacheable](https://github.com/jaredwray/cacheable). This will talk about how to `Open a Pull Request`, `Ask a Question`, or `Post an Issue`.
package/dist/index.cjs CHANGED
@@ -613,6 +613,7 @@ var CacheableMemory = class extends import_hookified.Hookified {
613
613
  delete(key) {
614
614
  const store = this.getStore(key);
615
615
  store.delete(key);
616
+ this._hashCache.delete(key);
616
617
  }
617
618
  /**
618
619
  * Delete the keys
@@ -697,7 +698,7 @@ var CacheableMemory = class extends import_hookified.Hookified {
697
698
  */
698
699
  hashKey(key) {
699
700
  const cacheHashNumber = this._hashCache.get(key);
700
- if (cacheHashNumber) {
701
+ if (typeof cacheHashNumber === "number") {
701
702
  return cacheHashNumber;
702
703
  }
703
704
  let hash2 = 0;
@@ -1147,6 +1148,35 @@ var CacheableStats = class {
1147
1148
  }
1148
1149
  };
1149
1150
 
1151
+ // src/ttl.ts
1152
+ function getTtlFromExpires(expires) {
1153
+ if (expires === void 0 || expires === null) {
1154
+ return void 0;
1155
+ }
1156
+ const now = Date.now();
1157
+ if (expires < now) {
1158
+ return void 0;
1159
+ }
1160
+ return expires - now;
1161
+ }
1162
+ function getCascadingTtl(cacheableTtl, primaryTtl, secondaryTtl) {
1163
+ return secondaryTtl ?? primaryTtl ?? shorthandToMilliseconds(cacheableTtl);
1164
+ }
1165
+ function calculateTtlFromExpiration(ttl, expires) {
1166
+ const ttlFromExpires = getTtlFromExpires(expires);
1167
+ const expiresFromTtl = ttl ? Date.now() + ttl : void 0;
1168
+ if (ttlFromExpires === void 0) {
1169
+ return ttl;
1170
+ }
1171
+ if (expiresFromTtl === void 0) {
1172
+ return ttlFromExpires;
1173
+ }
1174
+ if (expires > expiresFromTtl) {
1175
+ return ttl;
1176
+ }
1177
+ return ttlFromExpires;
1178
+ }
1179
+
1150
1180
  // src/index.ts
1151
1181
  var import_keyv3 = require("keyv");
1152
1182
  var CacheableHooks = /* @__PURE__ */ ((CacheableHooks2) => {
@@ -1158,6 +1188,7 @@ var CacheableHooks = /* @__PURE__ */ ((CacheableHooks2) => {
1158
1188
  CacheableHooks2["AFTER_GET"] = "AFTER_GET";
1159
1189
  CacheableHooks2["BEFORE_GET_MANY"] = "BEFORE_GET_MANY";
1160
1190
  CacheableHooks2["AFTER_GET_MANY"] = "AFTER_GET_MANY";
1191
+ CacheableHooks2["BEFORE_SECONDARY_SETS_PRIMARY"] = "BEFORE_SECONDARY_SETS_PRIMARY";
1161
1192
  return CacheableHooks2;
1162
1193
  })(CacheableHooks || {});
1163
1194
  var CacheableEvents = /* @__PURE__ */ ((CacheableEvents2) => {
@@ -1366,25 +1397,26 @@ var Cacheable = class extends import_hookified2.Hookified {
1366
1397
  }
1367
1398
  return this._namespace;
1368
1399
  }
1369
- /**
1370
- * Gets the value of the key. If the key does not exist in the primary store then it will check the secondary store.
1371
- * @param {string} key The key to get the value of
1372
- * @returns {Promise<T | undefined>} The value of the key or undefined if the key does not exist
1373
- */
1374
- async get(key) {
1400
+ async get(key, options = {}) {
1375
1401
  let result;
1402
+ const { raw = false } = options;
1376
1403
  try {
1377
1404
  await this.hook("BEFORE_GET" /* BEFORE_GET */, key);
1378
- result = await this._primary.get(key);
1405
+ result = await this._primary.get(key, { raw: true });
1406
+ let ttl;
1379
1407
  if (!result && this._secondary) {
1380
- const rawResult = await this._secondary.get(key, { raw: true });
1381
- if (rawResult) {
1382
- result = rawResult.value;
1383
- const finalTtl = rawResult.expires ?? void 0;
1384
- await this._primary.set(key, result, finalTtl);
1408
+ const secondaryResult = await this.getSecondaryRawResults(key);
1409
+ if (secondaryResult?.value) {
1410
+ result = secondaryResult;
1411
+ const cascadeTtl = getCascadingTtl(this._ttl, this._primary.ttl);
1412
+ const expires = secondaryResult.expires ?? void 0;
1413
+ ttl = calculateTtlFromExpiration(cascadeTtl, expires);
1414
+ const setItem = { key, value: result.value, ttl };
1415
+ await this.hook("BEFORE_SECONDARY_SETS_PRIMARY" /* BEFORE_SECONDARY_SETS_PRIMARY */, setItem);
1416
+ await this._primary.set(setItem.key, setItem.value, setItem.ttl);
1385
1417
  }
1386
1418
  }
1387
- await this.hook("AFTER_GET" /* AFTER_GET */, { key, result });
1419
+ await this.hook("AFTER_GET" /* AFTER_GET */, { key, result, ttl });
1388
1420
  } catch (error) {
1389
1421
  this.emit("error" /* ERROR */, error);
1390
1422
  }
@@ -1396,18 +1428,14 @@ var Cacheable = class extends import_hookified2.Hookified {
1396
1428
  }
1397
1429
  this.stats.incrementGets();
1398
1430
  }
1399
- return result;
1431
+ return raw ? result : result?.value;
1400
1432
  }
1401
- /**
1402
- * Gets the values of the keys. If the key does not exist in the primary store then it will check the secondary store.
1403
- * @param {string[]} keys The keys to get the values of
1404
- * @returns {Promise<Array<T | undefined>>} The values of the keys or undefined if the key does not exist
1405
- */
1406
- async getMany(keys) {
1433
+ async getMany(keys, options = {}) {
1407
1434
  let result = [];
1435
+ const { raw = false } = options;
1408
1436
  try {
1409
1437
  await this.hook("BEFORE_GET_MANY" /* BEFORE_GET_MANY */, keys);
1410
- result = await this._primary.get(keys);
1438
+ result = await this._primary.get(keys, { raw: true });
1411
1439
  if (this._secondary) {
1412
1440
  const missingKeys = [];
1413
1441
  for (const [i, key] of keys.entries()) {
@@ -1415,12 +1443,16 @@ var Cacheable = class extends import_hookified2.Hookified {
1415
1443
  missingKeys.push(key);
1416
1444
  }
1417
1445
  }
1418
- const secondaryResult = await this._secondary.get(missingKeys);
1419
- for (const [i, key] of keys.entries()) {
1420
- if (!result[i] && secondaryResult[i]) {
1421
- result[i] = secondaryResult[i];
1422
- const finalTtl = shorthandToMilliseconds(this._ttl);
1423
- await this._primary.set(key, secondaryResult[i], finalTtl);
1446
+ const secondaryResults = await this.getManySecondaryRawResults(missingKeys);
1447
+ for await (const [i, key] of keys.entries()) {
1448
+ if (!result[i] && secondaryResults[i]) {
1449
+ result[i] = secondaryResults[i];
1450
+ const cascadeTtl = getCascadingTtl(this._ttl, this._primary.ttl);
1451
+ const expires = secondaryResults[i].expires;
1452
+ const ttl = calculateTtlFromExpiration(cascadeTtl, expires);
1453
+ const setItem = { key, value: result[i].value, ttl };
1454
+ await this.hook("BEFORE_SECONDARY_SETS_PRIMARY" /* BEFORE_SECONDARY_SETS_PRIMARY */, setItem);
1455
+ await this._primary.set(setItem.key, setItem.value, setItem.ttl);
1424
1456
  }
1425
1457
  }
1426
1458
  }
@@ -1438,7 +1470,7 @@ var Cacheable = class extends import_hookified2.Hookified {
1438
1470
  }
1439
1471
  this.stats.incrementGets();
1440
1472
  }
1441
- return result;
1473
+ return raw ? result : result.map((item) => item?.value);
1442
1474
  }
1443
1475
  /**
1444
1476
  * Sets the value of the key. If the secondary store is set then it will also set the value in the secondary store.
@@ -1677,6 +1709,20 @@ var Cacheable = class extends import_hookified2.Hookified {
1677
1709
  hash(object, algorithm = "sha256") {
1678
1710
  return hash(object, algorithm);
1679
1711
  }
1712
+ async getSecondaryRawResults(key) {
1713
+ let result;
1714
+ if (this._secondary) {
1715
+ result = await this._secondary.get(key, { raw: true });
1716
+ }
1717
+ return result;
1718
+ }
1719
+ async getManySecondaryRawResults(keys) {
1720
+ let result = new Array();
1721
+ if (this._secondary) {
1722
+ result = await this._secondary.get(keys, { raw: true });
1723
+ }
1724
+ return result;
1725
+ }
1680
1726
  async deleteManyKeyv(keyv, keys) {
1681
1727
  const promises = [];
1682
1728
  for (const key of keys) {
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { KeyvStoreAdapter, StoredData, Keyv } from 'keyv';
1
+ import { KeyvStoreAdapter, StoredData, Keyv, StoredDataRaw } from 'keyv';
2
2
  export { Keyv, KeyvHooks, KeyvOptions, KeyvStoreAdapter } from 'keyv';
3
3
  import { Hookified } from 'hookified';
4
4
 
@@ -423,7 +423,8 @@ declare enum CacheableHooks {
423
423
  BEFORE_GET = "BEFORE_GET",
424
424
  AFTER_GET = "AFTER_GET",
425
425
  BEFORE_GET_MANY = "BEFORE_GET_MANY",
426
- AFTER_GET_MANY = "AFTER_GET_MANY"
426
+ AFTER_GET_MANY = "AFTER_GET_MANY",
427
+ BEFORE_SECONDARY_SETS_PRIMARY = "BEFORE_SECONDARY_SETS_PRIMARY"
427
428
  }
428
429
  declare enum CacheableEvents {
429
430
  ERROR = "error"
@@ -593,17 +594,46 @@ declare class Cacheable extends Hookified {
593
594
  setSecondary(secondary: Keyv | KeyvStoreAdapter): void;
594
595
  getNameSpace(): string | undefined;
595
596
  /**
596
- * Gets the value of the key. If the key does not exist in the primary store then it will check the secondary store.
597
- * @param {string} key The key to get the value of
598
- * @returns {Promise<T | undefined>} The value of the key or undefined if the key does not exist
599
- */
600
- get<T>(key: string): Promise<T | undefined>;
601
- /**
602
- * Gets the values of the keys. If the key does not exist in the primary store then it will check the secondary store.
603
- * @param {string[]} keys The keys to get the values of
604
- * @returns {Promise<Array<T | undefined>>} The values of the keys or undefined if the key does not exist
605
- */
606
- getMany<T>(keys: string[]): Promise<Array<T | undefined>>;
597
+ * Retrieves an entry from the cache, with an optional “raw” mode.
598
+ *
599
+ * Checks the primary store first; if not found and a secondary store is configured,
600
+ * it will fetch from the secondary, repopulate the primary, and return the result.
601
+ *
602
+ * @typeParam T - The expected type of the stored value.
603
+ * @param {string} key - The cache key to retrieve.
604
+ * @param {{ raw?: boolean }} [opts] - Options for retrieval.
605
+ * @param {boolean} [opts.raw=false] - If `true`, returns the full raw data object
606
+ * (`StoredDataRaw<T>`); otherwise returns just the value.
607
+ * @returns {Promise<T | StoredDataRaw<T> | undefined>}
608
+ * A promise that resolves to the cached value (or raw data) if found, or `undefined`.
609
+ */
610
+ get<T>(key: string, options?: {
611
+ raw?: false;
612
+ }): Promise<T | undefined>;
613
+ get<T>(key: string, options: {
614
+ raw: true;
615
+ }): Promise<StoredDataRaw<T>>;
616
+ /**
617
+ * Retrieves multiple entries from the cache.
618
+ * Checks the primary store for each key; if a key is missing and a secondary store is configured,
619
+ * it will fetch from the secondary store, repopulate the primary store, and return the results.
620
+ *
621
+ * @typeParam T - The expected type of the stored values.
622
+ * @param {string[]} keys - The cache keys to retrieve.
623
+ * @param {{ raw?: boolean }} [options] - Options for retrieval.
624
+ * @param {boolean} [options.raw=false] - When `true`, returns an array of raw data objects (`StoredDataRaw<T>`);
625
+ * when `false`, returns an array of unwrapped values (`T`) or `undefined` for misses.
626
+ * @returns {Promise<Array<T | undefined>> | Promise<Array<StoredDataRaw<T>>>}
627
+ * A promise that resolves to:
628
+ * - `Array<T | undefined>` if `raw` is `false` (default).
629
+ * - `Array<StoredDataRaw<T>>` if `raw` is `true`.
630
+ */
631
+ getMany<T>(keys: string[], options?: {
632
+ raw?: false;
633
+ }): Promise<Array<T | undefined>>;
634
+ getMany<T>(keys: string[], options: {
635
+ raw: true;
636
+ }): Promise<Array<StoredDataRaw<T>>>;
607
637
  /**
608
638
  * Sets the value of the key. If the secondary store is set then it will also set the value in the secondary store.
609
639
  * @param {string} key the key to set the value of
@@ -681,6 +711,8 @@ declare class Cacheable extends Hookified {
681
711
  * @returns {string} the hash of the object
682
712
  */
683
713
  hash(object: any, algorithm?: string): string;
714
+ private getSecondaryRawResults;
715
+ private getManySecondaryRawResults;
684
716
  private deleteManyKeyv;
685
717
  private setManyKeyv;
686
718
  private hasManyKeyv;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { KeyvStoreAdapter, StoredData, Keyv } from 'keyv';
1
+ import { KeyvStoreAdapter, StoredData, Keyv, StoredDataRaw } from 'keyv';
2
2
  export { Keyv, KeyvHooks, KeyvOptions, KeyvStoreAdapter } from 'keyv';
3
3
  import { Hookified } from 'hookified';
4
4
 
@@ -423,7 +423,8 @@ declare enum CacheableHooks {
423
423
  BEFORE_GET = "BEFORE_GET",
424
424
  AFTER_GET = "AFTER_GET",
425
425
  BEFORE_GET_MANY = "BEFORE_GET_MANY",
426
- AFTER_GET_MANY = "AFTER_GET_MANY"
426
+ AFTER_GET_MANY = "AFTER_GET_MANY",
427
+ BEFORE_SECONDARY_SETS_PRIMARY = "BEFORE_SECONDARY_SETS_PRIMARY"
427
428
  }
428
429
  declare enum CacheableEvents {
429
430
  ERROR = "error"
@@ -593,17 +594,46 @@ declare class Cacheable extends Hookified {
593
594
  setSecondary(secondary: Keyv | KeyvStoreAdapter): void;
594
595
  getNameSpace(): string | undefined;
595
596
  /**
596
- * Gets the value of the key. If the key does not exist in the primary store then it will check the secondary store.
597
- * @param {string} key The key to get the value of
598
- * @returns {Promise<T | undefined>} The value of the key or undefined if the key does not exist
599
- */
600
- get<T>(key: string): Promise<T | undefined>;
601
- /**
602
- * Gets the values of the keys. If the key does not exist in the primary store then it will check the secondary store.
603
- * @param {string[]} keys The keys to get the values of
604
- * @returns {Promise<Array<T | undefined>>} The values of the keys or undefined if the key does not exist
605
- */
606
- getMany<T>(keys: string[]): Promise<Array<T | undefined>>;
597
+ * Retrieves an entry from the cache, with an optional “raw” mode.
598
+ *
599
+ * Checks the primary store first; if not found and a secondary store is configured,
600
+ * it will fetch from the secondary, repopulate the primary, and return the result.
601
+ *
602
+ * @typeParam T - The expected type of the stored value.
603
+ * @param {string} key - The cache key to retrieve.
604
+ * @param {{ raw?: boolean }} [opts] - Options for retrieval.
605
+ * @param {boolean} [opts.raw=false] - If `true`, returns the full raw data object
606
+ * (`StoredDataRaw<T>`); otherwise returns just the value.
607
+ * @returns {Promise<T | StoredDataRaw<T> | undefined>}
608
+ * A promise that resolves to the cached value (or raw data) if found, or `undefined`.
609
+ */
610
+ get<T>(key: string, options?: {
611
+ raw?: false;
612
+ }): Promise<T | undefined>;
613
+ get<T>(key: string, options: {
614
+ raw: true;
615
+ }): Promise<StoredDataRaw<T>>;
616
+ /**
617
+ * Retrieves multiple entries from the cache.
618
+ * Checks the primary store for each key; if a key is missing and a secondary store is configured,
619
+ * it will fetch from the secondary store, repopulate the primary store, and return the results.
620
+ *
621
+ * @typeParam T - The expected type of the stored values.
622
+ * @param {string[]} keys - The cache keys to retrieve.
623
+ * @param {{ raw?: boolean }} [options] - Options for retrieval.
624
+ * @param {boolean} [options.raw=false] - When `true`, returns an array of raw data objects (`StoredDataRaw<T>`);
625
+ * when `false`, returns an array of unwrapped values (`T`) or `undefined` for misses.
626
+ * @returns {Promise<Array<T | undefined>> | Promise<Array<StoredDataRaw<T>>>}
627
+ * A promise that resolves to:
628
+ * - `Array<T | undefined>` if `raw` is `false` (default).
629
+ * - `Array<StoredDataRaw<T>>` if `raw` is `true`.
630
+ */
631
+ getMany<T>(keys: string[], options?: {
632
+ raw?: false;
633
+ }): Promise<Array<T | undefined>>;
634
+ getMany<T>(keys: string[], options: {
635
+ raw: true;
636
+ }): Promise<Array<StoredDataRaw<T>>>;
607
637
  /**
608
638
  * Sets the value of the key. If the secondary store is set then it will also set the value in the secondary store.
609
639
  * @param {string} key the key to set the value of
@@ -681,6 +711,8 @@ declare class Cacheable extends Hookified {
681
711
  * @returns {string} the hash of the object
682
712
  */
683
713
  hash(object: any, algorithm?: string): string;
714
+ private getSecondaryRawResults;
715
+ private getManySecondaryRawResults;
684
716
  private deleteManyKeyv;
685
717
  private setManyKeyv;
686
718
  private hasManyKeyv;
package/dist/index.js CHANGED
@@ -569,6 +569,7 @@ var CacheableMemory = class extends Hookified {
569
569
  delete(key) {
570
570
  const store = this.getStore(key);
571
571
  store.delete(key);
572
+ this._hashCache.delete(key);
572
573
  }
573
574
  /**
574
575
  * Delete the keys
@@ -653,7 +654,7 @@ var CacheableMemory = class extends Hookified {
653
654
  */
654
655
  hashKey(key) {
655
656
  const cacheHashNumber = this._hashCache.get(key);
656
- if (cacheHashNumber) {
657
+ if (typeof cacheHashNumber === "number") {
657
658
  return cacheHashNumber;
658
659
  }
659
660
  let hash2 = 0;
@@ -1103,6 +1104,35 @@ var CacheableStats = class {
1103
1104
  }
1104
1105
  };
1105
1106
 
1107
+ // src/ttl.ts
1108
+ function getTtlFromExpires(expires) {
1109
+ if (expires === void 0 || expires === null) {
1110
+ return void 0;
1111
+ }
1112
+ const now = Date.now();
1113
+ if (expires < now) {
1114
+ return void 0;
1115
+ }
1116
+ return expires - now;
1117
+ }
1118
+ function getCascadingTtl(cacheableTtl, primaryTtl, secondaryTtl) {
1119
+ return secondaryTtl ?? primaryTtl ?? shorthandToMilliseconds(cacheableTtl);
1120
+ }
1121
+ function calculateTtlFromExpiration(ttl, expires) {
1122
+ const ttlFromExpires = getTtlFromExpires(expires);
1123
+ const expiresFromTtl = ttl ? Date.now() + ttl : void 0;
1124
+ if (ttlFromExpires === void 0) {
1125
+ return ttl;
1126
+ }
1127
+ if (expiresFromTtl === void 0) {
1128
+ return ttlFromExpires;
1129
+ }
1130
+ if (expires > expiresFromTtl) {
1131
+ return ttl;
1132
+ }
1133
+ return ttlFromExpires;
1134
+ }
1135
+
1106
1136
  // src/index.ts
1107
1137
  import {
1108
1138
  KeyvHooks,
@@ -1117,6 +1147,7 @@ var CacheableHooks = /* @__PURE__ */ ((CacheableHooks2) => {
1117
1147
  CacheableHooks2["AFTER_GET"] = "AFTER_GET";
1118
1148
  CacheableHooks2["BEFORE_GET_MANY"] = "BEFORE_GET_MANY";
1119
1149
  CacheableHooks2["AFTER_GET_MANY"] = "AFTER_GET_MANY";
1150
+ CacheableHooks2["BEFORE_SECONDARY_SETS_PRIMARY"] = "BEFORE_SECONDARY_SETS_PRIMARY";
1120
1151
  return CacheableHooks2;
1121
1152
  })(CacheableHooks || {});
1122
1153
  var CacheableEvents = /* @__PURE__ */ ((CacheableEvents2) => {
@@ -1325,25 +1356,26 @@ var Cacheable = class extends Hookified2 {
1325
1356
  }
1326
1357
  return this._namespace;
1327
1358
  }
1328
- /**
1329
- * Gets the value of the key. If the key does not exist in the primary store then it will check the secondary store.
1330
- * @param {string} key The key to get the value of
1331
- * @returns {Promise<T | undefined>} The value of the key or undefined if the key does not exist
1332
- */
1333
- async get(key) {
1359
+ async get(key, options = {}) {
1334
1360
  let result;
1361
+ const { raw = false } = options;
1335
1362
  try {
1336
1363
  await this.hook("BEFORE_GET" /* BEFORE_GET */, key);
1337
- result = await this._primary.get(key);
1364
+ result = await this._primary.get(key, { raw: true });
1365
+ let ttl;
1338
1366
  if (!result && this._secondary) {
1339
- const rawResult = await this._secondary.get(key, { raw: true });
1340
- if (rawResult) {
1341
- result = rawResult.value;
1342
- const finalTtl = rawResult.expires ?? void 0;
1343
- await this._primary.set(key, result, finalTtl);
1367
+ const secondaryResult = await this.getSecondaryRawResults(key);
1368
+ if (secondaryResult?.value) {
1369
+ result = secondaryResult;
1370
+ const cascadeTtl = getCascadingTtl(this._ttl, this._primary.ttl);
1371
+ const expires = secondaryResult.expires ?? void 0;
1372
+ ttl = calculateTtlFromExpiration(cascadeTtl, expires);
1373
+ const setItem = { key, value: result.value, ttl };
1374
+ await this.hook("BEFORE_SECONDARY_SETS_PRIMARY" /* BEFORE_SECONDARY_SETS_PRIMARY */, setItem);
1375
+ await this._primary.set(setItem.key, setItem.value, setItem.ttl);
1344
1376
  }
1345
1377
  }
1346
- await this.hook("AFTER_GET" /* AFTER_GET */, { key, result });
1378
+ await this.hook("AFTER_GET" /* AFTER_GET */, { key, result, ttl });
1347
1379
  } catch (error) {
1348
1380
  this.emit("error" /* ERROR */, error);
1349
1381
  }
@@ -1355,18 +1387,14 @@ var Cacheable = class extends Hookified2 {
1355
1387
  }
1356
1388
  this.stats.incrementGets();
1357
1389
  }
1358
- return result;
1390
+ return raw ? result : result?.value;
1359
1391
  }
1360
- /**
1361
- * Gets the values of the keys. If the key does not exist in the primary store then it will check the secondary store.
1362
- * @param {string[]} keys The keys to get the values of
1363
- * @returns {Promise<Array<T | undefined>>} The values of the keys or undefined if the key does not exist
1364
- */
1365
- async getMany(keys) {
1392
+ async getMany(keys, options = {}) {
1366
1393
  let result = [];
1394
+ const { raw = false } = options;
1367
1395
  try {
1368
1396
  await this.hook("BEFORE_GET_MANY" /* BEFORE_GET_MANY */, keys);
1369
- result = await this._primary.get(keys);
1397
+ result = await this._primary.get(keys, { raw: true });
1370
1398
  if (this._secondary) {
1371
1399
  const missingKeys = [];
1372
1400
  for (const [i, key] of keys.entries()) {
@@ -1374,12 +1402,16 @@ var Cacheable = class extends Hookified2 {
1374
1402
  missingKeys.push(key);
1375
1403
  }
1376
1404
  }
1377
- const secondaryResult = await this._secondary.get(missingKeys);
1378
- for (const [i, key] of keys.entries()) {
1379
- if (!result[i] && secondaryResult[i]) {
1380
- result[i] = secondaryResult[i];
1381
- const finalTtl = shorthandToMilliseconds(this._ttl);
1382
- await this._primary.set(key, secondaryResult[i], finalTtl);
1405
+ const secondaryResults = await this.getManySecondaryRawResults(missingKeys);
1406
+ for await (const [i, key] of keys.entries()) {
1407
+ if (!result[i] && secondaryResults[i]) {
1408
+ result[i] = secondaryResults[i];
1409
+ const cascadeTtl = getCascadingTtl(this._ttl, this._primary.ttl);
1410
+ const expires = secondaryResults[i].expires;
1411
+ const ttl = calculateTtlFromExpiration(cascadeTtl, expires);
1412
+ const setItem = { key, value: result[i].value, ttl };
1413
+ await this.hook("BEFORE_SECONDARY_SETS_PRIMARY" /* BEFORE_SECONDARY_SETS_PRIMARY */, setItem);
1414
+ await this._primary.set(setItem.key, setItem.value, setItem.ttl);
1383
1415
  }
1384
1416
  }
1385
1417
  }
@@ -1397,7 +1429,7 @@ var Cacheable = class extends Hookified2 {
1397
1429
  }
1398
1430
  this.stats.incrementGets();
1399
1431
  }
1400
- return result;
1432
+ return raw ? result : result.map((item) => item?.value);
1401
1433
  }
1402
1434
  /**
1403
1435
  * Sets the value of the key. If the secondary store is set then it will also set the value in the secondary store.
@@ -1636,6 +1668,20 @@ var Cacheable = class extends Hookified2 {
1636
1668
  hash(object, algorithm = "sha256") {
1637
1669
  return hash(object, algorithm);
1638
1670
  }
1671
+ async getSecondaryRawResults(key) {
1672
+ let result;
1673
+ if (this._secondary) {
1674
+ result = await this._secondary.get(key, { raw: true });
1675
+ }
1676
+ return result;
1677
+ }
1678
+ async getManySecondaryRawResults(keys) {
1679
+ let result = new Array();
1680
+ if (this._secondary) {
1681
+ result = await this._secondary.get(keys, { raw: true });
1682
+ }
1683
+ return result;
1684
+ }
1639
1685
  async deleteManyKeyv(keyv, keys) {
1640
1686
  const promises = [];
1641
1687
  for (const key of keys) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cacheable",
3
- "version": "1.8.9",
3
+ "version": "1.9.0",
4
4
  "description": "High Performance Layer 1 / Layer 2 Caching with Keyv Storage",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -21,20 +21,20 @@
21
21
  "license": "MIT",
22
22
  "private": false,
23
23
  "devDependencies": {
24
- "@faker-js/faker": "^9.5.1",
25
- "@keyv/redis": "^4.3.1",
26
- "@types/node": "^22.13.9",
27
- "@vitest/coverage-v8": "^3.0.7",
28
- "lru-cache": "^11.0.2",
24
+ "@faker-js/faker": "^9.7.0",
25
+ "@keyv/redis": "^4.4.0",
26
+ "@types/node": "^22.15.3",
27
+ "@vitest/coverage-v8": "^3.1.3",
28
+ "lru-cache": "^11.1.0",
29
29
  "rimraf": "^6.0.1",
30
30
  "tsup": "^8.4.0",
31
- "typescript": "^5.8.2",
32
- "vitest": "^3.0.7",
31
+ "typescript": "^5.8.3",
32
+ "vitest": "^3.1.3",
33
33
  "xo": "^0.60.0"
34
34
  },
35
35
  "dependencies": {
36
- "hookified": "^1.7.1",
37
- "keyv": "^5.3.1"
36
+ "hookified": "^1.8.2",
37
+ "keyv": "^5.3.3"
38
38
  },
39
39
  "keywords": [
40
40
  "cacheable",