@gravito/stasis 3.1.1 → 3.2.1

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