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