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