@gravito/stasis 3.0.1 → 3.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/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -22,15 +32,19 @@ var index_exports = {};
22
32
  __export(index_exports, {
23
33
  CacheManager: () => CacheManager,
24
34
  CacheRepository: () => CacheRepository,
35
+ CircuitBreakerStore: () => CircuitBreakerStore,
25
36
  FileStore: () => FileStore,
26
37
  LockTimeoutError: () => LockTimeoutError,
38
+ MarkovPredictor: () => MarkovPredictor,
27
39
  MemoryCacheProvider: () => MemoryCacheProvider,
28
40
  MemoryStore: () => MemoryStore,
29
41
  NullStore: () => NullStore,
30
42
  OrbitCache: () => OrbitCache,
31
43
  OrbitStasis: () => OrbitStasis,
44
+ PredictiveStore: () => PredictiveStore,
32
45
  RateLimiter: () => RateLimiter,
33
46
  RedisStore: () => RedisStore,
47
+ TieredStore: () => TieredStore,
34
48
  default: () => orbitCache,
35
49
  isExpired: () => isExpired,
36
50
  isTaggableStore: () => isTaggableStore,
@@ -40,6 +54,14 @@ __export(index_exports, {
40
54
  });
41
55
  module.exports = __toCommonJS(index_exports);
42
56
 
57
+ // src/locks.ts
58
+ var LockTimeoutError = class extends Error {
59
+ name = "LockTimeoutError";
60
+ };
61
+ function sleep(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+
43
65
  // src/store.ts
44
66
  function isTaggableStore(store) {
45
67
  return typeof store.flushTags === "function" && typeof store.tagKey === "function" && typeof store.tagIndexAdd === "function" && typeof store.tagIndexRemove === "function";
@@ -77,7 +99,7 @@ function isExpired(expiresAt, now = Date.now()) {
77
99
  if (expiresAt === void 0) {
78
100
  return false;
79
101
  }
80
- return now > expiresAt;
102
+ return now >= expiresAt;
81
103
  }
82
104
 
83
105
  // src/CacheRepository.ts
@@ -86,6 +108,29 @@ var CacheRepository = class _CacheRepository {
86
108
  this.store = store;
87
109
  this.options = options;
88
110
  }
111
+ refreshSemaphore = /* @__PURE__ */ new Map();
112
+ coalesceSemaphore = /* @__PURE__ */ new Map();
113
+ flexibleStats = { refreshCount: 0, refreshFailures: 0, totalTime: 0 };
114
+ /**
115
+ * Retrieve statistics about flexible cache operations.
116
+ *
117
+ * Useful for monitoring the health and performance of background refreshes.
118
+ *
119
+ * @returns Current statistics for background refresh operations.
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const stats = cache.getFlexibleStats();
124
+ * console.log(`Refreshed ${stats.refreshCount} times`);
125
+ * ```
126
+ */
127
+ getFlexibleStats() {
128
+ return {
129
+ refreshCount: this.flexibleStats.refreshCount,
130
+ refreshFailures: this.flexibleStats.refreshFailures,
131
+ avgRefreshTime: this.flexibleStats.refreshCount > 0 ? this.flexibleStats.totalTime / this.flexibleStats.refreshCount : 0
132
+ };
133
+ }
89
134
  emit(event, payload = {}) {
90
135
  const mode = this.options.eventsMode ?? "async";
91
136
  if (mode === "off") {
@@ -149,15 +194,32 @@ var CacheRepository = class _CacheRepository {
149
194
  async forgetMetaKey(metaKey) {
150
195
  await this.store.forget(metaKey);
151
196
  }
197
+ /**
198
+ * Retrieve an item from the cache by its key.
199
+ *
200
+ * Fetches the value from the underlying store. If not found, returns the
201
+ * provided default value or executes the factory function.
202
+ *
203
+ * @param key - The unique cache key.
204
+ * @param defaultValue - A default value or factory function to use if the key is not found.
205
+ * @returns The cached value, or the default value if not found.
206
+ * @throws {Error} If the underlying store fails to retrieve the value.
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const user = await cache.get('user:1', { name: 'Guest' });
211
+ * const settings = await cache.get('settings', () => fetchSettings());
212
+ * ```
213
+ */
152
214
  async get(key, defaultValue) {
153
215
  const fullKey = this.key(key);
154
- const value = await this.store.get(fullKey);
155
- if (value !== null) {
216
+ const raw = await this.store.get(fullKey);
217
+ if (raw !== null) {
156
218
  const e2 = this.emit("hit", { key: fullKey });
157
219
  if (e2) {
158
220
  await e2;
159
221
  }
160
- return value;
222
+ return this.decompress(raw);
161
223
  }
162
224
  const e = this.emit("miss", { key: fullKey });
163
225
  if (e) {
@@ -171,24 +233,103 @@ var CacheRepository = class _CacheRepository {
171
233
  }
172
234
  return defaultValue;
173
235
  }
236
+ /**
237
+ * Determine if an item exists in the cache.
238
+ *
239
+ * Checks for the presence of a key without necessarily returning its value.
240
+ *
241
+ * @param key - The cache key.
242
+ * @returns True if the item exists, false otherwise.
243
+ * @throws {Error} If the underlying store fails to check existence.
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * if (await cache.has('session:active')) {
248
+ * // ...
249
+ * }
250
+ * ```
251
+ */
174
252
  async has(key) {
175
253
  return await this.get(key) !== null;
176
254
  }
255
+ /**
256
+ * Determine if an item is missing from the cache.
257
+ *
258
+ * Inverse of `has()`, used for cleaner conditional logic.
259
+ *
260
+ * @param key - The cache key.
261
+ * @returns True if the item is missing, false otherwise.
262
+ * @throws {Error} If the underlying store fails to check existence.
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * if (await cache.missing('config:loaded')) {
267
+ * await loadConfig();
268
+ * }
269
+ * ```
270
+ */
177
271
  async missing(key) {
178
272
  return !await this.has(key);
179
273
  }
274
+ /**
275
+ * Store an item in the cache for a specific duration.
276
+ *
277
+ * Persists the value in the underlying store with the given TTL.
278
+ *
279
+ * @param key - Unique cache key.
280
+ * @param value - Value to store.
281
+ * @param ttl - Expiration duration.
282
+ * @throws {Error} If the underlying store fails to persist the value or serialization fails.
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * await cache.put('token', 'xyz123', 3600);
287
+ * ```
288
+ */
180
289
  async put(key, value, ttl) {
181
290
  const fullKey = this.key(key);
182
- await this.store.put(fullKey, value, ttl);
291
+ const data = await this.compress(value);
292
+ await this.store.put(fullKey, data, ttl);
183
293
  const e = this.emit("write", { key: fullKey });
184
294
  if (e) {
185
295
  await e;
186
296
  }
187
297
  }
298
+ /**
299
+ * Store an item in the cache for a specific duration.
300
+ *
301
+ * Uses the repository's default TTL if none is provided.
302
+ *
303
+ * @param key - The unique cache key.
304
+ * @param value - The value to store.
305
+ * @param ttl - Optional time-to-live.
306
+ * @throws {Error} If the underlying store fails to persist the value.
307
+ *
308
+ * @example
309
+ * ```typescript
310
+ * await cache.set('theme', 'dark');
311
+ * ```
312
+ */
188
313
  async set(key, value, ttl) {
189
314
  const resolved = ttl ?? this.options.defaultTtl;
190
315
  await this.put(key, value, resolved);
191
316
  }
317
+ /**
318
+ * Store an item in the cache only if the key does not already exist.
319
+ *
320
+ * Atomic operation to prevent overwriting existing data.
321
+ *
322
+ * @param key - The unique cache key.
323
+ * @param value - The value to store.
324
+ * @param ttl - Optional time-to-live.
325
+ * @returns True if the item was added, false otherwise.
326
+ * @throws {Error} If the underlying store fails the atomic operation.
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * const added = await cache.add('lock:process', true, 60);
331
+ * ```
332
+ */
192
333
  async add(key, value, ttl) {
193
334
  const fullKey = this.key(key);
194
335
  const resolved = ttl ?? this.options.defaultTtl;
@@ -201,21 +342,93 @@ var CacheRepository = class _CacheRepository {
201
342
  }
202
343
  return ok;
203
344
  }
345
+ /**
346
+ * Store an item in the cache indefinitely.
347
+ *
348
+ * Sets the TTL to null, indicating the value should not expire automatically.
349
+ *
350
+ * @param key - The unique cache key.
351
+ * @param value - The value to store.
352
+ * @throws {Error} If the underlying store fails to persist the value.
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * await cache.forever('system:id', 'node-01');
357
+ * ```
358
+ */
204
359
  async forever(key, value) {
205
360
  await this.put(key, value, null);
206
361
  }
362
+ /**
363
+ * Get an item from the cache, or execute the given callback and store the result.
364
+ *
365
+ * Implements the "Cache-Aside" pattern, ensuring the callback is only executed
366
+ * on a cache miss.
367
+ *
368
+ * @param key - The unique cache key.
369
+ * @param ttl - Time-to-live.
370
+ * @param callback - The callback to execute if the key is not found.
371
+ * @returns The cached value or the result of the callback.
372
+ * @throws {Error} If the callback or the underlying store fails.
373
+ *
374
+ * @example
375
+ * ```typescript
376
+ * const data = await cache.remember('users:all', 300, () => db.users.findMany());
377
+ * ```
378
+ */
207
379
  async remember(key, ttl, callback) {
208
- const existing = await this.get(key);
209
- if (existing !== null) {
210
- return existing;
380
+ const fullKey = this.key(key);
381
+ if (this.coalesceSemaphore.has(fullKey)) {
382
+ return this.coalesceSemaphore.get(fullKey);
211
383
  }
212
- const value = await callback();
213
- await this.put(key, value, ttl);
214
- return value;
384
+ const promise = (async () => {
385
+ try {
386
+ const existing = await this.get(key);
387
+ if (existing !== null) {
388
+ return existing;
389
+ }
390
+ const value = await callback();
391
+ await this.put(key, value, ttl);
392
+ return value;
393
+ } finally {
394
+ this.coalesceSemaphore.delete(fullKey);
395
+ }
396
+ })();
397
+ this.coalesceSemaphore.set(fullKey, promise);
398
+ return promise;
215
399
  }
400
+ /**
401
+ * Get an item from the cache, or execute the given callback and store the result indefinitely.
402
+ *
403
+ * Similar to `remember()`, but the value is stored without an expiration time.
404
+ *
405
+ * @param key - The unique cache key.
406
+ * @param callback - The callback to execute if the key is not found.
407
+ * @returns The cached value or the result of the callback.
408
+ * @throws {Error} If the callback or the underlying store fails.
409
+ *
410
+ * @example
411
+ * ```typescript
412
+ * const config = await cache.rememberForever('app:config', () => loadConfig());
413
+ * ```
414
+ */
216
415
  async rememberForever(key, callback) {
217
416
  return this.remember(key, null, callback);
218
417
  }
418
+ /**
419
+ * Retrieve multiple items from the cache by their keys.
420
+ *
421
+ * Efficiently fetches multiple values, returning a map of keys to values.
422
+ *
423
+ * @param keys - An array of unique cache keys.
424
+ * @returns An object where keys are the original keys and values are the cached values.
425
+ * @throws {Error} If the underlying store fails to retrieve values.
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * const results = await cache.many(['user:1', 'user:2']);
430
+ * ```
431
+ */
219
432
  async many(keys) {
220
433
  const out = {};
221
434
  for (const key of keys) {
@@ -223,14 +436,40 @@ var CacheRepository = class _CacheRepository {
223
436
  }
224
437
  return out;
225
438
  }
439
+ /**
440
+ * Store multiple items in the cache for a specific duration.
441
+ *
442
+ * Persists multiple key-value pairs in a single operation if supported by the store.
443
+ *
444
+ * @param values - An object where keys are the unique cache keys and values are the values to store.
445
+ * @param ttl - Time-to-live.
446
+ * @throws {Error} If the underlying store fails to persist values.
447
+ *
448
+ * @example
449
+ * ```typescript
450
+ * await cache.putMany({ 'a': 1, 'b': 2 }, 60);
451
+ * ```
452
+ */
226
453
  async putMany(values, ttl) {
227
454
  await Promise.all(Object.entries(values).map(([k, v]) => this.put(k, v, ttl)));
228
455
  }
229
456
  /**
230
457
  * Laravel-like flexible cache (stale-while-revalidate).
231
458
  *
232
- * - `ttlSeconds`: how long the value is considered fresh
233
- * - `staleSeconds`: how long the stale value may be served while a refresh happens
459
+ * Serves stale content while revalidating the cache in the background. This
460
+ * minimizes latency for users by avoiding synchronous revalidation.
461
+ *
462
+ * @param key - The unique cache key.
463
+ * @param ttlSeconds - How long the value is considered fresh.
464
+ * @param staleSeconds - How long the stale value may be served while a refresh happens.
465
+ * @param callback - The callback to execute to refresh the cache.
466
+ * @returns The fresh or stale cached value.
467
+ * @throws {Error} If the callback fails on a cache miss.
468
+ *
469
+ * @example
470
+ * ```typescript
471
+ * const value = await cache.flexible('stats', 60, 30, () => fetchStats());
472
+ * ```
234
473
  */
235
474
  async flexible(key, ttlSeconds, staleSeconds, callback) {
236
475
  const fullKey = this.key(key);
@@ -265,17 +504,23 @@ var CacheRepository = class _CacheRepository {
265
504
  }
266
505
  const value = await callback();
267
506
  const totalTtl = ttlSeconds + staleSeconds;
268
- await this.store.put(fullKey, value, totalTtl);
507
+ await this.put(fullKey, value, totalTtl);
269
508
  await this.putMetaKey(metaKey, now + ttlMillis, totalTtl);
270
- {
271
- const e2 = this.emit("write", { key: fullKey });
272
- if (e2) {
273
- await e2;
274
- }
275
- }
276
509
  return value;
277
510
  }
278
511
  async refreshFlexible(fullKey, metaKey, ttlSeconds, staleSeconds, callback) {
512
+ if (this.refreshSemaphore.has(fullKey)) {
513
+ return;
514
+ }
515
+ const refreshPromise = this.doRefresh(fullKey, metaKey, ttlSeconds, staleSeconds, callback);
516
+ this.refreshSemaphore.set(fullKey, refreshPromise);
517
+ try {
518
+ await refreshPromise;
519
+ } finally {
520
+ this.refreshSemaphore.delete(fullKey);
521
+ }
522
+ }
523
+ async doRefresh(fullKey, metaKey, ttlSeconds, staleSeconds, callback) {
279
524
  if (!this.store.lock) {
280
525
  return;
281
526
  }
@@ -283,25 +528,79 @@ var CacheRepository = class _CacheRepository {
283
528
  if (!lock || !await lock.acquire()) {
284
529
  return;
285
530
  }
531
+ const startTime = Date.now();
286
532
  try {
287
- const value = await callback();
533
+ const timeoutMillis = this.options.refreshTimeout ?? 3e4;
534
+ const maxRetries = this.options.maxRetries ?? 0;
535
+ const retryDelay = this.options.retryDelay ?? 50;
536
+ let lastError;
537
+ let value;
538
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
539
+ try {
540
+ value = await Promise.race([
541
+ Promise.resolve(callback()),
542
+ new Promise(
543
+ (_, reject) => setTimeout(() => reject(new Error("Refresh timeout")), timeoutMillis)
544
+ )
545
+ ]);
546
+ break;
547
+ } catch (err) {
548
+ lastError = err;
549
+ if (attempt < maxRetries) {
550
+ await sleep(retryDelay);
551
+ }
552
+ }
553
+ }
554
+ if (value === void 0 && lastError) {
555
+ throw lastError;
556
+ }
288
557
  const totalTtl = ttlSeconds + staleSeconds;
289
558
  const now = Date.now();
290
- await this.store.put(fullKey, value, totalTtl);
559
+ await this.put(fullKey, value, totalTtl);
291
560
  await this.putMetaKey(metaKey, now + Math.max(0, ttlSeconds) * 1e3, totalTtl);
292
- const e = this.emit("write", { key: fullKey });
293
- if (e) {
294
- await e;
295
- }
561
+ this.flexibleStats.refreshCount++;
562
+ this.flexibleStats.totalTime += Date.now() - startTime;
563
+ } catch {
564
+ this.flexibleStats.refreshFailures++;
296
565
  } finally {
297
566
  await lock.release();
298
567
  }
299
568
  }
569
+ /**
570
+ * Retrieve an item from the cache and delete it.
571
+ *
572
+ * Atomic-like operation to fetch and immediately remove a value, often used
573
+ * for one-time tokens or flash messages.
574
+ *
575
+ * @param key - The unique cache key.
576
+ * @param defaultValue - A default value to use if the key is not found.
577
+ * @returns The cached value, or the default value if not found.
578
+ * @throws {Error} If the underlying store fails to retrieve or forget the value.
579
+ *
580
+ * @example
581
+ * ```typescript
582
+ * const message = await cache.pull('flash:status');
583
+ * ```
584
+ */
300
585
  async pull(key, defaultValue) {
301
586
  const value = await this.get(key, defaultValue);
302
587
  await this.forget(key);
303
588
  return value;
304
589
  }
590
+ /**
591
+ * Remove an item from the cache by its key.
592
+ *
593
+ * Deletes the value and any associated metadata from the underlying store.
594
+ *
595
+ * @param key - The cache key to remove.
596
+ * @returns True if the item existed and was removed.
597
+ * @throws {Error} If the underlying store fails to remove the value.
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * const deleted = await cache.forget('user:session');
602
+ * ```
603
+ */
305
604
  async forget(key) {
306
605
  const fullKey = this.key(key);
307
606
  const metaKey = this.flexibleFreshUntilKey(fullKey);
@@ -315,9 +614,36 @@ var CacheRepository = class _CacheRepository {
315
614
  }
316
615
  return ok;
317
616
  }
617
+ /**
618
+ * Alias for `forget`.
619
+ *
620
+ * Provides compatibility with standard `Map`-like or `Storage` APIs.
621
+ *
622
+ * @param key - The cache key to remove.
623
+ * @returns True if the item existed and was removed.
624
+ * @throws {Error} If the underlying store fails to remove the value.
625
+ *
626
+ * @example
627
+ * ```typescript
628
+ * await cache.delete('temp:data');
629
+ * ```
630
+ */
318
631
  async delete(key) {
319
632
  return this.forget(key);
320
633
  }
634
+ /**
635
+ * Remove all items from the cache storage.
636
+ *
637
+ * Clears the entire underlying store. Use with caution as this affects all
638
+ * keys regardless of prefix.
639
+ *
640
+ * @throws {Error} If the underlying store fails to flush.
641
+ *
642
+ * @example
643
+ * ```typescript
644
+ * await cache.flush();
645
+ * ```
646
+ */
321
647
  async flush() {
322
648
  await this.store.flush();
323
649
  const e = this.emit("flush");
@@ -325,18 +651,99 @@ var CacheRepository = class _CacheRepository {
325
651
  await e;
326
652
  }
327
653
  }
654
+ /**
655
+ * Alias for `flush`.
656
+ *
657
+ * Provides compatibility with standard `Map`-like or `Storage` APIs.
658
+ *
659
+ * @throws {Error} If the underlying store fails to clear.
660
+ *
661
+ * @example
662
+ * ```typescript
663
+ * await cache.clear();
664
+ * ```
665
+ */
328
666
  async clear() {
329
667
  return this.flush();
330
668
  }
669
+ /**
670
+ * Increment the value of a numeric item in the cache.
671
+ *
672
+ * Atomically increases the value of a key. If the key does not exist, it is
673
+ * typically initialized to 0 before incrementing.
674
+ *
675
+ * @param key - The cache key.
676
+ * @param value - The amount to increment by.
677
+ * @returns The new value.
678
+ * @throws {Error} If the underlying store fails the atomic increment.
679
+ *
680
+ * @example
681
+ * ```typescript
682
+ * const count = await cache.increment('page:views');
683
+ * ```
684
+ */
331
685
  increment(key, value) {
332
686
  return this.store.increment(this.key(key), value);
333
687
  }
688
+ /**
689
+ * Decrement the value of a numeric item in the cache.
690
+ *
691
+ * Atomically decreases the value of a key.
692
+ *
693
+ * @param key - The cache key.
694
+ * @param value - The amount to decrement by.
695
+ * @returns The new value.
696
+ * @throws {Error} If the underlying store fails the atomic decrement.
697
+ *
698
+ * @example
699
+ * ```typescript
700
+ * const remaining = await cache.decrement('stock:count');
701
+ * ```
702
+ */
334
703
  decrement(key, value) {
335
704
  return this.store.decrement(this.key(key), value);
336
705
  }
706
+ /**
707
+ * Get a distributed lock instance for the given name.
708
+ *
709
+ * Provides a mechanism for exclusive access to resources across multiple
710
+ * processes or servers.
711
+ *
712
+ * @param name - The lock name.
713
+ * @param seconds - Optional default duration for the lock in seconds.
714
+ * @returns A `CacheLock` instance if supported, otherwise undefined.
715
+ *
716
+ * @example
717
+ * ```typescript
718
+ * const lock = cache.lock('process:heavy', 10);
719
+ * if (await lock.acquire()) {
720
+ * try {
721
+ * // ...
722
+ * } finally {
723
+ * await lock.release();
724
+ * }
725
+ * }
726
+ * ```
727
+ */
337
728
  lock(name, seconds) {
338
729
  return this.store.lock ? this.store.lock(this.key(name), seconds) : void 0;
339
730
  }
731
+ /**
732
+ * Create a new repository instance with the given tags.
733
+ *
734
+ * Enables grouping of cache entries for collective operations like flushing
735
+ * all keys associated with specific tags.
736
+ *
737
+ * @param tags - An array of tag names.
738
+ * @returns A new `CacheRepository` instance that uses the given tags.
739
+ * @throws {Error} If the underlying store does not support tagging.
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * await cache.tags(['users', 'profiles']).put('user:1', data, 3600);
744
+ * await cache.tags(['users']).flush();
745
+ * ```
746
+ */
340
747
  tags(tags) {
341
748
  if (!isTaggableStore(this.store)) {
342
749
  throw new Error("This cache store does not support tags.");
@@ -344,11 +751,56 @@ var CacheRepository = class _CacheRepository {
344
751
  return new _CacheRepository(new TaggedStore(this.store, tags), this.options);
345
752
  }
346
753
  /**
347
- * Get the underlying store
754
+ * Retrieve the underlying cache store.
755
+ *
756
+ * Provides direct access to the low-level store implementation for advanced
757
+ * use cases or debugging.
758
+ *
759
+ * @returns The low-level cache store instance.
760
+ *
761
+ * @example
762
+ * ```typescript
763
+ * const store = cache.getStore();
764
+ * ```
348
765
  */
349
766
  getStore() {
350
767
  return this.store;
351
768
  }
769
+ /**
770
+ * Compress a value before storage if compression is enabled and thresholds are met.
771
+ */
772
+ async compress(value) {
773
+ const opts = this.options.compression;
774
+ if (!opts?.enabled || value === null || value === void 0) {
775
+ return value;
776
+ }
777
+ const json = JSON.stringify(value);
778
+ if (json.length < (opts.minSize ?? 1024)) {
779
+ return value;
780
+ }
781
+ const { gzipSync } = await import("zlib");
782
+ const compressed = gzipSync(Buffer.from(json), { level: opts.level ?? 6 });
783
+ return {
784
+ __gravito_compressed: true,
785
+ data: compressed.toString("base64")
786
+ };
787
+ }
788
+ /**
789
+ * Decompress a value after retrieval if it was previously compressed.
790
+ */
791
+ async decompress(value) {
792
+ if (value !== null && typeof value === "object" && "__gravito_compressed" in value && value.__gravito_compressed === true) {
793
+ const { gunzipSync } = await import("zlib");
794
+ const buffer = Buffer.from(value.data, "base64");
795
+ const decompressed = gunzipSync(buffer).toString();
796
+ try {
797
+ return JSON.parse(decompressed);
798
+ } catch {
799
+ return decompressed;
800
+ }
801
+ }
802
+ return value;
803
+ }
352
804
  };
353
805
  var TaggedStore = class {
354
806
  constructor(store, tags) {
@@ -399,14 +851,39 @@ var TaggedStore = class {
399
851
 
400
852
  // src/RateLimiter.ts
401
853
  var RateLimiter = class {
402
- constructor(store) {
403
- this.store = store;
854
+ /**
855
+ * Creates a new RateLimiter instance.
856
+ *
857
+ * @param store - Cache backend used to persist attempt counts.
858
+ *
859
+ * @example
860
+ * ```typescript
861
+ * const limiter = new RateLimiter(new MemoryStore());
862
+ * ```
863
+ */
864
+ constructor(store) {
865
+ this.store = store;
404
866
  }
405
867
  /**
406
- * Attempt to acquire a lock
407
- * @param key - The unique key (e.g., "ip:127.0.0.1")
408
- * @param maxAttempts - Maximum number of attempts allowed
409
- * @param decaySeconds - Time in seconds until the limit resets
868
+ * Attempt to consume a slot in the rate limit window.
869
+ *
870
+ * This method checks the current attempt count for the given key. If the
871
+ * count is below the limit, it increments the count and allows the request.
872
+ * Otherwise, it returns a rejected status with retry information.
873
+ *
874
+ * @param key - The unique identifier for the rate limit (e.g., IP address or user ID).
875
+ * @param maxAttempts - Maximum number of attempts allowed within the decay period.
876
+ * @param decaySeconds - Duration of the rate limit window in seconds.
877
+ * @returns A response indicating if the attempt was successful and the remaining capacity.
878
+ * @throws {Error} If the underlying cache store fails to read or write data.
879
+ *
880
+ * @example
881
+ * ```typescript
882
+ * const response = await limiter.attempt('api-client:123', 100, 3600);
883
+ * if (response.allowed) {
884
+ * // Proceed with request
885
+ * }
886
+ * ```
410
887
  */
411
888
  async attempt(key, maxAttempts, decaySeconds) {
412
889
  const current = await this.store.get(key);
@@ -420,11 +897,12 @@ var RateLimiter = class {
420
897
  };
421
898
  }
422
899
  if (current >= maxAttempts) {
900
+ const retryAfter = await this.availableIn(key, decaySeconds);
423
901
  return {
424
902
  allowed: false,
425
903
  remaining: 0,
426
- reset: now + decaySeconds
427
- // Approximation if we can't read TTL
904
+ reset: now + retryAfter,
905
+ retryAfter
428
906
  };
429
907
  }
430
908
  const next = await this.store.increment(key);
@@ -435,7 +913,76 @@ var RateLimiter = class {
435
913
  };
436
914
  }
437
915
  /**
438
- * Clear the limiter for a key
916
+ * Calculate the number of seconds until the rate limit resets.
917
+ *
918
+ * This helper method attempts to retrieve the TTL from the store. If the
919
+ * store does not support TTL inspection, it falls back to the provided
920
+ * decay period.
921
+ *
922
+ * @param key - Unique identifier for the rate limit.
923
+ * @param decaySeconds - Default decay period to use as a fallback.
924
+ * @returns Number of seconds until the key expires.
925
+ * @throws {Error} If the store fails to retrieve TTL metadata.
926
+ */
927
+ async availableIn(key, decaySeconds) {
928
+ if (typeof this.store.ttl === "function") {
929
+ const remaining = await this.store.ttl(key);
930
+ if (remaining !== null) {
931
+ return remaining;
932
+ }
933
+ }
934
+ return decaySeconds;
935
+ }
936
+ /**
937
+ * Get detailed information about the current rate limit status without consuming an attempt.
938
+ *
939
+ * Useful for returning rate limit headers (e.g., X-RateLimit-Limit) in
940
+ * middleware or for pre-flight checks.
941
+ *
942
+ * @param key - The unique identifier for the rate limit.
943
+ * @param maxAttempts - Maximum number of attempts allowed.
944
+ * @param decaySeconds - Duration of the rate limit window in seconds.
945
+ * @returns Current status including limit, remaining attempts, and reset time.
946
+ * @throws {Error} If the underlying cache store fails to retrieve data.
947
+ *
948
+ * @example
949
+ * ```typescript
950
+ * const info = await limiter.getInfo('user:42', 60, 60);
951
+ * console.log(`Remaining: ${info.remaining}/${info.limit}`);
952
+ * ```
953
+ */
954
+ async getInfo(key, maxAttempts, decaySeconds) {
955
+ const current = await this.store.get(key);
956
+ const now = Math.floor(Date.now() / 1e3);
957
+ if (current === null) {
958
+ return {
959
+ limit: maxAttempts,
960
+ remaining: maxAttempts,
961
+ reset: now + decaySeconds
962
+ };
963
+ }
964
+ const remaining = Math.max(0, maxAttempts - current);
965
+ const retryAfter = remaining === 0 ? await this.availableIn(key, decaySeconds) : void 0;
966
+ return {
967
+ limit: maxAttempts,
968
+ remaining,
969
+ reset: now + (retryAfter ?? decaySeconds),
970
+ retryAfter
971
+ };
972
+ }
973
+ /**
974
+ * Reset the rate limit counter for a specific key.
975
+ *
976
+ * Use this to manually clear a block, for example after a successful
977
+ * login or when an administrator manually unblocks a user.
978
+ *
979
+ * @param key - The unique identifier to clear.
980
+ * @throws {Error} If the store fails to delete the key.
981
+ *
982
+ * @example
983
+ * ```typescript
984
+ * await limiter.clear('login-attempts:user@example.com');
985
+ * ```
439
986
  */
440
987
  async clear(key) {
441
988
  await this.store.forget(key);
@@ -444,20 +991,61 @@ var RateLimiter = class {
444
991
 
445
992
  // src/CacheManager.ts
446
993
  var CacheManager = class {
994
+ /**
995
+ * Initialize a new CacheManager instance.
996
+ *
997
+ * @param storeFactory - Factory function to create low-level store instances by name.
998
+ * @param config - Configuration manifest for stores and global defaults.
999
+ * @param events - Optional event handlers for cache lifecycle hooks.
1000
+ * @param eventOptions - Configuration for how events are dispatched and handled.
1001
+ */
447
1002
  constructor(storeFactory, config = {}, events, eventOptions) {
448
1003
  this.storeFactory = storeFactory;
449
1004
  this.config = config;
450
1005
  this.events = events;
451
1006
  this.eventOptions = eventOptions;
452
1007
  }
1008
+ /**
1009
+ * Internal registry of initialized cache repositories.
1010
+ */
453
1011
  stores = /* @__PURE__ */ new Map();
454
1012
  /**
455
- * Get a rate limiter instance for a store
456
- * @param name - Store name (optional, defaults to default store)
1013
+ * Get a rate limiter instance for a specific store.
1014
+ *
1015
+ * Provides a specialized interface for throttling actions based on cache keys,
1016
+ * leveraging the underlying storage for persistence.
1017
+ *
1018
+ * @param name - Store name (defaults to the configured default store).
1019
+ * @returns A RateLimiter instance bound to the requested store.
1020
+ * @throws {Error} If the requested store cannot be initialized.
1021
+ *
1022
+ * @example
1023
+ * ```typescript
1024
+ * const limiter = cache.limiter('redis');
1025
+ * if (await limiter.tooManyAttempts('login:1', 5)) {
1026
+ * throw new Error('Too many attempts');
1027
+ * }
1028
+ * ```
457
1029
  */
458
1030
  limiter(name) {
459
1031
  return new RateLimiter(this.store(name).getStore());
460
1032
  }
1033
+ /**
1034
+ * Resolve a named cache repository.
1035
+ *
1036
+ * Lazily initializes and caches the repository instance for the given store name.
1037
+ * If no name is provided, it falls back to the default store.
1038
+ *
1039
+ * @param name - Store name to retrieve.
1040
+ * @returns Initialized CacheRepository instance.
1041
+ * @throws {Error} If the store factory fails to create the underlying store or the driver is unsupported.
1042
+ *
1043
+ * @example
1044
+ * ```typescript
1045
+ * const redis = cache.store('redis');
1046
+ * await redis.put('key', 'value', 60);
1047
+ * ```
1048
+ */
461
1049
  store(name) {
462
1050
  const storeName = name ?? this.config.default ?? "memory";
463
1051
  const existing = this.stores.get(storeName);
@@ -477,11 +1065,15 @@ var CacheManager = class {
477
1065
  }
478
1066
  // Laravel-like proxy methods (default store)
479
1067
  /**
480
- * Retrieve an item from the cache.
1068
+ * Retrieve an item from the default cache store.
481
1069
  *
482
- * @param key - The unique cache key.
483
- * @param defaultValue - The default value if the key is missing (can be a value or a closure).
484
- * @returns The cached value or the default.
1070
+ * If the key is missing, the provided default value or the result of the
1071
+ * default value closure will be returned.
1072
+ *
1073
+ * @param key - Unique cache key.
1074
+ * @param defaultValue - Fallback value or factory to execute on cache miss.
1075
+ * @returns Cached value or the resolved default.
1076
+ * @throws {Error} If the underlying store driver encounters a read error or connection failure.
485
1077
  *
486
1078
  * @example
487
1079
  * ```typescript
@@ -492,23 +1084,47 @@ var CacheManager = class {
492
1084
  return this.store().get(key, defaultValue);
493
1085
  }
494
1086
  /**
495
- * Check if an item exists in the cache.
1087
+ * Determine if an item exists in the default cache store.
1088
+ *
1089
+ * @param key - The unique cache key.
1090
+ * @returns True if the key exists and has not expired.
1091
+ * @throws {Error} If the underlying store driver encounters a connection error.
1092
+ *
1093
+ * @example
1094
+ * ```typescript
1095
+ * if (await cache.has('session:active')) {
1096
+ * // ...
1097
+ * }
1098
+ * ```
496
1099
  */
497
1100
  has(key) {
498
1101
  return this.store().has(key);
499
1102
  }
500
1103
  /**
501
- * Check if an item is missing from the cache.
1104
+ * Determine if an item is missing from the default cache store.
1105
+ *
1106
+ * @param key - The unique cache key.
1107
+ * @returns True if the key does not exist or has expired.
1108
+ * @throws {Error} If the underlying store driver encounters a connection error.
1109
+ *
1110
+ * @example
1111
+ * ```typescript
1112
+ * if (await cache.missing('config:loaded')) {
1113
+ * await loadConfig();
1114
+ * }
1115
+ * ```
502
1116
  */
503
1117
  missing(key) {
504
1118
  return this.store().missing(key);
505
1119
  }
506
1120
  /**
507
- * Store an item in the cache.
1121
+ * Store an item in the default cache store for a specific duration.
508
1122
  *
509
1123
  * @param key - The unique cache key.
510
- * @param value - The value to store.
511
- * @param ttl - Time to live in seconds (or Date).
1124
+ * @param value - The data to be cached.
1125
+ * @param ttl - Expiration time in seconds or a specific Date.
1126
+ * @returns A promise that resolves when the write is complete.
1127
+ * @throws {Error} If the value cannot be serialized or the store is read-only.
512
1128
  *
513
1129
  * @example
514
1130
  * ```typescript
@@ -519,21 +1135,51 @@ var CacheManager = class {
519
1135
  return this.store().put(key, value, ttl);
520
1136
  }
521
1137
  /**
522
- * Store an item in the cache (alias for put with optional TTL).
1138
+ * Store an item in the default cache store (alias for put).
1139
+ *
1140
+ * @param key - The unique cache key.
1141
+ * @param value - The data to be cached.
1142
+ * @param ttl - Optional expiration time.
1143
+ * @returns A promise that resolves when the write is complete.
1144
+ * @throws {Error} If the underlying store driver encounters a write error.
1145
+ *
1146
+ * @example
1147
+ * ```typescript
1148
+ * await cache.set('theme', 'dark');
1149
+ * ```
523
1150
  */
524
1151
  set(key, value, ttl) {
525
1152
  return this.store().set(key, value, ttl);
526
1153
  }
527
1154
  /**
528
- * Store an item in the cache if it doesn't already exist.
1155
+ * Store an item in the default cache store only if it does not already exist.
529
1156
  *
530
- * @returns True if added, false if it already existed.
1157
+ * @param key - The unique cache key.
1158
+ * @param value - The data to be cached.
1159
+ * @param ttl - Optional expiration time.
1160
+ * @returns True if the item was added, false if it already existed.
1161
+ * @throws {Error} If the underlying store driver encounters a write error.
1162
+ *
1163
+ * @example
1164
+ * ```typescript
1165
+ * const added = await cache.add('lock:process', true, 30);
1166
+ * ```
531
1167
  */
532
1168
  add(key, value, ttl) {
533
1169
  return this.store().add(key, value, ttl);
534
1170
  }
535
1171
  /**
536
- * Store an item in the cache indefinitely.
1172
+ * Store an item in the default cache store indefinitely.
1173
+ *
1174
+ * @param key - The unique cache key.
1175
+ * @param value - The data to be cached.
1176
+ * @returns A promise that resolves when the write is complete.
1177
+ * @throws {Error} If the underlying store driver encounters a write error.
1178
+ *
1179
+ * @example
1180
+ * ```typescript
1181
+ * await cache.forever('system:version', '1.0.0');
1182
+ * ```
537
1183
  */
538
1184
  forever(key, value) {
539
1185
  return this.store().forever(key, value);
@@ -541,10 +1187,14 @@ var CacheManager = class {
541
1187
  /**
542
1188
  * Get an item from the cache, or execute the callback and store the result.
543
1189
  *
544
- * @param key - The cache key.
545
- * @param ttl - Time to live if the item is missing.
546
- * @param callback - Closure to execute on miss.
547
- * @returns The cached or fetched value.
1190
+ * Ensures the value is cached after the first miss. This provides an atomic-like
1191
+ * "get or set" flow to prevent multiple concurrent fetches of the same data.
1192
+ *
1193
+ * @param key - Unique cache key.
1194
+ * @param ttl - Duration to cache the result if a miss occurs.
1195
+ * @param callback - Logic to execute to fetch fresh data.
1196
+ * @returns Cached or freshly fetched value.
1197
+ * @throws {Error} If the callback fails or the store write operation errors.
548
1198
  *
549
1199
  * @example
550
1200
  * ```typescript
@@ -558,18 +1208,49 @@ var CacheManager = class {
558
1208
  }
559
1209
  /**
560
1210
  * Get an item from the cache, or execute the callback and store the result forever.
1211
+ *
1212
+ * @param key - The unique cache key.
1213
+ * @param callback - The closure to execute to fetch the fresh data.
1214
+ * @returns The cached or freshly fetched value.
1215
+ * @throws {Error} If the callback throws or the store write fails.
1216
+ *
1217
+ * @example
1218
+ * ```typescript
1219
+ * const settings = await cache.rememberForever('global:settings', () => {
1220
+ * return fetchSettingsFromApi();
1221
+ * });
1222
+ * ```
561
1223
  */
562
1224
  rememberForever(key, callback) {
563
1225
  return this.store().rememberForever(key, callback);
564
1226
  }
565
1227
  /**
566
- * Retrieve multiple items from the cache.
1228
+ * Retrieve multiple items from the default cache store by their keys.
1229
+ *
1230
+ * @param keys - An array of unique cache keys.
1231
+ * @returns An object mapping keys to their cached values (or null if missing).
1232
+ * @throws {Error} If the underlying store driver encounters a read error.
1233
+ *
1234
+ * @example
1235
+ * ```typescript
1236
+ * const values = await cache.many(['key1', 'key2']);
1237
+ * ```
567
1238
  */
568
1239
  many(keys) {
569
1240
  return this.store().many(keys);
570
1241
  }
571
1242
  /**
572
- * Store multiple items in the cache.
1243
+ * Store multiple items in the default cache store for a specific duration.
1244
+ *
1245
+ * @param values - An object mapping keys to the values to be stored.
1246
+ * @param ttl - Expiration time in seconds or a specific Date.
1247
+ * @returns A promise that resolves when all writes are complete.
1248
+ * @throws {Error} If the underlying store driver encounters a write error.
1249
+ *
1250
+ * @example
1251
+ * ```typescript
1252
+ * await cache.putMany({ a: 1, b: 2 }, 60);
1253
+ * ```
573
1254
  */
574
1255
  putMany(values, ttl) {
575
1256
  return this.store().putMany(values, ttl);
@@ -577,99 +1258,474 @@ var CacheManager = class {
577
1258
  /**
578
1259
  * Get an item from the cache, allowing stale data while refreshing in background.
579
1260
  *
580
- * @param key - Cache key.
1261
+ * Implements the Stale-While-Revalidate pattern to minimize latency for
1262
+ * frequently accessed but expensive data.
1263
+ *
1264
+ * @param key - The unique cache key.
581
1265
  * @param ttlSeconds - How long the value is considered fresh.
582
1266
  * @param staleSeconds - How long to serve stale data while refreshing.
583
- * @param callback - Closure to refresh the data.
1267
+ * @param callback - The closure to execute to refresh the data.
1268
+ * @returns The cached (possibly stale) or freshly fetched value.
1269
+ * @throws {Error} If the callback throws during an initial fetch.
1270
+ *
1271
+ * @example
1272
+ * ```typescript
1273
+ * const data = await cache.flexible('stats', 60, 30, () => fetchStats());
1274
+ * ```
584
1275
  */
585
1276
  flexible(key, ttlSeconds, staleSeconds, callback) {
586
1277
  return this.store().flexible(key, ttlSeconds, staleSeconds, callback);
587
1278
  }
588
1279
  /**
589
- * Retrieve an item from the cache and delete it.
1280
+ * Retrieve an item from the default cache store and then delete it.
1281
+ *
1282
+ * Useful for one-time notifications or temporary tokens.
1283
+ *
1284
+ * @param key - The unique cache key.
1285
+ * @param defaultValue - Fallback value if the key is missing.
1286
+ * @returns The cached value before deletion, or the default.
1287
+ * @throws {Error} If the underlying store driver encounters a read or delete error.
1288
+ *
1289
+ * @example
1290
+ * ```typescript
1291
+ * const token = await cache.pull('temp_token');
1292
+ * ```
590
1293
  */
591
1294
  pull(key, defaultValue) {
592
1295
  return this.store().pull(key, defaultValue);
593
1296
  }
594
1297
  /**
595
- * Remove an item from the cache.
1298
+ * Remove an item from the default cache store.
1299
+ *
1300
+ * @param key - The unique cache key.
1301
+ * @returns True if the item was removed, false otherwise.
1302
+ * @throws {Error} If the underlying store driver encounters a delete error.
1303
+ *
1304
+ * @example
1305
+ * ```typescript
1306
+ * await cache.forget('user:1');
1307
+ * ```
596
1308
  */
597
1309
  forget(key) {
598
1310
  return this.store().forget(key);
599
1311
  }
600
1312
  /**
601
- * Remove an item from the cache (alias for forget).
1313
+ * Remove an item from the default cache store (alias for forget).
1314
+ *
1315
+ * @param key - The unique cache key.
1316
+ * @returns True if the item was removed, false otherwise.
1317
+ * @throws {Error} If the underlying store driver encounters a delete error.
1318
+ *
1319
+ * @example
1320
+ * ```typescript
1321
+ * await cache.delete('old_key');
1322
+ * ```
602
1323
  */
603
1324
  delete(key) {
604
1325
  return this.store().delete(key);
605
1326
  }
606
1327
  /**
607
- * Remove all items from the cache.
1328
+ * Remove all items from the default cache store.
1329
+ *
1330
+ * @returns A promise that resolves when the flush is complete.
1331
+ * @throws {Error} If the underlying store driver encounters a flush error.
1332
+ *
1333
+ * @example
1334
+ * ```typescript
1335
+ * await cache.flush();
1336
+ * ```
608
1337
  */
609
1338
  flush() {
610
1339
  return this.store().flush();
611
1340
  }
612
1341
  /**
613
- * Clear the entire cache (alias for flush).
1342
+ * Clear the entire default cache store (alias for flush).
1343
+ *
1344
+ * @returns A promise that resolves when the clear is complete.
1345
+ * @throws {Error} If the underlying store driver encounters a clear error.
1346
+ *
1347
+ * @example
1348
+ * ```typescript
1349
+ * await cache.clear();
1350
+ * ```
614
1351
  */
615
1352
  clear() {
616
1353
  return this.store().clear();
617
1354
  }
618
1355
  /**
619
- * Increment an integer item in the cache.
1356
+ * Increment the value of an integer item in the default cache store.
1357
+ *
1358
+ * @param key - Unique cache key.
1359
+ * @param value - Amount to add.
1360
+ * @returns New value after incrementing.
1361
+ * @throws {Error} If existing value is not a number or the store is read-only.
1362
+ *
1363
+ * @example
1364
+ * ```typescript
1365
+ * const count = await cache.increment('page_views');
1366
+ * ```
620
1367
  */
621
1368
  increment(key, value) {
622
1369
  return this.store().increment(key, value);
623
1370
  }
624
1371
  /**
625
- * Decrement an integer item in the cache.
1372
+ * Decrement the value of an integer item in the default cache store.
1373
+ *
1374
+ * @param key - Unique cache key.
1375
+ * @param value - Amount to subtract.
1376
+ * @returns New value after decrementing.
1377
+ * @throws {Error} If existing value is not a number or the store is read-only.
1378
+ *
1379
+ * @example
1380
+ * ```typescript
1381
+ * const remaining = await cache.decrement('stock:1');
1382
+ * ```
626
1383
  */
627
1384
  decrement(key, value) {
628
1385
  return this.store().decrement(key, value);
629
1386
  }
630
1387
  /**
631
- * Get a lock instance.
1388
+ * Get a distributed lock instance from the default cache store.
1389
+ *
1390
+ * @param name - The unique name of the lock.
1391
+ * @param seconds - The duration the lock should be held for.
1392
+ * @returns A CacheLock instance.
1393
+ * @throws {Error} If the underlying store does not support locking.
632
1394
  *
633
- * @param name - Lock name.
634
- * @param seconds - Lock duration.
635
- * @returns CacheLock instance.
1395
+ * @example
1396
+ * ```typescript
1397
+ * const lock = cache.lock('process_data', 10);
1398
+ * if (await lock.get()) {
1399
+ * // ...
1400
+ * await lock.release();
1401
+ * }
1402
+ * ```
636
1403
  */
637
1404
  lock(name, seconds) {
638
1405
  return this.store().lock(name, seconds);
639
1406
  }
640
1407
  /**
641
- * Access a tagged cache section (only supported by some stores).
1408
+ * Access a tagged cache section for grouped operations.
1409
+ *
1410
+ * Tags allow you to clear groups of cache entries simultaneously.
1411
+ * Note: This is only supported by specific drivers like 'memory'.
1412
+ *
1413
+ * @param tags - An array of tag names.
1414
+ * @returns A tagged cache repository instance.
1415
+ * @throws {Error} If the underlying store driver does not support tags.
1416
+ *
1417
+ * @example
1418
+ * ```typescript
1419
+ * await cache.tags(['users', 'profiles']).put('user:1', data, 60);
1420
+ * await cache.tags(['users']).flush(); // Clears all 'users' tagged entries
1421
+ * ```
642
1422
  */
643
1423
  tags(tags) {
644
1424
  return this.store().tags(tags);
645
1425
  }
646
1426
  };
647
1427
 
648
- // src/stores/FileStore.ts
649
- var import_node_crypto = require("crypto");
650
- var import_promises = require("fs/promises");
651
- var import_node_path = require("path");
1428
+ // src/prediction/AccessPredictor.ts
1429
+ var MarkovPredictor = class {
1430
+ transitions = /* @__PURE__ */ new Map();
1431
+ lastKey = null;
1432
+ maxNodes;
1433
+ maxEdgesPerNode;
1434
+ /**
1435
+ * Initialize a new MarkovPredictor.
1436
+ *
1437
+ * @param options - Limits for internal transition graph to manage memory.
1438
+ */
1439
+ constructor(options = {}) {
1440
+ this.maxNodes = options.maxNodes ?? 1e3;
1441
+ this.maxEdgesPerNode = options.maxEdgesPerNode ?? 10;
1442
+ }
1443
+ record(key) {
1444
+ if (this.lastKey && this.lastKey !== key) {
1445
+ if (!this.transitions.has(this.lastKey)) {
1446
+ if (this.transitions.size >= this.maxNodes) {
1447
+ this.transitions.clear();
1448
+ }
1449
+ this.transitions.set(this.lastKey, /* @__PURE__ */ new Map());
1450
+ }
1451
+ const edges = this.transitions.get(this.lastKey);
1452
+ const count = edges.get(key) ?? 0;
1453
+ edges.set(key, count + 1);
1454
+ if (edges.size > this.maxEdgesPerNode) {
1455
+ let minKey = "";
1456
+ let minCount = Infinity;
1457
+ for (const [k, c] of edges) {
1458
+ if (c < minCount) {
1459
+ minCount = c;
1460
+ minKey = k;
1461
+ }
1462
+ }
1463
+ if (minKey) {
1464
+ edges.delete(minKey);
1465
+ }
1466
+ }
1467
+ }
1468
+ this.lastKey = key;
1469
+ }
1470
+ predict(key) {
1471
+ const edges = this.transitions.get(key);
1472
+ if (!edges) {
1473
+ return [];
1474
+ }
1475
+ return Array.from(edges.entries()).sort((a, b) => b[1] - a[1]).map((entry) => entry[0]);
1476
+ }
1477
+ reset() {
1478
+ this.transitions.clear();
1479
+ this.lastKey = null;
1480
+ }
1481
+ };
652
1482
 
653
- // src/locks.ts
654
- var LockTimeoutError = class extends Error {
655
- name = "LockTimeoutError";
1483
+ // src/stores/CircuitBreakerStore.ts
1484
+ var CircuitBreakerStore = class {
1485
+ constructor(primary, options = {}) {
1486
+ this.primary = primary;
1487
+ this.options = {
1488
+ maxFailures: options.maxFailures ?? 5,
1489
+ resetTimeout: options.resetTimeout ?? 6e4,
1490
+ fallback: options.fallback
1491
+ };
1492
+ }
1493
+ state = "CLOSED";
1494
+ failures = 0;
1495
+ lastErrorTime = 0;
1496
+ options;
1497
+ async execute(operation, fallbackResult = null) {
1498
+ if (this.state === "OPEN") {
1499
+ if (Date.now() - this.lastErrorTime > this.options.resetTimeout) {
1500
+ this.state = "HALF_OPEN";
1501
+ } else {
1502
+ return this.handleFallback(operation, fallbackResult);
1503
+ }
1504
+ }
1505
+ try {
1506
+ const result = await operation(this.primary);
1507
+ this.onSuccess();
1508
+ return result;
1509
+ } catch (_error) {
1510
+ this.onFailure();
1511
+ return this.handleFallback(operation, fallbackResult);
1512
+ }
1513
+ }
1514
+ onSuccess() {
1515
+ this.failures = 0;
1516
+ this.state = "CLOSED";
1517
+ }
1518
+ onFailure() {
1519
+ this.failures++;
1520
+ this.lastErrorTime = Date.now();
1521
+ if (this.failures >= this.options.maxFailures) {
1522
+ this.state = "OPEN";
1523
+ }
1524
+ }
1525
+ async handleFallback(operation, fallbackResult) {
1526
+ if (this.options.fallback) {
1527
+ try {
1528
+ return await operation(this.options.fallback);
1529
+ } catch {
1530
+ }
1531
+ }
1532
+ return fallbackResult;
1533
+ }
1534
+ async get(key) {
1535
+ return this.execute((s) => s.get(key));
1536
+ }
1537
+ async put(key, value, ttl) {
1538
+ return this.execute((s) => s.put(key, value, ttl));
1539
+ }
1540
+ async add(key, value, ttl) {
1541
+ return this.execute((s) => s.add(key, value, ttl), false);
1542
+ }
1543
+ async forget(key) {
1544
+ return this.execute((s) => s.forget(key), false);
1545
+ }
1546
+ async flush() {
1547
+ return this.execute((s) => s.flush());
1548
+ }
1549
+ async increment(key, value) {
1550
+ return this.execute((s) => s.increment(key, value), 0);
1551
+ }
1552
+ async decrement(key, value) {
1553
+ return this.execute((s) => s.decrement(key, value), 0);
1554
+ }
1555
+ async ttl(key) {
1556
+ return this.execute(async (s) => s.ttl ? s.ttl(key) : null);
1557
+ }
1558
+ /**
1559
+ * Returns current state for monitoring.
1560
+ *
1561
+ * @returns Current state of the circuit breaker.
1562
+ *
1563
+ * @example
1564
+ * ```typescript
1565
+ * const state = store.getState();
1566
+ * if (state === 'OPEN') {
1567
+ * console.warn('Primary cache is unavailable');
1568
+ * }
1569
+ * ```
1570
+ */
1571
+ getState() {
1572
+ return this.state;
1573
+ }
656
1574
  };
657
- function sleep(ms) {
658
- return new Promise((resolve) => setTimeout(resolve, ms));
659
- }
660
1575
 
661
1576
  // src/stores/FileStore.ts
1577
+ var import_node_crypto = require("crypto");
1578
+ var import_promises = require("fs/promises");
1579
+ var import_node_path = require("path");
662
1580
  var FileStore = class {
1581
+ /**
1582
+ * Initializes a new instance of the FileStore.
1583
+ *
1584
+ * @param options - Configuration settings for the store.
1585
+ */
663
1586
  constructor(options) {
664
1587
  this.options = options;
1588
+ if (options.enableCleanup !== false) {
1589
+ this.startCleanupDaemon(options.cleanupInterval ?? 6e4);
1590
+ }
1591
+ }
1592
+ cleanupTimer = null;
1593
+ /**
1594
+ * Starts the background process for periodic cache maintenance.
1595
+ *
1596
+ * @param interval - Time between cleanup cycles in milliseconds.
1597
+ * @internal
1598
+ */
1599
+ startCleanupDaemon(interval) {
1600
+ this.cleanupTimer = setInterval(() => {
1601
+ this.cleanExpiredFiles().catch(() => {
1602
+ });
1603
+ }, interval);
1604
+ if (this.cleanupTimer.unref) {
1605
+ this.cleanupTimer.unref();
1606
+ }
1607
+ }
1608
+ /**
1609
+ * Scans the cache directory to remove expired files and enforce capacity limits.
1610
+ *
1611
+ * This method performs a recursive scan of the storage directory. It deletes
1612
+ * files that have passed their expiration time and, if `maxFiles` is configured,
1613
+ * evicts the oldest files to stay within the limit.
1614
+ *
1615
+ * @returns The total number of files removed during this operation.
1616
+ * @throws {Error} If the directory cannot be read or files cannot be deleted.
1617
+ *
1618
+ * @example
1619
+ * ```typescript
1620
+ * const removedCount = await store.cleanExpiredFiles();
1621
+ * console.log(`Cleaned up ${removedCount} files.`);
1622
+ * ```
1623
+ */
1624
+ async cleanExpiredFiles() {
1625
+ await this.ensureDir();
1626
+ let cleaned = 0;
1627
+ const validFiles = [];
1628
+ const scanDir = async (dir) => {
1629
+ const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
1630
+ for (const entry of entries) {
1631
+ const fullPath = (0, import_node_path.join)(dir, entry.name);
1632
+ if (entry.isDirectory()) {
1633
+ await scanDir(fullPath);
1634
+ try {
1635
+ await (0, import_promises.rm)(fullPath, { recursive: false });
1636
+ } catch {
1637
+ }
1638
+ } else if (entry.isFile()) {
1639
+ if (!entry.name.endsWith(".json") || entry.name.startsWith(".lock-")) {
1640
+ continue;
1641
+ }
1642
+ try {
1643
+ const raw = await (0, import_promises.readFile)(fullPath, "utf8");
1644
+ const data = JSON.parse(raw);
1645
+ if (isExpired(data.expiresAt)) {
1646
+ await (0, import_promises.rm)(fullPath, { force: true });
1647
+ cleaned++;
1648
+ } else if (this.options.maxFiles) {
1649
+ const stats = await (0, import_promises.stat)(fullPath);
1650
+ validFiles.push({ path: fullPath, mtime: stats.mtimeMs });
1651
+ }
1652
+ } catch {
1653
+ }
1654
+ }
1655
+ }
1656
+ };
1657
+ await scanDir(this.options.directory);
1658
+ if (this.options.maxFiles && validFiles.length > this.options.maxFiles) {
1659
+ validFiles.sort((a, b) => a.mtime - b.mtime);
1660
+ const toRemove = validFiles.slice(0, validFiles.length - this.options.maxFiles);
1661
+ await Promise.all(
1662
+ toRemove.map(async (f) => {
1663
+ try {
1664
+ await (0, import_promises.rm)(f.path, { force: true });
1665
+ cleaned++;
1666
+ } catch {
1667
+ }
1668
+ })
1669
+ );
1670
+ }
1671
+ return cleaned;
665
1672
  }
1673
+ /**
1674
+ * Stops the cleanup daemon and releases associated resources.
1675
+ *
1676
+ * Should be called when the store is no longer needed to prevent memory leaks
1677
+ * and allow the process to exit gracefully.
1678
+ *
1679
+ * @example
1680
+ * ```typescript
1681
+ * await store.destroy();
1682
+ * ```
1683
+ */
1684
+ async destroy() {
1685
+ if (this.cleanupTimer) {
1686
+ clearInterval(this.cleanupTimer);
1687
+ this.cleanupTimer = null;
1688
+ }
1689
+ }
1690
+ /**
1691
+ * Ensures that the base storage directory exists.
1692
+ *
1693
+ * @throws {Error} If the directory cannot be created due to permissions or path conflicts.
1694
+ * @internal
1695
+ */
666
1696
  async ensureDir() {
667
1697
  await (0, import_promises.mkdir)(this.options.directory, { recursive: true });
668
1698
  }
1699
+ /**
1700
+ * Resolves the filesystem path for a given cache key.
1701
+ *
1702
+ * @param key - Normalized cache key.
1703
+ * @returns Absolute path to the JSON file representing the key.
1704
+ * @internal
1705
+ */
669
1706
  filePathForKey(key) {
670
1707
  const hashed = hashKey(key);
1708
+ if (this.options.useSubdirectories) {
1709
+ const d1 = hashed.substring(0, 2);
1710
+ const d2 = hashed.substring(2, 4);
1711
+ return (0, import_node_path.join)(this.options.directory, d1, d2, `${hashed}.json`);
1712
+ }
671
1713
  return (0, import_node_path.join)(this.options.directory, `${hashed}.json`);
672
1714
  }
1715
+ /**
1716
+ * Retrieves an item from the cache by its key.
1717
+ *
1718
+ * If the item exists but has expired, it will be deleted and `null` will be returned.
1719
+ *
1720
+ * @param key - The unique identifier for the cache item.
1721
+ * @returns The cached value, or `null` if not found or expired.
1722
+ * @throws {Error} If the file exists but cannot be read or parsed.
1723
+ *
1724
+ * @example
1725
+ * ```typescript
1726
+ * const value = await store.get<string>('my-key');
1727
+ * ```
1728
+ */
673
1729
  async get(key) {
674
1730
  const normalized = normalizeCacheKey(key);
675
1731
  await this.ensureDir();
@@ -686,6 +1742,22 @@ var FileStore = class {
686
1742
  return null;
687
1743
  }
688
1744
  }
1745
+ /**
1746
+ * Stores an item in the cache with a specified expiration time.
1747
+ *
1748
+ * Uses an atomic write strategy (write to temp file then rename) to ensure
1749
+ * data integrity even if the process crashes during the write operation.
1750
+ *
1751
+ * @param key - The unique identifier for the cache item.
1752
+ * @param value - The data to be cached.
1753
+ * @param ttl - Time-to-live in seconds or a Date object for absolute expiration.
1754
+ * @throws {Error} If the file system is not writable or disk is full.
1755
+ *
1756
+ * @example
1757
+ * ```typescript
1758
+ * await store.put('settings', { theme: 'dark' }, 86400);
1759
+ * ```
1760
+ */
689
1761
  async put(key, value, ttl) {
690
1762
  const normalized = normalizeCacheKey(key);
691
1763
  await this.ensureDir();
@@ -695,9 +1767,33 @@ var FileStore = class {
695
1767
  return;
696
1768
  }
697
1769
  const file = this.filePathForKey(normalized);
1770
+ if (this.options.useSubdirectories) {
1771
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(file), { recursive: true });
1772
+ }
1773
+ const tempFile = `${file}.tmp.${Date.now()}.${(0, import_node_crypto.randomUUID)()}`;
698
1774
  const payload = { expiresAt: expiresAt ?? null, value };
699
- await (0, import_promises.writeFile)(file, JSON.stringify(payload), "utf8");
1775
+ try {
1776
+ await (0, import_promises.writeFile)(tempFile, JSON.stringify(payload), "utf8");
1777
+ await (0, import_promises.rename)(tempFile, file);
1778
+ } catch (error) {
1779
+ await (0, import_promises.rm)(tempFile, { force: true }).catch(() => {
1780
+ });
1781
+ throw error;
1782
+ }
700
1783
  }
1784
+ /**
1785
+ * Stores an item in the cache only if it does not already exist.
1786
+ *
1787
+ * @param key - The unique identifier for the cache item.
1788
+ * @param value - The data to be cached.
1789
+ * @param ttl - Time-to-live in seconds or a Date object.
1790
+ * @returns `true` if the item was stored, `false` if it already existed.
1791
+ *
1792
+ * @example
1793
+ * ```typescript
1794
+ * const success = await store.add('unique-task', { status: 'pending' }, 60);
1795
+ * ```
1796
+ */
701
1797
  async add(key, value, ttl) {
702
1798
  const normalized = normalizeCacheKey(key);
703
1799
  const existing = await this.get(normalized);
@@ -707,6 +1803,17 @@ var FileStore = class {
707
1803
  await this.put(normalized, value, ttl);
708
1804
  return true;
709
1805
  }
1806
+ /**
1807
+ * Removes an item from the cache by its key.
1808
+ *
1809
+ * @param key - The unique identifier for the cache item.
1810
+ * @returns `true` if the file was deleted or didn't exist, `false` on failure.
1811
+ *
1812
+ * @example
1813
+ * ```typescript
1814
+ * await store.forget('my-key');
1815
+ * ```
1816
+ */
710
1817
  async forget(key) {
711
1818
  const normalized = normalizeCacheKey(key);
712
1819
  await this.ensureDir();
@@ -718,13 +1825,39 @@ var FileStore = class {
718
1825
  return false;
719
1826
  }
720
1827
  }
1828
+ /**
1829
+ * Removes all items from the cache directory.
1830
+ *
1831
+ * This operation deletes the entire cache directory and recreates it.
1832
+ * Use with caution as it is destructive and non-reversible.
1833
+ *
1834
+ * @throws {Error} If the directory cannot be removed or recreated.
1835
+ *
1836
+ * @example
1837
+ * ```typescript
1838
+ * await store.flush();
1839
+ * ```
1840
+ */
721
1841
  async flush() {
722
1842
  await this.ensureDir();
723
- const files = await (0, import_promises.readdir)(this.options.directory);
724
- await Promise.all(
725
- files.filter((f) => f.endsWith(".json")).map((f) => (0, import_promises.rm)((0, import_node_path.join)(this.options.directory, f), { force: true }))
726
- );
1843
+ await (0, import_promises.rm)(this.options.directory, { recursive: true, force: true });
1844
+ await this.ensureDir();
727
1845
  }
1846
+ /**
1847
+ * Increments the value of an integer item in the cache.
1848
+ *
1849
+ * If the key does not exist, it is initialized to 0 before incrementing.
1850
+ *
1851
+ * @param key - The unique identifier for the cache item.
1852
+ * @param value - The amount to increment by.
1853
+ * @returns The new value after incrementing.
1854
+ * @throws {Error} If the existing value is not a number.
1855
+ *
1856
+ * @example
1857
+ * ```typescript
1858
+ * const newCount = await store.increment('page-views');
1859
+ * ```
1860
+ */
728
1861
  async increment(key, value = 1) {
729
1862
  const normalized = normalizeCacheKey(key);
730
1863
  const current = await this.get(normalized);
@@ -732,28 +1865,105 @@ var FileStore = class {
732
1865
  await this.put(normalized, next, null);
733
1866
  return next;
734
1867
  }
1868
+ /**
1869
+ * Decrements the value of an integer item in the cache.
1870
+ *
1871
+ * If the key does not exist, it is initialized to 0 before decrementing.
1872
+ *
1873
+ * @param key - The unique identifier for the cache item.
1874
+ * @param value - The amount to decrement by.
1875
+ * @returns The new value after decrementing.
1876
+ * @throws {Error} If the existing value is not a number.
1877
+ *
1878
+ * @example
1879
+ * ```typescript
1880
+ * const newCount = await store.decrement('stock-level');
1881
+ * ```
1882
+ */
735
1883
  async decrement(key, value = 1) {
736
1884
  return this.increment(key, -value);
737
1885
  }
1886
+ /**
1887
+ * Retrieves the remaining time-to-live for a cache item in seconds.
1888
+ *
1889
+ * @param key - The unique identifier for the cache item.
1890
+ * @returns The seconds remaining until expiration, or `null` if it never expires or doesn't exist.
1891
+ *
1892
+ * @example
1893
+ * ```typescript
1894
+ * const secondsLeft = await store.ttl('session:123');
1895
+ * ```
1896
+ */
1897
+ async ttl(key) {
1898
+ const normalized = normalizeCacheKey(key);
1899
+ const file = this.filePathForKey(normalized);
1900
+ try {
1901
+ const raw = await (0, import_promises.readFile)(file, "utf8");
1902
+ const data = JSON.parse(raw);
1903
+ if (data.expiresAt === null) {
1904
+ return null;
1905
+ }
1906
+ const remaining = Math.ceil((data.expiresAt - Date.now()) / 1e3);
1907
+ return remaining > 0 ? remaining : null;
1908
+ } catch {
1909
+ return null;
1910
+ }
1911
+ }
1912
+ /**
1913
+ * Creates a distributed lock instance based on the filesystem.
1914
+ *
1915
+ * Locks are implemented using atomic file creation (`wx` flag). They include
1916
+ * protection against stale locks by checking process IDs and expiration times.
1917
+ *
1918
+ * @param name - The unique name of the lock.
1919
+ * @param seconds - The duration in seconds for which the lock should be held.
1920
+ * @returns A `CacheLock` instance for managing the lock lifecycle.
1921
+ *
1922
+ * @example
1923
+ * ```typescript
1924
+ * const lock = store.lock('process-report', 60);
1925
+ * if (await lock.acquire()) {
1926
+ * try {
1927
+ * // Critical section
1928
+ * } finally {
1929
+ * await lock.release();
1930
+ * }
1931
+ * }
1932
+ * ```
1933
+ */
738
1934
  lock(name, seconds = 10) {
739
1935
  const normalizedName = normalizeCacheKey(name);
740
1936
  const lockFile = (0, import_node_path.join)(this.options.directory, `.lock-${hashKey(normalizedName)}`);
741
1937
  const ttlMillis = Math.max(1, seconds) * 1e3;
742
1938
  const owner = (0, import_node_crypto.randomUUID)();
1939
+ const isProcessAlive = (pid) => {
1940
+ try {
1941
+ process.kill(pid, 0);
1942
+ return true;
1943
+ } catch {
1944
+ return false;
1945
+ }
1946
+ };
743
1947
  const tryAcquire = async () => {
744
1948
  await this.ensureDir();
745
1949
  try {
746
1950
  const handle = await (0, import_promises.open)(lockFile, "wx");
747
- await handle.writeFile(JSON.stringify({ owner, expiresAt: Date.now() + ttlMillis }), "utf8");
1951
+ const lockData = {
1952
+ owner,
1953
+ expiresAt: Date.now() + ttlMillis,
1954
+ pid: process.pid
1955
+ };
1956
+ await handle.writeFile(JSON.stringify(lockData), "utf8");
748
1957
  await handle.close();
749
1958
  return true;
750
1959
  } catch {
751
1960
  try {
752
1961
  const raw = await (0, import_promises.readFile)(lockFile, "utf8");
753
1962
  const data = JSON.parse(raw);
754
- if (!data.expiresAt || Date.now() > data.expiresAt) {
1963
+ const isExpired2 = !data.expiresAt || Date.now() > data.expiresAt;
1964
+ const isProcessDead = data.pid && !isProcessAlive(data.pid);
1965
+ if (isExpired2 || isProcessDead) {
755
1966
  await (0, import_promises.rm)(lockFile, { force: true });
756
- return false;
757
1967
  }
758
1968
  } catch {
759
1969
  }
@@ -761,9 +1971,17 @@ var FileStore = class {
761
1971
  }
762
1972
  };
763
1973
  return {
1974
+ /**
1975
+ * Attempts to acquire the lock immediately.
1976
+ *
1977
+ * @returns `true` if the lock was successfully acquired, `false` otherwise.
1978
+ */
764
1979
  async acquire() {
765
1980
  return tryAcquire();
766
1981
  },
1982
+ /**
1983
+ * Releases the lock if it is owned by the current instance.
1984
+ */
767
1985
  async release() {
768
1986
  try {
769
1987
  const raw = await (0, import_promises.readFile)(lockFile, "utf8");
@@ -774,6 +1992,15 @@ var FileStore = class {
774
1992
  } catch {
775
1993
  }
776
1994
  },
1995
+ /**
1996
+ * Executes a callback within the lock, waiting if necessary.
1997
+ *
1998
+ * @param secondsToWait - Maximum time to wait for the lock in seconds.
1999
+ * @param callback - The function to execute once the lock is acquired.
2000
+ * @param options - Polling configuration.
2001
+ * @returns The result of the callback.
2002
+ * @throws {LockTimeoutError} If the lock cannot be acquired within the wait time.
2003
+ */
777
2004
  async block(secondsToWait, callback, options) {
778
2005
  const deadline = Date.now() + Math.max(0, secondsToWait) * 1e3;
779
2006
  const sleepMillis = options?.sleepMillis ?? 150;
@@ -794,43 +2021,238 @@ var FileStore = class {
794
2021
  };
795
2022
  }
796
2023
  };
797
- function hashKey(key) {
798
- return (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
799
- }
2024
+ function hashKey(key) {
2025
+ return (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
2026
+ }
2027
+
2028
+ // src/stores/MemoryStore.ts
2029
+ var import_node_crypto2 = require("crypto");
2030
+
2031
+ // src/utils/LRUCache.ts
2032
+ var LRUCache = class {
2033
+ /**
2034
+ * Creates a new LRU cache instance.
2035
+ *
2036
+ * @param maxSize - The maximum number of items allowed in the cache. Set to 0 for unlimited.
2037
+ * @param onEvict - Optional callback triggered when an item is evicted due to capacity limits.
2038
+ */
2039
+ constructor(maxSize, onEvict) {
2040
+ this.maxSize = maxSize;
2041
+ this.onEvict = onEvict;
2042
+ }
2043
+ map = /* @__PURE__ */ new Map();
2044
+ head = null;
2045
+ tail = null;
2046
+ /**
2047
+ * The current number of items stored in the cache.
2048
+ */
2049
+ get size() {
2050
+ return this.map.size;
2051
+ }
2052
+ /**
2053
+ * Checks if a key exists in the cache without updating its access order.
2054
+ *
2055
+ * @param key - The identifier to look for.
2056
+ * @returns True if the key exists, false otherwise.
2057
+ */
2058
+ has(key) {
2059
+ return this.map.has(key);
2060
+ }
2061
+ /**
2062
+ * Retrieves an item from the cache and marks it as most recently used.
2063
+ *
2064
+ * @param key - The identifier of the item to retrieve.
2065
+ * @returns The cached value, or undefined if not found.
2066
+ *
2067
+ * @example
2068
+ * ```typescript
2069
+ * const value = cache.get('my-key');
2070
+ * ```
2071
+ */
2072
+ get(key) {
2073
+ const node = this.map.get(key);
2074
+ if (!node) {
2075
+ return void 0;
2076
+ }
2077
+ this.moveToHead(node);
2078
+ return node.value;
2079
+ }
2080
+ /**
2081
+ * Retrieves an item from the cache without updating its access order.
2082
+ *
2083
+ * Useful for inspecting the cache without affecting eviction priority.
2084
+ *
2085
+ * @param key - The identifier of the item to peek.
2086
+ * @returns The cached value, or undefined if not found.
2087
+ */
2088
+ peek(key) {
2089
+ const node = this.map.get(key);
2090
+ return node?.value;
2091
+ }
2092
+ /**
2093
+ * Adds or updates an item in the cache, marking it as most recently used.
2094
+ *
2095
+ * If the cache is at capacity, the least recently used item will be evicted.
2096
+ *
2097
+ * @param key - The identifier for the item.
2098
+ * @param value - The data to store.
2099
+ *
2100
+ * @example
2101
+ * ```typescript
2102
+ * cache.set('user:1', { name: 'Alice' });
2103
+ * ```
2104
+ */
2105
+ set(key, value) {
2106
+ const existingNode = this.map.get(key);
2107
+ if (existingNode) {
2108
+ existingNode.value = value;
2109
+ this.moveToHead(existingNode);
2110
+ return;
2111
+ }
2112
+ if (this.maxSize > 0 && this.map.size >= this.maxSize) {
2113
+ this.evict();
2114
+ }
2115
+ const newNode = {
2116
+ key,
2117
+ value,
2118
+ prev: null,
2119
+ next: this.head
2120
+ };
2121
+ if (this.head) {
2122
+ this.head.prev = newNode;
2123
+ }
2124
+ this.head = newNode;
2125
+ if (!this.tail) {
2126
+ this.tail = newNode;
2127
+ }
2128
+ this.map.set(key, newNode);
2129
+ }
2130
+ /**
2131
+ * Removes an item from the cache.
2132
+ *
2133
+ * @param key - The identifier of the item to remove.
2134
+ * @returns True if the item was found and removed, false otherwise.
2135
+ */
2136
+ delete(key) {
2137
+ const node = this.map.get(key);
2138
+ if (!node) {
2139
+ return false;
2140
+ }
2141
+ this.removeNode(node);
2142
+ this.map.delete(key);
2143
+ return true;
2144
+ }
2145
+ /**
2146
+ * Removes all items from the cache.
2147
+ */
2148
+ clear() {
2149
+ this.map.clear();
2150
+ this.head = null;
2151
+ this.tail = null;
2152
+ }
2153
+ /**
2154
+ * Moves a node to the head of the linked list (most recently used).
2155
+ *
2156
+ * @param node - The node to promote.
2157
+ */
2158
+ moveToHead(node) {
2159
+ if (node === this.head) {
2160
+ return;
2161
+ }
2162
+ if (node.prev) {
2163
+ node.prev.next = node.next;
2164
+ }
2165
+ if (node.next) {
2166
+ node.next.prev = node.prev;
2167
+ }
2168
+ if (node === this.tail) {
2169
+ this.tail = node.prev;
2170
+ }
2171
+ node.prev = null;
2172
+ node.next = this.head;
2173
+ if (this.head) {
2174
+ this.head.prev = node;
2175
+ }
2176
+ this.head = node;
2177
+ }
2178
+ /**
2179
+ * Removes a node from the linked list.
2180
+ *
2181
+ * @param node - The node to remove.
2182
+ */
2183
+ removeNode(node) {
2184
+ if (node.prev) {
2185
+ node.prev.next = node.next;
2186
+ } else {
2187
+ this.head = node.next;
2188
+ }
2189
+ if (node.next) {
2190
+ node.next.prev = node.prev;
2191
+ } else {
2192
+ this.tail = node.prev;
2193
+ }
2194
+ node.prev = null;
2195
+ node.next = null;
2196
+ }
2197
+ /**
2198
+ * Evicts the least recently used item (the tail of the list).
2199
+ *
2200
+ * Triggers the `onEvict` callback if provided.
2201
+ */
2202
+ evict() {
2203
+ if (!this.tail) {
2204
+ return;
2205
+ }
2206
+ const node = this.tail;
2207
+ if (this.onEvict) {
2208
+ this.onEvict(node.key, node.value);
2209
+ }
2210
+ this.removeNode(node);
2211
+ this.map.delete(node.key);
2212
+ }
2213
+ };
800
2214
 
801
2215
  // src/stores/MemoryStore.ts
802
- var import_node_crypto2 = require("crypto");
803
2216
  var MemoryStore = class {
804
- constructor(options = {}) {
805
- this.options = options;
806
- }
807
- entries = /* @__PURE__ */ new Map();
2217
+ entries;
808
2218
  locks = /* @__PURE__ */ new Map();
2219
+ stats = { hits: 0, misses: 0, evictions: 0 };
809
2220
  tagToKeys = /* @__PURE__ */ new Map();
810
2221
  keyToTags = /* @__PURE__ */ new Map();
811
- touchLRU(key) {
812
- const entry = this.entries.get(key);
813
- if (!entry) {
814
- return;
815
- }
816
- this.entries.delete(key);
817
- this.entries.set(key, entry);
2222
+ /**
2223
+ * Creates a new MemoryStore instance.
2224
+ *
2225
+ * @param options - Configuration for capacity and eviction.
2226
+ */
2227
+ constructor(options = {}) {
2228
+ this.entries = new LRUCache(options.maxItems ?? 0, (key) => {
2229
+ this.tagIndexRemove(key);
2230
+ this.stats.evictions++;
2231
+ });
818
2232
  }
819
- pruneIfNeeded() {
820
- const maxItems = this.options.maxItems;
821
- if (!maxItems || maxItems <= 0) {
822
- return;
823
- }
824
- while (this.entries.size > maxItems) {
825
- const oldest = this.entries.keys().next().value;
826
- if (!oldest) {
827
- return;
828
- }
829
- void this.forget(oldest);
830
- }
2233
+ /**
2234
+ * Retrieves current performance metrics.
2235
+ *
2236
+ * @returns A snapshot of hits, misses, size, and eviction counts.
2237
+ *
2238
+ * @example
2239
+ * ```typescript
2240
+ * const stats = store.getStats();
2241
+ * console.log(`Cache hit rate: ${stats.hitRate * 100}%`);
2242
+ * ```
2243
+ */
2244
+ getStats() {
2245
+ const total = this.stats.hits + this.stats.misses;
2246
+ return {
2247
+ hits: this.stats.hits,
2248
+ misses: this.stats.misses,
2249
+ hitRate: total > 0 ? this.stats.hits / total : 0,
2250
+ size: this.entries.size,
2251
+ evictions: this.stats.evictions
2252
+ };
831
2253
  }
832
2254
  cleanupExpired(key, now = Date.now()) {
833
- const entry = this.entries.get(key);
2255
+ const entry = this.entries.peek(key);
834
2256
  if (!entry) {
835
2257
  return;
836
2258
  }
@@ -838,19 +2260,48 @@ var MemoryStore = class {
838
2260
  void this.forget(key);
839
2261
  }
840
2262
  }
2263
+ /**
2264
+ * Retrieves an item from the cache by its key.
2265
+ *
2266
+ * If the item is expired, it will be automatically removed and `null` will be returned.
2267
+ *
2268
+ * @param key - The unique identifier for the cached item.
2269
+ * @returns The cached value, or `null` if not found or expired.
2270
+ *
2271
+ * @example
2272
+ * ```typescript
2273
+ * const value = await store.get('my-key');
2274
+ * ```
2275
+ */
841
2276
  async get(key) {
842
2277
  const normalized = normalizeCacheKey(key);
843
2278
  const entry = this.entries.get(normalized);
844
2279
  if (!entry) {
2280
+ this.stats.misses++;
845
2281
  return null;
846
2282
  }
847
2283
  if (isExpired(entry.expiresAt)) {
848
2284
  await this.forget(normalized);
2285
+ this.stats.misses++;
849
2286
  return null;
850
2287
  }
851
- this.touchLRU(normalized);
2288
+ this.stats.hits++;
852
2289
  return entry.value;
853
2290
  }
2291
+ /**
2292
+ * Stores an item in the cache with a specific TTL.
2293
+ *
2294
+ * If the key already exists, it will be overwritten.
2295
+ *
2296
+ * @param key - The unique identifier for the item.
2297
+ * @param value - The data to store.
2298
+ * @param ttl - Time-to-live in seconds, or a Date object for absolute expiration.
2299
+ *
2300
+ * @example
2301
+ * ```typescript
2302
+ * await store.put('settings', { theme: 'dark' }, 3600);
2303
+ * ```
2304
+ */
854
2305
  async put(key, value, ttl) {
855
2306
  const normalized = normalizeCacheKey(key);
856
2307
  const expiresAt = ttlToExpiresAt(ttl);
@@ -859,8 +2310,20 @@ var MemoryStore = class {
859
2310
  return;
860
2311
  }
861
2312
  this.entries.set(normalized, { value, expiresAt: expiresAt ?? null });
862
- this.pruneIfNeeded();
863
2313
  }
2314
+ /**
2315
+ * Stores an item only if it does not already exist in the cache.
2316
+ *
2317
+ * @param key - The unique identifier for the item.
2318
+ * @param value - The data to store.
2319
+ * @param ttl - Time-to-live in seconds or absolute expiration.
2320
+ * @returns `true` if the item was added, `false` if it already existed.
2321
+ *
2322
+ * @example
2323
+ * ```typescript
2324
+ * const added = await store.add('unique-task', data, 60);
2325
+ * ```
2326
+ */
864
2327
  async add(key, value, ttl) {
865
2328
  const normalized = normalizeCacheKey(key);
866
2329
  this.cleanupExpired(normalized);
@@ -870,17 +2333,50 @@ var MemoryStore = class {
870
2333
  await this.put(normalized, value, ttl);
871
2334
  return true;
872
2335
  }
2336
+ /**
2337
+ * Removes an item from the cache.
2338
+ *
2339
+ * @param key - The unique identifier for the item to remove.
2340
+ * @returns `true` if the item existed and was removed, `false` otherwise.
2341
+ *
2342
+ * @example
2343
+ * ```typescript
2344
+ * await store.forget('user:session');
2345
+ * ```
2346
+ */
873
2347
  async forget(key) {
874
2348
  const normalized = normalizeCacheKey(key);
875
2349
  const existed = this.entries.delete(normalized);
876
2350
  this.tagIndexRemove(normalized);
877
2351
  return existed;
878
2352
  }
2353
+ /**
2354
+ * Removes all items from the cache and resets all internal indexes.
2355
+ *
2356
+ * @example
2357
+ * ```typescript
2358
+ * await store.flush();
2359
+ * ```
2360
+ */
879
2361
  async flush() {
880
2362
  this.entries.clear();
881
2363
  this.tagToKeys.clear();
882
2364
  this.keyToTags.clear();
883
2365
  }
2366
+ /**
2367
+ * Increments the value of an item in the cache.
2368
+ *
2369
+ * If the key does not exist, it starts from 0.
2370
+ *
2371
+ * @param key - The identifier for the numeric value.
2372
+ * @param value - The amount to increment by (defaults to 1).
2373
+ * @returns The new incremented value.
2374
+ *
2375
+ * @example
2376
+ * ```typescript
2377
+ * const count = await store.increment('page_views');
2378
+ * ```
2379
+ */
884
2380
  async increment(key, value = 1) {
885
2381
  const normalized = normalizeCacheKey(key);
886
2382
  const current = await this.get(normalized);
@@ -888,9 +2384,64 @@ var MemoryStore = class {
888
2384
  await this.put(normalized, next, null);
889
2385
  return next;
890
2386
  }
2387
+ /**
2388
+ * Decrements the value of an item in the cache.
2389
+ *
2390
+ * @param key - The identifier for the numeric value.
2391
+ * @param value - The amount to decrement by (defaults to 1).
2392
+ * @returns The new decremented value.
2393
+ *
2394
+ * @example
2395
+ * ```typescript
2396
+ * const remaining = await store.decrement('stock_count', 5);
2397
+ * ```
2398
+ */
891
2399
  async decrement(key, value = 1) {
892
2400
  return this.increment(key, -value);
893
2401
  }
2402
+ /**
2403
+ * Gets the remaining time-to-live for a cached item.
2404
+ *
2405
+ * @param key - The identifier for the cached item.
2406
+ * @returns Remaining seconds, or `null` if the item has no expiration or does not exist.
2407
+ *
2408
+ * @example
2409
+ * ```typescript
2410
+ * const secondsLeft = await store.ttl('token');
2411
+ * ```
2412
+ */
2413
+ async ttl(key) {
2414
+ const normalized = normalizeCacheKey(key);
2415
+ const entry = this.entries.peek(normalized);
2416
+ if (!entry || entry.expiresAt === null) {
2417
+ return null;
2418
+ }
2419
+ const now = Date.now();
2420
+ if (isExpired(entry.expiresAt, now)) {
2421
+ await this.forget(normalized);
2422
+ return null;
2423
+ }
2424
+ return Math.max(0, Math.ceil((entry.expiresAt - now) / 1e3));
2425
+ }
2426
+ /**
2427
+ * Creates a lock instance for managing exclusive access to a resource.
2428
+ *
2429
+ * @param name - The name of the lock.
2430
+ * @param seconds - The duration the lock should be held (defaults to 10).
2431
+ * @returns A `CacheLock` instance.
2432
+ *
2433
+ * @example
2434
+ * ```typescript
2435
+ * const lock = store.lock('process-report', 30);
2436
+ * if (await lock.acquire()) {
2437
+ * try {
2438
+ * // Critical section
2439
+ * } finally {
2440
+ * await lock.release();
2441
+ * }
2442
+ * }
2443
+ * ```
2444
+ */
894
2445
  lock(name, seconds = 10) {
895
2446
  const lockKey = `lock:${normalizeCacheKey(name)}`;
896
2447
  const ttlMillis = Math.max(1, seconds) * 1e3;
@@ -907,6 +2458,11 @@ var MemoryStore = class {
907
2458
  };
908
2459
  let owner;
909
2460
  return {
2461
+ /**
2462
+ * Attempts to acquire the lock.
2463
+ *
2464
+ * @returns `true` if acquired, `false` if already held by another process.
2465
+ */
910
2466
  async acquire() {
911
2467
  const result = await acquire();
912
2468
  if (!result.ok) {
@@ -915,6 +2471,9 @@ var MemoryStore = class {
915
2471
  owner = result.owner;
916
2472
  return true;
917
2473
  },
2474
+ /**
2475
+ * Releases the lock if it is held by the current owner.
2476
+ */
918
2477
  async release() {
919
2478
  if (!owner) {
920
2479
  return;
@@ -925,6 +2484,22 @@ var MemoryStore = class {
925
2484
  }
926
2485
  owner = void 0;
927
2486
  },
2487
+ /**
2488
+ * Attempts to acquire the lock and execute a callback, waiting if necessary.
2489
+ *
2490
+ * @param secondsToWait - How long to wait for the lock before timing out.
2491
+ * @param callback - The logic to execute while holding the lock.
2492
+ * @param options - Polling configuration.
2493
+ * @returns The result of the callback.
2494
+ * @throws {LockTimeoutError} If the lock cannot be acquired within the wait time.
2495
+ *
2496
+ * @example
2497
+ * ```typescript
2498
+ * await lock.block(5, async () => {
2499
+ * // This code runs exclusively
2500
+ * });
2501
+ * ```
2502
+ */
928
2503
  async block(secondsToWait, callback, options) {
929
2504
  const deadline = Date.now() + Math.max(0, secondsToWait) * 1e3;
930
2505
  const sleepMillis = options?.sleepMillis ?? 150;
@@ -944,6 +2519,15 @@ var MemoryStore = class {
944
2519
  }
945
2520
  };
946
2521
  }
2522
+ /**
2523
+ * Generates a tagged key for storage.
2524
+ *
2525
+ * Used internally to prefix keys with their associated tags.
2526
+ *
2527
+ * @param key - The original cache key.
2528
+ * @param tags - List of tags to associate with the key.
2529
+ * @returns A formatted string containing tags and the key.
2530
+ */
947
2531
  tagKey(key, tags) {
948
2532
  const normalizedKey = normalizeCacheKey(key);
949
2533
  const normalizedTags = [...tags].map(String).filter(Boolean).sort();
@@ -952,6 +2536,12 @@ var MemoryStore = class {
952
2536
  }
953
2537
  return `tags:${normalizedTags.join("|")}:${normalizedKey}`;
954
2538
  }
2539
+ /**
2540
+ * Indexes a tagged key for bulk invalidation.
2541
+ *
2542
+ * @param tags - The tags to index.
2543
+ * @param taggedKey - The full key (including tag prefix) to store.
2544
+ */
955
2545
  tagIndexAdd(tags, taggedKey) {
956
2546
  const normalizedTags = [...tags].map(String).filter(Boolean);
957
2547
  if (normalizedTags.length === 0) {
@@ -974,6 +2564,11 @@ var MemoryStore = class {
974
2564
  tagSet.add(tag);
975
2565
  }
976
2566
  }
2567
+ /**
2568
+ * Removes a key from the tag indexes.
2569
+ *
2570
+ * @param taggedKey - The key to remove from all tag sets.
2571
+ */
977
2572
  tagIndexRemove(taggedKey) {
978
2573
  const tags = this.keyToTags.get(taggedKey);
979
2574
  if (!tags) {
@@ -991,6 +2586,16 @@ var MemoryStore = class {
991
2586
  }
992
2587
  this.keyToTags.delete(taggedKey);
993
2588
  }
2589
+ /**
2590
+ * Invalidates all cache entries associated with any of the given tags.
2591
+ *
2592
+ * @param tags - The tags to flush.
2593
+ *
2594
+ * @example
2595
+ * ```typescript
2596
+ * await store.flushTags(['users', 'profiles']);
2597
+ * ```
2598
+ */
994
2599
  async flushTags(tags) {
995
2600
  const normalizedTags = [...tags].map(String).filter(Boolean);
996
2601
  if (normalizedTags.length === 0) {
@@ -1014,38 +2619,187 @@ var MemoryStore = class {
1014
2619
 
1015
2620
  // src/stores/NullStore.ts
1016
2621
  var NullStore = class {
2622
+ /**
2623
+ * Simulates a cache miss for any given key.
2624
+ *
2625
+ * @param _key - Identifier for the cached item.
2626
+ * @returns Always `null` regardless of requested key.
2627
+ *
2628
+ * @example
2629
+ * ```typescript
2630
+ * const value = await store.get('my-key');
2631
+ * ```
2632
+ */
1017
2633
  async get(_key) {
1018
2634
  return null;
1019
2635
  }
2636
+ /**
2637
+ * Discards the provided value instead of storing it.
2638
+ *
2639
+ * @param _key - The identifier for the item.
2640
+ * @param _value - The data to be cached.
2641
+ * @param _ttl - Time-to-live in seconds.
2642
+ * @returns Resolves immediately after discarding the data.
2643
+ *
2644
+ * @example
2645
+ * ```typescript
2646
+ * await store.put('user:1', { id: 1 }, 3600);
2647
+ * ```
2648
+ */
1020
2649
  async put(_key, _value, _ttl) {
1021
2650
  }
2651
+ /**
2652
+ * Simulates a failed attempt to add an item to the cache.
2653
+ *
2654
+ * Since NullStore does not store data, this method always indicates that
2655
+ * the item was not added.
2656
+ *
2657
+ * @param _key - The identifier for the item.
2658
+ * @param _value - The data to be cached.
2659
+ * @param _ttl - Time-to-live in seconds.
2660
+ * @returns Always returns `false`.
2661
+ *
2662
+ * @example
2663
+ * ```typescript
2664
+ * const added = await store.add('key', 'value', 60); // false
2665
+ * ```
2666
+ */
1022
2667
  async add(_key, _value, _ttl) {
1023
2668
  return false;
1024
2669
  }
2670
+ /**
2671
+ * Simulates a failed attempt to remove an item from the cache.
2672
+ *
2673
+ * Since no data is ever stored, there is nothing to remove.
2674
+ *
2675
+ * @param _key - The identifier for the item to remove.
2676
+ * @returns Always returns `false`.
2677
+ *
2678
+ * @example
2679
+ * ```typescript
2680
+ * const forgotten = await store.forget('key'); // false
2681
+ * ```
2682
+ */
1025
2683
  async forget(_key) {
1026
2684
  return false;
1027
2685
  }
2686
+ /**
2687
+ * Performs a no-op flush operation.
2688
+ *
2689
+ * @returns Resolves immediately as there is no data to clear.
2690
+ *
2691
+ * @example
2692
+ * ```typescript
2693
+ * await store.flush();
2694
+ * ```
2695
+ */
1028
2696
  async flush() {
1029
2697
  }
2698
+ /**
2699
+ * Simulates an increment operation on a non-existent key.
2700
+ *
2701
+ * @param _key - The identifier for the numeric item.
2702
+ * @param _value - The amount to increment by.
2703
+ * @returns Always returns `0`.
2704
+ *
2705
+ * @example
2706
+ * ```typescript
2707
+ * const newValue = await store.increment('counter', 1); // 0
2708
+ * ```
2709
+ */
1030
2710
  async increment(_key, _value = 1) {
1031
2711
  return 0;
1032
2712
  }
2713
+ /**
2714
+ * Simulates a decrement operation on a non-existent key.
2715
+ *
2716
+ * @param _key - The identifier for the numeric item.
2717
+ * @param _value - The amount to decrement by.
2718
+ * @returns Always returns `0`.
2719
+ *
2720
+ * @example
2721
+ * ```typescript
2722
+ * const newValue = await store.decrement('counter', 1); // 0
2723
+ * ```
2724
+ */
1033
2725
  async decrement(_key, _value = 1) {
1034
2726
  return 0;
1035
2727
  }
1036
2728
  };
1037
2729
 
2730
+ // src/stores/PredictiveStore.ts
2731
+ var PredictiveStore = class {
2732
+ constructor(store, options = {}) {
2733
+ this.store = store;
2734
+ this.predictor = options.predictor ?? new MarkovPredictor();
2735
+ }
2736
+ predictor;
2737
+ async get(key) {
2738
+ this.predictor.record(key);
2739
+ const candidates = this.predictor.predict(key);
2740
+ if (candidates.length > 0) {
2741
+ void Promise.all(candidates.map((k) => this.store.get(k).catch(() => {
2742
+ })));
2743
+ }
2744
+ return this.store.get(key);
2745
+ }
2746
+ async put(key, value, ttl) {
2747
+ return this.store.put(key, value, ttl);
2748
+ }
2749
+ async add(key, value, ttl) {
2750
+ return this.store.add(key, value, ttl);
2751
+ }
2752
+ async forget(key) {
2753
+ return this.store.forget(key);
2754
+ }
2755
+ async flush() {
2756
+ if (typeof this.predictor.reset === "function") {
2757
+ this.predictor.reset();
2758
+ }
2759
+ return this.store.flush();
2760
+ }
2761
+ async increment(key, value) {
2762
+ return this.store.increment(key, value);
2763
+ }
2764
+ async decrement(key, value) {
2765
+ return this.store.decrement(key, value);
2766
+ }
2767
+ lock(name, seconds) {
2768
+ return this.store.lock ? this.store.lock(name, seconds) : void 0;
2769
+ }
2770
+ async ttl(key) {
2771
+ return this.store.ttl ? this.store.ttl(key) : null;
2772
+ }
2773
+ };
2774
+
1038
2775
  // src/stores/RedisStore.ts
1039
2776
  var import_node_crypto3 = require("crypto");
1040
2777
  var import_plasma = require("@gravito/plasma");
1041
2778
  var RedisStore = class {
1042
2779
  connectionName;
2780
+ /**
2781
+ * Initialize a new RedisStore instance.
2782
+ *
2783
+ * @param options - Redis connection and prefix settings.
2784
+ *
2785
+ * @example
2786
+ * ```typescript
2787
+ * const store = new RedisStore({ prefix: 'app:' });
2788
+ * ```
2789
+ */
1043
2790
  constructor(options = {}) {
1044
2791
  this.connectionName = options.connection;
1045
2792
  }
1046
2793
  get client() {
1047
2794
  return import_plasma.Redis.connection(this.connectionName);
1048
2795
  }
2796
+ /**
2797
+ * Retrieve an item from Redis.
2798
+ *
2799
+ * @param key - Unique cache key identifier.
2800
+ * @returns Parsed JSON value or null if missing/expired.
2801
+ * @throws {Error} If Redis connection fails or read errors occur.
2802
+ */
1049
2803
  async get(key) {
1050
2804
  const normalized = normalizeCacheKey(key);
1051
2805
  const value = await this.client.get(normalized);
@@ -1058,6 +2812,14 @@ var RedisStore = class {
1058
2812
  return value;
1059
2813
  }
1060
2814
  }
2815
+ /**
2816
+ * Store an item in Redis.
2817
+ *
2818
+ * @param key - Unique cache key identifier.
2819
+ * @param value - Value to serialize and store.
2820
+ * @param ttl - Expiration duration.
2821
+ * @throws {Error} If Redis connection fails or write errors occur.
2822
+ */
1061
2823
  async put(key, value, ttl) {
1062
2824
  const normalized = normalizeCacheKey(key);
1063
2825
  const serialized = JSON.stringify(value);
@@ -1083,8 +2845,20 @@ var RedisStore = class {
1083
2845
  }
1084
2846
  async forget(key) {
1085
2847
  const normalized = normalizeCacheKey(key);
1086
- const count = await this.client.del(normalized);
1087
- return count > 0;
2848
+ const luaScript = `
2849
+ local key = KEYS[1]
2850
+ local tag_prefix = ARGV[1]
2851
+ local tags = redis.call('SMEMBERS', key .. ':tags')
2852
+ local del_result = redis.call('DEL', key)
2853
+ for _, tag in ipairs(tags) do
2854
+ redis.call('SREM', tag_prefix .. tag, key)
2855
+ end
2856
+ redis.call('DEL', key .. ':tags')
2857
+ return del_result
2858
+ `;
2859
+ const client = this.client;
2860
+ const result = await client.eval(luaScript, 1, normalized, "tag:");
2861
+ return result > 0;
1088
2862
  }
1089
2863
  async flush() {
1090
2864
  await this.client.flushdb();
@@ -1096,6 +2870,14 @@ var RedisStore = class {
1096
2870
  }
1097
2871
  return await this.client.incrby(normalized, value);
1098
2872
  }
2873
+ /**
2874
+ * Decrement a numeric value in Redis.
2875
+ *
2876
+ * @param key - Unique cache key identifier.
2877
+ * @param value - Amount to subtract.
2878
+ * @returns Updated numeric value.
2879
+ * @throws {Error} If key is not numeric or Redis errors occur.
2880
+ */
1099
2881
  async decrement(key, value = 1) {
1100
2882
  const normalized = normalizeCacheKey(key);
1101
2883
  if (value === 1) {
@@ -1114,13 +2896,25 @@ var RedisStore = class {
1114
2896
  return;
1115
2897
  }
1116
2898
  const pipeline = this.client.pipeline();
2899
+ pipeline.sadd(`${taggedKey}:tags`, ...tags);
1117
2900
  for (const tag of tags) {
1118
2901
  const tagSetKey = `tag:${tag}`;
1119
2902
  pipeline.sadd(tagSetKey, taggedKey);
1120
2903
  }
1121
2904
  await pipeline.exec();
1122
2905
  }
1123
- async tagIndexRemove(_taggedKey) {
2906
+ async tagIndexRemove(taggedKey) {
2907
+ const luaScript = `
2908
+ local key = KEYS[1]
2909
+ local tag_prefix = ARGV[1]
2910
+ local tags = redis.call('SMEMBERS', key .. ':tags')
2911
+ for _, tag in ipairs(tags) do
2912
+ redis.call('SREM', tag_prefix .. tag, key)
2913
+ end
2914
+ redis.call('DEL', key .. ':tags')
2915
+ `;
2916
+ const client = this.client;
2917
+ await client.eval(luaScript, 1, taggedKey, "tag:");
1124
2918
  }
1125
2919
  async flushTags(tags) {
1126
2920
  if (tags.length === 0) {
@@ -1148,6 +2942,11 @@ var RedisStore = class {
1148
2942
  // ============================================================================
1149
2943
  // Locks
1150
2944
  // ============================================================================
2945
+ async ttl(key) {
2946
+ const normalized = normalizeCacheKey(key);
2947
+ const result = await this.client.ttl(normalized);
2948
+ return result < 0 ? null : result;
2949
+ }
1151
2950
  lock(name, seconds = 10) {
1152
2951
  const lockKey = `lock:${normalizeCacheKey(name)}`;
1153
2952
  const owner = (0, import_node_crypto3.randomUUID)();
@@ -1159,15 +2958,49 @@ var RedisStore = class {
1159
2958
  return result === "OK";
1160
2959
  },
1161
2960
  async release() {
1162
- const current = await client.get(lockKey);
1163
- if (current === owner) {
1164
- await client.del(lockKey);
1165
- }
2961
+ const luaScript = `
2962
+ local current = redis.call('GET', KEYS[1])
2963
+ if current == ARGV[1] then
2964
+ return redis.call('DEL', KEYS[1])
2965
+ else
2966
+ return 0
2967
+ end
2968
+ `;
2969
+ const evalClient = client;
2970
+ await evalClient.eval(luaScript, 1, lockKey, owner);
2971
+ },
2972
+ async extend(extensionSeconds) {
2973
+ const luaScript = `
2974
+ local current = redis.call('GET', KEYS[1])
2975
+ if current == ARGV[1] then
2976
+ return redis.call('EXPIRE', KEYS[1], ARGV[2])
2977
+ else
2978
+ return 0
2979
+ end
2980
+ `;
2981
+ const evalClient = client;
2982
+ const result = await evalClient.eval(
2983
+ luaScript,
2984
+ 1,
2985
+ lockKey,
2986
+ owner,
2987
+ extensionSeconds.toString()
2988
+ );
2989
+ return result === 1;
2990
+ },
2991
+ async getRemainingTime() {
2992
+ return await client.ttl(lockKey);
1166
2993
  },
1167
2994
  async block(secondsToWait, callback, options) {
2995
+ const retryInterval = options?.retryInterval ?? options?.sleepMillis ?? 100;
2996
+ const maxRetries = options?.maxRetries ?? Number.POSITIVE_INFINITY;
2997
+ const signal = options?.signal;
1168
2998
  const deadline = Date.now() + Math.max(0, secondsToWait) * 1e3;
1169
- const sleepMillis = options?.sleepMillis ?? 150;
1170
- while (Date.now() <= deadline) {
2999
+ let attempt = 0;
3000
+ while (Date.now() <= deadline && attempt < maxRetries) {
3001
+ if (signal?.aborted) {
3002
+ throw new Error(`Lock acquisition for '${name}' was aborted`);
3003
+ }
1171
3004
  if (await this.acquire()) {
1172
3005
  try {
1173
3006
  return await callback();
@@ -1175,7 +3008,9 @@ var RedisStore = class {
1175
3008
  await this.release();
1176
3009
  }
1177
3010
  }
1178
- await sleep(sleepMillis);
3011
+ attempt++;
3012
+ const delay = Math.min(retryInterval * 1.5 ** Math.min(attempt, 10), 1e3);
3013
+ await sleep(delay);
1179
3014
  }
1180
3015
  throw new LockTimeoutError(
1181
3016
  `Failed to acquire lock '${name}' within ${secondsToWait} seconds.`
@@ -1185,6 +3020,64 @@ var RedisStore = class {
1185
3020
  }
1186
3021
  };
1187
3022
 
3023
+ // src/stores/TieredStore.ts
3024
+ var TieredStore = class {
3025
+ /**
3026
+ * Initializes a new TieredStore.
3027
+ *
3028
+ * @param local - The L1 cache store (usually MemoryStore).
3029
+ * @param remote - The L2 cache store (usually RedisStore or FileStore).
3030
+ */
3031
+ constructor(local, remote) {
3032
+ this.local = local;
3033
+ this.remote = remote;
3034
+ }
3035
+ async get(key) {
3036
+ const localValue = await this.local.get(key);
3037
+ if (localValue !== null) {
3038
+ return localValue;
3039
+ }
3040
+ const remoteValue = await this.remote.get(key);
3041
+ if (remoteValue !== null) {
3042
+ const ttl = this.remote.ttl ? await this.remote.ttl(key) : null;
3043
+ await this.local.put(key, remoteValue, ttl);
3044
+ }
3045
+ return remoteValue;
3046
+ }
3047
+ async put(key, value, ttl) {
3048
+ await Promise.all([this.local.put(key, value, ttl), this.remote.put(key, value, ttl)]);
3049
+ }
3050
+ async add(key, value, ttl) {
3051
+ const ok = await this.remote.add(key, value, ttl);
3052
+ if (ok) {
3053
+ await this.local.put(key, value, ttl);
3054
+ }
3055
+ return ok;
3056
+ }
3057
+ async forget(key) {
3058
+ const [localOk, remoteOk] = await Promise.all([this.local.forget(key), this.remote.forget(key)]);
3059
+ return localOk || remoteOk;
3060
+ }
3061
+ async flush() {
3062
+ await Promise.all([this.local.flush(), this.remote.flush()]);
3063
+ }
3064
+ async increment(key, value = 1) {
3065
+ const next = await this.remote.increment(key, value);
3066
+ const ttl = this.remote.ttl ? await this.remote.ttl(key) : null;
3067
+ await this.local.put(key, next, ttl);
3068
+ return next;
3069
+ }
3070
+ async decrement(key, value = 1) {
3071
+ return this.increment(key, -value);
3072
+ }
3073
+ async ttl(key) {
3074
+ if (this.remote.ttl) {
3075
+ return this.remote.ttl(key);
3076
+ }
3077
+ return this.local.ttl ? this.local.ttl(key) : null;
3078
+ }
3079
+ };
3080
+
1188
3081
  // src/index.ts
1189
3082
  var MemoryCacheProvider = class {
1190
3083
  store = new MemoryStore();
@@ -1278,6 +3171,26 @@ function createStoreFactory(config) {
1278
3171
  }
1279
3172
  };
1280
3173
  }
3174
+ if (storeConfig.driver === "tiered") {
3175
+ const factory = createStoreFactory(config);
3176
+ return new TieredStore(factory(storeConfig.local), factory(storeConfig.remote));
3177
+ }
3178
+ if (storeConfig.driver === "predictive") {
3179
+ const factory = createStoreFactory(config);
3180
+ return new PredictiveStore(factory(storeConfig.inner), {
3181
+ predictor: new MarkovPredictor({ maxNodes: storeConfig.maxNodes })
3182
+ });
3183
+ }
3184
+ if (storeConfig.driver === "circuit-breaker") {
3185
+ const factory = createStoreFactory(config);
3186
+ const primary = factory(storeConfig.primary);
3187
+ const fallback = storeConfig.fallback ? factory(storeConfig.fallback) : void 0;
3188
+ return new CircuitBreakerStore(primary, {
3189
+ maxFailures: storeConfig.maxFailures,
3190
+ resetTimeout: storeConfig.resetTimeout,
3191
+ fallback
3192
+ });
3193
+ }
1281
3194
  throw new Error(`Unsupported cache driver '${storeConfig.driver}'.`);
1282
3195
  };
1283
3196
  }
@@ -1345,15 +3258,19 @@ var OrbitCache = OrbitStasis;
1345
3258
  0 && (module.exports = {
1346
3259
  CacheManager,
1347
3260
  CacheRepository,
3261
+ CircuitBreakerStore,
1348
3262
  FileStore,
1349
3263
  LockTimeoutError,
3264
+ MarkovPredictor,
1350
3265
  MemoryCacheProvider,
1351
3266
  MemoryStore,
1352
3267
  NullStore,
1353
3268
  OrbitCache,
1354
3269
  OrbitStasis,
3270
+ PredictiveStore,
1355
3271
  RateLimiter,
1356
3272
  RedisStore,
3273
+ TieredStore,
1357
3274
  isExpired,
1358
3275
  isTaggableStore,
1359
3276
  normalizeCacheKey,