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