@gravito/stasis 3.2.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/dist/index.cjs +0 -1886
- package/dist/index.cjs.map +0 -27
package/dist/index.cjs
DELETED
|
@@ -1,1886 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
-
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
6
|
-
var __toCommonJS = (from) => {
|
|
7
|
-
var entry = __moduleCache.get(from), desc;
|
|
8
|
-
if (entry)
|
|
9
|
-
return entry;
|
|
10
|
-
entry = __defProp({}, "__esModule", { value: true });
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function")
|
|
12
|
-
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
13
|
-
get: () => from[key],
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
-
}));
|
|
16
|
-
__moduleCache.set(from, entry);
|
|
17
|
-
return entry;
|
|
18
|
-
};
|
|
19
|
-
var __export = (target, all) => {
|
|
20
|
-
for (var name in all)
|
|
21
|
-
__defProp(target, name, {
|
|
22
|
-
get: all[name],
|
|
23
|
-
enumerable: true,
|
|
24
|
-
configurable: true,
|
|
25
|
-
set: (newValue) => all[name] = () => newValue
|
|
26
|
-
});
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// src/index.ts
|
|
30
|
-
var exports_src = {};
|
|
31
|
-
__export(exports_src, {
|
|
32
|
-
ttlToExpiresAt: () => ttlToExpiresAt,
|
|
33
|
-
sleep: () => sleep,
|
|
34
|
-
normalizeCacheKey: () => normalizeCacheKey,
|
|
35
|
-
isTaggableStore: () => isTaggableStore,
|
|
36
|
-
isExpired: () => isExpired,
|
|
37
|
-
default: () => orbitCache,
|
|
38
|
-
TieredStore: () => TieredStore,
|
|
39
|
-
RedisStore: () => RedisStore,
|
|
40
|
-
RateLimiter: () => RateLimiter,
|
|
41
|
-
PredictiveStore: () => PredictiveStore,
|
|
42
|
-
OrbitStasis: () => OrbitStasis,
|
|
43
|
-
OrbitCache: () => OrbitCache,
|
|
44
|
-
NullStore: () => NullStore,
|
|
45
|
-
MemoryStore: () => MemoryStore,
|
|
46
|
-
MemoryCacheProvider: () => MemoryCacheProvider,
|
|
47
|
-
MarkovPredictor: () => MarkovPredictor,
|
|
48
|
-
LockTimeoutError: () => LockTimeoutError,
|
|
49
|
-
FileStore: () => FileStore,
|
|
50
|
-
CircuitBreakerStore: () => CircuitBreakerStore,
|
|
51
|
-
CacheRepository: () => CacheRepository,
|
|
52
|
-
CacheManager: () => CacheManager
|
|
53
|
-
});
|
|
54
|
-
module.exports = __toCommonJS(exports_src);
|
|
55
|
-
|
|
56
|
-
// src/cache-events.ts
|
|
57
|
-
function emitCacheEvent(event, payload, events, config) {
|
|
58
|
-
const { mode, throwOnError, onError } = config;
|
|
59
|
-
if (mode === "off") {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const fn = events?.[event];
|
|
63
|
-
if (!fn) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const invoke = () => {
|
|
67
|
-
if (event === "flush") {
|
|
68
|
-
return fn();
|
|
69
|
-
}
|
|
70
|
-
const key = payload.key ?? "";
|
|
71
|
-
return fn(key);
|
|
72
|
-
};
|
|
73
|
-
const reportError = (error) => {
|
|
74
|
-
try {
|
|
75
|
-
onError?.(error, event, payload);
|
|
76
|
-
} catch {}
|
|
77
|
-
};
|
|
78
|
-
if (mode === "sync") {
|
|
79
|
-
try {
|
|
80
|
-
return Promise.resolve(invoke()).catch((error) => {
|
|
81
|
-
reportError(error);
|
|
82
|
-
if (throwOnError) {
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
} catch (error) {
|
|
87
|
-
reportError(error);
|
|
88
|
-
if (throwOnError) {
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
queueMicrotask(() => {
|
|
95
|
-
try {
|
|
96
|
-
const result = invoke();
|
|
97
|
-
if (result && typeof result.catch === "function") {
|
|
98
|
-
result.catch(reportError);
|
|
99
|
-
}
|
|
100
|
-
} catch (error) {
|
|
101
|
-
reportError(error);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// src/locks.ts
|
|
107
|
-
class LockTimeoutError extends Error {
|
|
108
|
-
name = "LockTimeoutError";
|
|
109
|
-
}
|
|
110
|
-
function sleep(ms) {
|
|
111
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// src/store.ts
|
|
115
|
-
function isTaggableStore(store) {
|
|
116
|
-
return typeof store.flushTags === "function" && typeof store.tagKey === "function" && typeof store.tagIndexAdd === "function" && typeof store.tagIndexRemove === "function";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// src/types.ts
|
|
120
|
-
function normalizeCacheKey(key) {
|
|
121
|
-
if (!key) {
|
|
122
|
-
throw new Error("Cache key cannot be empty.");
|
|
123
|
-
}
|
|
124
|
-
return key;
|
|
125
|
-
}
|
|
126
|
-
function ttlToExpiresAt(ttl, now = Date.now()) {
|
|
127
|
-
if (ttl === undefined) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (ttl === null) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
if (ttl instanceof Date) {
|
|
134
|
-
return ttl.getTime();
|
|
135
|
-
}
|
|
136
|
-
if (typeof ttl === "number") {
|
|
137
|
-
if (ttl <= 0) {
|
|
138
|
-
return now;
|
|
139
|
-
}
|
|
140
|
-
return now + ttl * 1000;
|
|
141
|
-
}
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
function isExpired(expiresAt, now = Date.now()) {
|
|
145
|
-
if (expiresAt === null) {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
if (expiresAt === undefined) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
return now >= expiresAt;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// src/tagged-store.ts
|
|
155
|
-
class TaggedStore {
|
|
156
|
-
store;
|
|
157
|
-
tags;
|
|
158
|
-
constructor(store, tags) {
|
|
159
|
-
this.store = store;
|
|
160
|
-
this.tags = tags;
|
|
161
|
-
}
|
|
162
|
-
tagged(key) {
|
|
163
|
-
return this.store.tagKey(normalizeCacheKey(key), this.tags);
|
|
164
|
-
}
|
|
165
|
-
async get(key) {
|
|
166
|
-
return this.store.get(this.tagged(key));
|
|
167
|
-
}
|
|
168
|
-
async put(key, value, ttl) {
|
|
169
|
-
const taggedKey = this.tagged(key);
|
|
170
|
-
await this.store.put(taggedKey, value, ttl);
|
|
171
|
-
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
172
|
-
}
|
|
173
|
-
async add(key, value, ttl) {
|
|
174
|
-
const taggedKey = this.tagged(key);
|
|
175
|
-
const ok = await this.store.add(taggedKey, value, ttl);
|
|
176
|
-
if (ok) {
|
|
177
|
-
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
178
|
-
}
|
|
179
|
-
return ok;
|
|
180
|
-
}
|
|
181
|
-
async forget(key) {
|
|
182
|
-
return this.store.forget(this.tagged(key));
|
|
183
|
-
}
|
|
184
|
-
async flush() {
|
|
185
|
-
return this.store.flushTags(this.tags);
|
|
186
|
-
}
|
|
187
|
-
async increment(key, value) {
|
|
188
|
-
const taggedKey = this.tagged(key);
|
|
189
|
-
const next = await this.store.increment(taggedKey, value);
|
|
190
|
-
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
191
|
-
return next;
|
|
192
|
-
}
|
|
193
|
-
async decrement(key, value) {
|
|
194
|
-
const taggedKey = this.tagged(key);
|
|
195
|
-
const next = await this.store.decrement(taggedKey, value);
|
|
196
|
-
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
197
|
-
return next;
|
|
198
|
-
}
|
|
199
|
-
lock(name, seconds) {
|
|
200
|
-
return this.store.lock ? this.store.lock(this.tagged(name), seconds) : undefined;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// src/CacheRepository.ts
|
|
205
|
-
class CacheRepository {
|
|
206
|
-
store;
|
|
207
|
-
options;
|
|
208
|
-
refreshSemaphore = new Map;
|
|
209
|
-
coalesceSemaphore = new Map;
|
|
210
|
-
flexibleStats = { refreshCount: 0, refreshFailures: 0, totalTime: 0 };
|
|
211
|
-
eventEmitterConfig;
|
|
212
|
-
constructor(store, options = {}) {
|
|
213
|
-
this.store = store;
|
|
214
|
-
this.options = options;
|
|
215
|
-
this.eventEmitterConfig = {
|
|
216
|
-
mode: options.eventsMode ?? "async",
|
|
217
|
-
throwOnError: options.throwOnEventError,
|
|
218
|
-
onError: options.onEventError
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
getFlexibleStats() {
|
|
222
|
-
return {
|
|
223
|
-
refreshCount: this.flexibleStats.refreshCount,
|
|
224
|
-
refreshFailures: this.flexibleStats.refreshFailures,
|
|
225
|
-
avgRefreshTime: this.flexibleStats.refreshCount > 0 ? this.flexibleStats.totalTime / this.flexibleStats.refreshCount : 0
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
emit(event, payload = {}) {
|
|
229
|
-
return emitCacheEvent(event, payload, this.options.events, this.eventEmitterConfig);
|
|
230
|
-
}
|
|
231
|
-
key(key) {
|
|
232
|
-
const normalized = normalizeCacheKey(key);
|
|
233
|
-
const prefix = this.options.prefix ?? "";
|
|
234
|
-
return prefix ? `${prefix}${normalized}` : normalized;
|
|
235
|
-
}
|
|
236
|
-
flexibleFreshUntilKey(fullKey) {
|
|
237
|
-
return `__gravito:flexible:freshUntil:${fullKey}`;
|
|
238
|
-
}
|
|
239
|
-
async putMetaKey(metaKey, value, ttl) {
|
|
240
|
-
await this.store.put(metaKey, value, ttl);
|
|
241
|
-
}
|
|
242
|
-
async forgetMetaKey(metaKey) {
|
|
243
|
-
await this.store.forget(metaKey);
|
|
244
|
-
}
|
|
245
|
-
async get(key, defaultValue) {
|
|
246
|
-
const fullKey = this.key(key);
|
|
247
|
-
const raw = await this.store.get(fullKey);
|
|
248
|
-
if (raw !== null) {
|
|
249
|
-
const e2 = this.emit("hit", { key: fullKey });
|
|
250
|
-
if (e2) {
|
|
251
|
-
await e2;
|
|
252
|
-
}
|
|
253
|
-
return this.decompress(raw);
|
|
254
|
-
}
|
|
255
|
-
const e = this.emit("miss", { key: fullKey });
|
|
256
|
-
if (e) {
|
|
257
|
-
await e;
|
|
258
|
-
}
|
|
259
|
-
if (defaultValue === undefined) {
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
if (typeof defaultValue === "function") {
|
|
263
|
-
return defaultValue();
|
|
264
|
-
}
|
|
265
|
-
return defaultValue;
|
|
266
|
-
}
|
|
267
|
-
async has(key) {
|
|
268
|
-
return await this.get(key) !== null;
|
|
269
|
-
}
|
|
270
|
-
async missing(key) {
|
|
271
|
-
return !await this.has(key);
|
|
272
|
-
}
|
|
273
|
-
async put(key, value, ttl) {
|
|
274
|
-
const fullKey = this.key(key);
|
|
275
|
-
const data = await this.compress(value);
|
|
276
|
-
await this.store.put(fullKey, data, ttl);
|
|
277
|
-
const e = this.emit("write", { key: fullKey });
|
|
278
|
-
if (e) {
|
|
279
|
-
await e;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
async set(key, value, ttl) {
|
|
283
|
-
const resolved = ttl ?? this.options.defaultTtl;
|
|
284
|
-
await this.put(key, value, resolved);
|
|
285
|
-
}
|
|
286
|
-
async add(key, value, ttl) {
|
|
287
|
-
const fullKey = this.key(key);
|
|
288
|
-
const resolved = ttl ?? this.options.defaultTtl;
|
|
289
|
-
const ok = await this.store.add(fullKey, value, resolved);
|
|
290
|
-
if (ok) {
|
|
291
|
-
const e = this.emit("write", { key: fullKey });
|
|
292
|
-
if (e) {
|
|
293
|
-
await e;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return ok;
|
|
297
|
-
}
|
|
298
|
-
async forever(key, value) {
|
|
299
|
-
await this.put(key, value, null);
|
|
300
|
-
}
|
|
301
|
-
async remember(key, ttl, callback) {
|
|
302
|
-
const fullKey = this.key(key);
|
|
303
|
-
if (this.coalesceSemaphore.has(fullKey)) {
|
|
304
|
-
return this.coalesceSemaphore.get(fullKey);
|
|
305
|
-
}
|
|
306
|
-
const promise = (async () => {
|
|
307
|
-
try {
|
|
308
|
-
const existing = await this.get(key);
|
|
309
|
-
if (existing !== null) {
|
|
310
|
-
return existing;
|
|
311
|
-
}
|
|
312
|
-
const value = await callback();
|
|
313
|
-
await this.put(key, value, ttl);
|
|
314
|
-
return value;
|
|
315
|
-
} finally {
|
|
316
|
-
this.coalesceSemaphore.delete(fullKey);
|
|
317
|
-
}
|
|
318
|
-
})();
|
|
319
|
-
this.coalesceSemaphore.set(fullKey, promise);
|
|
320
|
-
return promise;
|
|
321
|
-
}
|
|
322
|
-
async rememberForever(key, callback) {
|
|
323
|
-
return this.remember(key, null, callback);
|
|
324
|
-
}
|
|
325
|
-
async many(keys) {
|
|
326
|
-
const out = {};
|
|
327
|
-
for (const key of keys) {
|
|
328
|
-
out[String(key)] = await this.get(key);
|
|
329
|
-
}
|
|
330
|
-
return out;
|
|
331
|
-
}
|
|
332
|
-
async putMany(values, ttl) {
|
|
333
|
-
await Promise.all(Object.entries(values).map(([k, v]) => this.put(k, v, ttl)));
|
|
334
|
-
}
|
|
335
|
-
async flexible(key, ttlSeconds, staleSeconds, callback) {
|
|
336
|
-
const fullKey = this.key(key);
|
|
337
|
-
const metaKey = this.flexibleFreshUntilKey(fullKey);
|
|
338
|
-
const now = Date.now();
|
|
339
|
-
const ttlMillis = Math.max(0, ttlSeconds) * 1000;
|
|
340
|
-
const staleMillis = Math.max(0, staleSeconds) * 1000;
|
|
341
|
-
const [freshUntil, cachedValue] = await Promise.all([
|
|
342
|
-
this.store.get(metaKey),
|
|
343
|
-
this.store.get(fullKey)
|
|
344
|
-
]);
|
|
345
|
-
if (freshUntil !== null && cachedValue !== null) {
|
|
346
|
-
if (now <= freshUntil) {
|
|
347
|
-
const e2 = this.emit("hit", { key: fullKey });
|
|
348
|
-
if (e2) {
|
|
349
|
-
await e2;
|
|
350
|
-
}
|
|
351
|
-
return cachedValue;
|
|
352
|
-
}
|
|
353
|
-
if (now <= freshUntil + staleMillis) {
|
|
354
|
-
const e2 = this.emit("hit", { key: fullKey });
|
|
355
|
-
if (e2) {
|
|
356
|
-
await e2;
|
|
357
|
-
}
|
|
358
|
-
this.refreshFlexible(fullKey, metaKey, ttlSeconds, staleSeconds, callback);
|
|
359
|
-
return cachedValue;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
const e = this.emit("miss", { key: fullKey });
|
|
363
|
-
if (e) {
|
|
364
|
-
await e;
|
|
365
|
-
}
|
|
366
|
-
const value = await callback();
|
|
367
|
-
const totalTtl = ttlSeconds + staleSeconds;
|
|
368
|
-
await this.put(fullKey, value, totalTtl);
|
|
369
|
-
await this.putMetaKey(metaKey, now + ttlMillis, totalTtl);
|
|
370
|
-
return value;
|
|
371
|
-
}
|
|
372
|
-
async refreshFlexible(fullKey, metaKey, ttlSeconds, staleSeconds, callback) {
|
|
373
|
-
if (this.refreshSemaphore.has(fullKey)) {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const refreshPromise = this.doRefresh(fullKey, metaKey, ttlSeconds, staleSeconds, callback);
|
|
377
|
-
this.refreshSemaphore.set(fullKey, refreshPromise);
|
|
378
|
-
try {
|
|
379
|
-
await refreshPromise;
|
|
380
|
-
} finally {
|
|
381
|
-
this.refreshSemaphore.delete(fullKey);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
async doRefresh(fullKey, metaKey, ttlSeconds, staleSeconds, callback) {
|
|
385
|
-
if (!this.store.lock) {
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const lock = this.store.lock(`flexible:${metaKey}`, Math.max(1, ttlSeconds));
|
|
389
|
-
if (!lock || !await lock.acquire()) {
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
const startTime = Date.now();
|
|
393
|
-
try {
|
|
394
|
-
const timeoutMillis = this.options.refreshTimeout ?? 30000;
|
|
395
|
-
const maxRetries = this.options.maxRetries ?? 0;
|
|
396
|
-
const retryDelay = this.options.retryDelay ?? 50;
|
|
397
|
-
let lastError;
|
|
398
|
-
let value;
|
|
399
|
-
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
400
|
-
try {
|
|
401
|
-
value = await Promise.race([
|
|
402
|
-
Promise.resolve(callback()),
|
|
403
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Refresh timeout")), timeoutMillis))
|
|
404
|
-
]);
|
|
405
|
-
break;
|
|
406
|
-
} catch (err) {
|
|
407
|
-
lastError = err;
|
|
408
|
-
if (attempt < maxRetries) {
|
|
409
|
-
await sleep(retryDelay);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
if (value === undefined && lastError) {
|
|
414
|
-
throw lastError;
|
|
415
|
-
}
|
|
416
|
-
const totalTtl = ttlSeconds + staleSeconds;
|
|
417
|
-
const now = Date.now();
|
|
418
|
-
await this.put(fullKey, value, totalTtl);
|
|
419
|
-
await this.putMetaKey(metaKey, now + Math.max(0, ttlSeconds) * 1000, totalTtl);
|
|
420
|
-
this.flexibleStats.refreshCount++;
|
|
421
|
-
this.flexibleStats.totalTime += Date.now() - startTime;
|
|
422
|
-
} catch {
|
|
423
|
-
this.flexibleStats.refreshFailures++;
|
|
424
|
-
} finally {
|
|
425
|
-
await lock.release();
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
async pull(key, defaultValue) {
|
|
429
|
-
const value = await this.get(key, defaultValue);
|
|
430
|
-
await this.forget(key);
|
|
431
|
-
return value;
|
|
432
|
-
}
|
|
433
|
-
async forget(key) {
|
|
434
|
-
const fullKey = this.key(key);
|
|
435
|
-
const metaKey = this.flexibleFreshUntilKey(fullKey);
|
|
436
|
-
const ok = await this.store.forget(fullKey);
|
|
437
|
-
await this.forgetMetaKey(metaKey);
|
|
438
|
-
if (ok) {
|
|
439
|
-
const e = this.emit("forget", { key: fullKey });
|
|
440
|
-
if (e) {
|
|
441
|
-
await e;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return ok;
|
|
445
|
-
}
|
|
446
|
-
async delete(key) {
|
|
447
|
-
return this.forget(key);
|
|
448
|
-
}
|
|
449
|
-
async flush() {
|
|
450
|
-
await this.store.flush();
|
|
451
|
-
const e = this.emit("flush");
|
|
452
|
-
if (e) {
|
|
453
|
-
await e;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
async clear() {
|
|
457
|
-
return this.flush();
|
|
458
|
-
}
|
|
459
|
-
increment(key, value) {
|
|
460
|
-
return this.store.increment(this.key(key), value);
|
|
461
|
-
}
|
|
462
|
-
decrement(key, value) {
|
|
463
|
-
return this.store.decrement(this.key(key), value);
|
|
464
|
-
}
|
|
465
|
-
lock(name, seconds) {
|
|
466
|
-
return this.store.lock ? this.store.lock(this.key(name), seconds) : undefined;
|
|
467
|
-
}
|
|
468
|
-
tags(tags) {
|
|
469
|
-
if (!isTaggableStore(this.store)) {
|
|
470
|
-
throw new Error("This cache store does not support tags.");
|
|
471
|
-
}
|
|
472
|
-
return new CacheRepository(new TaggedStore(this.store, tags), this.options);
|
|
473
|
-
}
|
|
474
|
-
getStore() {
|
|
475
|
-
return this.store;
|
|
476
|
-
}
|
|
477
|
-
async compress(value) {
|
|
478
|
-
const opts = this.options.compression;
|
|
479
|
-
if (!opts?.enabled || value === null || value === undefined) {
|
|
480
|
-
return value;
|
|
481
|
-
}
|
|
482
|
-
const json = JSON.stringify(value);
|
|
483
|
-
if (json.length < (opts.minSize ?? 1024)) {
|
|
484
|
-
return value;
|
|
485
|
-
}
|
|
486
|
-
const { getCompressionAdapter } = await import("@gravito/core");
|
|
487
|
-
const adapter = getCompressionAdapter();
|
|
488
|
-
const input = new TextEncoder().encode(json);
|
|
489
|
-
const compressed = adapter.gzipSync(input, { level: opts.level ?? 6 });
|
|
490
|
-
return {
|
|
491
|
-
__gravito_compressed: true,
|
|
492
|
-
data: Buffer.from(compressed).toString("base64")
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
async decompress(value) {
|
|
496
|
-
if (value !== null && typeof value === "object" && "__gravito_compressed" in value && value.__gravito_compressed === true) {
|
|
497
|
-
const { getCompressionAdapter } = await import("@gravito/core");
|
|
498
|
-
const adapter = getCompressionAdapter();
|
|
499
|
-
const buffer = new Uint8Array(Buffer.from(value.data, "base64"));
|
|
500
|
-
const decompressedBytes = adapter.gunzipSync(buffer);
|
|
501
|
-
const decompressed = new TextDecoder().decode(decompressedBytes);
|
|
502
|
-
try {
|
|
503
|
-
return JSON.parse(decompressed);
|
|
504
|
-
} catch {
|
|
505
|
-
return decompressed;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
return value;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// src/RateLimiter.ts
|
|
513
|
-
class RateLimiter {
|
|
514
|
-
store;
|
|
515
|
-
constructor(store) {
|
|
516
|
-
this.store = store;
|
|
517
|
-
}
|
|
518
|
-
async attempt(key, maxAttempts, decaySeconds) {
|
|
519
|
-
const current = await this.store.get(key);
|
|
520
|
-
const now = Math.floor(Date.now() / 1000);
|
|
521
|
-
if (current === null) {
|
|
522
|
-
await this.store.put(key, 1, decaySeconds);
|
|
523
|
-
return {
|
|
524
|
-
allowed: true,
|
|
525
|
-
remaining: maxAttempts - 1,
|
|
526
|
-
reset: now + decaySeconds
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
if (current >= maxAttempts) {
|
|
530
|
-
const retryAfter = await this.availableIn(key, decaySeconds);
|
|
531
|
-
return {
|
|
532
|
-
allowed: false,
|
|
533
|
-
remaining: 0,
|
|
534
|
-
reset: now + retryAfter,
|
|
535
|
-
retryAfter
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
const next = await this.store.increment(key);
|
|
539
|
-
return {
|
|
540
|
-
allowed: true,
|
|
541
|
-
remaining: maxAttempts - next,
|
|
542
|
-
reset: now + decaySeconds
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
async availableIn(key, decaySeconds) {
|
|
546
|
-
if (typeof this.store.ttl === "function") {
|
|
547
|
-
const remaining = await this.store.ttl(key);
|
|
548
|
-
if (remaining !== null) {
|
|
549
|
-
return remaining;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return decaySeconds;
|
|
553
|
-
}
|
|
554
|
-
async getInfo(key, maxAttempts, decaySeconds) {
|
|
555
|
-
const current = await this.store.get(key);
|
|
556
|
-
const now = Math.floor(Date.now() / 1000);
|
|
557
|
-
if (current === null) {
|
|
558
|
-
return {
|
|
559
|
-
limit: maxAttempts,
|
|
560
|
-
remaining: maxAttempts,
|
|
561
|
-
reset: now + decaySeconds
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
const remaining = Math.max(0, maxAttempts - current);
|
|
565
|
-
const retryAfter = remaining === 0 ? await this.availableIn(key, decaySeconds) : undefined;
|
|
566
|
-
return {
|
|
567
|
-
limit: maxAttempts,
|
|
568
|
-
remaining,
|
|
569
|
-
reset: now + (retryAfter ?? decaySeconds),
|
|
570
|
-
retryAfter
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
async clear(key) {
|
|
574
|
-
await this.store.forget(key);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// src/CacheManager.ts
|
|
579
|
-
class CacheManager {
|
|
580
|
-
storeFactory;
|
|
581
|
-
config;
|
|
582
|
-
events;
|
|
583
|
-
eventOptions;
|
|
584
|
-
stores = new Map;
|
|
585
|
-
constructor(storeFactory, config = {}, events, eventOptions) {
|
|
586
|
-
this.storeFactory = storeFactory;
|
|
587
|
-
this.config = config;
|
|
588
|
-
this.events = events;
|
|
589
|
-
this.eventOptions = eventOptions;
|
|
590
|
-
}
|
|
591
|
-
limiter(name) {
|
|
592
|
-
return new RateLimiter(this.store(name).getStore());
|
|
593
|
-
}
|
|
594
|
-
store(name) {
|
|
595
|
-
const storeName = name ?? this.config.default ?? "memory";
|
|
596
|
-
const existing = this.stores.get(storeName);
|
|
597
|
-
if (existing) {
|
|
598
|
-
return existing;
|
|
599
|
-
}
|
|
600
|
-
const repo = new CacheRepository(this.storeFactory(storeName), {
|
|
601
|
-
prefix: this.config.prefix,
|
|
602
|
-
defaultTtl: this.config.defaultTtl,
|
|
603
|
-
events: this.events,
|
|
604
|
-
eventsMode: this.eventOptions?.mode,
|
|
605
|
-
throwOnEventError: this.eventOptions?.throwOnError,
|
|
606
|
-
onEventError: this.eventOptions?.onError
|
|
607
|
-
});
|
|
608
|
-
this.stores.set(storeName, repo);
|
|
609
|
-
return repo;
|
|
610
|
-
}
|
|
611
|
-
get(key, defaultValue) {
|
|
612
|
-
return this.store().get(key, defaultValue);
|
|
613
|
-
}
|
|
614
|
-
has(key) {
|
|
615
|
-
return this.store().has(key);
|
|
616
|
-
}
|
|
617
|
-
missing(key) {
|
|
618
|
-
return this.store().missing(key);
|
|
619
|
-
}
|
|
620
|
-
put(key, value, ttl) {
|
|
621
|
-
return this.store().put(key, value, ttl);
|
|
622
|
-
}
|
|
623
|
-
set(key, value, ttl) {
|
|
624
|
-
return this.store().set(key, value, ttl);
|
|
625
|
-
}
|
|
626
|
-
add(key, value, ttl) {
|
|
627
|
-
return this.store().add(key, value, ttl);
|
|
628
|
-
}
|
|
629
|
-
forever(key, value) {
|
|
630
|
-
return this.store().forever(key, value);
|
|
631
|
-
}
|
|
632
|
-
remember(key, ttl, callback) {
|
|
633
|
-
return this.store().remember(key, ttl, callback);
|
|
634
|
-
}
|
|
635
|
-
rememberForever(key, callback) {
|
|
636
|
-
return this.store().rememberForever(key, callback);
|
|
637
|
-
}
|
|
638
|
-
many(keys) {
|
|
639
|
-
return this.store().many(keys);
|
|
640
|
-
}
|
|
641
|
-
putMany(values, ttl) {
|
|
642
|
-
return this.store().putMany(values, ttl);
|
|
643
|
-
}
|
|
644
|
-
flexible(key, ttlSeconds, staleSeconds, callback) {
|
|
645
|
-
return this.store().flexible(key, ttlSeconds, staleSeconds, callback);
|
|
646
|
-
}
|
|
647
|
-
pull(key, defaultValue) {
|
|
648
|
-
return this.store().pull(key, defaultValue);
|
|
649
|
-
}
|
|
650
|
-
forget(key) {
|
|
651
|
-
return this.store().forget(key);
|
|
652
|
-
}
|
|
653
|
-
delete(key) {
|
|
654
|
-
return this.store().delete(key);
|
|
655
|
-
}
|
|
656
|
-
flush() {
|
|
657
|
-
return this.store().flush();
|
|
658
|
-
}
|
|
659
|
-
clear() {
|
|
660
|
-
return this.store().clear();
|
|
661
|
-
}
|
|
662
|
-
increment(key, value) {
|
|
663
|
-
return this.store().increment(key, value);
|
|
664
|
-
}
|
|
665
|
-
decrement(key, value) {
|
|
666
|
-
return this.store().decrement(key, value);
|
|
667
|
-
}
|
|
668
|
-
lock(name, seconds) {
|
|
669
|
-
return this.store().lock(name, seconds);
|
|
670
|
-
}
|
|
671
|
-
tags(tags) {
|
|
672
|
-
return this.store().tags(tags);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// src/prediction/AccessPredictor.ts
|
|
677
|
-
class MarkovPredictor {
|
|
678
|
-
transitions = new Map;
|
|
679
|
-
lastKey = null;
|
|
680
|
-
maxNodes;
|
|
681
|
-
maxEdgesPerNode;
|
|
682
|
-
constructor(options = {}) {
|
|
683
|
-
this.maxNodes = options.maxNodes ?? 1000;
|
|
684
|
-
this.maxEdgesPerNode = options.maxEdgesPerNode ?? 10;
|
|
685
|
-
}
|
|
686
|
-
record(key) {
|
|
687
|
-
if (this.lastKey && this.lastKey !== key) {
|
|
688
|
-
if (!this.transitions.has(this.lastKey)) {
|
|
689
|
-
if (this.transitions.size >= this.maxNodes) {
|
|
690
|
-
this.transitions.clear();
|
|
691
|
-
}
|
|
692
|
-
this.transitions.set(this.lastKey, new Map);
|
|
693
|
-
}
|
|
694
|
-
const edges = this.transitions.get(this.lastKey);
|
|
695
|
-
const count = edges.get(key) ?? 0;
|
|
696
|
-
edges.set(key, count + 1);
|
|
697
|
-
if (edges.size > this.maxEdgesPerNode) {
|
|
698
|
-
let minKey = "";
|
|
699
|
-
let minCount = Infinity;
|
|
700
|
-
for (const [k, c] of edges) {
|
|
701
|
-
if (c < minCount) {
|
|
702
|
-
minCount = c;
|
|
703
|
-
minKey = k;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
if (minKey) {
|
|
707
|
-
edges.delete(minKey);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
this.lastKey = key;
|
|
712
|
-
}
|
|
713
|
-
predict(key) {
|
|
714
|
-
const edges = this.transitions.get(key);
|
|
715
|
-
if (!edges) {
|
|
716
|
-
return [];
|
|
717
|
-
}
|
|
718
|
-
return Array.from(edges.entries()).sort((a, b) => b[1] - a[1]).map((entry) => entry[0]);
|
|
719
|
-
}
|
|
720
|
-
reset() {
|
|
721
|
-
this.transitions.clear();
|
|
722
|
-
this.lastKey = null;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// src/stores/CircuitBreakerStore.ts
|
|
727
|
-
class CircuitBreakerStore {
|
|
728
|
-
primary;
|
|
729
|
-
state = "CLOSED";
|
|
730
|
-
failures = 0;
|
|
731
|
-
lastErrorTime = 0;
|
|
732
|
-
options;
|
|
733
|
-
constructor(primary, options = {}) {
|
|
734
|
-
this.primary = primary;
|
|
735
|
-
this.options = {
|
|
736
|
-
maxFailures: options.maxFailures ?? 5,
|
|
737
|
-
resetTimeout: options.resetTimeout ?? 60000,
|
|
738
|
-
fallback: options.fallback
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
async execute(operation, fallbackResult = null) {
|
|
742
|
-
if (this.state === "OPEN") {
|
|
743
|
-
if (Date.now() - this.lastErrorTime > this.options.resetTimeout) {
|
|
744
|
-
this.state = "HALF_OPEN";
|
|
745
|
-
} else {
|
|
746
|
-
return this.handleFallback(operation, fallbackResult);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
try {
|
|
750
|
-
const result = await operation(this.primary);
|
|
751
|
-
this.onSuccess();
|
|
752
|
-
return result;
|
|
753
|
-
} catch (_error) {
|
|
754
|
-
this.onFailure();
|
|
755
|
-
return this.handleFallback(operation, fallbackResult);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
onSuccess() {
|
|
759
|
-
this.failures = 0;
|
|
760
|
-
this.state = "CLOSED";
|
|
761
|
-
}
|
|
762
|
-
onFailure() {
|
|
763
|
-
this.failures++;
|
|
764
|
-
this.lastErrorTime = Date.now();
|
|
765
|
-
if (this.failures >= this.options.maxFailures) {
|
|
766
|
-
this.state = "OPEN";
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
async handleFallback(operation, fallbackResult) {
|
|
770
|
-
if (this.options.fallback) {
|
|
771
|
-
try {
|
|
772
|
-
return await operation(this.options.fallback);
|
|
773
|
-
} catch {}
|
|
774
|
-
}
|
|
775
|
-
return fallbackResult;
|
|
776
|
-
}
|
|
777
|
-
async get(key) {
|
|
778
|
-
return this.execute((s) => s.get(key));
|
|
779
|
-
}
|
|
780
|
-
async put(key, value, ttl) {
|
|
781
|
-
return this.execute((s) => s.put(key, value, ttl));
|
|
782
|
-
}
|
|
783
|
-
async add(key, value, ttl) {
|
|
784
|
-
return this.execute((s) => s.add(key, value, ttl), false);
|
|
785
|
-
}
|
|
786
|
-
async forget(key) {
|
|
787
|
-
return this.execute((s) => s.forget(key), false);
|
|
788
|
-
}
|
|
789
|
-
async flush() {
|
|
790
|
-
return this.execute((s) => s.flush());
|
|
791
|
-
}
|
|
792
|
-
async increment(key, value) {
|
|
793
|
-
return this.execute((s) => s.increment(key, value), 0);
|
|
794
|
-
}
|
|
795
|
-
async decrement(key, value) {
|
|
796
|
-
return this.execute((s) => s.decrement(key, value), 0);
|
|
797
|
-
}
|
|
798
|
-
async ttl(key) {
|
|
799
|
-
return this.execute(async (s) => s.ttl ? s.ttl(key) : null);
|
|
800
|
-
}
|
|
801
|
-
getState() {
|
|
802
|
-
return this.state;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// src/stores/FileStore.ts
|
|
807
|
-
var import_node_path = require("node:path");
|
|
808
|
-
var import_core = require("@gravito/core");
|
|
809
|
-
var import_ffi = require("@gravito/core/ffi");
|
|
810
|
-
class FileStore {
|
|
811
|
-
options;
|
|
812
|
-
cleanupTimer = null;
|
|
813
|
-
runtime = import_core.getRuntimeAdapter();
|
|
814
|
-
constructor(options) {
|
|
815
|
-
this.options = options;
|
|
816
|
-
if (options.enableCleanup !== false) {
|
|
817
|
-
this.startCleanupDaemon(options.cleanupInterval ?? 60000);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
startCleanupDaemon(interval) {
|
|
821
|
-
this.cleanupTimer = setInterval(() => {
|
|
822
|
-
this.cleanExpiredFiles().catch(() => {});
|
|
823
|
-
}, interval);
|
|
824
|
-
if (this.cleanupTimer.unref) {
|
|
825
|
-
this.cleanupTimer.unref();
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
async cleanExpiredFiles() {
|
|
829
|
-
await this.ensureDir();
|
|
830
|
-
let cleaned = 0;
|
|
831
|
-
const validFiles = [];
|
|
832
|
-
const scanDir = async (dir) => {
|
|
833
|
-
const entries = await import_core.runtimeReadDir(this.runtime, dir);
|
|
834
|
-
for (const entry of entries) {
|
|
835
|
-
const fullPath = import_node_path.join(dir, entry.name);
|
|
836
|
-
if (entry.isDirectory) {
|
|
837
|
-
await scanDir(fullPath);
|
|
838
|
-
try {
|
|
839
|
-
const { rmdir } = await import("node:fs/promises");
|
|
840
|
-
await rmdir(fullPath);
|
|
841
|
-
} catch {}
|
|
842
|
-
} else if (entry.isFile) {
|
|
843
|
-
if (!entry.name.endsWith(".json") || entry.name.startsWith(".lock-")) {
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
try {
|
|
847
|
-
const raw = await import_core.runtimeReadText(this.runtime, fullPath);
|
|
848
|
-
const data = JSON.parse(raw);
|
|
849
|
-
if (isExpired(data.expiresAt)) {
|
|
850
|
-
await import_core.runtimeRemoveRecursive(this.runtime, fullPath);
|
|
851
|
-
cleaned++;
|
|
852
|
-
} else if (this.options.maxFiles) {
|
|
853
|
-
const stats = await import_core.runtimeStatFull(this.runtime, fullPath);
|
|
854
|
-
validFiles.push({ path: fullPath, mtime: stats.mtimeMs });
|
|
855
|
-
}
|
|
856
|
-
} catch {}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
};
|
|
860
|
-
await scanDir(this.options.directory);
|
|
861
|
-
if (this.options.maxFiles && validFiles.length > this.options.maxFiles) {
|
|
862
|
-
validFiles.sort((a, b) => a.mtime - b.mtime);
|
|
863
|
-
const toRemove = validFiles.slice(0, validFiles.length - this.options.maxFiles);
|
|
864
|
-
await Promise.all(toRemove.map(async (f) => {
|
|
865
|
-
try {
|
|
866
|
-
await import_core.runtimeRemoveRecursive(this.runtime, f.path);
|
|
867
|
-
cleaned++;
|
|
868
|
-
} catch {}
|
|
869
|
-
}));
|
|
870
|
-
}
|
|
871
|
-
return cleaned;
|
|
872
|
-
}
|
|
873
|
-
async destroy() {
|
|
874
|
-
if (this.cleanupTimer) {
|
|
875
|
-
clearInterval(this.cleanupTimer);
|
|
876
|
-
this.cleanupTimer = null;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
async ensureDir() {
|
|
880
|
-
await import_core.runtimeMkdir(this.runtime, this.options.directory, { recursive: true });
|
|
881
|
-
}
|
|
882
|
-
async filePathForKey(key) {
|
|
883
|
-
const hashed = hashKey(key);
|
|
884
|
-
if (this.options.useSubdirectories) {
|
|
885
|
-
const d1 = hashed.substring(0, 2);
|
|
886
|
-
const d2 = hashed.substring(2, 4);
|
|
887
|
-
return import_node_path.join(this.options.directory, d1, d2, `${hashed}.json`);
|
|
888
|
-
}
|
|
889
|
-
return import_node_path.join(this.options.directory, `${hashed}.json`);
|
|
890
|
-
}
|
|
891
|
-
async get(key) {
|
|
892
|
-
const normalized = normalizeCacheKey(key);
|
|
893
|
-
await this.ensureDir();
|
|
894
|
-
const file = await this.filePathForKey(normalized);
|
|
895
|
-
try {
|
|
896
|
-
const raw = await import_core.runtimeReadText(this.runtime, file);
|
|
897
|
-
const data = JSON.parse(raw);
|
|
898
|
-
if (isExpired(data.expiresAt)) {
|
|
899
|
-
await this.forget(normalized);
|
|
900
|
-
return null;
|
|
901
|
-
}
|
|
902
|
-
return data.value;
|
|
903
|
-
} catch {
|
|
904
|
-
return null;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
async put(key, value, ttl) {
|
|
908
|
-
const normalized = normalizeCacheKey(key);
|
|
909
|
-
await this.ensureDir();
|
|
910
|
-
const expiresAt = ttlToExpiresAt(ttl);
|
|
911
|
-
if (expiresAt !== null && expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
912
|
-
await this.forget(normalized);
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
const file = await this.filePathForKey(normalized);
|
|
916
|
-
if (this.options.useSubdirectories) {
|
|
917
|
-
await import_core.runtimeMkdir(this.runtime, import_node_path.dirname(file), { recursive: true });
|
|
918
|
-
}
|
|
919
|
-
const tempFile = `${file}.tmp.${Date.now()}.${crypto.randomUUID()}`;
|
|
920
|
-
const payload = { expiresAt: expiresAt ?? null, value };
|
|
921
|
-
try {
|
|
922
|
-
await this.runtime.writeFile(tempFile, JSON.stringify(payload));
|
|
923
|
-
await import_core.runtimeRename(this.runtime, tempFile, file);
|
|
924
|
-
} catch (error) {
|
|
925
|
-
await import_core.runtimeRemoveRecursive(this.runtime, tempFile).catch(() => {});
|
|
926
|
-
throw error;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
async add(key, value, ttl) {
|
|
930
|
-
const normalized = normalizeCacheKey(key);
|
|
931
|
-
const existing = await this.get(normalized);
|
|
932
|
-
if (existing !== null) {
|
|
933
|
-
return false;
|
|
934
|
-
}
|
|
935
|
-
await this.put(normalized, value, ttl);
|
|
936
|
-
return true;
|
|
937
|
-
}
|
|
938
|
-
async forget(key) {
|
|
939
|
-
const normalized = normalizeCacheKey(key);
|
|
940
|
-
await this.ensureDir();
|
|
941
|
-
const file = await this.filePathForKey(normalized);
|
|
942
|
-
try {
|
|
943
|
-
await import_core.runtimeRemoveRecursive(this.runtime, file);
|
|
944
|
-
return true;
|
|
945
|
-
} catch {
|
|
946
|
-
return false;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
async flush() {
|
|
950
|
-
await this.ensureDir();
|
|
951
|
-
await import_core.runtimeRemoveRecursive(this.runtime, this.options.directory);
|
|
952
|
-
await this.ensureDir();
|
|
953
|
-
}
|
|
954
|
-
async increment(key, value = 1) {
|
|
955
|
-
const normalized = normalizeCacheKey(key);
|
|
956
|
-
const current = await this.get(normalized);
|
|
957
|
-
const next = (current ?? 0) + value;
|
|
958
|
-
await this.put(normalized, next, null);
|
|
959
|
-
return next;
|
|
960
|
-
}
|
|
961
|
-
async decrement(key, value = 1) {
|
|
962
|
-
return this.increment(key, -value);
|
|
963
|
-
}
|
|
964
|
-
async ttl(key) {
|
|
965
|
-
const normalized = normalizeCacheKey(key);
|
|
966
|
-
const file = await this.filePathForKey(normalized);
|
|
967
|
-
try {
|
|
968
|
-
const raw = await import_core.runtimeReadText(this.runtime, file);
|
|
969
|
-
const data = JSON.parse(raw);
|
|
970
|
-
if (data.expiresAt === null) {
|
|
971
|
-
return null;
|
|
972
|
-
}
|
|
973
|
-
const remaining = Math.ceil((data.expiresAt - Date.now()) / 1000);
|
|
974
|
-
return remaining > 0 ? remaining : null;
|
|
975
|
-
} catch {
|
|
976
|
-
return null;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
lock(name, seconds = 10) {
|
|
980
|
-
const normalizedName = normalizeCacheKey(name);
|
|
981
|
-
const lockFile = import_node_path.join(this.options.directory, `.lock-${syncHashKey(normalizedName)}`);
|
|
982
|
-
const ttlMillis = Math.max(1, seconds) * 1000;
|
|
983
|
-
const owner = crypto.randomUUID();
|
|
984
|
-
const runtime = this.runtime;
|
|
985
|
-
const isProcessAlive = (pid) => {
|
|
986
|
-
try {
|
|
987
|
-
process.kill(pid, 0);
|
|
988
|
-
return true;
|
|
989
|
-
} catch {
|
|
990
|
-
return false;
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
const tryAcquire = async () => {
|
|
994
|
-
await this.ensureDir();
|
|
995
|
-
try {
|
|
996
|
-
const lockData = {
|
|
997
|
-
owner,
|
|
998
|
-
expiresAt: Date.now() + ttlMillis,
|
|
999
|
-
pid: process.pid
|
|
1000
|
-
};
|
|
1001
|
-
await import_core.runtimeWriteFileExclusive(runtime, lockFile, JSON.stringify(lockData));
|
|
1002
|
-
return true;
|
|
1003
|
-
} catch {
|
|
1004
|
-
try {
|
|
1005
|
-
const raw = await import_core.runtimeReadText(runtime, lockFile);
|
|
1006
|
-
const data = JSON.parse(raw);
|
|
1007
|
-
const isExpiredLock = !data.expiresAt || Date.now() > data.expiresAt;
|
|
1008
|
-
const isProcessDead = data.pid && !isProcessAlive(data.pid);
|
|
1009
|
-
if (isExpiredLock || isProcessDead) {
|
|
1010
|
-
await import_core.runtimeRemoveRecursive(runtime, lockFile);
|
|
1011
|
-
}
|
|
1012
|
-
} catch {}
|
|
1013
|
-
return false;
|
|
1014
|
-
}
|
|
1015
|
-
};
|
|
1016
|
-
return {
|
|
1017
|
-
async acquire() {
|
|
1018
|
-
return tryAcquire();
|
|
1019
|
-
},
|
|
1020
|
-
async release() {
|
|
1021
|
-
try {
|
|
1022
|
-
const raw = await import_core.runtimeReadText(runtime, lockFile);
|
|
1023
|
-
const data = JSON.parse(raw);
|
|
1024
|
-
if (data.owner === owner) {
|
|
1025
|
-
await import_core.runtimeRemoveRecursive(runtime, lockFile);
|
|
1026
|
-
}
|
|
1027
|
-
} catch {}
|
|
1028
|
-
},
|
|
1029
|
-
async block(secondsToWait, callback, options) {
|
|
1030
|
-
const deadline = Date.now() + Math.max(0, secondsToWait) * 1000;
|
|
1031
|
-
const sleepMillis = options?.sleepMillis ?? 150;
|
|
1032
|
-
while (Date.now() <= deadline) {
|
|
1033
|
-
if (await this.acquire()) {
|
|
1034
|
-
try {
|
|
1035
|
-
return await callback();
|
|
1036
|
-
} finally {
|
|
1037
|
-
await this.release();
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
await sleep(sleepMillis);
|
|
1041
|
-
}
|
|
1042
|
-
throw new LockTimeoutError(`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`);
|
|
1043
|
-
}
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
function hashKey(key) {
|
|
1048
|
-
return import_ffi.NativeHasher.sha256(key);
|
|
1049
|
-
}
|
|
1050
|
-
function syncHashKey(key) {
|
|
1051
|
-
let hash = 5381;
|
|
1052
|
-
for (let i = 0;i < key.length; i++) {
|
|
1053
|
-
hash = (hash << 5) + hash ^ key.charCodeAt(i);
|
|
1054
|
-
hash = hash >>> 0;
|
|
1055
|
-
}
|
|
1056
|
-
return hash.toString(16).padStart(8, "0");
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// src/utils/LRUCache.ts
|
|
1060
|
-
class LRUCache {
|
|
1061
|
-
maxSize;
|
|
1062
|
-
onEvict;
|
|
1063
|
-
map = new Map;
|
|
1064
|
-
head = null;
|
|
1065
|
-
tail = null;
|
|
1066
|
-
constructor(maxSize, onEvict) {
|
|
1067
|
-
this.maxSize = maxSize;
|
|
1068
|
-
this.onEvict = onEvict;
|
|
1069
|
-
}
|
|
1070
|
-
get size() {
|
|
1071
|
-
return this.map.size;
|
|
1072
|
-
}
|
|
1073
|
-
has(key) {
|
|
1074
|
-
return this.map.has(key);
|
|
1075
|
-
}
|
|
1076
|
-
get(key) {
|
|
1077
|
-
const node = this.map.get(key);
|
|
1078
|
-
if (!node) {
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
this.moveToHead(node);
|
|
1082
|
-
return node.value;
|
|
1083
|
-
}
|
|
1084
|
-
peek(key) {
|
|
1085
|
-
const node = this.map.get(key);
|
|
1086
|
-
return node?.value;
|
|
1087
|
-
}
|
|
1088
|
-
set(key, value) {
|
|
1089
|
-
const existingNode = this.map.get(key);
|
|
1090
|
-
if (existingNode) {
|
|
1091
|
-
existingNode.value = value;
|
|
1092
|
-
this.moveToHead(existingNode);
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
if (this.maxSize > 0 && this.map.size >= this.maxSize) {
|
|
1096
|
-
this.evict();
|
|
1097
|
-
}
|
|
1098
|
-
const newNode = {
|
|
1099
|
-
key,
|
|
1100
|
-
value,
|
|
1101
|
-
prev: null,
|
|
1102
|
-
next: this.head
|
|
1103
|
-
};
|
|
1104
|
-
if (this.head) {
|
|
1105
|
-
this.head.prev = newNode;
|
|
1106
|
-
}
|
|
1107
|
-
this.head = newNode;
|
|
1108
|
-
if (!this.tail) {
|
|
1109
|
-
this.tail = newNode;
|
|
1110
|
-
}
|
|
1111
|
-
this.map.set(key, newNode);
|
|
1112
|
-
}
|
|
1113
|
-
delete(key) {
|
|
1114
|
-
const node = this.map.get(key);
|
|
1115
|
-
if (!node) {
|
|
1116
|
-
return false;
|
|
1117
|
-
}
|
|
1118
|
-
this.removeNode(node);
|
|
1119
|
-
this.map.delete(key);
|
|
1120
|
-
return true;
|
|
1121
|
-
}
|
|
1122
|
-
clear() {
|
|
1123
|
-
this.map.clear();
|
|
1124
|
-
this.head = null;
|
|
1125
|
-
this.tail = null;
|
|
1126
|
-
}
|
|
1127
|
-
moveToHead(node) {
|
|
1128
|
-
if (node === this.head) {
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
if (node.prev) {
|
|
1132
|
-
node.prev.next = node.next;
|
|
1133
|
-
}
|
|
1134
|
-
if (node.next) {
|
|
1135
|
-
node.next.prev = node.prev;
|
|
1136
|
-
}
|
|
1137
|
-
if (node === this.tail) {
|
|
1138
|
-
this.tail = node.prev;
|
|
1139
|
-
}
|
|
1140
|
-
node.prev = null;
|
|
1141
|
-
node.next = this.head;
|
|
1142
|
-
if (this.head) {
|
|
1143
|
-
this.head.prev = node;
|
|
1144
|
-
}
|
|
1145
|
-
this.head = node;
|
|
1146
|
-
}
|
|
1147
|
-
removeNode(node) {
|
|
1148
|
-
if (node.prev) {
|
|
1149
|
-
node.prev.next = node.next;
|
|
1150
|
-
} else {
|
|
1151
|
-
this.head = node.next;
|
|
1152
|
-
}
|
|
1153
|
-
if (node.next) {
|
|
1154
|
-
node.next.prev = node.prev;
|
|
1155
|
-
} else {
|
|
1156
|
-
this.tail = node.prev;
|
|
1157
|
-
}
|
|
1158
|
-
node.prev = null;
|
|
1159
|
-
node.next = null;
|
|
1160
|
-
}
|
|
1161
|
-
evict() {
|
|
1162
|
-
if (!this.tail) {
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
const node = this.tail;
|
|
1166
|
-
if (this.onEvict) {
|
|
1167
|
-
this.onEvict(node.key, node.value);
|
|
1168
|
-
}
|
|
1169
|
-
this.removeNode(node);
|
|
1170
|
-
this.map.delete(node.key);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// src/stores/MemoryStore.ts
|
|
1175
|
-
class MemoryStore {
|
|
1176
|
-
entries;
|
|
1177
|
-
locks = new Map;
|
|
1178
|
-
stats = { hits: 0, misses: 0, evictions: 0 };
|
|
1179
|
-
tagToKeys = new Map;
|
|
1180
|
-
keyToTags = new Map;
|
|
1181
|
-
constructor(options = {}) {
|
|
1182
|
-
this.entries = new LRUCache(options.maxItems ?? 0, (key) => {
|
|
1183
|
-
this.tagIndexRemove(key);
|
|
1184
|
-
this.stats.evictions++;
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
getStats() {
|
|
1188
|
-
const total = this.stats.hits + this.stats.misses;
|
|
1189
|
-
return {
|
|
1190
|
-
hits: this.stats.hits,
|
|
1191
|
-
misses: this.stats.misses,
|
|
1192
|
-
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
1193
|
-
size: this.entries.size,
|
|
1194
|
-
evictions: this.stats.evictions
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
cleanupExpired(key, now = Date.now()) {
|
|
1198
|
-
const entry = this.entries.peek(key);
|
|
1199
|
-
if (!entry) {
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
if (isExpired(entry.expiresAt, now)) {
|
|
1203
|
-
this.forget(key);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
async get(key) {
|
|
1207
|
-
const normalized = normalizeCacheKey(key);
|
|
1208
|
-
const entry = this.entries.get(normalized);
|
|
1209
|
-
if (!entry) {
|
|
1210
|
-
this.stats.misses++;
|
|
1211
|
-
return null;
|
|
1212
|
-
}
|
|
1213
|
-
if (isExpired(entry.expiresAt)) {
|
|
1214
|
-
await this.forget(normalized);
|
|
1215
|
-
this.stats.misses++;
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
this.stats.hits++;
|
|
1219
|
-
return entry.value;
|
|
1220
|
-
}
|
|
1221
|
-
async put(key, value, ttl) {
|
|
1222
|
-
const normalized = normalizeCacheKey(key);
|
|
1223
|
-
const expiresAt = ttlToExpiresAt(ttl);
|
|
1224
|
-
if (expiresAt !== null && expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
1225
|
-
await this.forget(normalized);
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
this.entries.set(normalized, { value, expiresAt: expiresAt ?? null });
|
|
1229
|
-
}
|
|
1230
|
-
async add(key, value, ttl) {
|
|
1231
|
-
const normalized = normalizeCacheKey(key);
|
|
1232
|
-
this.cleanupExpired(normalized);
|
|
1233
|
-
if (this.entries.has(normalized)) {
|
|
1234
|
-
return false;
|
|
1235
|
-
}
|
|
1236
|
-
await this.put(normalized, value, ttl);
|
|
1237
|
-
return true;
|
|
1238
|
-
}
|
|
1239
|
-
async forget(key) {
|
|
1240
|
-
const normalized = normalizeCacheKey(key);
|
|
1241
|
-
const existed = this.entries.delete(normalized);
|
|
1242
|
-
this.tagIndexRemove(normalized);
|
|
1243
|
-
return existed;
|
|
1244
|
-
}
|
|
1245
|
-
async flush() {
|
|
1246
|
-
this.entries.clear();
|
|
1247
|
-
this.tagToKeys.clear();
|
|
1248
|
-
this.keyToTags.clear();
|
|
1249
|
-
}
|
|
1250
|
-
async increment(key, value = 1) {
|
|
1251
|
-
const normalized = normalizeCacheKey(key);
|
|
1252
|
-
const current = await this.get(normalized);
|
|
1253
|
-
const next = (current ?? 0) + value;
|
|
1254
|
-
await this.put(normalized, next, null);
|
|
1255
|
-
return next;
|
|
1256
|
-
}
|
|
1257
|
-
async decrement(key, value = 1) {
|
|
1258
|
-
return this.increment(key, -value);
|
|
1259
|
-
}
|
|
1260
|
-
async ttl(key) {
|
|
1261
|
-
const normalized = normalizeCacheKey(key);
|
|
1262
|
-
const entry = this.entries.peek(normalized);
|
|
1263
|
-
if (!entry || entry.expiresAt === null) {
|
|
1264
|
-
return null;
|
|
1265
|
-
}
|
|
1266
|
-
const now = Date.now();
|
|
1267
|
-
if (isExpired(entry.expiresAt, now)) {
|
|
1268
|
-
await this.forget(normalized);
|
|
1269
|
-
return null;
|
|
1270
|
-
}
|
|
1271
|
-
return Math.max(0, Math.ceil((entry.expiresAt - now) / 1000));
|
|
1272
|
-
}
|
|
1273
|
-
lock(name, seconds = 10) {
|
|
1274
|
-
const lockKey = `lock:${normalizeCacheKey(name)}`;
|
|
1275
|
-
const ttlMillis = Math.max(1, seconds) * 1000;
|
|
1276
|
-
const locks = this.locks;
|
|
1277
|
-
const acquire = async () => {
|
|
1278
|
-
const now = Date.now();
|
|
1279
|
-
const existing = locks.get(lockKey);
|
|
1280
|
-
if (existing && existing.expiresAt > now) {
|
|
1281
|
-
return { ok: false };
|
|
1282
|
-
}
|
|
1283
|
-
const owner2 = crypto.randomUUID();
|
|
1284
|
-
locks.set(lockKey, { owner: owner2, expiresAt: now + ttlMillis });
|
|
1285
|
-
return { ok: true, owner: owner2 };
|
|
1286
|
-
};
|
|
1287
|
-
let owner;
|
|
1288
|
-
return {
|
|
1289
|
-
async acquire() {
|
|
1290
|
-
const result = await acquire();
|
|
1291
|
-
if (!result.ok) {
|
|
1292
|
-
return false;
|
|
1293
|
-
}
|
|
1294
|
-
owner = result.owner;
|
|
1295
|
-
return true;
|
|
1296
|
-
},
|
|
1297
|
-
async release() {
|
|
1298
|
-
if (!owner) {
|
|
1299
|
-
return;
|
|
1300
|
-
}
|
|
1301
|
-
const existing = locks.get(lockKey);
|
|
1302
|
-
if (existing?.owner === owner) {
|
|
1303
|
-
locks.delete(lockKey);
|
|
1304
|
-
}
|
|
1305
|
-
owner = undefined;
|
|
1306
|
-
},
|
|
1307
|
-
async block(secondsToWait, callback, options) {
|
|
1308
|
-
const deadline = Date.now() + Math.max(0, secondsToWait) * 1000;
|
|
1309
|
-
const sleepMillis = options?.sleepMillis ?? 150;
|
|
1310
|
-
while (Date.now() <= deadline) {
|
|
1311
|
-
if (await this.acquire()) {
|
|
1312
|
-
try {
|
|
1313
|
-
return await callback();
|
|
1314
|
-
} finally {
|
|
1315
|
-
await this.release();
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
await sleep(sleepMillis);
|
|
1319
|
-
}
|
|
1320
|
-
throw new LockTimeoutError(`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`);
|
|
1321
|
-
}
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
tagKey(key, tags) {
|
|
1325
|
-
const normalizedKey = normalizeCacheKey(key);
|
|
1326
|
-
const normalizedTags = [...tags].map(String).filter(Boolean).sort();
|
|
1327
|
-
if (normalizedTags.length === 0) {
|
|
1328
|
-
return normalizedKey;
|
|
1329
|
-
}
|
|
1330
|
-
return `tags:${normalizedTags.join("|")}:${normalizedKey}`;
|
|
1331
|
-
}
|
|
1332
|
-
tagIndexAdd(tags, taggedKey) {
|
|
1333
|
-
const normalizedTags = [...tags].map(String).filter(Boolean);
|
|
1334
|
-
if (normalizedTags.length === 0) {
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
for (const tag of normalizedTags) {
|
|
1338
|
-
let keys = this.tagToKeys.get(tag);
|
|
1339
|
-
if (!keys) {
|
|
1340
|
-
keys = new Set;
|
|
1341
|
-
this.tagToKeys.set(tag, keys);
|
|
1342
|
-
}
|
|
1343
|
-
keys.add(taggedKey);
|
|
1344
|
-
}
|
|
1345
|
-
let tagSet = this.keyToTags.get(taggedKey);
|
|
1346
|
-
if (!tagSet) {
|
|
1347
|
-
tagSet = new Set;
|
|
1348
|
-
this.keyToTags.set(taggedKey, tagSet);
|
|
1349
|
-
}
|
|
1350
|
-
for (const tag of normalizedTags) {
|
|
1351
|
-
tagSet.add(tag);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
tagIndexRemove(taggedKey) {
|
|
1355
|
-
const tags = this.keyToTags.get(taggedKey);
|
|
1356
|
-
if (!tags) {
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1359
|
-
for (const tag of tags) {
|
|
1360
|
-
const keys = this.tagToKeys.get(tag);
|
|
1361
|
-
if (!keys) {
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
keys.delete(taggedKey);
|
|
1365
|
-
if (keys.size === 0) {
|
|
1366
|
-
this.tagToKeys.delete(tag);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
this.keyToTags.delete(taggedKey);
|
|
1370
|
-
}
|
|
1371
|
-
async flushTags(tags) {
|
|
1372
|
-
const normalizedTags = [...tags].map(String).filter(Boolean);
|
|
1373
|
-
if (normalizedTags.length === 0) {
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
const keysToDelete = new Set;
|
|
1377
|
-
for (const tag of normalizedTags) {
|
|
1378
|
-
const keys = this.tagToKeys.get(tag);
|
|
1379
|
-
if (!keys) {
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
for (const k of keys) {
|
|
1383
|
-
keysToDelete.add(k);
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
for (const key of keysToDelete) {
|
|
1387
|
-
await this.forget(key);
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// src/stores/NullStore.ts
|
|
1393
|
-
class NullStore {
|
|
1394
|
-
async get(_key) {
|
|
1395
|
-
return null;
|
|
1396
|
-
}
|
|
1397
|
-
async put(_key, _value, _ttl) {}
|
|
1398
|
-
async add(_key, _value, _ttl) {
|
|
1399
|
-
return false;
|
|
1400
|
-
}
|
|
1401
|
-
async forget(_key) {
|
|
1402
|
-
return false;
|
|
1403
|
-
}
|
|
1404
|
-
async flush() {}
|
|
1405
|
-
async increment(_key, _value = 1) {
|
|
1406
|
-
return 0;
|
|
1407
|
-
}
|
|
1408
|
-
async decrement(_key, _value = 1) {
|
|
1409
|
-
return 0;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// src/stores/PredictiveStore.ts
|
|
1414
|
-
class PredictiveStore {
|
|
1415
|
-
store;
|
|
1416
|
-
predictor;
|
|
1417
|
-
constructor(store, options = {}) {
|
|
1418
|
-
this.store = store;
|
|
1419
|
-
this.predictor = options.predictor ?? new MarkovPredictor;
|
|
1420
|
-
}
|
|
1421
|
-
async get(key) {
|
|
1422
|
-
this.predictor.record(key);
|
|
1423
|
-
const candidates = this.predictor.predict(key);
|
|
1424
|
-
if (candidates.length > 0) {
|
|
1425
|
-
Promise.all(candidates.map((k) => this.store.get(k).catch(() => {})));
|
|
1426
|
-
}
|
|
1427
|
-
return this.store.get(key);
|
|
1428
|
-
}
|
|
1429
|
-
async put(key, value, ttl) {
|
|
1430
|
-
return this.store.put(key, value, ttl);
|
|
1431
|
-
}
|
|
1432
|
-
async add(key, value, ttl) {
|
|
1433
|
-
return this.store.add(key, value, ttl);
|
|
1434
|
-
}
|
|
1435
|
-
async forget(key) {
|
|
1436
|
-
return this.store.forget(key);
|
|
1437
|
-
}
|
|
1438
|
-
async flush() {
|
|
1439
|
-
if (typeof this.predictor.reset === "function") {
|
|
1440
|
-
this.predictor.reset();
|
|
1441
|
-
}
|
|
1442
|
-
return this.store.flush();
|
|
1443
|
-
}
|
|
1444
|
-
async increment(key, value) {
|
|
1445
|
-
return this.store.increment(key, value);
|
|
1446
|
-
}
|
|
1447
|
-
async decrement(key, value) {
|
|
1448
|
-
return this.store.decrement(key, value);
|
|
1449
|
-
}
|
|
1450
|
-
lock(name, seconds) {
|
|
1451
|
-
return this.store.lock ? this.store.lock(name, seconds) : undefined;
|
|
1452
|
-
}
|
|
1453
|
-
async ttl(key) {
|
|
1454
|
-
return this.store.ttl ? this.store.ttl(key) : null;
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
// src/stores/RedisStore.ts
|
|
1459
|
-
var import_plasma = require("@gravito/plasma");
|
|
1460
|
-
class RedisStore {
|
|
1461
|
-
connectionName;
|
|
1462
|
-
constructor(options = {}) {
|
|
1463
|
-
this.connectionName = options.connection;
|
|
1464
|
-
}
|
|
1465
|
-
get client() {
|
|
1466
|
-
return import_plasma.Redis.connection(this.connectionName);
|
|
1467
|
-
}
|
|
1468
|
-
async get(key) {
|
|
1469
|
-
const normalized = normalizeCacheKey(key);
|
|
1470
|
-
const value = await this.client.get(normalized);
|
|
1471
|
-
if (value === null) {
|
|
1472
|
-
return null;
|
|
1473
|
-
}
|
|
1474
|
-
try {
|
|
1475
|
-
return JSON.parse(value);
|
|
1476
|
-
} catch {
|
|
1477
|
-
return value;
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
async put(key, value, ttl) {
|
|
1481
|
-
const normalized = normalizeCacheKey(key);
|
|
1482
|
-
const serialized = JSON.stringify(value);
|
|
1483
|
-
const expiresAt = ttlToExpiresAt(ttl);
|
|
1484
|
-
const options = {};
|
|
1485
|
-
if (expiresAt) {
|
|
1486
|
-
const ttlMs = Math.max(1, expiresAt - Date.now());
|
|
1487
|
-
options.px = ttlMs;
|
|
1488
|
-
}
|
|
1489
|
-
await this.client.set(normalized, serialized, options);
|
|
1490
|
-
}
|
|
1491
|
-
async add(key, value, ttl) {
|
|
1492
|
-
const normalized = normalizeCacheKey(key);
|
|
1493
|
-
const serialized = JSON.stringify(value);
|
|
1494
|
-
const expiresAt = ttlToExpiresAt(ttl);
|
|
1495
|
-
const options = { nx: true };
|
|
1496
|
-
if (expiresAt) {
|
|
1497
|
-
const ttlMs = Math.max(1, expiresAt - Date.now());
|
|
1498
|
-
options.px = ttlMs;
|
|
1499
|
-
}
|
|
1500
|
-
const result = await this.client.set(normalized, serialized, options);
|
|
1501
|
-
return result === "OK";
|
|
1502
|
-
}
|
|
1503
|
-
async forget(key) {
|
|
1504
|
-
const normalized = normalizeCacheKey(key);
|
|
1505
|
-
const luaScript = `
|
|
1506
|
-
local key = KEYS[1]
|
|
1507
|
-
local tag_prefix = ARGV[1]
|
|
1508
|
-
local tags = redis.call('SMEMBERS', key .. ':tags')
|
|
1509
|
-
local del_result = redis.call('DEL', key)
|
|
1510
|
-
for _, tag in ipairs(tags) do
|
|
1511
|
-
redis.call('SREM', tag_prefix .. tag, key)
|
|
1512
|
-
end
|
|
1513
|
-
redis.call('DEL', key .. ':tags')
|
|
1514
|
-
return del_result
|
|
1515
|
-
`;
|
|
1516
|
-
const client = this.client;
|
|
1517
|
-
const result = await client.eval(luaScript, 1, normalized, "tag:");
|
|
1518
|
-
return result > 0;
|
|
1519
|
-
}
|
|
1520
|
-
async flush() {
|
|
1521
|
-
await this.client.flushdb();
|
|
1522
|
-
}
|
|
1523
|
-
async increment(key, value = 1) {
|
|
1524
|
-
const normalized = normalizeCacheKey(key);
|
|
1525
|
-
if (value === 1) {
|
|
1526
|
-
return await this.client.incr(normalized);
|
|
1527
|
-
}
|
|
1528
|
-
return await this.client.incrby(normalized, value);
|
|
1529
|
-
}
|
|
1530
|
-
async decrement(key, value = 1) {
|
|
1531
|
-
const normalized = normalizeCacheKey(key);
|
|
1532
|
-
if (value === 1) {
|
|
1533
|
-
return await this.client.decr(normalized);
|
|
1534
|
-
}
|
|
1535
|
-
return await this.client.decrby(normalized, value);
|
|
1536
|
-
}
|
|
1537
|
-
tagKey(key, _tags) {
|
|
1538
|
-
return key;
|
|
1539
|
-
}
|
|
1540
|
-
async tagIndexAdd(tags, taggedKey) {
|
|
1541
|
-
if (tags.length === 0) {
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
const pipeline = this.client.pipeline();
|
|
1545
|
-
pipeline.sadd(`${taggedKey}:tags`, ...tags);
|
|
1546
|
-
for (const tag of tags) {
|
|
1547
|
-
const tagSetKey = `tag:${tag}`;
|
|
1548
|
-
pipeline.sadd(tagSetKey, taggedKey);
|
|
1549
|
-
}
|
|
1550
|
-
await pipeline.exec();
|
|
1551
|
-
}
|
|
1552
|
-
async tagIndexRemove(taggedKey) {
|
|
1553
|
-
const luaScript = `
|
|
1554
|
-
local key = KEYS[1]
|
|
1555
|
-
local tag_prefix = ARGV[1]
|
|
1556
|
-
local tags = redis.call('SMEMBERS', key .. ':tags')
|
|
1557
|
-
for _, tag in ipairs(tags) do
|
|
1558
|
-
redis.call('SREM', tag_prefix .. tag, key)
|
|
1559
|
-
end
|
|
1560
|
-
redis.call('DEL', key .. ':tags')
|
|
1561
|
-
`;
|
|
1562
|
-
const client = this.client;
|
|
1563
|
-
await client.eval(luaScript, 1, taggedKey, "tag:");
|
|
1564
|
-
}
|
|
1565
|
-
async flushTags(tags) {
|
|
1566
|
-
if (tags.length === 0) {
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
const tagKeys = tags.map((tag) => `tag:${tag}`);
|
|
1570
|
-
const pipeline = this.client.pipeline();
|
|
1571
|
-
for (const tagKey of tagKeys) {
|
|
1572
|
-
pipeline.smembers(tagKey);
|
|
1573
|
-
}
|
|
1574
|
-
const results = await pipeline.exec();
|
|
1575
|
-
const keysToDelete = new Set;
|
|
1576
|
-
for (const [err, keys] of results) {
|
|
1577
|
-
if (!err && Array.isArray(keys)) {
|
|
1578
|
-
for (const k of keys) {
|
|
1579
|
-
keysToDelete.add(k);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
if (keysToDelete.size > 0) {
|
|
1584
|
-
await this.client.del(...Array.from(keysToDelete));
|
|
1585
|
-
}
|
|
1586
|
-
await this.client.del(...tagKeys);
|
|
1587
|
-
}
|
|
1588
|
-
async ttl(key) {
|
|
1589
|
-
const normalized = normalizeCacheKey(key);
|
|
1590
|
-
const result = await this.client.ttl(normalized);
|
|
1591
|
-
return result < 0 ? null : result;
|
|
1592
|
-
}
|
|
1593
|
-
lock(name, seconds = 10) {
|
|
1594
|
-
const lockKey = `lock:${normalizeCacheKey(name)}`;
|
|
1595
|
-
const owner = crypto.randomUUID();
|
|
1596
|
-
const ttlMs = Math.max(1, seconds) * 1000;
|
|
1597
|
-
const client = this.client;
|
|
1598
|
-
return {
|
|
1599
|
-
async acquire() {
|
|
1600
|
-
const result = await client.set(lockKey, owner, { nx: true, px: ttlMs });
|
|
1601
|
-
return result === "OK";
|
|
1602
|
-
},
|
|
1603
|
-
async release() {
|
|
1604
|
-
const luaScript = `
|
|
1605
|
-
local current = redis.call('GET', KEYS[1])
|
|
1606
|
-
if current == ARGV[1] then
|
|
1607
|
-
return redis.call('DEL', KEYS[1])
|
|
1608
|
-
else
|
|
1609
|
-
return 0
|
|
1610
|
-
end
|
|
1611
|
-
`;
|
|
1612
|
-
const evalClient = client;
|
|
1613
|
-
await evalClient.eval(luaScript, 1, lockKey, owner);
|
|
1614
|
-
},
|
|
1615
|
-
async extend(extensionSeconds) {
|
|
1616
|
-
const luaScript = `
|
|
1617
|
-
local current = redis.call('GET', KEYS[1])
|
|
1618
|
-
if current == ARGV[1] then
|
|
1619
|
-
return redis.call('EXPIRE', KEYS[1], ARGV[2])
|
|
1620
|
-
else
|
|
1621
|
-
return 0
|
|
1622
|
-
end
|
|
1623
|
-
`;
|
|
1624
|
-
const evalClient = client;
|
|
1625
|
-
const result = await evalClient.eval(luaScript, 1, lockKey, owner, extensionSeconds.toString());
|
|
1626
|
-
return result === 1;
|
|
1627
|
-
},
|
|
1628
|
-
async getRemainingTime() {
|
|
1629
|
-
return await client.ttl(lockKey);
|
|
1630
|
-
},
|
|
1631
|
-
async block(secondsToWait, callback, options) {
|
|
1632
|
-
const retryInterval = options?.retryInterval ?? options?.sleepMillis ?? 100;
|
|
1633
|
-
const maxRetries = options?.maxRetries ?? Number.POSITIVE_INFINITY;
|
|
1634
|
-
const signal = options?.signal;
|
|
1635
|
-
const deadline = Date.now() + Math.max(0, secondsToWait) * 1000;
|
|
1636
|
-
let attempt = 0;
|
|
1637
|
-
while (Date.now() <= deadline && attempt < maxRetries) {
|
|
1638
|
-
if (signal?.aborted) {
|
|
1639
|
-
throw new Error(`Lock acquisition for '${name}' was aborted`);
|
|
1640
|
-
}
|
|
1641
|
-
if (await this.acquire()) {
|
|
1642
|
-
try {
|
|
1643
|
-
return await callback();
|
|
1644
|
-
} finally {
|
|
1645
|
-
await this.release();
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
attempt++;
|
|
1649
|
-
const delay = Math.min(retryInterval * 1.5 ** Math.min(attempt, 10), 1000);
|
|
1650
|
-
await sleep(delay);
|
|
1651
|
-
}
|
|
1652
|
-
throw new LockTimeoutError(`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`);
|
|
1653
|
-
}
|
|
1654
|
-
};
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
// src/stores/TieredStore.ts
|
|
1659
|
-
class TieredStore {
|
|
1660
|
-
local;
|
|
1661
|
-
remote;
|
|
1662
|
-
constructor(local, remote) {
|
|
1663
|
-
this.local = local;
|
|
1664
|
-
this.remote = remote;
|
|
1665
|
-
}
|
|
1666
|
-
async get(key) {
|
|
1667
|
-
const localValue = await this.local.get(key);
|
|
1668
|
-
if (localValue !== null) {
|
|
1669
|
-
return localValue;
|
|
1670
|
-
}
|
|
1671
|
-
const remoteValue = await this.remote.get(key);
|
|
1672
|
-
if (remoteValue !== null) {
|
|
1673
|
-
const ttl = this.remote.ttl ? await this.remote.ttl(key) : null;
|
|
1674
|
-
await this.local.put(key, remoteValue, ttl);
|
|
1675
|
-
}
|
|
1676
|
-
return remoteValue;
|
|
1677
|
-
}
|
|
1678
|
-
async put(key, value, ttl) {
|
|
1679
|
-
await Promise.all([this.local.put(key, value, ttl), this.remote.put(key, value, ttl)]);
|
|
1680
|
-
}
|
|
1681
|
-
async add(key, value, ttl) {
|
|
1682
|
-
const ok = await this.remote.add(key, value, ttl);
|
|
1683
|
-
if (ok) {
|
|
1684
|
-
await this.local.put(key, value, ttl);
|
|
1685
|
-
}
|
|
1686
|
-
return ok;
|
|
1687
|
-
}
|
|
1688
|
-
async forget(key) {
|
|
1689
|
-
const [localOk, remoteOk] = await Promise.all([this.local.forget(key), this.remote.forget(key)]);
|
|
1690
|
-
return localOk || remoteOk;
|
|
1691
|
-
}
|
|
1692
|
-
async flush() {
|
|
1693
|
-
await Promise.all([this.local.flush(), this.remote.flush()]);
|
|
1694
|
-
}
|
|
1695
|
-
async increment(key, value = 1) {
|
|
1696
|
-
const next = await this.remote.increment(key, value);
|
|
1697
|
-
const ttl = this.remote.ttl ? await this.remote.ttl(key) : null;
|
|
1698
|
-
await this.local.put(key, next, ttl);
|
|
1699
|
-
return next;
|
|
1700
|
-
}
|
|
1701
|
-
async decrement(key, value = 1) {
|
|
1702
|
-
return this.increment(key, -value);
|
|
1703
|
-
}
|
|
1704
|
-
async ttl(key) {
|
|
1705
|
-
if (this.remote.ttl) {
|
|
1706
|
-
return this.remote.ttl(key);
|
|
1707
|
-
}
|
|
1708
|
-
return this.local.ttl ? this.local.ttl(key) : null;
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
// src/index.ts
|
|
1713
|
-
class MemoryCacheProvider {
|
|
1714
|
-
store = new MemoryStore;
|
|
1715
|
-
async get(key) {
|
|
1716
|
-
return this.store.get(key);
|
|
1717
|
-
}
|
|
1718
|
-
async set(key, value, ttl = 60) {
|
|
1719
|
-
await this.store.put(key, value, ttl);
|
|
1720
|
-
}
|
|
1721
|
-
async delete(key) {
|
|
1722
|
-
await this.store.forget(key);
|
|
1723
|
-
}
|
|
1724
|
-
async clear() {
|
|
1725
|
-
await this.store.flush();
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
function resolveStoreConfig(core, options) {
|
|
1729
|
-
if (options) {
|
|
1730
|
-
return options;
|
|
1731
|
-
}
|
|
1732
|
-
if (core.config.has("cache")) {
|
|
1733
|
-
return core.config.get("cache");
|
|
1734
|
-
}
|
|
1735
|
-
return {};
|
|
1736
|
-
}
|
|
1737
|
-
function createStoreFactory(config) {
|
|
1738
|
-
const stores = config.stores ?? {};
|
|
1739
|
-
const defaultSeconds = typeof config.defaultTtl === "number" ? config.defaultTtl : 60;
|
|
1740
|
-
return (name) => {
|
|
1741
|
-
const storeConfig = stores[name];
|
|
1742
|
-
const hasExplicitStores = Object.keys(stores).length > 0;
|
|
1743
|
-
if (!storeConfig) {
|
|
1744
|
-
if (name === "memory") {
|
|
1745
|
-
return new MemoryStore;
|
|
1746
|
-
}
|
|
1747
|
-
if (name === "null") {
|
|
1748
|
-
return new NullStore;
|
|
1749
|
-
}
|
|
1750
|
-
if (hasExplicitStores) {
|
|
1751
|
-
throw new Error(`Cache store '${name}' is not defined.`);
|
|
1752
|
-
}
|
|
1753
|
-
return new MemoryStore;
|
|
1754
|
-
}
|
|
1755
|
-
if (storeConfig.driver === "memory") {
|
|
1756
|
-
return new MemoryStore({ maxItems: storeConfig.maxItems });
|
|
1757
|
-
}
|
|
1758
|
-
if (storeConfig.driver === "file") {
|
|
1759
|
-
return new FileStore({ directory: storeConfig.directory });
|
|
1760
|
-
}
|
|
1761
|
-
if (storeConfig.driver === "redis") {
|
|
1762
|
-
return new RedisStore({ connection: storeConfig.connection, prefix: storeConfig.prefix });
|
|
1763
|
-
}
|
|
1764
|
-
if (storeConfig.driver === "null") {
|
|
1765
|
-
return new NullStore;
|
|
1766
|
-
}
|
|
1767
|
-
if (storeConfig.driver === "custom") {
|
|
1768
|
-
return storeConfig.store;
|
|
1769
|
-
}
|
|
1770
|
-
if (storeConfig.driver === "provider") {
|
|
1771
|
-
const provider = storeConfig.provider;
|
|
1772
|
-
if (!provider) {
|
|
1773
|
-
throw new Error(`Cache store '${name}' is missing a provider.`);
|
|
1774
|
-
}
|
|
1775
|
-
return {
|
|
1776
|
-
get: (key) => provider.get(key),
|
|
1777
|
-
put: (key, value, ttl) => provider.set(key, value, typeof ttl === "number" ? ttl : defaultSeconds),
|
|
1778
|
-
add: async (key, value, ttl) => {
|
|
1779
|
-
const existing = await provider.get(key);
|
|
1780
|
-
if (existing !== null) {
|
|
1781
|
-
return false;
|
|
1782
|
-
}
|
|
1783
|
-
await provider.set(key, value, typeof ttl === "number" ? ttl : defaultSeconds);
|
|
1784
|
-
return true;
|
|
1785
|
-
},
|
|
1786
|
-
forget: async (key) => {
|
|
1787
|
-
await provider.delete(key);
|
|
1788
|
-
return true;
|
|
1789
|
-
},
|
|
1790
|
-
flush: () => provider.clear(),
|
|
1791
|
-
increment: async (key, value = 1) => {
|
|
1792
|
-
const current = await provider.get(key);
|
|
1793
|
-
const next = (current ?? 0) + value;
|
|
1794
|
-
await provider.set(key, next, defaultSeconds);
|
|
1795
|
-
return next;
|
|
1796
|
-
},
|
|
1797
|
-
decrement: async (key, value = 1) => {
|
|
1798
|
-
const current = await provider.get(key);
|
|
1799
|
-
const next = (current ?? 0) - value;
|
|
1800
|
-
await provider.set(key, next, defaultSeconds);
|
|
1801
|
-
return next;
|
|
1802
|
-
}
|
|
1803
|
-
};
|
|
1804
|
-
}
|
|
1805
|
-
if (storeConfig.driver === "tiered") {
|
|
1806
|
-
const factory = createStoreFactory(config);
|
|
1807
|
-
return new TieredStore(factory(storeConfig.local), factory(storeConfig.remote));
|
|
1808
|
-
}
|
|
1809
|
-
if (storeConfig.driver === "predictive") {
|
|
1810
|
-
const factory = createStoreFactory(config);
|
|
1811
|
-
return new PredictiveStore(factory(storeConfig.inner), {
|
|
1812
|
-
predictor: new MarkovPredictor({ maxNodes: storeConfig.maxNodes })
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
if (storeConfig.driver === "circuit-breaker") {
|
|
1816
|
-
const factory = createStoreFactory(config);
|
|
1817
|
-
const primary = factory(storeConfig.primary);
|
|
1818
|
-
const fallback = storeConfig.fallback ? factory(storeConfig.fallback) : undefined;
|
|
1819
|
-
return new CircuitBreakerStore(primary, {
|
|
1820
|
-
maxFailures: storeConfig.maxFailures,
|
|
1821
|
-
resetTimeout: storeConfig.resetTimeout,
|
|
1822
|
-
fallback
|
|
1823
|
-
});
|
|
1824
|
-
}
|
|
1825
|
-
throw new Error(`Unsupported cache driver '${storeConfig.driver}'.`);
|
|
1826
|
-
};
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
class OrbitStasis {
|
|
1830
|
-
options;
|
|
1831
|
-
manager;
|
|
1832
|
-
constructor(options) {
|
|
1833
|
-
this.options = options;
|
|
1834
|
-
}
|
|
1835
|
-
install(core) {
|
|
1836
|
-
const resolvedConfig = resolveStoreConfig(core, this.options);
|
|
1837
|
-
const exposeAs = resolvedConfig.exposeAs ?? "cache";
|
|
1838
|
-
const defaultStore = resolvedConfig.default ?? (resolvedConfig.provider ? "default" : "memory");
|
|
1839
|
-
const defaultTtl = resolvedConfig.defaultTtl ?? (typeof resolvedConfig.defaultTTL === "number" ? resolvedConfig.defaultTTL : undefined) ?? 60;
|
|
1840
|
-
const prefix = resolvedConfig.prefix ?? "";
|
|
1841
|
-
const logger = core.logger;
|
|
1842
|
-
logger.info(`[OrbitCache] Initializing Cache (Exposed as: ${exposeAs})`);
|
|
1843
|
-
const events = {
|
|
1844
|
-
hit: (key) => core.hooks.doAction("cache:hit", { key }),
|
|
1845
|
-
miss: (key) => core.hooks.doAction("cache:miss", { key }),
|
|
1846
|
-
write: (key) => core.hooks.doAction("cache:write", { key }),
|
|
1847
|
-
forget: (key) => core.hooks.doAction("cache:forget", { key }),
|
|
1848
|
-
flush: () => core.hooks.doAction("cache:flush", {})
|
|
1849
|
-
};
|
|
1850
|
-
const onEventError = resolvedConfig.onEventError ?? ((error, event, payload) => {
|
|
1851
|
-
const key = payload.key ? ` (key: ${payload.key})` : "";
|
|
1852
|
-
logger.error(`[OrbitCache] cache event '${event}' failed${key}`, error);
|
|
1853
|
-
});
|
|
1854
|
-
const stores = resolvedConfig.stores ?? (resolvedConfig.provider ? { default: { driver: "provider", provider: resolvedConfig.provider } } : undefined);
|
|
1855
|
-
const manager = new CacheManager(createStoreFactory({ ...resolvedConfig, stores }), {
|
|
1856
|
-
default: defaultStore,
|
|
1857
|
-
prefix,
|
|
1858
|
-
defaultTtl
|
|
1859
|
-
}, events, {
|
|
1860
|
-
mode: resolvedConfig.eventsMode ?? "async",
|
|
1861
|
-
throwOnError: resolvedConfig.throwOnEventError,
|
|
1862
|
-
onError: onEventError
|
|
1863
|
-
});
|
|
1864
|
-
this.manager = manager;
|
|
1865
|
-
core.adapter.use("*", async (c, next) => {
|
|
1866
|
-
c.set(exposeAs, manager);
|
|
1867
|
-
return await next();
|
|
1868
|
-
});
|
|
1869
|
-
core.container.instance(exposeAs, manager);
|
|
1870
|
-
core.hooks.doAction("cache:init", manager);
|
|
1871
|
-
}
|
|
1872
|
-
getCache() {
|
|
1873
|
-
if (!this.manager) {
|
|
1874
|
-
throw new Error("OrbitCache not installed yet.");
|
|
1875
|
-
}
|
|
1876
|
-
return this.manager;
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
function orbitCache(core, options = {}) {
|
|
1880
|
-
const orbit = new OrbitStasis(options);
|
|
1881
|
-
orbit.install(core);
|
|
1882
|
-
return orbit.getCache();
|
|
1883
|
-
}
|
|
1884
|
-
var OrbitCache = OrbitStasis;
|
|
1885
|
-
|
|
1886
|
-
//# debugId=66831AF7E477960E64756E2164756E21
|