cacheable 2.0.3 → 2.1.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
@@ -8,7 +8,7 @@
8
8
  [![npm](https://img.shields.io/npm/v/cacheable)](https://www.npmjs.com/package/cacheable)
9
9
  [![license](https://img.shields.io/github/license/jaredwray/cacheable)](https://github.com/jaredwray/cacheable/blob/main/LICENSE)
10
10
 
11
- `cacheable` is a high performance layer 1 / layer 2 caching engine that is focused on distributed caching with enterprise features such as `CacheSync` (coming soon). It is built on top of the robust storage engine [Keyv](https://keyv.org) and provides a simple API to cache and retrieve data.
11
+ `cacheable` is a high performance layer 1 / layer 2 caching engine that is focused on distributed caching with enterprise features such as `CacheSync`. It is built on top of the robust storage engine [Keyv](https://keyv.org) and provides a simple API to cache and retrieve data.
12
12
 
13
13
  * Simple to use with robust API
14
14
  * Not bloated with additional modules
@@ -19,7 +19,7 @@
19
19
  * Hooks and Events to extend functionality
20
20
  * Shorthand for ttl in milliseconds `(1m = 60000) (1h = 3600000) (1d = 86400000)`
21
21
  * Non-blocking operations for layer 2 caching
22
- * Distributed Caching Sync via Pub/Sub (coming soon)
22
+ * **Distributed Caching Sync via Pub/Sub with CacheSync**
23
23
  * Comprehensive testing and code coverage
24
24
  * ESM and CommonJS support with Typescript
25
25
  * Maintained and supported regularly
@@ -34,7 +34,7 @@
34
34
  * [Shorthand for Time to Live (ttl)](#shorthand-for-time-to-live-ttl)
35
35
  * [Non-Blocking Operations](#non-blocking-operations)
36
36
  * [Non-Blocking with @keyv/redis](#non-blocking-with-keyvredis)
37
- * [CacheSync - Distributed Updates](#cachesync---distributed-updates)
37
+ * [CacheableSync - Distributed Updates](#cacheablesync---distributed-updates)
38
38
  * [Cacheable Options](#cacheable-options)
39
39
  * [Cacheable Statistics (Instance Only)](#cacheable-statistics-instance-only)
40
40
  * [Cacheable - API](#cacheable---api)
@@ -359,17 +359,108 @@ const secondary = new KeyvRedis('redis://user:pass@localhost:6379');
359
359
  const cache = new Cacheable({secondary, nonBlocking: true});
360
360
  ```
361
361
 
362
- # CacheSync - Distributed Updates
362
+ # CacheableSync - Distributed Updates
363
363
 
364
- `cacheable` has a feature called `CacheSync` that is coming soon. This feature will allow you to have distributed caching with Pub/Sub. This will allow you to have multiple instances of `cacheable` running and when a value is set, deleted, or cleared it will update all instances of `cacheable` with the same value. Current plan is to support the following:
364
+ `cacheable` includes `CacheableSync`, a feature that enables distributed cache synchronization across multiple instances using Pub/Sub messaging via [Qified](https://github.com/jaredwray/qified). When a value is set or deleted in one cache instance, all other connected instances automatically receive and apply the update.
365
365
 
366
- * [AWS SQS](https://aws.amazon.com/sqs)
367
- * [RabbitMQ](https://www.rabbitmq.com)
368
- * [Nats](https://nats.io)
369
- * [Azure Service Bus](https://azure.microsoft.com/en-us/services/service-bus)
370
- * [Redis Pub/Sub](https://redis.io/topics/pubsub)
366
+ ## How It Works
371
367
 
372
- This feature should be live by end of year.
368
+ `CacheableSync` uses message providers from Qified to broadcast cache operations (SET and DELETE) to all connected cache instances. Each instance subscribes to these events and automatically updates its `primary` (example: in-memory) storage when receiving updates from other instances.
369
+
370
+ ## Supported Message Providers
371
+
372
+ `Qified` supports multiple providers and you can learn more by going to https://qified.org.
373
+
374
+ ## Basic Usage
375
+
376
+ ```javascript
377
+ import { Cacheable } from 'cacheable';
378
+ import { RedisMessageProvider } from '@qified/redis';
379
+
380
+ // Create a Redis message provider
381
+ const provider = new RedisMessageProvider({
382
+ connection: { host: 'localhost', port: 6379 }
383
+ });
384
+
385
+ // Create cache instances with sync enabled
386
+ const cache1 = new Cacheable({
387
+ sync: { qified: provider }
388
+ });
389
+
390
+ const cache2 = new Cacheable({
391
+ sync: { qified: provider }
392
+ });
393
+
394
+ // Set a value in cache1
395
+ await cache1.set('key', 'value');
396
+
397
+ // Note: you might want to sleep for a bit based on the backend.
398
+
399
+ // The value is automatically synced to cache2
400
+ const value = await cache2.get('key'); // Returns 'value'
401
+ ```
402
+
403
+ ## Using Multiple Message Providers
404
+
405
+ You can use multiple message providers for redundancy:
406
+
407
+ ```javascript
408
+ import { Cacheable } from 'cacheable';
409
+ import { RedisMessageProvider } from '@qified/redis';
410
+ import { NatsMessageProvider } from '@qified/nats';
411
+
412
+ const redisProvider = new RedisMessageProvider({
413
+ connection: { host: 'localhost', port: 6379 }
414
+ });
415
+
416
+ const natsProvider = new NatsMessageProvider({
417
+ servers: ['nats://localhost:4222']
418
+ });
419
+
420
+ const cache = new Cacheable({
421
+ sync: { qified: [redisProvider, natsProvider] }
422
+ });
423
+ ```
424
+
425
+ ## Using an Existing Qified Instance
426
+
427
+ You can also pass a pre-configured Qified instance:
428
+
429
+ ```javascript
430
+ import { Cacheable } from 'cacheable';
431
+ import { Qified } from 'qified';
432
+ import { RedisMessageProvider } from '@qified/redis';
433
+
434
+ const provider = new RedisMessageProvider({
435
+ connection: { host: 'localhost', port: 6379 }
436
+ });
437
+
438
+ const qified = new Qified({ messageProviders: [provider] });
439
+
440
+ const cache = new Cacheable({
441
+ sync: { qified }
442
+ });
443
+ ```
444
+
445
+ ## How Sync Works
446
+
447
+ 1. **SET Operations**: When you call `cache.set()` or `cache.setMany()`, the cache:
448
+ - Updates the local primary storage and secondary storage
449
+ - Publishes a `cache:set` event with the key, value, ttl, and cacheId
450
+ - Other cache instances receive the event and update their `primary` storage (excluding the originating instance)
451
+
452
+ 2. **DELETE Operations**: When you call `cache.delete()` or `cache.deleteMany()`, the cache:
453
+ - Removes the key from primary and secondary storage
454
+ - Publishes a `cache:delete` event with the key and cacheId
455
+ - Other cache instances receive the event and remove the key from their storage
456
+
457
+ ## Important Notes
458
+
459
+ * Cache sync only works with the **primary storage layer**. Secondary storage is usually handled by the instance doing the initial work.
460
+ * Each cache instance should have a unique `cacheId` to properly filter sync events. This is setup by default but you can set it if you want.
461
+ * Sync events are **eventually consistent** - there may be a small delay between when a value is set and when it appears in other instances.
462
+ * The sync feature requires a message provider to be running and accessible by all cache instances.
463
+ * Each cache instance has a unique `cacheId`. Events are only applied if they come from a different instance, preventing infinite loops.
373
464
 
374
465
  # Cacheable Options
375
466
 
@@ -381,6 +472,10 @@ The following options are available for you to configure `cacheable`:
381
472
  * `stats`: To enable statistics for this instance. Default is `false`.
382
473
  * `ttl`: The default time to live for the cache in milliseconds. Default is `undefined` which is disabled.
383
474
  * `namespace`: The namespace for the cache. Default is `undefined`.
475
+ * `cacheId`: A unique identifier for this cache instance. Used for sync filtering. Default is a random string.
476
+ * `sync`: Enable distributed cache synchronization. Can be:
477
+ - `CacheableSync` instance
478
+ - `CacheableSyncOptions` object with `{ qified: MessageProvider | MessageProvider[] | Qified }`
384
479
 
385
480
  # Cacheable Statistics (Instance Only)
386
481
 
package/dist/index.cjs CHANGED
@@ -25,6 +25,8 @@ __export(index_exports, {
25
25
  CacheableHooks: () => CacheableHooks,
26
26
  CacheableMemory: () => import_memory2.CacheableMemory,
27
27
  CacheableStats: () => import_utils2.Stats,
28
+ CacheableSync: () => CacheableSync,
29
+ CacheableSyncEvents: () => CacheableSyncEvents,
28
30
  HashAlgorithm: () => import_utils2.HashAlgorithm,
29
31
  Keyv: () => import_keyv2.Keyv,
30
32
  KeyvCacheableMemory: () => import_memory2.KeyvCacheableMemory,
@@ -43,7 +45,7 @@ module.exports = __toCommonJS(index_exports);
43
45
  var import_memoize = require("@cacheable/memoize");
44
46
  var import_memory = require("@cacheable/memory");
45
47
  var import_utils = require("@cacheable/utils");
46
- var import_hookified = require("hookified");
48
+ var import_hookified2 = require("hookified");
47
49
  var import_keyv = require("keyv");
48
50
 
49
51
  // src/enums.ts
@@ -66,12 +68,92 @@ var CacheableEvents = /* @__PURE__ */ ((CacheableEvents2) => {
66
68
  return CacheableEvents2;
67
69
  })(CacheableEvents || {});
68
70
 
71
+ // src/sync.ts
72
+ var import_hookified = require("hookified");
73
+ var import_qified = require("qified");
74
+ var CacheableSyncEvents = /* @__PURE__ */ ((CacheableSyncEvents2) => {
75
+ CacheableSyncEvents2["ERROR"] = "error";
76
+ CacheableSyncEvents2["SET"] = "cache:set";
77
+ CacheableSyncEvents2["DELETE"] = "cache:delete";
78
+ return CacheableSyncEvents2;
79
+ })(CacheableSyncEvents || {});
80
+ var CacheableSync = class extends import_hookified.Hookified {
81
+ _qified = new import_qified.Qified();
82
+ /**
83
+ * Creates an instance of CacheableSync
84
+ * @param options - Configuration options for CacheableSync
85
+ */
86
+ constructor(options) {
87
+ super(options);
88
+ this._qified = this.createQified(options.qified);
89
+ }
90
+ /**
91
+ * Gets the Qified instance used for synchronization
92
+ * @returns The Qified instance
93
+ */
94
+ get qified() {
95
+ return this._qified;
96
+ }
97
+ /**
98
+ * Sets the Qified instance used for synchronization
99
+ * @param value - Either an existing Qified instance or MessageProvider(s)
100
+ */
101
+ set qified(value) {
102
+ this._qified = this.createQified(value);
103
+ }
104
+ /**
105
+ * Publishes a cache event to all the cache instances
106
+ * @param data - The cache item data containing cacheId, key, value, and optional ttl
107
+ */
108
+ async publish(event, data) {
109
+ await this._qified.publish(event, {
110
+ id: crypto.randomUUID(),
111
+ data
112
+ });
113
+ }
114
+ /**
115
+ * Subscribes to sync events and updates the provided storage
116
+ * @param storage - The Keyv storage instance to update
117
+ * @param cacheId - The cache ID to identify this instance
118
+ */
119
+ subscribe(storage, cacheId) {
120
+ this._qified.subscribe("cache:set" /* SET */, {
121
+ handler: async (message) => {
122
+ const data = message.data;
123
+ if (data.cacheId !== cacheId) {
124
+ await storage.set(data.key, data.value, data.ttl);
125
+ }
126
+ }
127
+ });
128
+ this._qified.subscribe("cache:delete" /* DELETE */, {
129
+ handler: async (message) => {
130
+ const data = message.data;
131
+ if (data.cacheId !== cacheId) {
132
+ await storage.delete(data.key);
133
+ }
134
+ }
135
+ });
136
+ }
137
+ /**
138
+ * Creates or returns a Qified instance from the provided value
139
+ * @param value - Either an existing Qified instance or MessageProvider(s)
140
+ * @returns A Qified instance configured with the provided message provider(s)
141
+ */
142
+ createQified(value) {
143
+ if (value instanceof import_qified.Qified) {
144
+ return value;
145
+ }
146
+ const providers = Array.isArray(value) ? value : [value];
147
+ return new import_qified.Qified({ messageProviders: providers });
148
+ }
149
+ };
150
+
69
151
  // src/index.ts
70
152
  var import_memoize2 = require("@cacheable/memoize");
71
153
  var import_memory2 = require("@cacheable/memory");
72
154
  var import_utils2 = require("@cacheable/utils");
73
155
  var import_keyv2 = require("keyv");
74
- var Cacheable = class extends import_hookified.Hookified {
156
+ var Cacheable = class extends import_hookified2.Hookified {
75
157
  _primary = (0, import_memory.createKeyv)();
76
158
  _secondary;
77
159
  _nonBlocking = false;
@@ -79,6 +161,7 @@ var Cacheable = class extends import_hookified.Hookified {
79
161
  _stats = new import_utils.Stats({ enabled: false });
80
162
  _namespace;
81
163
  _cacheId = Math.random().toString(36).slice(2);
164
+ _sync;
82
165
  /**
83
166
  * Creates a new cacheable instance
84
167
  * @param {CacheableOptions} [options] The options for the cacheable instance
@@ -110,6 +193,10 @@ var Cacheable = class extends import_hookified.Hookified {
110
193
  this._secondary.namespace = this.getNameSpace();
111
194
  }
112
195
  }
196
+ if (options?.sync) {
197
+ this._sync = options.sync instanceof CacheableSync ? options.sync : new CacheableSync(options.sync);
198
+ this._sync.subscribe(this._primary, this._cacheId);
199
+ }
113
200
  }
114
201
  /**
115
202
  * The namespace for the cacheable instance
@@ -245,13 +332,30 @@ var Cacheable = class extends import_hookified.Hookified {
245
332
  set cacheId(cacheId) {
246
333
  this._cacheId = cacheId;
247
334
  }
335
+ /**
336
+ * Gets the sync instance for the cacheable instance
337
+ * @returns {CacheableSync | undefined} The sync instance for the cacheable instance
338
+ */
339
+ get sync() {
340
+ return this._sync;
341
+ }
342
+ /**
343
+ * Sets the sync instance for the cacheable instance
344
+ * @param {CacheableSync | undefined} sync The sync instance for the cacheable instance
345
+ */
346
+ set sync(sync) {
347
+ this._sync = sync;
348
+ if (this._sync) {
349
+ this._sync.subscribe(this._primary, this._cacheId);
350
+ }
351
+ }
248
352
  /**
249
353
  * Sets the primary store for the cacheable instance
250
354
  * @param {Keyv | KeyvStoreAdapter} primary The primary store for the cacheable instance
251
355
  * @returns {void}
252
356
  */
253
357
  setPrimary(primary) {
254
- if (this.isKeyvInstance(primary)) {
358
+ if ((0, import_utils.isKeyvInstance)(primary)) {
255
359
  this._primary = primary;
256
360
  } else {
257
361
  this._primary = new import_keyv.Keyv(primary);
@@ -266,7 +370,7 @@ var Cacheable = class extends import_hookified.Hookified {
266
370
  * @returns {void}
267
371
  */
268
372
  setSecondary(secondary) {
269
- if (this.isKeyvInstance(secondary)) {
373
+ if ((0, import_utils.isKeyvInstance)(secondary)) {
270
374
  this._secondary = secondary;
271
375
  } else {
272
376
  this._secondary = new import_keyv.Keyv(secondary);
@@ -275,28 +379,6 @@ var Cacheable = class extends import_hookified.Hookified {
275
379
  this.emit("error" /* ERROR */, error);
276
380
  });
277
381
  }
278
- // biome-ignore lint/suspicious/noExplicitAny: type format
279
- isKeyvInstance(keyv) {
280
- if (keyv instanceof import_keyv.Keyv) {
281
- return true;
282
- }
283
- const keyvMethods = [
284
- "generateIterator",
285
- "get",
286
- "getMany",
287
- "set",
288
- "setMany",
289
- "delete",
290
- "deleteMany",
291
- "has",
292
- "hasMany",
293
- "clear",
294
- "disconnect",
295
- "serialize",
296
- "deserialize"
297
- ];
298
- return keyvMethods.every((method) => typeof keyv[method] === "function");
299
- }
300
382
  getNameSpace() {
301
383
  if (typeof this._namespace === "function") {
302
384
  return this._namespace();
@@ -489,6 +571,14 @@ var Cacheable = class extends import_hookified.Hookified {
489
571
  result = results[0];
490
572
  }
491
573
  await this.hook("AFTER_SET" /* AFTER_SET */, item);
574
+ if (this._sync && result) {
575
+ await this._sync.publish("cache:set" /* SET */, {
576
+ cacheId: this._cacheId,
577
+ key: item.key,
578
+ value: item.value,
579
+ ttl: item.ttl
580
+ });
581
+ }
492
582
  } catch (error) {
493
583
  this.emit("error" /* ERROR */, error);
494
584
  }
@@ -520,6 +610,16 @@ var Cacheable = class extends import_hookified.Hookified {
520
610
  }
521
611
  }
522
612
  await this.hook("AFTER_SET_MANY" /* AFTER_SET_MANY */, items);
613
+ if (this._sync && result) {
614
+ for (const item of items) {
615
+ await this._sync.publish("cache:set" /* SET */, {
616
+ cacheId: this._cacheId,
617
+ key: item.key,
618
+ value: item.value,
619
+ ttl: (0, import_utils.shorthandToMilliseconds)(item.ttl)
620
+ });
621
+ }
622
+ }
523
623
  } catch (error) {
524
624
  this.emit("error" /* ERROR */, error);
525
625
  }
@@ -626,6 +726,12 @@ var Cacheable = class extends import_hookified.Hookified {
626
726
  const resultAll = await Promise.all(promises);
627
727
  result = resultAll[0];
628
728
  }
729
+ if (this._sync && result) {
730
+ await this._sync.publish("cache:delete" /* DELETE */, {
731
+ cacheId: this._cacheId,
732
+ key
733
+ });
734
+ }
629
735
  return result;
630
736
  }
631
737
  /**
@@ -653,6 +759,14 @@ var Cacheable = class extends import_hookified.Hookified {
653
759
  await this._secondary.deleteMany(keys);
654
760
  }
655
761
  }
762
+ if (this._sync && result) {
763
+ for (const key of keys) {
764
+ await this._sync.publish("cache:delete" /* DELETE */, {
765
+ cacheId: this._cacheId,
766
+ key
767
+ });
768
+ }
769
+ }
656
770
  return result;
657
771
  }
658
772
  /**
@@ -955,6 +1069,8 @@ var Cacheable = class extends import_hookified.Hookified {
955
1069
  CacheableHooks,
956
1070
  CacheableMemory,
957
1071
  CacheableStats,
1072
+ CacheableSync,
1073
+ CacheableSyncEvents,
958
1074
  HashAlgorithm,
959
1075
  Keyv,
960
1076
  KeyvCacheableMemory,
package/dist/index.d.cts CHANGED
@@ -2,11 +2,75 @@ import { WrapFunctionOptions, GetOrSetKey, GetOrSetFunctionOptions } from '@cach
2
2
  export { GetOrSetFunctionOptions, GetOrSetKey, GetOrSetOptions, WrapOptions, WrapSyncOptions, getOrSet, wrap, wrapSync } from '@cacheable/memoize';
3
3
  import { Stats, CacheableItem, HashAlgorithm } from '@cacheable/utils';
4
4
  export { CacheableItem, Stats as CacheableStats, HashAlgorithm, calculateTtlFromExpiration, getCascadingTtl, hash, shorthandToMilliseconds, shorthandToTime } from '@cacheable/utils';
5
- import { Hookified } from 'hookified';
5
+ import { Hookified, HookifiedOptions } from 'hookified';
6
6
  import { Keyv, KeyvStoreAdapter, StoredDataRaw } from 'keyv';
7
7
  export { Keyv, KeyvHooks, KeyvOptions, KeyvStoreAdapter } from 'keyv';
8
+ import { Qified, MessageProvider } from 'qified';
8
9
  export { CacheableMemory, CacheableMemoryOptions, KeyvCacheableMemory, KeyvCacheableMemoryOptions, createKeyv } from '@cacheable/memory';
9
10
 
11
+ /**
12
+ * Events emitted by CacheableSync
13
+ */
14
+ declare enum CacheableSyncEvents {
15
+ ERROR = "error",
16
+ SET = "cache:set",
17
+ DELETE = "cache:delete"
18
+ }
19
+ /**
20
+ * Configuration options for CacheableSync
21
+ */
22
+ type CacheableSyncOptions = {
23
+ /**
24
+ * Qified instance or message provider(s) for synchronization
25
+ */
26
+ qified: Qified | MessageProvider | MessageProvider[];
27
+ } & HookifiedOptions;
28
+ type CacheableSyncItem = {
29
+ cacheId: string;
30
+ key: string;
31
+ value?: unknown;
32
+ ttl?: number;
33
+ };
34
+ /**
35
+ * CacheableSync provides synchronization capabilities for cacheable items
36
+ * using message providers from Qified
37
+ */
38
+ declare class CacheableSync extends Hookified {
39
+ private _qified;
40
+ /**
41
+ * Creates an instance of CacheableSync
42
+ * @param options - Configuration options for CacheableSync
43
+ */
44
+ constructor(options: CacheableSyncOptions);
45
+ /**
46
+ * Gets the Qified instance used for synchronization
47
+ * @returns The Qified instance
48
+ */
49
+ get qified(): Qified;
50
+ /**
51
+ * Sets the Qified instance used for synchronization
52
+ * @param value - Either an existing Qified instance or MessageProvider(s)
53
+ */
54
+ set qified(value: Qified | MessageProvider | MessageProvider[]);
55
+ /**
56
+ * Publishes a cache event to all the cache instances
57
+ * @param data - The cache item data containing cacheId, key, value, and optional ttl
58
+ */
59
+ publish(event: CacheableSyncEvents, data: CacheableSyncItem): Promise<void>;
60
+ /**
61
+ * Subscribes to sync events and updates the provided storage
62
+ * @param storage - The Keyv storage instance to update
63
+ * @param cacheId - The cache ID to identify this instance
64
+ */
65
+ subscribe(storage: Keyv, cacheId: string): void;
66
+ /**
67
+ * Creates or returns a Qified instance from the provided value
68
+ * @param value - Either an existing Qified instance or MessageProvider(s)
69
+ * @returns A Qified instance configured with the provided message provider(s)
70
+ */
71
+ createQified(value: Qified | MessageProvider | MessageProvider[]): Qified;
72
+ }
73
+
10
74
  type CacheableOptions = {
11
75
  /**
12
76
  * The primary store for the cacheable instance
@@ -40,6 +104,10 @@ type CacheableOptions = {
40
104
  * If it is not set then it will be a random string that is generated
41
105
  */
42
106
  cacheId?: string;
107
+ /**
108
+ * The sync instance for the cacheable instance to enable synchronization across cache instances
109
+ */
110
+ sync?: CacheableSync | CacheableSyncOptions;
43
111
  };
44
112
  type GetOptions = {
45
113
  /**
@@ -74,6 +142,7 @@ declare class Cacheable extends Hookified {
74
142
  private readonly _stats;
75
143
  private _namespace?;
76
144
  private _cacheId;
145
+ private _sync?;
77
146
  /**
78
147
  * Creates a new cacheable instance
79
148
  * @param {CacheableOptions} [options] The options for the cacheable instance
@@ -183,6 +252,16 @@ declare class Cacheable extends Hookified {
183
252
  * @param {string} cacheId The cacheId for the cacheable instance
184
253
  */
185
254
  set cacheId(cacheId: string);
255
+ /**
256
+ * Gets the sync instance for the cacheable instance
257
+ * @returns {CacheableSync | undefined} The sync instance for the cacheable instance
258
+ */
259
+ get sync(): CacheableSync | undefined;
260
+ /**
261
+ * Sets the sync instance for the cacheable instance
262
+ * @param {CacheableSync | undefined} sync The sync instance for the cacheable instance
263
+ */
264
+ set sync(sync: CacheableSync | undefined);
186
265
  /**
187
266
  * Sets the primary store for the cacheable instance
188
267
  * @param {Keyv | KeyvStoreAdapter} primary The primary store for the cacheable instance
@@ -195,7 +274,6 @@ declare class Cacheable extends Hookified {
195
274
  * @returns {void}
196
275
  */
197
276
  setSecondary(secondary: Keyv | KeyvStoreAdapter): void;
198
- isKeyvInstance(keyv: any): boolean;
199
277
  getNameSpace(): string | undefined;
200
278
  /**
201
279
  * Retrieves an entry from the cache.
@@ -376,4 +454,4 @@ declare class Cacheable extends Hookified {
376
454
  private setTtl;
377
455
  }
378
456
 
379
- export { Cacheable, CacheableEvents, CacheableHooks, type CacheableOptions };
457
+ export { Cacheable, CacheableEvents, CacheableHooks, type CacheableOptions, CacheableSync, CacheableSyncEvents, type CacheableSyncItem, type CacheableSyncOptions };
package/dist/index.d.ts CHANGED
@@ -2,11 +2,75 @@ import { WrapFunctionOptions, GetOrSetKey, GetOrSetFunctionOptions } from '@cach
2
2
  export { GetOrSetFunctionOptions, GetOrSetKey, GetOrSetOptions, WrapOptions, WrapSyncOptions, getOrSet, wrap, wrapSync } from '@cacheable/memoize';
3
3
  import { Stats, CacheableItem, HashAlgorithm } from '@cacheable/utils';
4
4
  export { CacheableItem, Stats as CacheableStats, HashAlgorithm, calculateTtlFromExpiration, getCascadingTtl, hash, shorthandToMilliseconds, shorthandToTime } from '@cacheable/utils';
5
- import { Hookified } from 'hookified';
5
+ import { Hookified, HookifiedOptions } from 'hookified';
6
6
  import { Keyv, KeyvStoreAdapter, StoredDataRaw } from 'keyv';
7
7
  export { Keyv, KeyvHooks, KeyvOptions, KeyvStoreAdapter } from 'keyv';
8
+ import { Qified, MessageProvider } from 'qified';
8
9
  export { CacheableMemory, CacheableMemoryOptions, KeyvCacheableMemory, KeyvCacheableMemoryOptions, createKeyv } from '@cacheable/memory';
9
10
 
11
+ /**
12
+ * Events emitted by CacheableSync
13
+ */
14
+ declare enum CacheableSyncEvents {
15
+ ERROR = "error",
16
+ SET = "cache:set",
17
+ DELETE = "cache:delete"
18
+ }
19
+ /**
20
+ * Configuration options for CacheableSync
21
+ */
22
+ type CacheableSyncOptions = {
23
+ /**
24
+ * Qified instance or message provider(s) for synchronization
25
+ */
26
+ qified: Qified | MessageProvider | MessageProvider[];
27
+ } & HookifiedOptions;
28
+ type CacheableSyncItem = {
29
+ cacheId: string;
30
+ key: string;
31
+ value?: unknown;
32
+ ttl?: number;
33
+ };
34
+ /**
35
+ * CacheableSync provides synchronization capabilities for cacheable items
36
+ * using message providers from Qified
37
+ */
38
+ declare class CacheableSync extends Hookified {
39
+ private _qified;
40
+ /**
41
+ * Creates an instance of CacheableSync
42
+ * @param options - Configuration options for CacheableSync
43
+ */
44
+ constructor(options: CacheableSyncOptions);
45
+ /**
46
+ * Gets the Qified instance used for synchronization
47
+ * @returns The Qified instance
48
+ */
49
+ get qified(): Qified;
50
+ /**
51
+ * Sets the Qified instance used for synchronization
52
+ * @param value - Either an existing Qified instance or MessageProvider(s)
53
+ */
54
+ set qified(value: Qified | MessageProvider | MessageProvider[]);
55
+ /**
56
+ * Publishes a cache event to all the cache instances
57
+ * @param data - The cache item data containing cacheId, key, value, and optional ttl
58
+ */
59
+ publish(event: CacheableSyncEvents, data: CacheableSyncItem): Promise<void>;
60
+ /**
61
+ * Subscribes to sync events and updates the provided storage
62
+ * @param storage - The Keyv storage instance to update
63
+ * @param cacheId - The cache ID to identify this instance
64
+ */
65
+ subscribe(storage: Keyv, cacheId: string): void;
66
+ /**
67
+ * Creates or returns a Qified instance from the provided value
68
+ * @param value - Either an existing Qified instance or MessageProvider(s)
69
+ * @returns A Qified instance configured with the provided message provider(s)
70
+ */
71
+ createQified(value: Qified | MessageProvider | MessageProvider[]): Qified;
72
+ }
73
+
10
74
  type CacheableOptions = {
11
75
  /**
12
76
  * The primary store for the cacheable instance
@@ -40,6 +104,10 @@ type CacheableOptions = {
40
104
  * If it is not set then it will be a random string that is generated
41
105
  */
42
106
  cacheId?: string;
107
+ /**
108
+ * The sync instance for the cacheable instance to enable synchronization across cache instances
109
+ */
110
+ sync?: CacheableSync | CacheableSyncOptions;
43
111
  };
44
112
  type GetOptions = {
45
113
  /**
@@ -74,6 +142,7 @@ declare class Cacheable extends Hookified {
74
142
  private readonly _stats;
75
143
  private _namespace?;
76
144
  private _cacheId;
145
+ private _sync?;
77
146
  /**
78
147
  * Creates a new cacheable instance
79
148
  * @param {CacheableOptions} [options] The options for the cacheable instance
@@ -183,6 +252,16 @@ declare class Cacheable extends Hookified {
183
252
  * @param {string} cacheId The cacheId for the cacheable instance
184
253
  */
185
254
  set cacheId(cacheId: string);
255
+ /**
256
+ * Gets the sync instance for the cacheable instance
257
+ * @returns {CacheableSync | undefined} The sync instance for the cacheable instance
258
+ */
259
+ get sync(): CacheableSync | undefined;
260
+ /**
261
+ * Sets the sync instance for the cacheable instance
262
+ * @param {CacheableSync | undefined} sync The sync instance for the cacheable instance
263
+ */
264
+ set sync(sync: CacheableSync | undefined);
186
265
  /**
187
266
  * Sets the primary store for the cacheable instance
188
267
  * @param {Keyv | KeyvStoreAdapter} primary The primary store for the cacheable instance
@@ -195,7 +274,6 @@ declare class Cacheable extends Hookified {
195
274
  * @returns {void}
196
275
  */
197
276
  setSecondary(secondary: Keyv | KeyvStoreAdapter): void;
198
- isKeyvInstance(keyv: any): boolean;
199
277
  getNameSpace(): string | undefined;
200
278
  /**
201
279
  * Retrieves an entry from the cache.
@@ -376,4 +454,4 @@ declare class Cacheable extends Hookified {
376
454
  private setTtl;
377
455
  }
378
456
 
379
- export { Cacheable, CacheableEvents, CacheableHooks, type CacheableOptions };
457
+ export { Cacheable, CacheableEvents, CacheableHooks, type CacheableOptions, CacheableSync, CacheableSyncEvents, type CacheableSyncItem, type CacheableSyncOptions };
package/dist/index.js CHANGED
@@ -10,9 +10,10 @@ import {
10
10
  getCascadingTtl,
11
11
  HashAlgorithm,
12
12
  hash,
13
+ isKeyvInstance,
13
14
  shorthandToMilliseconds
14
15
  } from "@cacheable/utils";
15
- import { Hookified } from "hookified";
16
+ import { Hookified as Hookified2 } from "hookified";
16
17
  import {
17
18
  Keyv
18
19
  } from "keyv";
@@ -37,6 +38,86 @@ var CacheableEvents = /* @__PURE__ */ ((CacheableEvents2) => {
37
38
  return CacheableEvents2;
38
39
  })(CacheableEvents || {});
39
40
 
41
+ // src/sync.ts
42
+ import { Hookified } from "hookified";
43
+ import { Qified } from "qified";
44
+ var CacheableSyncEvents = /* @__PURE__ */ ((CacheableSyncEvents2) => {
45
+ CacheableSyncEvents2["ERROR"] = "error";
46
+ CacheableSyncEvents2["SET"] = "cache:set";
47
+ CacheableSyncEvents2["DELETE"] = "cache:delete";
48
+ return CacheableSyncEvents2;
49
+ })(CacheableSyncEvents || {});
50
+ var CacheableSync = class extends Hookified {
51
+ _qified = new Qified();
52
+ /**
53
+ * Creates an instance of CacheableSync
54
+ * @param options - Configuration options for CacheableSync
55
+ */
56
+ constructor(options) {
57
+ super(options);
58
+ this._qified = this.createQified(options.qified);
59
+ }
60
+ /**
61
+ * Gets the Qified instance used for synchronization
62
+ * @returns The Qified instance
63
+ */
64
+ get qified() {
65
+ return this._qified;
66
+ }
67
+ /**
68
+ * Sets the Qified instance used for synchronization
69
+ * @param value - Either an existing Qified instance or MessageProvider(s)
70
+ */
71
+ set qified(value) {
72
+ this._qified = this.createQified(value);
73
+ }
74
+ /**
75
+ * Publishes a cache event to all the cache instances
76
+ * @param data - The cache item data containing cacheId, key, value, and optional ttl
77
+ */
78
+ async publish(event, data) {
79
+ await this._qified.publish(event, {
80
+ id: crypto.randomUUID(),
81
+ data
82
+ });
83
+ }
84
+ /**
85
+ * Subscribes to sync events and updates the provided storage
86
+ * @param storage - The Keyv storage instance to update
87
+ * @param cacheId - The cache ID to identify this instance
88
+ */
89
+ subscribe(storage, cacheId) {
90
+ this._qified.subscribe("cache:set" /* SET */, {
91
+ handler: async (message) => {
92
+ const data = message.data;
93
+ if (data.cacheId !== cacheId) {
94
+ await storage.set(data.key, data.value, data.ttl);
95
+ }
96
+ }
97
+ });
98
+ this._qified.subscribe("cache:delete" /* DELETE */, {
99
+ handler: async (message) => {
100
+ const data = message.data;
101
+ if (data.cacheId !== cacheId) {
102
+ await storage.delete(data.key);
103
+ }
104
+ }
105
+ });
106
+ }
107
+ /**
108
+ * Creates or returns a Qified instance from the provided value
109
+ * @param value - Either an existing Qified instance or MessageProvider(s)
110
+ * @returns A Qified instance configured with the provided message provider(s)
111
+ */
112
+ createQified(value) {
113
+ if (value instanceof Qified) {
114
+ return value;
115
+ }
116
+ const providers = Array.isArray(value) ? value : [value];
117
+ return new Qified({ messageProviders: providers });
118
+ }
119
+ };
120
+
40
121
  // src/index.ts
41
122
  import {
42
123
  getOrSet as getOrSet2,
@@ -58,7 +139,7 @@ import {
58
139
  shorthandToTime
59
140
  } from "@cacheable/utils";
60
141
  import { Keyv as Keyv2, KeyvHooks } from "keyv";
61
- var Cacheable = class extends Hookified {
142
+ var Cacheable = class extends Hookified2 {
62
143
  _primary = createKeyv();
63
144
  _secondary;
64
145
  _nonBlocking = false;
@@ -66,6 +147,7 @@ var Cacheable = class extends Hookified {
66
147
  _stats = new CacheableStats({ enabled: false });
67
148
  _namespace;
68
149
  _cacheId = Math.random().toString(36).slice(2);
150
+ _sync;
69
151
  /**
70
152
  * Creates a new cacheable instance
71
153
  * @param {CacheableOptions} [options] The options for the cacheable instance
@@ -97,6 +179,10 @@ var Cacheable = class extends Hookified {
97
179
  this._secondary.namespace = this.getNameSpace();
98
180
  }
99
181
  }
182
+ if (options?.sync) {
183
+ this._sync = options.sync instanceof CacheableSync ? options.sync : new CacheableSync(options.sync);
184
+ this._sync.subscribe(this._primary, this._cacheId);
185
+ }
100
186
  }
101
187
  /**
102
188
  * The namespace for the cacheable instance
@@ -232,13 +318,30 @@ var Cacheable = class extends Hookified {
232
318
  set cacheId(cacheId) {
233
319
  this._cacheId = cacheId;
234
320
  }
321
+ /**
322
+ * Gets the sync instance for the cacheable instance
323
+ * @returns {CacheableSync | undefined} The sync instance for the cacheable instance
324
+ */
325
+ get sync() {
326
+ return this._sync;
327
+ }
328
+ /**
329
+ * Sets the sync instance for the cacheable instance
330
+ * @param {CacheableSync | undefined} sync The sync instance for the cacheable instance
331
+ */
332
+ set sync(sync) {
333
+ this._sync = sync;
334
+ if (this._sync) {
335
+ this._sync.subscribe(this._primary, this._cacheId);
336
+ }
337
+ }
235
338
  /**
236
339
  * Sets the primary store for the cacheable instance
237
340
  * @param {Keyv | KeyvStoreAdapter} primary The primary store for the cacheable instance
238
341
  * @returns {void}
239
342
  */
240
343
  setPrimary(primary) {
241
- if (this.isKeyvInstance(primary)) {
344
+ if (isKeyvInstance(primary)) {
242
345
  this._primary = primary;
243
346
  } else {
244
347
  this._primary = new Keyv(primary);
@@ -253,7 +356,7 @@ var Cacheable = class extends Hookified {
253
356
  * @returns {void}
254
357
  */
255
358
  setSecondary(secondary) {
256
- if (this.isKeyvInstance(secondary)) {
359
+ if (isKeyvInstance(secondary)) {
257
360
  this._secondary = secondary;
258
361
  } else {
259
362
  this._secondary = new Keyv(secondary);
@@ -262,28 +365,6 @@ var Cacheable = class extends Hookified {
262
365
  this.emit("error" /* ERROR */, error);
263
366
  });
264
367
  }
265
- // biome-ignore lint/suspicious/noExplicitAny: type format
266
- isKeyvInstance(keyv) {
267
- if (keyv instanceof Keyv) {
268
- return true;
269
- }
270
- const keyvMethods = [
271
- "generateIterator",
272
- "get",
273
- "getMany",
274
- "set",
275
- "setMany",
276
- "delete",
277
- "deleteMany",
278
- "has",
279
- "hasMany",
280
- "clear",
281
- "disconnect",
282
- "serialize",
283
- "deserialize"
284
- ];
285
- return keyvMethods.every((method) => typeof keyv[method] === "function");
286
- }
287
368
  getNameSpace() {
288
369
  if (typeof this._namespace === "function") {
289
370
  return this._namespace();
@@ -476,6 +557,14 @@ var Cacheable = class extends Hookified {
476
557
  result = results[0];
477
558
  }
478
559
  await this.hook("AFTER_SET" /* AFTER_SET */, item);
560
+ if (this._sync && result) {
561
+ await this._sync.publish("cache:set" /* SET */, {
562
+ cacheId: this._cacheId,
563
+ key: item.key,
564
+ value: item.value,
565
+ ttl: item.ttl
566
+ });
567
+ }
479
568
  } catch (error) {
480
569
  this.emit("error" /* ERROR */, error);
481
570
  }
@@ -507,6 +596,16 @@ var Cacheable = class extends Hookified {
507
596
  }
508
597
  }
509
598
  await this.hook("AFTER_SET_MANY" /* AFTER_SET_MANY */, items);
599
+ if (this._sync && result) {
600
+ for (const item of items) {
601
+ await this._sync.publish("cache:set" /* SET */, {
602
+ cacheId: this._cacheId,
603
+ key: item.key,
604
+ value: item.value,
605
+ ttl: shorthandToMilliseconds(item.ttl)
606
+ });
607
+ }
608
+ }
510
609
  } catch (error) {
511
610
  this.emit("error" /* ERROR */, error);
512
611
  }
@@ -613,6 +712,12 @@ var Cacheable = class extends Hookified {
613
712
  const resultAll = await Promise.all(promises);
614
713
  result = resultAll[0];
615
714
  }
715
+ if (this._sync && result) {
716
+ await this._sync.publish("cache:delete" /* DELETE */, {
717
+ cacheId: this._cacheId,
718
+ key
719
+ });
720
+ }
616
721
  return result;
617
722
  }
618
723
  /**
@@ -640,6 +745,14 @@ var Cacheable = class extends Hookified {
640
745
  await this._secondary.deleteMany(keys);
641
746
  }
642
747
  }
748
+ if (this._sync && result) {
749
+ for (const key of keys) {
750
+ await this._sync.publish("cache:delete" /* DELETE */, {
751
+ cacheId: this._cacheId,
752
+ key
753
+ });
754
+ }
755
+ }
643
756
  return result;
644
757
  }
645
758
  /**
@@ -941,6 +1054,8 @@ export {
941
1054
  CacheableHooks,
942
1055
  CacheableMemory,
943
1056
  Stats as CacheableStats,
1057
+ CacheableSync,
1058
+ CacheableSyncEvents,
944
1059
  HashAlgorithm2 as HashAlgorithm,
945
1060
  Keyv2 as Keyv,
946
1061
  KeyvCacheableMemory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cacheable",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "description": "High Performance Layer 1 / Layer 2 Caching with Keyv Storage",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -21,24 +21,26 @@
21
21
  "license": "MIT",
22
22
  "private": false,
23
23
  "devDependencies": {
24
- "@biomejs/biome": "^2.2.4",
25
- "@faker-js/faker": "^10.0.0",
26
- "@keyv/redis": "^5.1.2",
27
- "@keyv/valkey": "^1.0.8",
28
- "@types/node": "^24.5.2",
24
+ "@biomejs/biome": "^2.2.6",
25
+ "@faker-js/faker": "^10.1.0",
26
+ "@keyv/redis": "^5.1.3",
27
+ "@keyv/valkey": "^1.0.10",
28
+ "@qified/redis": "^0.5.0",
29
+ "@types/node": "^24.8.1",
29
30
  "@vitest/coverage-v8": "^3.2.4",
30
- "lru-cache": "^11.2.1",
31
+ "lru-cache": "^11.2.2",
31
32
  "rimraf": "^6.0.1",
32
33
  "tsup": "^8.5.0",
33
- "typescript": "^5.9.2",
34
+ "typescript": "^5.9.3",
34
35
  "vitest": "^3.2.4"
35
36
  },
36
37
  "dependencies": {
37
- "hookified": "^1.12.1",
38
+ "hookified": "^1.12.2",
38
39
  "keyv": "^5.5.3",
40
+ "qified": "^0.5.0",
39
41
  "@cacheable/memoize": "^2.0.3",
40
42
  "@cacheable/memory": "^2.0.3",
41
- "@cacheable/utils": "^2.0.3"
43
+ "@cacheable/utils": "^2.1.0"
42
44
  },
43
45
  "keywords": [
44
46
  "cacheable",