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