@gravito/stasis 1.0.0-beta.5 → 1.0.0
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/index.cjs +316 -10061
- package/dist/index.d.cts +450 -0
- package/dist/index.d.ts +450 -0
- package/dist/index.js +1321 -0
- package/package.json +14 -13
- package/dist/index.cjs.map +0 -74
- package/dist/index.mjs +0 -11077
- package/dist/index.mjs.map +0 -73
package/dist/index.js
ADDED
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
// src/store.ts
|
|
2
|
+
function isTaggableStore(store) {
|
|
3
|
+
return typeof store.flushTags === "function" && typeof store.tagKey === "function" && typeof store.tagIndexAdd === "function" && typeof store.tagIndexRemove === "function";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/types.ts
|
|
7
|
+
function normalizeCacheKey(key) {
|
|
8
|
+
if (!key) {
|
|
9
|
+
throw new Error("Cache key cannot be empty.");
|
|
10
|
+
}
|
|
11
|
+
return key;
|
|
12
|
+
}
|
|
13
|
+
function ttlToExpiresAt(ttl, now = Date.now()) {
|
|
14
|
+
if (ttl === void 0) {
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
if (ttl === null) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (ttl instanceof Date) {
|
|
21
|
+
return ttl.getTime();
|
|
22
|
+
}
|
|
23
|
+
if (typeof ttl === "number") {
|
|
24
|
+
if (ttl <= 0) {
|
|
25
|
+
return now;
|
|
26
|
+
}
|
|
27
|
+
return now + ttl * 1e3;
|
|
28
|
+
}
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
function isExpired(expiresAt, now = Date.now()) {
|
|
32
|
+
if (expiresAt === null) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (expiresAt === void 0) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return now > expiresAt;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/CacheRepository.ts
|
|
42
|
+
var CacheRepository = class _CacheRepository {
|
|
43
|
+
constructor(store, options = {}) {
|
|
44
|
+
this.store = store;
|
|
45
|
+
this.options = options;
|
|
46
|
+
}
|
|
47
|
+
emit(event, payload = {}) {
|
|
48
|
+
const mode = this.options.eventsMode ?? "async";
|
|
49
|
+
if (mode === "off") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const fn = this.options.events?.[event];
|
|
53
|
+
if (!fn) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const invoke = () => {
|
|
57
|
+
if (event === "flush") {
|
|
58
|
+
return fn();
|
|
59
|
+
}
|
|
60
|
+
const key = payload.key ?? "";
|
|
61
|
+
return fn(key);
|
|
62
|
+
};
|
|
63
|
+
const reportError = (error) => {
|
|
64
|
+
try {
|
|
65
|
+
this.options.onEventError?.(error, event, payload);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
if (mode === "sync") {
|
|
70
|
+
try {
|
|
71
|
+
return Promise.resolve(invoke()).catch((error) => {
|
|
72
|
+
reportError(error);
|
|
73
|
+
if (this.options.throwOnEventError) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
reportError(error);
|
|
79
|
+
if (this.options.throwOnEventError) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
queueMicrotask(() => {
|
|
86
|
+
try {
|
|
87
|
+
const result = invoke();
|
|
88
|
+
if (result && typeof result.catch === "function") {
|
|
89
|
+
void result.catch(reportError);
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
reportError(error);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
key(key) {
|
|
97
|
+
const normalized = normalizeCacheKey(key);
|
|
98
|
+
const prefix = this.options.prefix ?? "";
|
|
99
|
+
return prefix ? `${prefix}${normalized}` : normalized;
|
|
100
|
+
}
|
|
101
|
+
flexibleFreshUntilKey(fullKey) {
|
|
102
|
+
return `__gravito:flexible:freshUntil:${fullKey}`;
|
|
103
|
+
}
|
|
104
|
+
async putMetaKey(metaKey, value, ttl) {
|
|
105
|
+
await this.store.put(metaKey, value, ttl);
|
|
106
|
+
}
|
|
107
|
+
async forgetMetaKey(metaKey) {
|
|
108
|
+
await this.store.forget(metaKey);
|
|
109
|
+
}
|
|
110
|
+
async get(key, defaultValue) {
|
|
111
|
+
const fullKey = this.key(key);
|
|
112
|
+
const value = await this.store.get(fullKey);
|
|
113
|
+
if (value !== null) {
|
|
114
|
+
const e2 = this.emit("hit", { key: fullKey });
|
|
115
|
+
if (e2) {
|
|
116
|
+
await e2;
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
const e = this.emit("miss", { key: fullKey });
|
|
121
|
+
if (e) {
|
|
122
|
+
await e;
|
|
123
|
+
}
|
|
124
|
+
if (defaultValue === void 0) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (typeof defaultValue === "function") {
|
|
128
|
+
return defaultValue();
|
|
129
|
+
}
|
|
130
|
+
return defaultValue;
|
|
131
|
+
}
|
|
132
|
+
async has(key) {
|
|
133
|
+
return await this.get(key) !== null;
|
|
134
|
+
}
|
|
135
|
+
async missing(key) {
|
|
136
|
+
return !await this.has(key);
|
|
137
|
+
}
|
|
138
|
+
async put(key, value, ttl) {
|
|
139
|
+
const fullKey = this.key(key);
|
|
140
|
+
await this.store.put(fullKey, value, ttl);
|
|
141
|
+
const e = this.emit("write", { key: fullKey });
|
|
142
|
+
if (e) {
|
|
143
|
+
await e;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async set(key, value, ttl) {
|
|
147
|
+
const resolved = ttl ?? this.options.defaultTtl;
|
|
148
|
+
await this.put(key, value, resolved);
|
|
149
|
+
}
|
|
150
|
+
async add(key, value, ttl) {
|
|
151
|
+
const fullKey = this.key(key);
|
|
152
|
+
const resolved = ttl ?? this.options.defaultTtl;
|
|
153
|
+
const ok = await this.store.add(fullKey, value, resolved);
|
|
154
|
+
if (ok) {
|
|
155
|
+
const e = this.emit("write", { key: fullKey });
|
|
156
|
+
if (e) {
|
|
157
|
+
await e;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return ok;
|
|
161
|
+
}
|
|
162
|
+
async forever(key, value) {
|
|
163
|
+
await this.put(key, value, null);
|
|
164
|
+
}
|
|
165
|
+
async remember(key, ttl, callback) {
|
|
166
|
+
const existing = await this.get(key);
|
|
167
|
+
if (existing !== null) {
|
|
168
|
+
return existing;
|
|
169
|
+
}
|
|
170
|
+
const value = await callback();
|
|
171
|
+
await this.put(key, value, ttl);
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
async rememberForever(key, callback) {
|
|
175
|
+
return this.remember(key, null, callback);
|
|
176
|
+
}
|
|
177
|
+
async many(keys) {
|
|
178
|
+
const out = {};
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
out[String(key)] = await this.get(key);
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
async putMany(values, ttl) {
|
|
185
|
+
await Promise.all(Object.entries(values).map(([k, v]) => this.put(k, v, ttl)));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Laravel-like flexible cache (stale-while-revalidate).
|
|
189
|
+
*
|
|
190
|
+
* - `ttlSeconds`: how long the value is considered fresh
|
|
191
|
+
* - `staleSeconds`: how long the stale value may be served while a refresh happens
|
|
192
|
+
*/
|
|
193
|
+
async flexible(key, ttlSeconds, staleSeconds, callback) {
|
|
194
|
+
const fullKey = this.key(key);
|
|
195
|
+
const metaKey = this.flexibleFreshUntilKey(fullKey);
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
const ttlMillis = Math.max(0, ttlSeconds) * 1e3;
|
|
198
|
+
const staleMillis = Math.max(0, staleSeconds) * 1e3;
|
|
199
|
+
const [freshUntil, cachedValue] = await Promise.all([
|
|
200
|
+
this.store.get(metaKey),
|
|
201
|
+
this.store.get(fullKey)
|
|
202
|
+
]);
|
|
203
|
+
if (freshUntil !== null && cachedValue !== null) {
|
|
204
|
+
if (now <= freshUntil) {
|
|
205
|
+
const e2 = this.emit("hit", { key: fullKey });
|
|
206
|
+
if (e2) {
|
|
207
|
+
await e2;
|
|
208
|
+
}
|
|
209
|
+
return cachedValue;
|
|
210
|
+
}
|
|
211
|
+
if (now <= freshUntil + staleMillis) {
|
|
212
|
+
const e2 = this.emit("hit", { key: fullKey });
|
|
213
|
+
if (e2) {
|
|
214
|
+
await e2;
|
|
215
|
+
}
|
|
216
|
+
void this.refreshFlexible(fullKey, metaKey, ttlSeconds, staleSeconds, callback);
|
|
217
|
+
return cachedValue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const e = this.emit("miss", { key: fullKey });
|
|
221
|
+
if (e) {
|
|
222
|
+
await e;
|
|
223
|
+
}
|
|
224
|
+
const value = await callback();
|
|
225
|
+
const totalTtl = ttlSeconds + staleSeconds;
|
|
226
|
+
await this.store.put(fullKey, value, totalTtl);
|
|
227
|
+
await this.putMetaKey(metaKey, now + ttlMillis, totalTtl);
|
|
228
|
+
{
|
|
229
|
+
const e2 = this.emit("write", { key: fullKey });
|
|
230
|
+
if (e2) {
|
|
231
|
+
await e2;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
async refreshFlexible(fullKey, metaKey, ttlSeconds, staleSeconds, callback) {
|
|
237
|
+
if (!this.store.lock) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const lock = this.store.lock(`flexible:${metaKey}`, Math.max(1, ttlSeconds));
|
|
241
|
+
if (!lock || !await lock.acquire()) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const value = await callback();
|
|
246
|
+
const totalTtl = ttlSeconds + staleSeconds;
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
await this.store.put(fullKey, value, totalTtl);
|
|
249
|
+
await this.putMetaKey(metaKey, now + Math.max(0, ttlSeconds) * 1e3, totalTtl);
|
|
250
|
+
const e = this.emit("write", { key: fullKey });
|
|
251
|
+
if (e) {
|
|
252
|
+
await e;
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
await lock.release();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async pull(key, defaultValue) {
|
|
259
|
+
const value = await this.get(key, defaultValue);
|
|
260
|
+
await this.forget(key);
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
async forget(key) {
|
|
264
|
+
const fullKey = this.key(key);
|
|
265
|
+
const metaKey = this.flexibleFreshUntilKey(fullKey);
|
|
266
|
+
const ok = await this.store.forget(fullKey);
|
|
267
|
+
await this.forgetMetaKey(metaKey);
|
|
268
|
+
if (ok) {
|
|
269
|
+
const e = this.emit("forget", { key: fullKey });
|
|
270
|
+
if (e) {
|
|
271
|
+
await e;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return ok;
|
|
275
|
+
}
|
|
276
|
+
async delete(key) {
|
|
277
|
+
return this.forget(key);
|
|
278
|
+
}
|
|
279
|
+
async flush() {
|
|
280
|
+
await this.store.flush();
|
|
281
|
+
const e = this.emit("flush");
|
|
282
|
+
if (e) {
|
|
283
|
+
await e;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async clear() {
|
|
287
|
+
return this.flush();
|
|
288
|
+
}
|
|
289
|
+
increment(key, value) {
|
|
290
|
+
return this.store.increment(this.key(key), value);
|
|
291
|
+
}
|
|
292
|
+
decrement(key, value) {
|
|
293
|
+
return this.store.decrement(this.key(key), value);
|
|
294
|
+
}
|
|
295
|
+
lock(name, seconds) {
|
|
296
|
+
return this.store.lock ? this.store.lock(this.key(name), seconds) : void 0;
|
|
297
|
+
}
|
|
298
|
+
tags(tags) {
|
|
299
|
+
if (!isTaggableStore(this.store)) {
|
|
300
|
+
throw new Error("This cache store does not support tags.");
|
|
301
|
+
}
|
|
302
|
+
return new _CacheRepository(new TaggedStore(this.store, tags), this.options);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get the underlying store
|
|
306
|
+
*/
|
|
307
|
+
getStore() {
|
|
308
|
+
return this.store;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var TaggedStore = class {
|
|
312
|
+
constructor(store, tags) {
|
|
313
|
+
this.store = store;
|
|
314
|
+
this.tags = tags;
|
|
315
|
+
}
|
|
316
|
+
tagged(key) {
|
|
317
|
+
return this.store.tagKey(normalizeCacheKey(key), this.tags);
|
|
318
|
+
}
|
|
319
|
+
async get(key) {
|
|
320
|
+
return this.store.get(this.tagged(key));
|
|
321
|
+
}
|
|
322
|
+
async put(key, value, ttl) {
|
|
323
|
+
const taggedKey = this.tagged(key);
|
|
324
|
+
await this.store.put(taggedKey, value, ttl);
|
|
325
|
+
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
326
|
+
}
|
|
327
|
+
async add(key, value, ttl) {
|
|
328
|
+
const taggedKey = this.tagged(key);
|
|
329
|
+
const ok = await this.store.add(taggedKey, value, ttl);
|
|
330
|
+
if (ok) {
|
|
331
|
+
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
332
|
+
}
|
|
333
|
+
return ok;
|
|
334
|
+
}
|
|
335
|
+
async forget(key) {
|
|
336
|
+
return this.store.forget(this.tagged(key));
|
|
337
|
+
}
|
|
338
|
+
async flush() {
|
|
339
|
+
return this.store.flushTags(this.tags);
|
|
340
|
+
}
|
|
341
|
+
async increment(key, value) {
|
|
342
|
+
const taggedKey = this.tagged(key);
|
|
343
|
+
const next = await this.store.increment(taggedKey, value);
|
|
344
|
+
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
345
|
+
return next;
|
|
346
|
+
}
|
|
347
|
+
async decrement(key, value) {
|
|
348
|
+
const taggedKey = this.tagged(key);
|
|
349
|
+
const next = await this.store.decrement(taggedKey, value);
|
|
350
|
+
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
351
|
+
return next;
|
|
352
|
+
}
|
|
353
|
+
lock(name, seconds) {
|
|
354
|
+
return this.store.lock ? this.store.lock(this.tagged(name), seconds) : void 0;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// src/RateLimiter.ts
|
|
359
|
+
var RateLimiter = class {
|
|
360
|
+
constructor(store) {
|
|
361
|
+
this.store = store;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Attempt to acquire a lock
|
|
365
|
+
* @param key - The unique key (e.g., "ip:127.0.0.1")
|
|
366
|
+
* @param maxAttempts - Maximum number of attempts allowed
|
|
367
|
+
* @param decaySeconds - Time in seconds until the limit resets
|
|
368
|
+
*/
|
|
369
|
+
async attempt(key, maxAttempts, decaySeconds) {
|
|
370
|
+
const current = await this.store.get(key);
|
|
371
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
372
|
+
if (current === null) {
|
|
373
|
+
await this.store.put(key, 1, decaySeconds);
|
|
374
|
+
return {
|
|
375
|
+
allowed: true,
|
|
376
|
+
remaining: maxAttempts - 1,
|
|
377
|
+
reset: now + decaySeconds
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (current >= maxAttempts) {
|
|
381
|
+
return {
|
|
382
|
+
allowed: false,
|
|
383
|
+
remaining: 0,
|
|
384
|
+
reset: now + decaySeconds
|
|
385
|
+
// Approximation if we can't read TTL
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const next = await this.store.increment(key);
|
|
389
|
+
return {
|
|
390
|
+
allowed: true,
|
|
391
|
+
remaining: maxAttempts - next,
|
|
392
|
+
reset: now + decaySeconds
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Clear the limiter for a key
|
|
397
|
+
*/
|
|
398
|
+
async clear(key) {
|
|
399
|
+
await this.store.forget(key);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// src/CacheManager.ts
|
|
404
|
+
var CacheManager = class {
|
|
405
|
+
constructor(storeFactory, config = {}, events, eventOptions) {
|
|
406
|
+
this.storeFactory = storeFactory;
|
|
407
|
+
this.config = config;
|
|
408
|
+
this.events = events;
|
|
409
|
+
this.eventOptions = eventOptions;
|
|
410
|
+
}
|
|
411
|
+
stores = /* @__PURE__ */ new Map();
|
|
412
|
+
/**
|
|
413
|
+
* Get a rate limiter instance for a store
|
|
414
|
+
* @param name - Store name (optional, defaults to default store)
|
|
415
|
+
*/
|
|
416
|
+
limiter(name) {
|
|
417
|
+
return new RateLimiter(this.store(name).getStore());
|
|
418
|
+
}
|
|
419
|
+
store(name) {
|
|
420
|
+
const storeName = name ?? this.config.default ?? "memory";
|
|
421
|
+
const existing = this.stores.get(storeName);
|
|
422
|
+
if (existing) {
|
|
423
|
+
return existing;
|
|
424
|
+
}
|
|
425
|
+
const repo = new CacheRepository(this.storeFactory(storeName), {
|
|
426
|
+
prefix: this.config.prefix,
|
|
427
|
+
defaultTtl: this.config.defaultTtl,
|
|
428
|
+
events: this.events,
|
|
429
|
+
eventsMode: this.eventOptions?.mode,
|
|
430
|
+
throwOnEventError: this.eventOptions?.throwOnError,
|
|
431
|
+
onEventError: this.eventOptions?.onError
|
|
432
|
+
});
|
|
433
|
+
this.stores.set(storeName, repo);
|
|
434
|
+
return repo;
|
|
435
|
+
}
|
|
436
|
+
// Laravel-like proxy methods (default store)
|
|
437
|
+
/**
|
|
438
|
+
* Retrieve an item from the cache.
|
|
439
|
+
*
|
|
440
|
+
* @param key - The unique cache key.
|
|
441
|
+
* @param defaultValue - The default value if the key is missing (can be a value or a closure).
|
|
442
|
+
* @returns The cached value or the default.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```typescript
|
|
446
|
+
* const value = await cache.get('user:1', { name: 'Guest' });
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
get(key, defaultValue) {
|
|
450
|
+
return this.store().get(key, defaultValue);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Check if an item exists in the cache.
|
|
454
|
+
*/
|
|
455
|
+
has(key) {
|
|
456
|
+
return this.store().has(key);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Check if an item is missing from the cache.
|
|
460
|
+
*/
|
|
461
|
+
missing(key) {
|
|
462
|
+
return this.store().missing(key);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Store an item in the cache.
|
|
466
|
+
*
|
|
467
|
+
* @param key - The unique cache key.
|
|
468
|
+
* @param value - The value to store.
|
|
469
|
+
* @param ttl - Time to live in seconds (or Date).
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```typescript
|
|
473
|
+
* await cache.put('key', 'value', 60); // 60 seconds
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
476
|
+
put(key, value, ttl) {
|
|
477
|
+
return this.store().put(key, value, ttl);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Store an item in the cache (alias for put with optional TTL).
|
|
481
|
+
*/
|
|
482
|
+
set(key, value, ttl) {
|
|
483
|
+
return this.store().set(key, value, ttl);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Store an item in the cache if it doesn't already exist.
|
|
487
|
+
*
|
|
488
|
+
* @returns True if added, false if it already existed.
|
|
489
|
+
*/
|
|
490
|
+
add(key, value, ttl) {
|
|
491
|
+
return this.store().add(key, value, ttl);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Store an item in the cache indefinitely.
|
|
495
|
+
*/
|
|
496
|
+
forever(key, value) {
|
|
497
|
+
return this.store().forever(key, value);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Get an item from the cache, or execute the callback and store the result.
|
|
501
|
+
*
|
|
502
|
+
* @param key - The cache key.
|
|
503
|
+
* @param ttl - Time to live if the item is missing.
|
|
504
|
+
* @param callback - Closure to execute on miss.
|
|
505
|
+
* @returns The cached or fetched value.
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```typescript
|
|
509
|
+
* const user = await cache.remember('user:1', 60, async () => {
|
|
510
|
+
* return await db.findUser(1);
|
|
511
|
+
* });
|
|
512
|
+
* ```
|
|
513
|
+
*/
|
|
514
|
+
remember(key, ttl, callback) {
|
|
515
|
+
return this.store().remember(key, ttl, callback);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Get an item from the cache, or execute the callback and store the result forever.
|
|
519
|
+
*/
|
|
520
|
+
rememberForever(key, callback) {
|
|
521
|
+
return this.store().rememberForever(key, callback);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Retrieve multiple items from the cache.
|
|
525
|
+
*/
|
|
526
|
+
many(keys) {
|
|
527
|
+
return this.store().many(keys);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Store multiple items in the cache.
|
|
531
|
+
*/
|
|
532
|
+
putMany(values, ttl) {
|
|
533
|
+
return this.store().putMany(values, ttl);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Get an item from the cache, allowing stale data while refreshing in background.
|
|
537
|
+
*
|
|
538
|
+
* @param key - Cache key.
|
|
539
|
+
* @param ttlSeconds - How long the value is considered fresh.
|
|
540
|
+
* @param staleSeconds - How long to serve stale data while refreshing.
|
|
541
|
+
* @param callback - Closure to refresh the data.
|
|
542
|
+
*/
|
|
543
|
+
flexible(key, ttlSeconds, staleSeconds, callback) {
|
|
544
|
+
return this.store().flexible(key, ttlSeconds, staleSeconds, callback);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Retrieve an item from the cache and delete it.
|
|
548
|
+
*/
|
|
549
|
+
pull(key, defaultValue) {
|
|
550
|
+
return this.store().pull(key, defaultValue);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Remove an item from the cache.
|
|
554
|
+
*/
|
|
555
|
+
forget(key) {
|
|
556
|
+
return this.store().forget(key);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Remove an item from the cache (alias for forget).
|
|
560
|
+
*/
|
|
561
|
+
delete(key) {
|
|
562
|
+
return this.store().delete(key);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Remove all items from the cache.
|
|
566
|
+
*/
|
|
567
|
+
flush() {
|
|
568
|
+
return this.store().flush();
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Clear the entire cache (alias for flush).
|
|
572
|
+
*/
|
|
573
|
+
clear() {
|
|
574
|
+
return this.store().clear();
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Increment an integer item in the cache.
|
|
578
|
+
*/
|
|
579
|
+
increment(key, value) {
|
|
580
|
+
return this.store().increment(key, value);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Decrement an integer item in the cache.
|
|
584
|
+
*/
|
|
585
|
+
decrement(key, value) {
|
|
586
|
+
return this.store().decrement(key, value);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Get a lock instance.
|
|
590
|
+
*
|
|
591
|
+
* @param name - Lock name.
|
|
592
|
+
* @param seconds - Lock duration.
|
|
593
|
+
* @returns CacheLock instance.
|
|
594
|
+
*/
|
|
595
|
+
lock(name, seconds) {
|
|
596
|
+
return this.store().lock(name, seconds);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Access a tagged cache section (only supported by some stores).
|
|
600
|
+
*/
|
|
601
|
+
tags(tags) {
|
|
602
|
+
return this.store().tags(tags);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// src/stores/FileStore.ts
|
|
607
|
+
import { createHash, randomUUID } from "crypto";
|
|
608
|
+
import { mkdir, open, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
609
|
+
import { join } from "path";
|
|
610
|
+
|
|
611
|
+
// src/locks.ts
|
|
612
|
+
var LockTimeoutError = class extends Error {
|
|
613
|
+
name = "LockTimeoutError";
|
|
614
|
+
};
|
|
615
|
+
function sleep(ms) {
|
|
616
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/stores/FileStore.ts
|
|
620
|
+
function hashKey(key) {
|
|
621
|
+
return createHash("sha1").update(key).digest("hex");
|
|
622
|
+
}
|
|
623
|
+
var FileStore = class {
|
|
624
|
+
constructor(options) {
|
|
625
|
+
this.options = options;
|
|
626
|
+
}
|
|
627
|
+
async ensureDir() {
|
|
628
|
+
await mkdir(this.options.directory, { recursive: true });
|
|
629
|
+
}
|
|
630
|
+
filePathForKey(key) {
|
|
631
|
+
const hashed = hashKey(key);
|
|
632
|
+
return join(this.options.directory, `${hashed}.json`);
|
|
633
|
+
}
|
|
634
|
+
async get(key) {
|
|
635
|
+
const normalized = normalizeCacheKey(key);
|
|
636
|
+
await this.ensureDir();
|
|
637
|
+
const file = this.filePathForKey(normalized);
|
|
638
|
+
try {
|
|
639
|
+
const raw = await readFile(file, "utf8");
|
|
640
|
+
const data = JSON.parse(raw);
|
|
641
|
+
if (isExpired(data.expiresAt)) {
|
|
642
|
+
await this.forget(normalized);
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
return data.value;
|
|
646
|
+
} catch {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async put(key, value, ttl) {
|
|
651
|
+
const normalized = normalizeCacheKey(key);
|
|
652
|
+
await this.ensureDir();
|
|
653
|
+
const expiresAt = ttlToExpiresAt(ttl);
|
|
654
|
+
if (expiresAt !== null && expiresAt !== void 0 && expiresAt <= Date.now()) {
|
|
655
|
+
await this.forget(normalized);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const file = this.filePathForKey(normalized);
|
|
659
|
+
const payload = { expiresAt: expiresAt ?? null, value };
|
|
660
|
+
await writeFile(file, JSON.stringify(payload), "utf8");
|
|
661
|
+
}
|
|
662
|
+
async add(key, value, ttl) {
|
|
663
|
+
const normalized = normalizeCacheKey(key);
|
|
664
|
+
const existing = await this.get(normalized);
|
|
665
|
+
if (existing !== null) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
await this.put(normalized, value, ttl);
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
async forget(key) {
|
|
672
|
+
const normalized = normalizeCacheKey(key);
|
|
673
|
+
await this.ensureDir();
|
|
674
|
+
const file = this.filePathForKey(normalized);
|
|
675
|
+
try {
|
|
676
|
+
await rm(file, { force: true });
|
|
677
|
+
return true;
|
|
678
|
+
} catch {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async flush() {
|
|
683
|
+
await this.ensureDir();
|
|
684
|
+
const files = await readdir(this.options.directory);
|
|
685
|
+
await Promise.all(
|
|
686
|
+
files.filter((f) => f.endsWith(".json")).map((f) => rm(join(this.options.directory, f), { force: true }))
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
async increment(key, value = 1) {
|
|
690
|
+
const normalized = normalizeCacheKey(key);
|
|
691
|
+
const current = await this.get(normalized);
|
|
692
|
+
const next = (current ?? 0) + value;
|
|
693
|
+
await this.put(normalized, next, null);
|
|
694
|
+
return next;
|
|
695
|
+
}
|
|
696
|
+
async decrement(key, value = 1) {
|
|
697
|
+
return this.increment(key, -value);
|
|
698
|
+
}
|
|
699
|
+
lock(name, seconds = 10) {
|
|
700
|
+
const normalizedName = normalizeCacheKey(name);
|
|
701
|
+
const lockFile = join(this.options.directory, `.lock-${hashKey(normalizedName)}`);
|
|
702
|
+
const ttlMillis = Math.max(1, seconds) * 1e3;
|
|
703
|
+
const owner = randomUUID();
|
|
704
|
+
const tryAcquire = async () => {
|
|
705
|
+
await this.ensureDir();
|
|
706
|
+
try {
|
|
707
|
+
const handle = await open(lockFile, "wx");
|
|
708
|
+
await handle.writeFile(JSON.stringify({ owner, expiresAt: Date.now() + ttlMillis }), "utf8");
|
|
709
|
+
await handle.close();
|
|
710
|
+
return true;
|
|
711
|
+
} catch {
|
|
712
|
+
try {
|
|
713
|
+
const raw = await readFile(lockFile, "utf8");
|
|
714
|
+
const data = JSON.parse(raw);
|
|
715
|
+
if (!data.expiresAt || Date.now() > data.expiresAt) {
|
|
716
|
+
await rm(lockFile, { force: true });
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
return {
|
|
725
|
+
async acquire() {
|
|
726
|
+
return tryAcquire();
|
|
727
|
+
},
|
|
728
|
+
async release() {
|
|
729
|
+
try {
|
|
730
|
+
const raw = await readFile(lockFile, "utf8");
|
|
731
|
+
const data = JSON.parse(raw);
|
|
732
|
+
if (data.owner === owner) {
|
|
733
|
+
await rm(lockFile, { force: true });
|
|
734
|
+
}
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
async block(secondsToWait, callback, options) {
|
|
739
|
+
const deadline = Date.now() + Math.max(0, secondsToWait) * 1e3;
|
|
740
|
+
const sleepMillis = options?.sleepMillis ?? 150;
|
|
741
|
+
while (Date.now() <= deadline) {
|
|
742
|
+
if (await this.acquire()) {
|
|
743
|
+
try {
|
|
744
|
+
return await callback();
|
|
745
|
+
} finally {
|
|
746
|
+
await this.release();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
await sleep(sleepMillis);
|
|
750
|
+
}
|
|
751
|
+
throw new LockTimeoutError(
|
|
752
|
+
`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// src/stores/MemoryStore.ts
|
|
760
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
761
|
+
var MemoryStore = class {
|
|
762
|
+
constructor(options = {}) {
|
|
763
|
+
this.options = options;
|
|
764
|
+
}
|
|
765
|
+
entries = /* @__PURE__ */ new Map();
|
|
766
|
+
locks = /* @__PURE__ */ new Map();
|
|
767
|
+
tagToKeys = /* @__PURE__ */ new Map();
|
|
768
|
+
keyToTags = /* @__PURE__ */ new Map();
|
|
769
|
+
touchLRU(key) {
|
|
770
|
+
const entry = this.entries.get(key);
|
|
771
|
+
if (!entry) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
this.entries.delete(key);
|
|
775
|
+
this.entries.set(key, entry);
|
|
776
|
+
}
|
|
777
|
+
pruneIfNeeded() {
|
|
778
|
+
const maxItems = this.options.maxItems;
|
|
779
|
+
if (!maxItems || maxItems <= 0) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
while (this.entries.size > maxItems) {
|
|
783
|
+
const oldest = this.entries.keys().next().value;
|
|
784
|
+
if (!oldest) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
void this.forget(oldest);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
cleanupExpired(key, now = Date.now()) {
|
|
791
|
+
const entry = this.entries.get(key);
|
|
792
|
+
if (!entry) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (isExpired(entry.expiresAt, now)) {
|
|
796
|
+
void this.forget(key);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async get(key) {
|
|
800
|
+
const normalized = normalizeCacheKey(key);
|
|
801
|
+
const entry = this.entries.get(normalized);
|
|
802
|
+
if (!entry) {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
if (isExpired(entry.expiresAt)) {
|
|
806
|
+
await this.forget(normalized);
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
this.touchLRU(normalized);
|
|
810
|
+
return entry.value;
|
|
811
|
+
}
|
|
812
|
+
async put(key, value, ttl) {
|
|
813
|
+
const normalized = normalizeCacheKey(key);
|
|
814
|
+
const expiresAt = ttlToExpiresAt(ttl);
|
|
815
|
+
if (expiresAt !== null && expiresAt !== void 0 && expiresAt <= Date.now()) {
|
|
816
|
+
await this.forget(normalized);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
this.entries.set(normalized, { value, expiresAt: expiresAt ?? null });
|
|
820
|
+
this.pruneIfNeeded();
|
|
821
|
+
}
|
|
822
|
+
async add(key, value, ttl) {
|
|
823
|
+
const normalized = normalizeCacheKey(key);
|
|
824
|
+
this.cleanupExpired(normalized);
|
|
825
|
+
if (this.entries.has(normalized)) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
await this.put(normalized, value, ttl);
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
async forget(key) {
|
|
832
|
+
const normalized = normalizeCacheKey(key);
|
|
833
|
+
const existed = this.entries.delete(normalized);
|
|
834
|
+
this.tagIndexRemove(normalized);
|
|
835
|
+
return existed;
|
|
836
|
+
}
|
|
837
|
+
async flush() {
|
|
838
|
+
this.entries.clear();
|
|
839
|
+
this.tagToKeys.clear();
|
|
840
|
+
this.keyToTags.clear();
|
|
841
|
+
}
|
|
842
|
+
async increment(key, value = 1) {
|
|
843
|
+
const normalized = normalizeCacheKey(key);
|
|
844
|
+
const current = await this.get(normalized);
|
|
845
|
+
const next = (current ?? 0) + value;
|
|
846
|
+
await this.put(normalized, next, null);
|
|
847
|
+
return next;
|
|
848
|
+
}
|
|
849
|
+
async decrement(key, value = 1) {
|
|
850
|
+
return this.increment(key, -value);
|
|
851
|
+
}
|
|
852
|
+
lock(name, seconds = 10) {
|
|
853
|
+
const lockKey = `lock:${normalizeCacheKey(name)}`;
|
|
854
|
+
const ttlMillis = Math.max(1, seconds) * 1e3;
|
|
855
|
+
const locks = this.locks;
|
|
856
|
+
const acquire = async () => {
|
|
857
|
+
const now = Date.now();
|
|
858
|
+
const existing = locks.get(lockKey);
|
|
859
|
+
if (existing && existing.expiresAt > now) {
|
|
860
|
+
return { ok: false };
|
|
861
|
+
}
|
|
862
|
+
const owner2 = randomUUID2();
|
|
863
|
+
locks.set(lockKey, { owner: owner2, expiresAt: now + ttlMillis });
|
|
864
|
+
return { ok: true, owner: owner2 };
|
|
865
|
+
};
|
|
866
|
+
let owner;
|
|
867
|
+
return {
|
|
868
|
+
async acquire() {
|
|
869
|
+
const result = await acquire();
|
|
870
|
+
if (!result.ok) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
owner = result.owner;
|
|
874
|
+
return true;
|
|
875
|
+
},
|
|
876
|
+
async release() {
|
|
877
|
+
if (!owner) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const existing = locks.get(lockKey);
|
|
881
|
+
if (existing?.owner === owner) {
|
|
882
|
+
locks.delete(lockKey);
|
|
883
|
+
}
|
|
884
|
+
owner = void 0;
|
|
885
|
+
},
|
|
886
|
+
async block(secondsToWait, callback, options) {
|
|
887
|
+
const deadline = Date.now() + Math.max(0, secondsToWait) * 1e3;
|
|
888
|
+
const sleepMillis = options?.sleepMillis ?? 150;
|
|
889
|
+
while (Date.now() <= deadline) {
|
|
890
|
+
if (await this.acquire()) {
|
|
891
|
+
try {
|
|
892
|
+
return await callback();
|
|
893
|
+
} finally {
|
|
894
|
+
await this.release();
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
await sleep(sleepMillis);
|
|
898
|
+
}
|
|
899
|
+
throw new LockTimeoutError(
|
|
900
|
+
`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
tagKey(key, tags) {
|
|
906
|
+
const normalizedKey = normalizeCacheKey(key);
|
|
907
|
+
const normalizedTags = [...tags].map(String).filter(Boolean).sort();
|
|
908
|
+
if (normalizedTags.length === 0) {
|
|
909
|
+
return normalizedKey;
|
|
910
|
+
}
|
|
911
|
+
return `tags:${normalizedTags.join("|")}:${normalizedKey}`;
|
|
912
|
+
}
|
|
913
|
+
tagIndexAdd(tags, taggedKey) {
|
|
914
|
+
const normalizedTags = [...tags].map(String).filter(Boolean);
|
|
915
|
+
if (normalizedTags.length === 0) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
for (const tag of normalizedTags) {
|
|
919
|
+
let keys = this.tagToKeys.get(tag);
|
|
920
|
+
if (!keys) {
|
|
921
|
+
keys = /* @__PURE__ */ new Set();
|
|
922
|
+
this.tagToKeys.set(tag, keys);
|
|
923
|
+
}
|
|
924
|
+
keys.add(taggedKey);
|
|
925
|
+
}
|
|
926
|
+
let tagSet = this.keyToTags.get(taggedKey);
|
|
927
|
+
if (!tagSet) {
|
|
928
|
+
tagSet = /* @__PURE__ */ new Set();
|
|
929
|
+
this.keyToTags.set(taggedKey, tagSet);
|
|
930
|
+
}
|
|
931
|
+
for (const tag of normalizedTags) {
|
|
932
|
+
tagSet.add(tag);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
tagIndexRemove(taggedKey) {
|
|
936
|
+
const tags = this.keyToTags.get(taggedKey);
|
|
937
|
+
if (!tags) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
for (const tag of tags) {
|
|
941
|
+
const keys = this.tagToKeys.get(tag);
|
|
942
|
+
if (!keys) {
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
keys.delete(taggedKey);
|
|
946
|
+
if (keys.size === 0) {
|
|
947
|
+
this.tagToKeys.delete(tag);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
this.keyToTags.delete(taggedKey);
|
|
951
|
+
}
|
|
952
|
+
async flushTags(tags) {
|
|
953
|
+
const normalizedTags = [...tags].map(String).filter(Boolean);
|
|
954
|
+
if (normalizedTags.length === 0) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
958
|
+
for (const tag of normalizedTags) {
|
|
959
|
+
const keys = this.tagToKeys.get(tag);
|
|
960
|
+
if (!keys) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
for (const k of keys) {
|
|
964
|
+
keysToDelete.add(k);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
for (const key of keysToDelete) {
|
|
968
|
+
await this.forget(key);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
// src/stores/NullStore.ts
|
|
974
|
+
var NullStore = class {
|
|
975
|
+
async get(_key) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
async put(_key, _value, _ttl) {
|
|
979
|
+
}
|
|
980
|
+
async add(_key, _value, _ttl) {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
async forget(_key) {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
async flush() {
|
|
987
|
+
}
|
|
988
|
+
async increment(_key, _value = 1) {
|
|
989
|
+
return 0;
|
|
990
|
+
}
|
|
991
|
+
async decrement(_key, _value = 1) {
|
|
992
|
+
return 0;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// src/stores/RedisStore.ts
|
|
997
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
998
|
+
import { Redis } from "@gravito/plasma";
|
|
999
|
+
var RedisStore = class {
|
|
1000
|
+
connectionName;
|
|
1001
|
+
constructor(options = {}) {
|
|
1002
|
+
this.connectionName = options.connection;
|
|
1003
|
+
}
|
|
1004
|
+
get client() {
|
|
1005
|
+
return Redis.connection(this.connectionName);
|
|
1006
|
+
}
|
|
1007
|
+
async get(key) {
|
|
1008
|
+
const normalized = normalizeCacheKey(key);
|
|
1009
|
+
const value = await this.client.get(normalized);
|
|
1010
|
+
if (value === null) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
return JSON.parse(value);
|
|
1015
|
+
} catch {
|
|
1016
|
+
return value;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async put(key, value, ttl) {
|
|
1020
|
+
const normalized = normalizeCacheKey(key);
|
|
1021
|
+
const serialized = JSON.stringify(value);
|
|
1022
|
+
const expiresAt = ttlToExpiresAt(ttl);
|
|
1023
|
+
const options = {};
|
|
1024
|
+
if (expiresAt) {
|
|
1025
|
+
const ttlMs = Math.max(1, expiresAt - Date.now());
|
|
1026
|
+
options.px = ttlMs;
|
|
1027
|
+
}
|
|
1028
|
+
await this.client.set(normalized, serialized, options);
|
|
1029
|
+
}
|
|
1030
|
+
async add(key, value, ttl) {
|
|
1031
|
+
const normalized = normalizeCacheKey(key);
|
|
1032
|
+
const serialized = JSON.stringify(value);
|
|
1033
|
+
const expiresAt = ttlToExpiresAt(ttl);
|
|
1034
|
+
const options = { nx: true };
|
|
1035
|
+
if (expiresAt) {
|
|
1036
|
+
const ttlMs = Math.max(1, expiresAt - Date.now());
|
|
1037
|
+
options.px = ttlMs;
|
|
1038
|
+
}
|
|
1039
|
+
const result = await this.client.set(normalized, serialized, options);
|
|
1040
|
+
return result === "OK";
|
|
1041
|
+
}
|
|
1042
|
+
async forget(key) {
|
|
1043
|
+
const normalized = normalizeCacheKey(key);
|
|
1044
|
+
const count = await this.client.del(normalized);
|
|
1045
|
+
return count > 0;
|
|
1046
|
+
}
|
|
1047
|
+
async flush() {
|
|
1048
|
+
await this.client.flushdb();
|
|
1049
|
+
}
|
|
1050
|
+
async increment(key, value = 1) {
|
|
1051
|
+
const normalized = normalizeCacheKey(key);
|
|
1052
|
+
if (value === 1) {
|
|
1053
|
+
return await this.client.incr(normalized);
|
|
1054
|
+
}
|
|
1055
|
+
return await this.client.incrby(normalized, value);
|
|
1056
|
+
}
|
|
1057
|
+
async decrement(key, value = 1) {
|
|
1058
|
+
const normalized = normalizeCacheKey(key);
|
|
1059
|
+
if (value === 1) {
|
|
1060
|
+
return await this.client.decr(normalized);
|
|
1061
|
+
}
|
|
1062
|
+
return await this.client.decrby(normalized, value);
|
|
1063
|
+
}
|
|
1064
|
+
// ============================================================================
|
|
1065
|
+
// Tags
|
|
1066
|
+
// ============================================================================
|
|
1067
|
+
tagKey(key, _tags) {
|
|
1068
|
+
return key;
|
|
1069
|
+
}
|
|
1070
|
+
async tagIndexAdd(tags, taggedKey) {
|
|
1071
|
+
if (tags.length === 0) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const pipeline = this.client.pipeline();
|
|
1075
|
+
for (const tag of tags) {
|
|
1076
|
+
const tagSetKey = `tag:${tag}`;
|
|
1077
|
+
pipeline.sadd(tagSetKey, taggedKey);
|
|
1078
|
+
}
|
|
1079
|
+
await pipeline.exec();
|
|
1080
|
+
}
|
|
1081
|
+
async tagIndexRemove(_taggedKey) {
|
|
1082
|
+
}
|
|
1083
|
+
async flushTags(tags) {
|
|
1084
|
+
if (tags.length === 0) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const tagKeys = tags.map((tag) => `tag:${tag}`);
|
|
1088
|
+
const pipeline = this.client.pipeline();
|
|
1089
|
+
for (const tagKey of tagKeys) {
|
|
1090
|
+
pipeline.smembers(tagKey);
|
|
1091
|
+
}
|
|
1092
|
+
const results = await pipeline.exec();
|
|
1093
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
1094
|
+
for (const [err, keys] of results) {
|
|
1095
|
+
if (!err && Array.isArray(keys)) {
|
|
1096
|
+
for (const k of keys) {
|
|
1097
|
+
keysToDelete.add(k);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (keysToDelete.size > 0) {
|
|
1102
|
+
await this.client.del(...Array.from(keysToDelete));
|
|
1103
|
+
}
|
|
1104
|
+
await this.client.del(...tagKeys);
|
|
1105
|
+
}
|
|
1106
|
+
// ============================================================================
|
|
1107
|
+
// Locks
|
|
1108
|
+
// ============================================================================
|
|
1109
|
+
lock(name, seconds = 10) {
|
|
1110
|
+
const lockKey = `lock:${normalizeCacheKey(name)}`;
|
|
1111
|
+
const owner = randomUUID3();
|
|
1112
|
+
const ttlMs = Math.max(1, seconds) * 1e3;
|
|
1113
|
+
const client = this.client;
|
|
1114
|
+
return {
|
|
1115
|
+
async acquire() {
|
|
1116
|
+
const result = await client.set(lockKey, owner, { nx: true, px: ttlMs });
|
|
1117
|
+
return result === "OK";
|
|
1118
|
+
},
|
|
1119
|
+
async release() {
|
|
1120
|
+
const current = await client.get(lockKey);
|
|
1121
|
+
if (current === owner) {
|
|
1122
|
+
await client.del(lockKey);
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
1125
|
+
async block(secondsToWait, callback, options) {
|
|
1126
|
+
const deadline = Date.now() + Math.max(0, secondsToWait) * 1e3;
|
|
1127
|
+
const sleepMillis = options?.sleepMillis ?? 150;
|
|
1128
|
+
while (Date.now() <= deadline) {
|
|
1129
|
+
if (await this.acquire()) {
|
|
1130
|
+
try {
|
|
1131
|
+
return await callback();
|
|
1132
|
+
} finally {
|
|
1133
|
+
await this.release();
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
await sleep(sleepMillis);
|
|
1137
|
+
}
|
|
1138
|
+
throw new LockTimeoutError(
|
|
1139
|
+
`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// src/index.ts
|
|
1147
|
+
var MemoryCacheProvider = class {
|
|
1148
|
+
store = new MemoryStore();
|
|
1149
|
+
async get(key) {
|
|
1150
|
+
return this.store.get(key);
|
|
1151
|
+
}
|
|
1152
|
+
async set(key, value, ttl = 60) {
|
|
1153
|
+
await this.store.put(key, value, ttl);
|
|
1154
|
+
}
|
|
1155
|
+
async delete(key) {
|
|
1156
|
+
await this.store.forget(key);
|
|
1157
|
+
}
|
|
1158
|
+
async clear() {
|
|
1159
|
+
await this.store.flush();
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
function resolveStoreConfig(core, options) {
|
|
1163
|
+
if (options) {
|
|
1164
|
+
return options;
|
|
1165
|
+
}
|
|
1166
|
+
if (core.config.has("cache")) {
|
|
1167
|
+
return core.config.get("cache");
|
|
1168
|
+
}
|
|
1169
|
+
return {};
|
|
1170
|
+
}
|
|
1171
|
+
function createStoreFactory(config) {
|
|
1172
|
+
const stores = config.stores ?? {};
|
|
1173
|
+
const defaultSeconds = typeof config.defaultTtl === "number" ? config.defaultTtl : 60;
|
|
1174
|
+
return (name) => {
|
|
1175
|
+
const storeConfig = stores[name];
|
|
1176
|
+
const hasExplicitStores = Object.keys(stores).length > 0;
|
|
1177
|
+
if (!storeConfig) {
|
|
1178
|
+
if (name === "memory") {
|
|
1179
|
+
return new MemoryStore();
|
|
1180
|
+
}
|
|
1181
|
+
if (name === "null") {
|
|
1182
|
+
return new NullStore();
|
|
1183
|
+
}
|
|
1184
|
+
if (hasExplicitStores) {
|
|
1185
|
+
throw new Error(`Cache store '${name}' is not defined.`);
|
|
1186
|
+
}
|
|
1187
|
+
return new MemoryStore();
|
|
1188
|
+
}
|
|
1189
|
+
if (storeConfig.driver === "memory") {
|
|
1190
|
+
return new MemoryStore({ maxItems: storeConfig.maxItems });
|
|
1191
|
+
}
|
|
1192
|
+
if (storeConfig.driver === "file") {
|
|
1193
|
+
return new FileStore({ directory: storeConfig.directory });
|
|
1194
|
+
}
|
|
1195
|
+
if (storeConfig.driver === "redis") {
|
|
1196
|
+
return new RedisStore({ connection: storeConfig.connection, prefix: storeConfig.prefix });
|
|
1197
|
+
}
|
|
1198
|
+
if (storeConfig.driver === "null") {
|
|
1199
|
+
return new NullStore();
|
|
1200
|
+
}
|
|
1201
|
+
if (storeConfig.driver === "custom") {
|
|
1202
|
+
return storeConfig.store;
|
|
1203
|
+
}
|
|
1204
|
+
if (storeConfig.driver === "provider") {
|
|
1205
|
+
const provider = storeConfig.provider;
|
|
1206
|
+
if (!provider) {
|
|
1207
|
+
throw new Error(`Cache store '${name}' is missing a provider.`);
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
get: (key) => provider.get(key),
|
|
1211
|
+
put: (key, value, ttl) => provider.set(key, value, typeof ttl === "number" ? ttl : defaultSeconds),
|
|
1212
|
+
add: async (key, value, ttl) => {
|
|
1213
|
+
const existing = await provider.get(key);
|
|
1214
|
+
if (existing !== null) {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
await provider.set(key, value, typeof ttl === "number" ? ttl : defaultSeconds);
|
|
1218
|
+
return true;
|
|
1219
|
+
},
|
|
1220
|
+
forget: async (key) => {
|
|
1221
|
+
await provider.delete(key);
|
|
1222
|
+
return true;
|
|
1223
|
+
},
|
|
1224
|
+
flush: () => provider.clear(),
|
|
1225
|
+
increment: async (key, value = 1) => {
|
|
1226
|
+
const current = await provider.get(key);
|
|
1227
|
+
const next = (current ?? 0) + value;
|
|
1228
|
+
await provider.set(key, next, defaultSeconds);
|
|
1229
|
+
return next;
|
|
1230
|
+
},
|
|
1231
|
+
decrement: async (key, value = 1) => {
|
|
1232
|
+
const current = await provider.get(key);
|
|
1233
|
+
const next = (current ?? 0) - value;
|
|
1234
|
+
await provider.set(key, next, defaultSeconds);
|
|
1235
|
+
return next;
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
throw new Error(`Unsupported cache driver '${storeConfig.driver}'.`);
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
var OrbitStasis = class {
|
|
1243
|
+
constructor(options) {
|
|
1244
|
+
this.options = options;
|
|
1245
|
+
}
|
|
1246
|
+
manager;
|
|
1247
|
+
install(core) {
|
|
1248
|
+
const resolvedConfig = resolveStoreConfig(core, this.options);
|
|
1249
|
+
const exposeAs = resolvedConfig.exposeAs ?? "cache";
|
|
1250
|
+
const defaultStore = resolvedConfig.default ?? (resolvedConfig.provider ? "default" : "memory");
|
|
1251
|
+
const defaultTtl = resolvedConfig.defaultTtl ?? (typeof resolvedConfig.defaultTTL === "number" ? resolvedConfig.defaultTTL : void 0) ?? 60;
|
|
1252
|
+
const prefix = resolvedConfig.prefix ?? "";
|
|
1253
|
+
const logger = core.logger;
|
|
1254
|
+
logger.info(`[OrbitCache] Initializing Cache (Exposed as: ${exposeAs})`);
|
|
1255
|
+
const events = {
|
|
1256
|
+
hit: (key) => core.hooks.doAction("cache:hit", { key }),
|
|
1257
|
+
miss: (key) => core.hooks.doAction("cache:miss", { key }),
|
|
1258
|
+
write: (key) => core.hooks.doAction("cache:write", { key }),
|
|
1259
|
+
forget: (key) => core.hooks.doAction("cache:forget", { key }),
|
|
1260
|
+
flush: () => core.hooks.doAction("cache:flush", {})
|
|
1261
|
+
};
|
|
1262
|
+
const onEventError = resolvedConfig.onEventError ?? ((error, event, payload) => {
|
|
1263
|
+
const key = payload.key ? ` (key: ${payload.key})` : "";
|
|
1264
|
+
logger.error(`[OrbitCache] cache event '${event}' failed${key}`, error);
|
|
1265
|
+
});
|
|
1266
|
+
const stores = resolvedConfig.stores ?? (resolvedConfig.provider ? { default: { driver: "provider", provider: resolvedConfig.provider } } : void 0);
|
|
1267
|
+
const manager = new CacheManager(
|
|
1268
|
+
createStoreFactory({ ...resolvedConfig, stores }),
|
|
1269
|
+
{
|
|
1270
|
+
default: defaultStore,
|
|
1271
|
+
prefix,
|
|
1272
|
+
defaultTtl
|
|
1273
|
+
},
|
|
1274
|
+
events,
|
|
1275
|
+
{
|
|
1276
|
+
mode: resolvedConfig.eventsMode ?? "async",
|
|
1277
|
+
throwOnError: resolvedConfig.throwOnEventError,
|
|
1278
|
+
onError: onEventError
|
|
1279
|
+
}
|
|
1280
|
+
);
|
|
1281
|
+
this.manager = manager;
|
|
1282
|
+
core.adapter.use("*", async (c, next) => {
|
|
1283
|
+
c.set(exposeAs, manager);
|
|
1284
|
+
await next();
|
|
1285
|
+
return void 0;
|
|
1286
|
+
});
|
|
1287
|
+
core.services.set(exposeAs, manager);
|
|
1288
|
+
core.hooks.doAction("cache:init", manager);
|
|
1289
|
+
}
|
|
1290
|
+
getCache() {
|
|
1291
|
+
if (!this.manager) {
|
|
1292
|
+
throw new Error("OrbitCache not installed yet.");
|
|
1293
|
+
}
|
|
1294
|
+
return this.manager;
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
function orbitCache(core, options = {}) {
|
|
1298
|
+
const orbit = new OrbitStasis(options);
|
|
1299
|
+
orbit.install(core);
|
|
1300
|
+
return orbit.getCache();
|
|
1301
|
+
}
|
|
1302
|
+
var OrbitCache = OrbitStasis;
|
|
1303
|
+
export {
|
|
1304
|
+
CacheManager,
|
|
1305
|
+
CacheRepository,
|
|
1306
|
+
FileStore,
|
|
1307
|
+
LockTimeoutError,
|
|
1308
|
+
MemoryCacheProvider,
|
|
1309
|
+
MemoryStore,
|
|
1310
|
+
NullStore,
|
|
1311
|
+
OrbitCache,
|
|
1312
|
+
OrbitStasis,
|
|
1313
|
+
RateLimiter,
|
|
1314
|
+
RedisStore,
|
|
1315
|
+
orbitCache as default,
|
|
1316
|
+
isExpired,
|
|
1317
|
+
isTaggableStore,
|
|
1318
|
+
normalizeCacheKey,
|
|
1319
|
+
sleep,
|
|
1320
|
+
ttlToExpiresAt
|
|
1321
|
+
};
|