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