@gravito/stasis 3.1.0 → 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 +64 -80
- package/README.zh-TW.md +40 -79
- 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 +373 -1762
- package/dist/index.cjs.map +27 -0
- package/dist/index.d.ts +29 -2676
- package/dist/index.js +356 -1709
- 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 +8 -6
- 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
|
-
return now
|
|
151
|
+
return now >= expiresAt;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/tagged-store.ts
|
|
155
|
+
class TaggedStore {
|
|
156
|
+
store;
|
|
157
|
+
tags;
|
|
158
|
+
constructor(store, tags) {
|
|
159
|
+
this.store = store;
|
|
160
|
+
this.tags = tags;
|
|
161
|
+
}
|
|
162
|
+
tagged(key) {
|
|
163
|
+
return this.store.tagKey(normalizeCacheKey(key), this.tags);
|
|
164
|
+
}
|
|
165
|
+
async get(key) {
|
|
166
|
+
return this.store.get(this.tagged(key));
|
|
167
|
+
}
|
|
168
|
+
async put(key, value, ttl) {
|
|
169
|
+
const taggedKey = this.tagged(key);
|
|
170
|
+
await this.store.put(taggedKey, value, ttl);
|
|
171
|
+
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
172
|
+
}
|
|
173
|
+
async add(key, value, ttl) {
|
|
174
|
+
const taggedKey = this.tagged(key);
|
|
175
|
+
const ok = await this.store.add(taggedKey, value, ttl);
|
|
176
|
+
if (ok) {
|
|
177
|
+
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
178
|
+
}
|
|
179
|
+
return ok;
|
|
180
|
+
}
|
|
181
|
+
async forget(key) {
|
|
182
|
+
return this.store.forget(this.tagged(key));
|
|
183
|
+
}
|
|
184
|
+
async flush() {
|
|
185
|
+
return this.store.flushTags(this.tags);
|
|
186
|
+
}
|
|
187
|
+
async increment(key, value) {
|
|
188
|
+
const taggedKey = this.tagged(key);
|
|
189
|
+
const next = await this.store.increment(taggedKey, value);
|
|
190
|
+
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
191
|
+
return next;
|
|
192
|
+
}
|
|
193
|
+
async decrement(key, value) {
|
|
194
|
+
const taggedKey = this.tagged(key);
|
|
195
|
+
const next = await this.store.decrement(taggedKey, value);
|
|
196
|
+
this.store.tagIndexAdd(this.tags, taggedKey);
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
199
|
+
lock(name, seconds) {
|
|
200
|
+
return this.store.lock ? this.store.lock(this.tagged(name), seconds) : undefined;
|
|
201
|
+
}
|
|
103
202
|
}
|
|
104
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;
|
|
@@ -1460,36 +703,41 @@ var MarkovPredictor = class {
|
|
|
1460
703
|
minKey = k;
|
|
1461
704
|
}
|
|
1462
705
|
}
|
|
1463
|
-
if (minKey)
|
|
706
|
+
if (minKey) {
|
|
707
|
+
edges.delete(minKey);
|
|
708
|
+
}
|
|
1464
709
|
}
|
|
1465
710
|
}
|
|
1466
711
|
this.lastKey = key;
|
|
1467
712
|
}
|
|
1468
713
|
predict(key) {
|
|
1469
714
|
const edges = this.transitions.get(key);
|
|
1470
|
-
if (!edges)
|
|
715
|
+
if (!edges) {
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
1471
718
|
return Array.from(edges.entries()).sort((a, b) => b[1] - a[1]).map((entry) => entry[0]);
|
|
1472
719
|
}
|
|
1473
720
|
reset() {
|
|
1474
721
|
this.transitions.clear();
|
|
1475
722
|
this.lastKey = null;
|
|
1476
723
|
}
|
|
1477
|
-
}
|
|
724
|
+
}
|
|
1478
725
|
|
|
1479
726
|
// src/stores/CircuitBreakerStore.ts
|
|
1480
|
-
|
|
727
|
+
class CircuitBreakerStore {
|
|
728
|
+
primary;
|
|
729
|
+
state = "CLOSED";
|
|
730
|
+
failures = 0;
|
|
731
|
+
lastErrorTime = 0;
|
|
732
|
+
options;
|
|
1481
733
|
constructor(primary, options = {}) {
|
|
1482
734
|
this.primary = primary;
|
|
1483
735
|
this.options = {
|
|
1484
736
|
maxFailures: options.maxFailures ?? 5,
|
|
1485
|
-
resetTimeout: options.resetTimeout ??
|
|
737
|
+
resetTimeout: options.resetTimeout ?? 60000,
|
|
1486
738
|
fallback: options.fallback
|
|
1487
739
|
};
|
|
1488
740
|
}
|
|
1489
|
-
state = "CLOSED";
|
|
1490
|
-
failures = 0;
|
|
1491
|
-
lastErrorTime = 0;
|
|
1492
|
-
options;
|
|
1493
741
|
async execute(operation, fallbackResult = null) {
|
|
1494
742
|
if (this.state === "OPEN") {
|
|
1495
743
|
if (Date.now() - this.lastErrorTime > this.options.resetTimeout) {
|
|
@@ -1522,8 +770,7 @@ var CircuitBreakerStore = class {
|
|
|
1522
770
|
if (this.options.fallback) {
|
|
1523
771
|
try {
|
|
1524
772
|
return await operation(this.options.fallback);
|
|
1525
|
-
} catch {
|
|
1526
|
-
}
|
|
773
|
+
} catch {}
|
|
1527
774
|
}
|
|
1528
775
|
return fallbackResult;
|
|
1529
776
|
}
|
|
@@ -1551,102 +798,62 @@ var CircuitBreakerStore = class {
|
|
|
1551
798
|
async ttl(key) {
|
|
1552
799
|
return this.execute(async (s) => s.ttl ? s.ttl(key) : null);
|
|
1553
800
|
}
|
|
1554
|
-
/**
|
|
1555
|
-
* Returns current state for monitoring.
|
|
1556
|
-
*
|
|
1557
|
-
* @returns Current state of the circuit breaker.
|
|
1558
|
-
*
|
|
1559
|
-
* @example
|
|
1560
|
-
* ```typescript
|
|
1561
|
-
* const state = store.getState();
|
|
1562
|
-
* if (state === 'OPEN') {
|
|
1563
|
-
* console.warn('Primary cache is unavailable');
|
|
1564
|
-
* }
|
|
1565
|
-
* ```
|
|
1566
|
-
*/
|
|
1567
801
|
getState() {
|
|
1568
802
|
return this.state;
|
|
1569
803
|
}
|
|
1570
|
-
}
|
|
804
|
+
}
|
|
1571
805
|
|
|
1572
806
|
// src/stores/FileStore.ts
|
|
1573
|
-
var
|
|
1574
|
-
var
|
|
1575
|
-
var
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
* @param options - Configuration settings for the store.
|
|
1581
|
-
*/
|
|
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();
|
|
1582
814
|
constructor(options) {
|
|
1583
815
|
this.options = options;
|
|
1584
816
|
if (options.enableCleanup !== false) {
|
|
1585
|
-
this.startCleanupDaemon(options.cleanupInterval ??
|
|
817
|
+
this.startCleanupDaemon(options.cleanupInterval ?? 60000);
|
|
1586
818
|
}
|
|
1587
819
|
}
|
|
1588
|
-
cleanupTimer = null;
|
|
1589
|
-
/**
|
|
1590
|
-
* Starts the background process for periodic cache maintenance.
|
|
1591
|
-
*
|
|
1592
|
-
* @param interval - Time between cleanup cycles in milliseconds.
|
|
1593
|
-
* @internal
|
|
1594
|
-
*/
|
|
1595
820
|
startCleanupDaemon(interval) {
|
|
1596
821
|
this.cleanupTimer = setInterval(() => {
|
|
1597
|
-
this.cleanExpiredFiles().catch(() => {
|
|
1598
|
-
});
|
|
822
|
+
this.cleanExpiredFiles().catch(() => {});
|
|
1599
823
|
}, interval);
|
|
1600
824
|
if (this.cleanupTimer.unref) {
|
|
1601
825
|
this.cleanupTimer.unref();
|
|
1602
826
|
}
|
|
1603
827
|
}
|
|
1604
|
-
/**
|
|
1605
|
-
* Scans the cache directory to remove expired files and enforce capacity limits.
|
|
1606
|
-
*
|
|
1607
|
-
* This method performs a recursive scan of the storage directory. It deletes
|
|
1608
|
-
* files that have passed their expiration time and, if `maxFiles` is configured,
|
|
1609
|
-
* evicts the oldest files to stay within the limit.
|
|
1610
|
-
*
|
|
1611
|
-
* @returns The total number of files removed during this operation.
|
|
1612
|
-
* @throws {Error} If the directory cannot be read or files cannot be deleted.
|
|
1613
|
-
*
|
|
1614
|
-
* @example
|
|
1615
|
-
* ```typescript
|
|
1616
|
-
* const removedCount = await store.cleanExpiredFiles();
|
|
1617
|
-
* console.log(`Cleaned up ${removedCount} files.`);
|
|
1618
|
-
* ```
|
|
1619
|
-
*/
|
|
1620
828
|
async cleanExpiredFiles() {
|
|
1621
829
|
await this.ensureDir();
|
|
1622
830
|
let cleaned = 0;
|
|
1623
831
|
const validFiles = [];
|
|
1624
832
|
const scanDir = async (dir) => {
|
|
1625
|
-
const entries = await (
|
|
833
|
+
const entries = await import_core.runtimeReadDir(this.runtime, dir);
|
|
1626
834
|
for (const entry of entries) {
|
|
1627
|
-
const fullPath =
|
|
1628
|
-
if (entry.isDirectory
|
|
835
|
+
const fullPath = import_node_path.join(dir, entry.name);
|
|
836
|
+
if (entry.isDirectory) {
|
|
1629
837
|
await scanDir(fullPath);
|
|
1630
838
|
try {
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
}
|
|
1634
|
-
} else if (entry.isFile
|
|
839
|
+
const { rmdir } = await import("node:fs/promises");
|
|
840
|
+
await rmdir(fullPath);
|
|
841
|
+
} catch {}
|
|
842
|
+
} else if (entry.isFile) {
|
|
1635
843
|
if (!entry.name.endsWith(".json") || entry.name.startsWith(".lock-")) {
|
|
1636
844
|
continue;
|
|
1637
845
|
}
|
|
1638
846
|
try {
|
|
1639
|
-
const raw = await (
|
|
847
|
+
const raw = await import_core.runtimeReadText(this.runtime, fullPath);
|
|
1640
848
|
const data = JSON.parse(raw);
|
|
1641
849
|
if (isExpired(data.expiresAt)) {
|
|
1642
|
-
await (
|
|
850
|
+
await import_core.runtimeRemoveRecursive(this.runtime, fullPath);
|
|
1643
851
|
cleaned++;
|
|
1644
852
|
} else if (this.options.maxFiles) {
|
|
1645
|
-
const stats = await (
|
|
853
|
+
const stats = await import_core.runtimeStatFull(this.runtime, fullPath);
|
|
1646
854
|
validFiles.push({ path: fullPath, mtime: stats.mtimeMs });
|
|
1647
855
|
}
|
|
1648
|
-
} catch {
|
|
1649
|
-
}
|
|
856
|
+
} catch {}
|
|
1650
857
|
}
|
|
1651
858
|
}
|
|
1652
859
|
};
|
|
@@ -1654,80 +861,39 @@ var FileStore = class {
|
|
|
1654
861
|
if (this.options.maxFiles && validFiles.length > this.options.maxFiles) {
|
|
1655
862
|
validFiles.sort((a, b) => a.mtime - b.mtime);
|
|
1656
863
|
const toRemove = validFiles.slice(0, validFiles.length - this.options.maxFiles);
|
|
1657
|
-
await Promise.all(
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
}
|
|
1664
|
-
})
|
|
1665
|
-
);
|
|
864
|
+
await Promise.all(toRemove.map(async (f) => {
|
|
865
|
+
try {
|
|
866
|
+
await import_core.runtimeRemoveRecursive(this.runtime, f.path);
|
|
867
|
+
cleaned++;
|
|
868
|
+
} catch {}
|
|
869
|
+
}));
|
|
1666
870
|
}
|
|
1667
871
|
return cleaned;
|
|
1668
872
|
}
|
|
1669
|
-
/**
|
|
1670
|
-
* Stops the cleanup daemon and releases associated resources.
|
|
1671
|
-
*
|
|
1672
|
-
* Should be called when the store is no longer needed to prevent memory leaks
|
|
1673
|
-
* and allow the process to exit gracefully.
|
|
1674
|
-
*
|
|
1675
|
-
* @example
|
|
1676
|
-
* ```typescript
|
|
1677
|
-
* await store.destroy();
|
|
1678
|
-
* ```
|
|
1679
|
-
*/
|
|
1680
873
|
async destroy() {
|
|
1681
874
|
if (this.cleanupTimer) {
|
|
1682
875
|
clearInterval(this.cleanupTimer);
|
|
1683
876
|
this.cleanupTimer = null;
|
|
1684
877
|
}
|
|
1685
878
|
}
|
|
1686
|
-
/**
|
|
1687
|
-
* Ensures that the base storage directory exists.
|
|
1688
|
-
*
|
|
1689
|
-
* @throws {Error} If the directory cannot be created due to permissions or path conflicts.
|
|
1690
|
-
* @internal
|
|
1691
|
-
*/
|
|
1692
879
|
async ensureDir() {
|
|
1693
|
-
await (
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
* Resolves the filesystem path for a given cache key.
|
|
1697
|
-
*
|
|
1698
|
-
* @param key - Normalized cache key.
|
|
1699
|
-
* @returns Absolute path to the JSON file representing the key.
|
|
1700
|
-
* @internal
|
|
1701
|
-
*/
|
|
1702
|
-
filePathForKey(key) {
|
|
880
|
+
await import_core.runtimeMkdir(this.runtime, this.options.directory, { recursive: true });
|
|
881
|
+
}
|
|
882
|
+
async filePathForKey(key) {
|
|
1703
883
|
const hashed = hashKey(key);
|
|
1704
884
|
if (this.options.useSubdirectories) {
|
|
1705
885
|
const d1 = hashed.substring(0, 2);
|
|
1706
886
|
const d2 = hashed.substring(2, 4);
|
|
1707
|
-
return
|
|
1708
|
-
}
|
|
1709
|
-
return
|
|
1710
|
-
}
|
|
1711
|
-
/**
|
|
1712
|
-
* Retrieves an item from the cache by its key.
|
|
1713
|
-
*
|
|
1714
|
-
* If the item exists but has expired, it will be deleted and `null` will be returned.
|
|
1715
|
-
*
|
|
1716
|
-
* @param key - The unique identifier for the cache item.
|
|
1717
|
-
* @returns The cached value, or `null` if not found or expired.
|
|
1718
|
-
* @throws {Error} If the file exists but cannot be read or parsed.
|
|
1719
|
-
*
|
|
1720
|
-
* @example
|
|
1721
|
-
* ```typescript
|
|
1722
|
-
* const value = await store.get<string>('my-key');
|
|
1723
|
-
* ```
|
|
1724
|
-
*/
|
|
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
|
+
}
|
|
1725
891
|
async get(key) {
|
|
1726
892
|
const normalized = normalizeCacheKey(key);
|
|
1727
893
|
await this.ensureDir();
|
|
1728
|
-
const file = this.filePathForKey(normalized);
|
|
894
|
+
const file = await this.filePathForKey(normalized);
|
|
1729
895
|
try {
|
|
1730
|
-
const raw = await (
|
|
896
|
+
const raw = await import_core.runtimeReadText(this.runtime, file);
|
|
1731
897
|
const data = JSON.parse(raw);
|
|
1732
898
|
if (isExpired(data.expiresAt)) {
|
|
1733
899
|
await this.forget(normalized);
|
|
@@ -1738,58 +904,28 @@ var FileStore = class {
|
|
|
1738
904
|
return null;
|
|
1739
905
|
}
|
|
1740
906
|
}
|
|
1741
|
-
/**
|
|
1742
|
-
* Stores an item in the cache with a specified expiration time.
|
|
1743
|
-
*
|
|
1744
|
-
* Uses an atomic write strategy (write to temp file then rename) to ensure
|
|
1745
|
-
* data integrity even if the process crashes during the write operation.
|
|
1746
|
-
*
|
|
1747
|
-
* @param key - The unique identifier for the cache item.
|
|
1748
|
-
* @param value - The data to be cached.
|
|
1749
|
-
* @param ttl - Time-to-live in seconds or a Date object for absolute expiration.
|
|
1750
|
-
* @throws {Error} If the file system is not writable or disk is full.
|
|
1751
|
-
*
|
|
1752
|
-
* @example
|
|
1753
|
-
* ```typescript
|
|
1754
|
-
* await store.put('settings', { theme: 'dark' }, 86400);
|
|
1755
|
-
* ```
|
|
1756
|
-
*/
|
|
1757
907
|
async put(key, value, ttl) {
|
|
1758
908
|
const normalized = normalizeCacheKey(key);
|
|
1759
909
|
await this.ensureDir();
|
|
1760
910
|
const expiresAt = ttlToExpiresAt(ttl);
|
|
1761
|
-
if (expiresAt !== null && expiresAt !==
|
|
911
|
+
if (expiresAt !== null && expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
1762
912
|
await this.forget(normalized);
|
|
1763
913
|
return;
|
|
1764
914
|
}
|
|
1765
|
-
const file = this.filePathForKey(normalized);
|
|
915
|
+
const file = await this.filePathForKey(normalized);
|
|
1766
916
|
if (this.options.useSubdirectories) {
|
|
1767
|
-
await (
|
|
917
|
+
await import_core.runtimeMkdir(this.runtime, import_node_path.dirname(file), { recursive: true });
|
|
1768
918
|
}
|
|
1769
|
-
const tempFile = `${file}.tmp.${Date.now()}.${
|
|
919
|
+
const tempFile = `${file}.tmp.${Date.now()}.${crypto.randomUUID()}`;
|
|
1770
920
|
const payload = { expiresAt: expiresAt ?? null, value };
|
|
1771
921
|
try {
|
|
1772
|
-
await
|
|
1773
|
-
await (
|
|
922
|
+
await this.runtime.writeFile(tempFile, JSON.stringify(payload));
|
|
923
|
+
await import_core.runtimeRename(this.runtime, tempFile, file);
|
|
1774
924
|
} catch (error) {
|
|
1775
|
-
await (
|
|
1776
|
-
});
|
|
925
|
+
await import_core.runtimeRemoveRecursive(this.runtime, tempFile).catch(() => {});
|
|
1777
926
|
throw error;
|
|
1778
927
|
}
|
|
1779
928
|
}
|
|
1780
|
-
/**
|
|
1781
|
-
* Stores an item in the cache only if it does not already exist.
|
|
1782
|
-
*
|
|
1783
|
-
* @param key - The unique identifier for the cache item.
|
|
1784
|
-
* @param value - The data to be cached.
|
|
1785
|
-
* @param ttl - Time-to-live in seconds or a Date object.
|
|
1786
|
-
* @returns `true` if the item was stored, `false` if it already existed.
|
|
1787
|
-
*
|
|
1788
|
-
* @example
|
|
1789
|
-
* ```typescript
|
|
1790
|
-
* const success = await store.add('unique-task', { status: 'pending' }, 60);
|
|
1791
|
-
* ```
|
|
1792
|
-
*/
|
|
1793
929
|
async add(key, value, ttl) {
|
|
1794
930
|
const normalized = normalizeCacheKey(key);
|
|
1795
931
|
const existing = await this.get(normalized);
|
|
@@ -1799,61 +935,22 @@ var FileStore = class {
|
|
|
1799
935
|
await this.put(normalized, value, ttl);
|
|
1800
936
|
return true;
|
|
1801
937
|
}
|
|
1802
|
-
/**
|
|
1803
|
-
* Removes an item from the cache by its key.
|
|
1804
|
-
*
|
|
1805
|
-
* @param key - The unique identifier for the cache item.
|
|
1806
|
-
* @returns `true` if the file was deleted or didn't exist, `false` on failure.
|
|
1807
|
-
*
|
|
1808
|
-
* @example
|
|
1809
|
-
* ```typescript
|
|
1810
|
-
* await store.forget('my-key');
|
|
1811
|
-
* ```
|
|
1812
|
-
*/
|
|
1813
938
|
async forget(key) {
|
|
1814
939
|
const normalized = normalizeCacheKey(key);
|
|
1815
940
|
await this.ensureDir();
|
|
1816
|
-
const file = this.filePathForKey(normalized);
|
|
941
|
+
const file = await this.filePathForKey(normalized);
|
|
1817
942
|
try {
|
|
1818
|
-
await (
|
|
943
|
+
await import_core.runtimeRemoveRecursive(this.runtime, file);
|
|
1819
944
|
return true;
|
|
1820
945
|
} catch {
|
|
1821
946
|
return false;
|
|
1822
947
|
}
|
|
1823
948
|
}
|
|
1824
|
-
/**
|
|
1825
|
-
* Removes all items from the cache directory.
|
|
1826
|
-
*
|
|
1827
|
-
* This operation deletes the entire cache directory and recreates it.
|
|
1828
|
-
* Use with caution as it is destructive and non-reversible.
|
|
1829
|
-
*
|
|
1830
|
-
* @throws {Error} If the directory cannot be removed or recreated.
|
|
1831
|
-
*
|
|
1832
|
-
* @example
|
|
1833
|
-
* ```typescript
|
|
1834
|
-
* await store.flush();
|
|
1835
|
-
* ```
|
|
1836
|
-
*/
|
|
1837
949
|
async flush() {
|
|
1838
950
|
await this.ensureDir();
|
|
1839
|
-
await (
|
|
951
|
+
await import_core.runtimeRemoveRecursive(this.runtime, this.options.directory);
|
|
1840
952
|
await this.ensureDir();
|
|
1841
953
|
}
|
|
1842
|
-
/**
|
|
1843
|
-
* Increments the value of an integer item in the cache.
|
|
1844
|
-
*
|
|
1845
|
-
* If the key does not exist, it is initialized to 0 before incrementing.
|
|
1846
|
-
*
|
|
1847
|
-
* @param key - The unique identifier for the cache item.
|
|
1848
|
-
* @param value - The amount to increment by.
|
|
1849
|
-
* @returns The new value after incrementing.
|
|
1850
|
-
* @throws {Error} If the existing value is not a number.
|
|
1851
|
-
*
|
|
1852
|
-
* @example
|
|
1853
|
-
* ```typescript
|
|
1854
|
-
* const newCount = await store.increment('page-views');
|
|
1855
|
-
* ```
|
|
1856
|
-
*/
|
|
1857
954
|
async increment(key, value = 1) {
|
|
1858
955
|
const normalized = normalizeCacheKey(key);
|
|
1859
956
|
const current = await this.get(normalized);
|
|
@@ -1861,77 +958,30 @@ var FileStore = class {
|
|
|
1861
958
|
await this.put(normalized, next, null);
|
|
1862
959
|
return next;
|
|
1863
960
|
}
|
|
1864
|
-
/**
|
|
1865
|
-
* Decrements the value of an integer item in the cache.
|
|
1866
|
-
*
|
|
1867
|
-
* If the key does not exist, it is initialized to 0 before decrementing.
|
|
1868
|
-
*
|
|
1869
|
-
* @param key - The unique identifier for the cache item.
|
|
1870
|
-
* @param value - The amount to decrement by.
|
|
1871
|
-
* @returns The new value after decrementing.
|
|
1872
|
-
* @throws {Error} If the existing value is not a number.
|
|
1873
|
-
*
|
|
1874
|
-
* @example
|
|
1875
|
-
* ```typescript
|
|
1876
|
-
* const newCount = await store.decrement('stock-level');
|
|
1877
|
-
* ```
|
|
1878
|
-
*/
|
|
1879
961
|
async decrement(key, value = 1) {
|
|
1880
962
|
return this.increment(key, -value);
|
|
1881
963
|
}
|
|
1882
|
-
/**
|
|
1883
|
-
* Retrieves the remaining time-to-live for a cache item in seconds.
|
|
1884
|
-
*
|
|
1885
|
-
* @param key - The unique identifier for the cache item.
|
|
1886
|
-
* @returns The seconds remaining until expiration, or `null` if it never expires or doesn't exist.
|
|
1887
|
-
*
|
|
1888
|
-
* @example
|
|
1889
|
-
* ```typescript
|
|
1890
|
-
* const secondsLeft = await store.ttl('session:123');
|
|
1891
|
-
* ```
|
|
1892
|
-
*/
|
|
1893
964
|
async ttl(key) {
|
|
1894
965
|
const normalized = normalizeCacheKey(key);
|
|
1895
|
-
const file = this.filePathForKey(normalized);
|
|
966
|
+
const file = await this.filePathForKey(normalized);
|
|
1896
967
|
try {
|
|
1897
|
-
const raw = await (
|
|
968
|
+
const raw = await import_core.runtimeReadText(this.runtime, file);
|
|
1898
969
|
const data = JSON.parse(raw);
|
|
1899
970
|
if (data.expiresAt === null) {
|
|
1900
971
|
return null;
|
|
1901
972
|
}
|
|
1902
|
-
const remaining = Math.ceil((data.expiresAt - Date.now()) /
|
|
973
|
+
const remaining = Math.ceil((data.expiresAt - Date.now()) / 1000);
|
|
1903
974
|
return remaining > 0 ? remaining : null;
|
|
1904
975
|
} catch {
|
|
1905
976
|
return null;
|
|
1906
977
|
}
|
|
1907
978
|
}
|
|
1908
|
-
/**
|
|
1909
|
-
* Creates a distributed lock instance based on the filesystem.
|
|
1910
|
-
*
|
|
1911
|
-
* Locks are implemented using atomic file creation (`wx` flag). They include
|
|
1912
|
-
* protection against stale locks by checking process IDs and expiration times.
|
|
1913
|
-
*
|
|
1914
|
-
* @param name - The unique name of the lock.
|
|
1915
|
-
* @param seconds - The duration in seconds for which the lock should be held.
|
|
1916
|
-
* @returns A `CacheLock` instance for managing the lock lifecycle.
|
|
1917
|
-
*
|
|
1918
|
-
* @example
|
|
1919
|
-
* ```typescript
|
|
1920
|
-
* const lock = store.lock('process-report', 60);
|
|
1921
|
-
* if (await lock.acquire()) {
|
|
1922
|
-
* try {
|
|
1923
|
-
* // Critical section
|
|
1924
|
-
* } finally {
|
|
1925
|
-
* await lock.release();
|
|
1926
|
-
* }
|
|
1927
|
-
* }
|
|
1928
|
-
* ```
|
|
1929
|
-
*/
|
|
1930
979
|
lock(name, seconds = 10) {
|
|
1931
980
|
const normalizedName = normalizeCacheKey(name);
|
|
1932
|
-
const lockFile =
|
|
1933
|
-
const ttlMillis = Math.max(1, seconds) *
|
|
1934
|
-
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;
|
|
1935
985
|
const isProcessAlive = (pid) => {
|
|
1936
986
|
try {
|
|
1937
987
|
process.kill(pid, 0);
|
|
@@ -1943,62 +993,41 @@ var FileStore = class {
|
|
|
1943
993
|
const tryAcquire = async () => {
|
|
1944
994
|
await this.ensureDir();
|
|
1945
995
|
try {
|
|
1946
|
-
const handle = await (0, import_promises.open)(lockFile, "wx");
|
|
1947
996
|
const lockData = {
|
|
1948
997
|
owner,
|
|
1949
998
|
expiresAt: Date.now() + ttlMillis,
|
|
1950
999
|
pid: process.pid
|
|
1951
1000
|
};
|
|
1952
|
-
await
|
|
1953
|
-
await handle.close();
|
|
1001
|
+
await import_core.runtimeWriteFileExclusive(runtime, lockFile, JSON.stringify(lockData));
|
|
1954
1002
|
return true;
|
|
1955
1003
|
} catch {
|
|
1956
1004
|
try {
|
|
1957
|
-
const raw = await (
|
|
1005
|
+
const raw = await import_core.runtimeReadText(runtime, lockFile);
|
|
1958
1006
|
const data = JSON.parse(raw);
|
|
1959
|
-
const
|
|
1007
|
+
const isExpiredLock = !data.expiresAt || Date.now() > data.expiresAt;
|
|
1960
1008
|
const isProcessDead = data.pid && !isProcessAlive(data.pid);
|
|
1961
|
-
if (
|
|
1962
|
-
await (
|
|
1009
|
+
if (isExpiredLock || isProcessDead) {
|
|
1010
|
+
await import_core.runtimeRemoveRecursive(runtime, lockFile);
|
|
1963
1011
|
}
|
|
1964
|
-
} catch {
|
|
1965
|
-
}
|
|
1012
|
+
} catch {}
|
|
1966
1013
|
return false;
|
|
1967
1014
|
}
|
|
1968
1015
|
};
|
|
1969
1016
|
return {
|
|
1970
|
-
/**
|
|
1971
|
-
* Attempts to acquire the lock immediately.
|
|
1972
|
-
*
|
|
1973
|
-
* @returns `true` if the lock was successfully acquired, `false` otherwise.
|
|
1974
|
-
*/
|
|
1975
1017
|
async acquire() {
|
|
1976
1018
|
return tryAcquire();
|
|
1977
1019
|
},
|
|
1978
|
-
/**
|
|
1979
|
-
* Releases the lock if it is owned by the current instance.
|
|
1980
|
-
*/
|
|
1981
1020
|
async release() {
|
|
1982
1021
|
try {
|
|
1983
|
-
const raw = await (
|
|
1022
|
+
const raw = await import_core.runtimeReadText(runtime, lockFile);
|
|
1984
1023
|
const data = JSON.parse(raw);
|
|
1985
1024
|
if (data.owner === owner) {
|
|
1986
|
-
await (
|
|
1025
|
+
await import_core.runtimeRemoveRecursive(runtime, lockFile);
|
|
1987
1026
|
}
|
|
1988
|
-
} catch {
|
|
1989
|
-
}
|
|
1027
|
+
} catch {}
|
|
1990
1028
|
},
|
|
1991
|
-
/**
|
|
1992
|
-
* Executes a callback within the lock, waiting if necessary.
|
|
1993
|
-
*
|
|
1994
|
-
* @param secondsToWait - Maximum time to wait for the lock in seconds.
|
|
1995
|
-
* @param callback - The function to execute once the lock is acquired.
|
|
1996
|
-
* @param options - Polling configuration.
|
|
1997
|
-
* @returns The result of the callback.
|
|
1998
|
-
* @throws {LockTimeoutError} If the lock cannot be acquired within the wait time.
|
|
1999
|
-
*/
|
|
2000
1029
|
async block(secondsToWait, callback, options) {
|
|
2001
|
-
const deadline = Date.now() + Math.max(0, secondsToWait) *
|
|
1030
|
+
const deadline = Date.now() + Math.max(0, secondsToWait) * 1000;
|
|
2002
1031
|
const sleepMillis = options?.sleepMillis ?? 150;
|
|
2003
1032
|
while (Date.now() <= deadline) {
|
|
2004
1033
|
if (await this.acquire()) {
|
|
@@ -2010,94 +1039,52 @@ var FileStore = class {
|
|
|
2010
1039
|
}
|
|
2011
1040
|
await sleep(sleepMillis);
|
|
2012
1041
|
}
|
|
2013
|
-
throw new LockTimeoutError(
|
|
2014
|
-
`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`
|
|
2015
|
-
);
|
|
1042
|
+
throw new LockTimeoutError(`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`);
|
|
2016
1043
|
}
|
|
2017
1044
|
};
|
|
2018
1045
|
}
|
|
2019
|
-
}
|
|
1046
|
+
}
|
|
2020
1047
|
function hashKey(key) {
|
|
2021
|
-
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");
|
|
2022
1057
|
}
|
|
2023
|
-
|
|
2024
|
-
// src/stores/MemoryStore.ts
|
|
2025
|
-
var import_node_crypto2 = require("crypto");
|
|
2026
1058
|
|
|
2027
1059
|
// src/utils/LRUCache.ts
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
*/
|
|
1060
|
+
class LRUCache {
|
|
1061
|
+
maxSize;
|
|
1062
|
+
onEvict;
|
|
1063
|
+
map = new Map;
|
|
1064
|
+
head = null;
|
|
1065
|
+
tail = null;
|
|
2035
1066
|
constructor(maxSize, onEvict) {
|
|
2036
1067
|
this.maxSize = maxSize;
|
|
2037
1068
|
this.onEvict = onEvict;
|
|
2038
1069
|
}
|
|
2039
|
-
map = /* @__PURE__ */ new Map();
|
|
2040
|
-
head = null;
|
|
2041
|
-
tail = null;
|
|
2042
|
-
/**
|
|
2043
|
-
* The current number of items stored in the cache.
|
|
2044
|
-
*/
|
|
2045
1070
|
get size() {
|
|
2046
1071
|
return this.map.size;
|
|
2047
1072
|
}
|
|
2048
|
-
/**
|
|
2049
|
-
* Checks if a key exists in the cache without updating its access order.
|
|
2050
|
-
*
|
|
2051
|
-
* @param key - The identifier to look for.
|
|
2052
|
-
* @returns True if the key exists, false otherwise.
|
|
2053
|
-
*/
|
|
2054
1073
|
has(key) {
|
|
2055
1074
|
return this.map.has(key);
|
|
2056
1075
|
}
|
|
2057
|
-
/**
|
|
2058
|
-
* Retrieves an item from the cache and marks it as most recently used.
|
|
2059
|
-
*
|
|
2060
|
-
* @param key - The identifier of the item to retrieve.
|
|
2061
|
-
* @returns The cached value, or undefined if not found.
|
|
2062
|
-
*
|
|
2063
|
-
* @example
|
|
2064
|
-
* ```typescript
|
|
2065
|
-
* const value = cache.get('my-key');
|
|
2066
|
-
* ```
|
|
2067
|
-
*/
|
|
2068
1076
|
get(key) {
|
|
2069
1077
|
const node = this.map.get(key);
|
|
2070
1078
|
if (!node) {
|
|
2071
|
-
return
|
|
1079
|
+
return;
|
|
2072
1080
|
}
|
|
2073
1081
|
this.moveToHead(node);
|
|
2074
1082
|
return node.value;
|
|
2075
1083
|
}
|
|
2076
|
-
/**
|
|
2077
|
-
* Retrieves an item from the cache without updating its access order.
|
|
2078
|
-
*
|
|
2079
|
-
* Useful for inspecting the cache without affecting eviction priority.
|
|
2080
|
-
*
|
|
2081
|
-
* @param key - The identifier of the item to peek.
|
|
2082
|
-
* @returns The cached value, or undefined if not found.
|
|
2083
|
-
*/
|
|
2084
1084
|
peek(key) {
|
|
2085
1085
|
const node = this.map.get(key);
|
|
2086
1086
|
return node?.value;
|
|
2087
1087
|
}
|
|
2088
|
-
/**
|
|
2089
|
-
* Adds or updates an item in the cache, marking it as most recently used.
|
|
2090
|
-
*
|
|
2091
|
-
* If the cache is at capacity, the least recently used item will be evicted.
|
|
2092
|
-
*
|
|
2093
|
-
* @param key - The identifier for the item.
|
|
2094
|
-
* @param value - The data to store.
|
|
2095
|
-
*
|
|
2096
|
-
* @example
|
|
2097
|
-
* ```typescript
|
|
2098
|
-
* cache.set('user:1', { name: 'Alice' });
|
|
2099
|
-
* ```
|
|
2100
|
-
*/
|
|
2101
1088
|
set(key, value) {
|
|
2102
1089
|
const existingNode = this.map.get(key);
|
|
2103
1090
|
if (existingNode) {
|
|
@@ -2123,12 +1110,6 @@ var LRUCache = class {
|
|
|
2123
1110
|
}
|
|
2124
1111
|
this.map.set(key, newNode);
|
|
2125
1112
|
}
|
|
2126
|
-
/**
|
|
2127
|
-
* Removes an item from the cache.
|
|
2128
|
-
*
|
|
2129
|
-
* @param key - The identifier of the item to remove.
|
|
2130
|
-
* @returns True if the item was found and removed, false otherwise.
|
|
2131
|
-
*/
|
|
2132
1113
|
delete(key) {
|
|
2133
1114
|
const node = this.map.get(key);
|
|
2134
1115
|
if (!node) {
|
|
@@ -2138,19 +1119,11 @@ var LRUCache = class {
|
|
|
2138
1119
|
this.map.delete(key);
|
|
2139
1120
|
return true;
|
|
2140
1121
|
}
|
|
2141
|
-
/**
|
|
2142
|
-
* Removes all items from the cache.
|
|
2143
|
-
*/
|
|
2144
1122
|
clear() {
|
|
2145
1123
|
this.map.clear();
|
|
2146
1124
|
this.head = null;
|
|
2147
1125
|
this.tail = null;
|
|
2148
1126
|
}
|
|
2149
|
-
/**
|
|
2150
|
-
* Moves a node to the head of the linked list (most recently used).
|
|
2151
|
-
*
|
|
2152
|
-
* @param node - The node to promote.
|
|
2153
|
-
*/
|
|
2154
1127
|
moveToHead(node) {
|
|
2155
1128
|
if (node === this.head) {
|
|
2156
1129
|
return;
|
|
@@ -2171,11 +1144,6 @@ var LRUCache = class {
|
|
|
2171
1144
|
}
|
|
2172
1145
|
this.head = node;
|
|
2173
1146
|
}
|
|
2174
|
-
/**
|
|
2175
|
-
* Removes a node from the linked list.
|
|
2176
|
-
*
|
|
2177
|
-
* @param node - The node to remove.
|
|
2178
|
-
*/
|
|
2179
1147
|
removeNode(node) {
|
|
2180
1148
|
if (node.prev) {
|
|
2181
1149
|
node.prev.next = node.next;
|
|
@@ -2190,11 +1158,6 @@ var LRUCache = class {
|
|
|
2190
1158
|
node.prev = null;
|
|
2191
1159
|
node.next = null;
|
|
2192
1160
|
}
|
|
2193
|
-
/**
|
|
2194
|
-
* Evicts the least recently used item (the tail of the list).
|
|
2195
|
-
*
|
|
2196
|
-
* Triggers the `onEvict` callback if provided.
|
|
2197
|
-
*/
|
|
2198
1161
|
evict() {
|
|
2199
1162
|
if (!this.tail) {
|
|
2200
1163
|
return;
|
|
@@ -2206,37 +1169,21 @@ var LRUCache = class {
|
|
|
2206
1169
|
this.removeNode(node);
|
|
2207
1170
|
this.map.delete(node.key);
|
|
2208
1171
|
}
|
|
2209
|
-
}
|
|
1172
|
+
}
|
|
2210
1173
|
|
|
2211
1174
|
// src/stores/MemoryStore.ts
|
|
2212
|
-
|
|
1175
|
+
class MemoryStore {
|
|
2213
1176
|
entries;
|
|
2214
|
-
locks =
|
|
1177
|
+
locks = new Map;
|
|
2215
1178
|
stats = { hits: 0, misses: 0, evictions: 0 };
|
|
2216
|
-
tagToKeys =
|
|
2217
|
-
keyToTags =
|
|
2218
|
-
/**
|
|
2219
|
-
* Creates a new MemoryStore instance.
|
|
2220
|
-
*
|
|
2221
|
-
* @param options - Configuration for capacity and eviction.
|
|
2222
|
-
*/
|
|
1179
|
+
tagToKeys = new Map;
|
|
1180
|
+
keyToTags = new Map;
|
|
2223
1181
|
constructor(options = {}) {
|
|
2224
1182
|
this.entries = new LRUCache(options.maxItems ?? 0, (key) => {
|
|
2225
1183
|
this.tagIndexRemove(key);
|
|
2226
1184
|
this.stats.evictions++;
|
|
2227
1185
|
});
|
|
2228
1186
|
}
|
|
2229
|
-
/**
|
|
2230
|
-
* Retrieves current performance metrics.
|
|
2231
|
-
*
|
|
2232
|
-
* @returns A snapshot of hits, misses, size, and eviction counts.
|
|
2233
|
-
*
|
|
2234
|
-
* @example
|
|
2235
|
-
* ```typescript
|
|
2236
|
-
* const stats = store.getStats();
|
|
2237
|
-
* console.log(`Cache hit rate: ${stats.hitRate * 100}%`);
|
|
2238
|
-
* ```
|
|
2239
|
-
*/
|
|
2240
1187
|
getStats() {
|
|
2241
1188
|
const total = this.stats.hits + this.stats.misses;
|
|
2242
1189
|
return {
|
|
@@ -2253,22 +1200,9 @@ var MemoryStore = class {
|
|
|
2253
1200
|
return;
|
|
2254
1201
|
}
|
|
2255
1202
|
if (isExpired(entry.expiresAt, now)) {
|
|
2256
|
-
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
/**
|
|
2260
|
-
* Retrieves an item from the cache by its key.
|
|
2261
|
-
*
|
|
2262
|
-
* If the item is expired, it will be automatically removed and `null` will be returned.
|
|
2263
|
-
*
|
|
2264
|
-
* @param key - The unique identifier for the cached item.
|
|
2265
|
-
* @returns The cached value, or `null` if not found or expired.
|
|
2266
|
-
*
|
|
2267
|
-
* @example
|
|
2268
|
-
* ```typescript
|
|
2269
|
-
* const value = await store.get('my-key');
|
|
2270
|
-
* ```
|
|
2271
|
-
*/
|
|
1203
|
+
this.forget(key);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
2272
1206
|
async get(key) {
|
|
2273
1207
|
const normalized = normalizeCacheKey(key);
|
|
2274
1208
|
const entry = this.entries.get(normalized);
|
|
@@ -2284,42 +1218,15 @@ var MemoryStore = class {
|
|
|
2284
1218
|
this.stats.hits++;
|
|
2285
1219
|
return entry.value;
|
|
2286
1220
|
}
|
|
2287
|
-
/**
|
|
2288
|
-
* Stores an item in the cache with a specific TTL.
|
|
2289
|
-
*
|
|
2290
|
-
* If the key already exists, it will be overwritten.
|
|
2291
|
-
*
|
|
2292
|
-
* @param key - The unique identifier for the item.
|
|
2293
|
-
* @param value - The data to store.
|
|
2294
|
-
* @param ttl - Time-to-live in seconds, or a Date object for absolute expiration.
|
|
2295
|
-
*
|
|
2296
|
-
* @example
|
|
2297
|
-
* ```typescript
|
|
2298
|
-
* await store.put('settings', { theme: 'dark' }, 3600);
|
|
2299
|
-
* ```
|
|
2300
|
-
*/
|
|
2301
1221
|
async put(key, value, ttl) {
|
|
2302
1222
|
const normalized = normalizeCacheKey(key);
|
|
2303
1223
|
const expiresAt = ttlToExpiresAt(ttl);
|
|
2304
|
-
if (expiresAt !== null && expiresAt !==
|
|
1224
|
+
if (expiresAt !== null && expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
2305
1225
|
await this.forget(normalized);
|
|
2306
1226
|
return;
|
|
2307
1227
|
}
|
|
2308
1228
|
this.entries.set(normalized, { value, expiresAt: expiresAt ?? null });
|
|
2309
1229
|
}
|
|
2310
|
-
/**
|
|
2311
|
-
* Stores an item only if it does not already exist in the cache.
|
|
2312
|
-
*
|
|
2313
|
-
* @param key - The unique identifier for the item.
|
|
2314
|
-
* @param value - The data to store.
|
|
2315
|
-
* @param ttl - Time-to-live in seconds or absolute expiration.
|
|
2316
|
-
* @returns `true` if the item was added, `false` if it already existed.
|
|
2317
|
-
*
|
|
2318
|
-
* @example
|
|
2319
|
-
* ```typescript
|
|
2320
|
-
* const added = await store.add('unique-task', data, 60);
|
|
2321
|
-
* ```
|
|
2322
|
-
*/
|
|
2323
1230
|
async add(key, value, ttl) {
|
|
2324
1231
|
const normalized = normalizeCacheKey(key);
|
|
2325
1232
|
this.cleanupExpired(normalized);
|
|
@@ -2329,50 +1236,17 @@ var MemoryStore = class {
|
|
|
2329
1236
|
await this.put(normalized, value, ttl);
|
|
2330
1237
|
return true;
|
|
2331
1238
|
}
|
|
2332
|
-
/**
|
|
2333
|
-
* Removes an item from the cache.
|
|
2334
|
-
*
|
|
2335
|
-
* @param key - The unique identifier for the item to remove.
|
|
2336
|
-
* @returns `true` if the item existed and was removed, `false` otherwise.
|
|
2337
|
-
*
|
|
2338
|
-
* @example
|
|
2339
|
-
* ```typescript
|
|
2340
|
-
* await store.forget('user:session');
|
|
2341
|
-
* ```
|
|
2342
|
-
*/
|
|
2343
1239
|
async forget(key) {
|
|
2344
1240
|
const normalized = normalizeCacheKey(key);
|
|
2345
1241
|
const existed = this.entries.delete(normalized);
|
|
2346
1242
|
this.tagIndexRemove(normalized);
|
|
2347
1243
|
return existed;
|
|
2348
1244
|
}
|
|
2349
|
-
/**
|
|
2350
|
-
* Removes all items from the cache and resets all internal indexes.
|
|
2351
|
-
*
|
|
2352
|
-
* @example
|
|
2353
|
-
* ```typescript
|
|
2354
|
-
* await store.flush();
|
|
2355
|
-
* ```
|
|
2356
|
-
*/
|
|
2357
1245
|
async flush() {
|
|
2358
1246
|
this.entries.clear();
|
|
2359
1247
|
this.tagToKeys.clear();
|
|
2360
1248
|
this.keyToTags.clear();
|
|
2361
1249
|
}
|
|
2362
|
-
/**
|
|
2363
|
-
* Increments the value of an item in the cache.
|
|
2364
|
-
*
|
|
2365
|
-
* If the key does not exist, it starts from 0.
|
|
2366
|
-
*
|
|
2367
|
-
* @param key - The identifier for the numeric value.
|
|
2368
|
-
* @param value - The amount to increment by (defaults to 1).
|
|
2369
|
-
* @returns The new incremented value.
|
|
2370
|
-
*
|
|
2371
|
-
* @example
|
|
2372
|
-
* ```typescript
|
|
2373
|
-
* const count = await store.increment('page_views');
|
|
2374
|
-
* ```
|
|
2375
|
-
*/
|
|
2376
1250
|
async increment(key, value = 1) {
|
|
2377
1251
|
const normalized = normalizeCacheKey(key);
|
|
2378
1252
|
const current = await this.get(normalized);
|
|
@@ -2380,32 +1254,9 @@ var MemoryStore = class {
|
|
|
2380
1254
|
await this.put(normalized, next, null);
|
|
2381
1255
|
return next;
|
|
2382
1256
|
}
|
|
2383
|
-
/**
|
|
2384
|
-
* Decrements the value of an item in the cache.
|
|
2385
|
-
*
|
|
2386
|
-
* @param key - The identifier for the numeric value.
|
|
2387
|
-
* @param value - The amount to decrement by (defaults to 1).
|
|
2388
|
-
* @returns The new decremented value.
|
|
2389
|
-
*
|
|
2390
|
-
* @example
|
|
2391
|
-
* ```typescript
|
|
2392
|
-
* const remaining = await store.decrement('stock_count', 5);
|
|
2393
|
-
* ```
|
|
2394
|
-
*/
|
|
2395
1257
|
async decrement(key, value = 1) {
|
|
2396
1258
|
return this.increment(key, -value);
|
|
2397
1259
|
}
|
|
2398
|
-
/**
|
|
2399
|
-
* Gets the remaining time-to-live for a cached item.
|
|
2400
|
-
*
|
|
2401
|
-
* @param key - The identifier for the cached item.
|
|
2402
|
-
* @returns Remaining seconds, or `null` if the item has no expiration or does not exist.
|
|
2403
|
-
*
|
|
2404
|
-
* @example
|
|
2405
|
-
* ```typescript
|
|
2406
|
-
* const secondsLeft = await store.ttl('token');
|
|
2407
|
-
* ```
|
|
2408
|
-
*/
|
|
2409
1260
|
async ttl(key) {
|
|
2410
1261
|
const normalized = normalizeCacheKey(key);
|
|
2411
1262
|
const entry = this.entries.peek(normalized);
|
|
@@ -2417,30 +1268,11 @@ var MemoryStore = class {
|
|
|
2417
1268
|
await this.forget(normalized);
|
|
2418
1269
|
return null;
|
|
2419
1270
|
}
|
|
2420
|
-
return Math.max(0, Math.ceil((entry.expiresAt - now) /
|
|
2421
|
-
}
|
|
2422
|
-
/**
|
|
2423
|
-
* Creates a lock instance for managing exclusive access to a resource.
|
|
2424
|
-
*
|
|
2425
|
-
* @param name - The name of the lock.
|
|
2426
|
-
* @param seconds - The duration the lock should be held (defaults to 10).
|
|
2427
|
-
* @returns A `CacheLock` instance.
|
|
2428
|
-
*
|
|
2429
|
-
* @example
|
|
2430
|
-
* ```typescript
|
|
2431
|
-
* const lock = store.lock('process-report', 30);
|
|
2432
|
-
* if (await lock.acquire()) {
|
|
2433
|
-
* try {
|
|
2434
|
-
* // Critical section
|
|
2435
|
-
* } finally {
|
|
2436
|
-
* await lock.release();
|
|
2437
|
-
* }
|
|
2438
|
-
* }
|
|
2439
|
-
* ```
|
|
2440
|
-
*/
|
|
1271
|
+
return Math.max(0, Math.ceil((entry.expiresAt - now) / 1000));
|
|
1272
|
+
}
|
|
2441
1273
|
lock(name, seconds = 10) {
|
|
2442
1274
|
const lockKey = `lock:${normalizeCacheKey(name)}`;
|
|
2443
|
-
const ttlMillis = Math.max(1, seconds) *
|
|
1275
|
+
const ttlMillis = Math.max(1, seconds) * 1000;
|
|
2444
1276
|
const locks = this.locks;
|
|
2445
1277
|
const acquire = async () => {
|
|
2446
1278
|
const now = Date.now();
|
|
@@ -2448,17 +1280,12 @@ var MemoryStore = class {
|
|
|
2448
1280
|
if (existing && existing.expiresAt > now) {
|
|
2449
1281
|
return { ok: false };
|
|
2450
1282
|
}
|
|
2451
|
-
const owner2 =
|
|
1283
|
+
const owner2 = crypto.randomUUID();
|
|
2452
1284
|
locks.set(lockKey, { owner: owner2, expiresAt: now + ttlMillis });
|
|
2453
1285
|
return { ok: true, owner: owner2 };
|
|
2454
1286
|
};
|
|
2455
1287
|
let owner;
|
|
2456
1288
|
return {
|
|
2457
|
-
/**
|
|
2458
|
-
* Attempts to acquire the lock.
|
|
2459
|
-
*
|
|
2460
|
-
* @returns `true` if acquired, `false` if already held by another process.
|
|
2461
|
-
*/
|
|
2462
1289
|
async acquire() {
|
|
2463
1290
|
const result = await acquire();
|
|
2464
1291
|
if (!result.ok) {
|
|
@@ -2467,9 +1294,6 @@ var MemoryStore = class {
|
|
|
2467
1294
|
owner = result.owner;
|
|
2468
1295
|
return true;
|
|
2469
1296
|
},
|
|
2470
|
-
/**
|
|
2471
|
-
* Releases the lock if it is held by the current owner.
|
|
2472
|
-
*/
|
|
2473
1297
|
async release() {
|
|
2474
1298
|
if (!owner) {
|
|
2475
1299
|
return;
|
|
@@ -2478,26 +1302,10 @@ var MemoryStore = class {
|
|
|
2478
1302
|
if (existing?.owner === owner) {
|
|
2479
1303
|
locks.delete(lockKey);
|
|
2480
1304
|
}
|
|
2481
|
-
owner =
|
|
1305
|
+
owner = undefined;
|
|
2482
1306
|
},
|
|
2483
|
-
/**
|
|
2484
|
-
* Attempts to acquire the lock and execute a callback, waiting if necessary.
|
|
2485
|
-
*
|
|
2486
|
-
* @param secondsToWait - How long to wait for the lock before timing out.
|
|
2487
|
-
* @param callback - The logic to execute while holding the lock.
|
|
2488
|
-
* @param options - Polling configuration.
|
|
2489
|
-
* @returns The result of the callback.
|
|
2490
|
-
* @throws {LockTimeoutError} If the lock cannot be acquired within the wait time.
|
|
2491
|
-
*
|
|
2492
|
-
* @example
|
|
2493
|
-
* ```typescript
|
|
2494
|
-
* await lock.block(5, async () => {
|
|
2495
|
-
* // This code runs exclusively
|
|
2496
|
-
* });
|
|
2497
|
-
* ```
|
|
2498
|
-
*/
|
|
2499
1307
|
async block(secondsToWait, callback, options) {
|
|
2500
|
-
const deadline = Date.now() + Math.max(0, secondsToWait) *
|
|
1308
|
+
const deadline = Date.now() + Math.max(0, secondsToWait) * 1000;
|
|
2501
1309
|
const sleepMillis = options?.sleepMillis ?? 150;
|
|
2502
1310
|
while (Date.now() <= deadline) {
|
|
2503
1311
|
if (await this.acquire()) {
|
|
@@ -2509,21 +1317,10 @@ var MemoryStore = class {
|
|
|
2509
1317
|
}
|
|
2510
1318
|
await sleep(sleepMillis);
|
|
2511
1319
|
}
|
|
2512
|
-
throw new LockTimeoutError(
|
|
2513
|
-
`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`
|
|
2514
|
-
);
|
|
1320
|
+
throw new LockTimeoutError(`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`);
|
|
2515
1321
|
}
|
|
2516
1322
|
};
|
|
2517
1323
|
}
|
|
2518
|
-
/**
|
|
2519
|
-
* Generates a tagged key for storage.
|
|
2520
|
-
*
|
|
2521
|
-
* Used internally to prefix keys with their associated tags.
|
|
2522
|
-
*
|
|
2523
|
-
* @param key - The original cache key.
|
|
2524
|
-
* @param tags - List of tags to associate with the key.
|
|
2525
|
-
* @returns A formatted string containing tags and the key.
|
|
2526
|
-
*/
|
|
2527
1324
|
tagKey(key, tags) {
|
|
2528
1325
|
const normalizedKey = normalizeCacheKey(key);
|
|
2529
1326
|
const normalizedTags = [...tags].map(String).filter(Boolean).sort();
|
|
@@ -2532,12 +1329,6 @@ var MemoryStore = class {
|
|
|
2532
1329
|
}
|
|
2533
1330
|
return `tags:${normalizedTags.join("|")}:${normalizedKey}`;
|
|
2534
1331
|
}
|
|
2535
|
-
/**
|
|
2536
|
-
* Indexes a tagged key for bulk invalidation.
|
|
2537
|
-
*
|
|
2538
|
-
* @param tags - The tags to index.
|
|
2539
|
-
* @param taggedKey - The full key (including tag prefix) to store.
|
|
2540
|
-
*/
|
|
2541
1332
|
tagIndexAdd(tags, taggedKey) {
|
|
2542
1333
|
const normalizedTags = [...tags].map(String).filter(Boolean);
|
|
2543
1334
|
if (normalizedTags.length === 0) {
|
|
@@ -2546,25 +1337,20 @@ var MemoryStore = class {
|
|
|
2546
1337
|
for (const tag of normalizedTags) {
|
|
2547
1338
|
let keys = this.tagToKeys.get(tag);
|
|
2548
1339
|
if (!keys) {
|
|
2549
|
-
keys =
|
|
1340
|
+
keys = new Set;
|
|
2550
1341
|
this.tagToKeys.set(tag, keys);
|
|
2551
1342
|
}
|
|
2552
1343
|
keys.add(taggedKey);
|
|
2553
1344
|
}
|
|
2554
1345
|
let tagSet = this.keyToTags.get(taggedKey);
|
|
2555
1346
|
if (!tagSet) {
|
|
2556
|
-
tagSet =
|
|
1347
|
+
tagSet = new Set;
|
|
2557
1348
|
this.keyToTags.set(taggedKey, tagSet);
|
|
2558
1349
|
}
|
|
2559
1350
|
for (const tag of normalizedTags) {
|
|
2560
1351
|
tagSet.add(tag);
|
|
2561
1352
|
}
|
|
2562
1353
|
}
|
|
2563
|
-
/**
|
|
2564
|
-
* Removes a key from the tag indexes.
|
|
2565
|
-
*
|
|
2566
|
-
* @param taggedKey - The key to remove from all tag sets.
|
|
2567
|
-
*/
|
|
2568
1354
|
tagIndexRemove(taggedKey) {
|
|
2569
1355
|
const tags = this.keyToTags.get(taggedKey);
|
|
2570
1356
|
if (!tags) {
|
|
@@ -2582,22 +1368,12 @@ var MemoryStore = class {
|
|
|
2582
1368
|
}
|
|
2583
1369
|
this.keyToTags.delete(taggedKey);
|
|
2584
1370
|
}
|
|
2585
|
-
/**
|
|
2586
|
-
* Invalidates all cache entries associated with any of the given tags.
|
|
2587
|
-
*
|
|
2588
|
-
* @param tags - The tags to flush.
|
|
2589
|
-
*
|
|
2590
|
-
* @example
|
|
2591
|
-
* ```typescript
|
|
2592
|
-
* await store.flushTags(['users', 'profiles']);
|
|
2593
|
-
* ```
|
|
2594
|
-
*/
|
|
2595
1371
|
async flushTags(tags) {
|
|
2596
1372
|
const normalizedTags = [...tags].map(String).filter(Boolean);
|
|
2597
1373
|
if (normalizedTags.length === 0) {
|
|
2598
1374
|
return;
|
|
2599
1375
|
}
|
|
2600
|
-
const keysToDelete =
|
|
1376
|
+
const keysToDelete = new Set;
|
|
2601
1377
|
for (const tag of normalizedTags) {
|
|
2602
1378
|
const keys = this.tagToKeys.get(tag);
|
|
2603
1379
|
if (!keys) {
|
|
@@ -2611,131 +1387,42 @@ var MemoryStore = class {
|
|
|
2611
1387
|
await this.forget(key);
|
|
2612
1388
|
}
|
|
2613
1389
|
}
|
|
2614
|
-
}
|
|
1390
|
+
}
|
|
2615
1391
|
|
|
2616
1392
|
// src/stores/NullStore.ts
|
|
2617
|
-
|
|
2618
|
-
/**
|
|
2619
|
-
* Simulates a cache miss for any given key.
|
|
2620
|
-
*
|
|
2621
|
-
* @param _key - Identifier for the cached item.
|
|
2622
|
-
* @returns Always `null` regardless of requested key.
|
|
2623
|
-
*
|
|
2624
|
-
* @example
|
|
2625
|
-
* ```typescript
|
|
2626
|
-
* const value = await store.get('my-key');
|
|
2627
|
-
* ```
|
|
2628
|
-
*/
|
|
1393
|
+
class NullStore {
|
|
2629
1394
|
async get(_key) {
|
|
2630
1395
|
return null;
|
|
2631
1396
|
}
|
|
2632
|
-
|
|
2633
|
-
* Discards the provided value instead of storing it.
|
|
2634
|
-
*
|
|
2635
|
-
* @param _key - The identifier for the item.
|
|
2636
|
-
* @param _value - The data to be cached.
|
|
2637
|
-
* @param _ttl - Time-to-live in seconds.
|
|
2638
|
-
* @returns Resolves immediately after discarding the data.
|
|
2639
|
-
*
|
|
2640
|
-
* @example
|
|
2641
|
-
* ```typescript
|
|
2642
|
-
* await store.put('user:1', { id: 1 }, 3600);
|
|
2643
|
-
* ```
|
|
2644
|
-
*/
|
|
2645
|
-
async put(_key, _value, _ttl) {
|
|
2646
|
-
}
|
|
2647
|
-
/**
|
|
2648
|
-
* Simulates a failed attempt to add an item to the cache.
|
|
2649
|
-
*
|
|
2650
|
-
* Since NullStore does not store data, this method always indicates that
|
|
2651
|
-
* the item was not added.
|
|
2652
|
-
*
|
|
2653
|
-
* @param _key - The identifier for the item.
|
|
2654
|
-
* @param _value - The data to be cached.
|
|
2655
|
-
* @param _ttl - Time-to-live in seconds.
|
|
2656
|
-
* @returns Always returns `false`.
|
|
2657
|
-
*
|
|
2658
|
-
* @example
|
|
2659
|
-
* ```typescript
|
|
2660
|
-
* const added = await store.add('key', 'value', 60); // false
|
|
2661
|
-
* ```
|
|
2662
|
-
*/
|
|
1397
|
+
async put(_key, _value, _ttl) {}
|
|
2663
1398
|
async add(_key, _value, _ttl) {
|
|
2664
1399
|
return false;
|
|
2665
1400
|
}
|
|
2666
|
-
/**
|
|
2667
|
-
* Simulates a failed attempt to remove an item from the cache.
|
|
2668
|
-
*
|
|
2669
|
-
* Since no data is ever stored, there is nothing to remove.
|
|
2670
|
-
*
|
|
2671
|
-
* @param _key - The identifier for the item to remove.
|
|
2672
|
-
* @returns Always returns `false`.
|
|
2673
|
-
*
|
|
2674
|
-
* @example
|
|
2675
|
-
* ```typescript
|
|
2676
|
-
* const forgotten = await store.forget('key'); // false
|
|
2677
|
-
* ```
|
|
2678
|
-
*/
|
|
2679
1401
|
async forget(_key) {
|
|
2680
1402
|
return false;
|
|
2681
1403
|
}
|
|
2682
|
-
|
|
2683
|
-
* Performs a no-op flush operation.
|
|
2684
|
-
*
|
|
2685
|
-
* @returns Resolves immediately as there is no data to clear.
|
|
2686
|
-
*
|
|
2687
|
-
* @example
|
|
2688
|
-
* ```typescript
|
|
2689
|
-
* await store.flush();
|
|
2690
|
-
* ```
|
|
2691
|
-
*/
|
|
2692
|
-
async flush() {
|
|
2693
|
-
}
|
|
2694
|
-
/**
|
|
2695
|
-
* Simulates an increment operation on a non-existent key.
|
|
2696
|
-
*
|
|
2697
|
-
* @param _key - The identifier for the numeric item.
|
|
2698
|
-
* @param _value - The amount to increment by.
|
|
2699
|
-
* @returns Always returns `0`.
|
|
2700
|
-
*
|
|
2701
|
-
* @example
|
|
2702
|
-
* ```typescript
|
|
2703
|
-
* const newValue = await store.increment('counter', 1); // 0
|
|
2704
|
-
* ```
|
|
2705
|
-
*/
|
|
1404
|
+
async flush() {}
|
|
2706
1405
|
async increment(_key, _value = 1) {
|
|
2707
1406
|
return 0;
|
|
2708
1407
|
}
|
|
2709
|
-
/**
|
|
2710
|
-
* Simulates a decrement operation on a non-existent key.
|
|
2711
|
-
*
|
|
2712
|
-
* @param _key - The identifier for the numeric item.
|
|
2713
|
-
* @param _value - The amount to decrement by.
|
|
2714
|
-
* @returns Always returns `0`.
|
|
2715
|
-
*
|
|
2716
|
-
* @example
|
|
2717
|
-
* ```typescript
|
|
2718
|
-
* const newValue = await store.decrement('counter', 1); // 0
|
|
2719
|
-
* ```
|
|
2720
|
-
*/
|
|
2721
1408
|
async decrement(_key, _value = 1) {
|
|
2722
1409
|
return 0;
|
|
2723
1410
|
}
|
|
2724
|
-
}
|
|
1411
|
+
}
|
|
2725
1412
|
|
|
2726
1413
|
// src/stores/PredictiveStore.ts
|
|
2727
|
-
|
|
1414
|
+
class PredictiveStore {
|
|
1415
|
+
store;
|
|
1416
|
+
predictor;
|
|
2728
1417
|
constructor(store, options = {}) {
|
|
2729
1418
|
this.store = store;
|
|
2730
|
-
this.predictor = options.predictor ?? new MarkovPredictor
|
|
1419
|
+
this.predictor = options.predictor ?? new MarkovPredictor;
|
|
2731
1420
|
}
|
|
2732
|
-
predictor;
|
|
2733
1421
|
async get(key) {
|
|
2734
1422
|
this.predictor.record(key);
|
|
2735
1423
|
const candidates = this.predictor.predict(key);
|
|
2736
1424
|
if (candidates.length > 0) {
|
|
2737
|
-
|
|
2738
|
-
})));
|
|
1425
|
+
Promise.all(candidates.map((k) => this.store.get(k).catch(() => {})));
|
|
2739
1426
|
}
|
|
2740
1427
|
return this.store.get(key);
|
|
2741
1428
|
}
|
|
@@ -2761,41 +1448,23 @@ var PredictiveStore = class {
|
|
|
2761
1448
|
return this.store.decrement(key, value);
|
|
2762
1449
|
}
|
|
2763
1450
|
lock(name, seconds) {
|
|
2764
|
-
return this.store.lock ? this.store.lock(name, seconds) :
|
|
1451
|
+
return this.store.lock ? this.store.lock(name, seconds) : undefined;
|
|
2765
1452
|
}
|
|
2766
1453
|
async ttl(key) {
|
|
2767
1454
|
return this.store.ttl ? this.store.ttl(key) : null;
|
|
2768
1455
|
}
|
|
2769
|
-
}
|
|
1456
|
+
}
|
|
2770
1457
|
|
|
2771
1458
|
// src/stores/RedisStore.ts
|
|
2772
|
-
var import_node_crypto3 = require("crypto");
|
|
2773
1459
|
var import_plasma = require("@gravito/plasma");
|
|
2774
|
-
|
|
1460
|
+
class RedisStore {
|
|
2775
1461
|
connectionName;
|
|
2776
|
-
/**
|
|
2777
|
-
* Initialize a new RedisStore instance.
|
|
2778
|
-
*
|
|
2779
|
-
* @param options - Redis connection and prefix settings.
|
|
2780
|
-
*
|
|
2781
|
-
* @example
|
|
2782
|
-
* ```typescript
|
|
2783
|
-
* const store = new RedisStore({ prefix: 'app:' });
|
|
2784
|
-
* ```
|
|
2785
|
-
*/
|
|
2786
1462
|
constructor(options = {}) {
|
|
2787
1463
|
this.connectionName = options.connection;
|
|
2788
1464
|
}
|
|
2789
1465
|
get client() {
|
|
2790
1466
|
return import_plasma.Redis.connection(this.connectionName);
|
|
2791
1467
|
}
|
|
2792
|
-
/**
|
|
2793
|
-
* Retrieve an item from Redis.
|
|
2794
|
-
*
|
|
2795
|
-
* @param key - Unique cache key identifier.
|
|
2796
|
-
* @returns Parsed JSON value or null if missing/expired.
|
|
2797
|
-
* @throws {Error} If Redis connection fails or read errors occur.
|
|
2798
|
-
*/
|
|
2799
1468
|
async get(key) {
|
|
2800
1469
|
const normalized = normalizeCacheKey(key);
|
|
2801
1470
|
const value = await this.client.get(normalized);
|
|
@@ -2808,14 +1477,6 @@ var RedisStore = class {
|
|
|
2808
1477
|
return value;
|
|
2809
1478
|
}
|
|
2810
1479
|
}
|
|
2811
|
-
/**
|
|
2812
|
-
* Store an item in Redis.
|
|
2813
|
-
*
|
|
2814
|
-
* @param key - Unique cache key identifier.
|
|
2815
|
-
* @param value - Value to serialize and store.
|
|
2816
|
-
* @param ttl - Expiration duration.
|
|
2817
|
-
* @throws {Error} If Redis connection fails or write errors occur.
|
|
2818
|
-
*/
|
|
2819
1480
|
async put(key, value, ttl) {
|
|
2820
1481
|
const normalized = normalizeCacheKey(key);
|
|
2821
1482
|
const serialized = JSON.stringify(value);
|
|
@@ -2866,14 +1527,6 @@ var RedisStore = class {
|
|
|
2866
1527
|
}
|
|
2867
1528
|
return await this.client.incrby(normalized, value);
|
|
2868
1529
|
}
|
|
2869
|
-
/**
|
|
2870
|
-
* Decrement a numeric value in Redis.
|
|
2871
|
-
*
|
|
2872
|
-
* @param key - Unique cache key identifier.
|
|
2873
|
-
* @param value - Amount to subtract.
|
|
2874
|
-
* @returns Updated numeric value.
|
|
2875
|
-
* @throws {Error} If key is not numeric or Redis errors occur.
|
|
2876
|
-
*/
|
|
2877
1530
|
async decrement(key, value = 1) {
|
|
2878
1531
|
const normalized = normalizeCacheKey(key);
|
|
2879
1532
|
if (value === 1) {
|
|
@@ -2881,9 +1534,6 @@ var RedisStore = class {
|
|
|
2881
1534
|
}
|
|
2882
1535
|
return await this.client.decrby(normalized, value);
|
|
2883
1536
|
}
|
|
2884
|
-
// ============================================================================
|
|
2885
|
-
// Tags
|
|
2886
|
-
// ============================================================================
|
|
2887
1537
|
tagKey(key, _tags) {
|
|
2888
1538
|
return key;
|
|
2889
1539
|
}
|
|
@@ -2922,7 +1572,7 @@ var RedisStore = class {
|
|
|
2922
1572
|
pipeline.smembers(tagKey);
|
|
2923
1573
|
}
|
|
2924
1574
|
const results = await pipeline.exec();
|
|
2925
|
-
const keysToDelete =
|
|
1575
|
+
const keysToDelete = new Set;
|
|
2926
1576
|
for (const [err, keys] of results) {
|
|
2927
1577
|
if (!err && Array.isArray(keys)) {
|
|
2928
1578
|
for (const k of keys) {
|
|
@@ -2935,9 +1585,6 @@ var RedisStore = class {
|
|
|
2935
1585
|
}
|
|
2936
1586
|
await this.client.del(...tagKeys);
|
|
2937
1587
|
}
|
|
2938
|
-
// ============================================================================
|
|
2939
|
-
// Locks
|
|
2940
|
-
// ============================================================================
|
|
2941
1588
|
async ttl(key) {
|
|
2942
1589
|
const normalized = normalizeCacheKey(key);
|
|
2943
1590
|
const result = await this.client.ttl(normalized);
|
|
@@ -2945,8 +1592,8 @@ var RedisStore = class {
|
|
|
2945
1592
|
}
|
|
2946
1593
|
lock(name, seconds = 10) {
|
|
2947
1594
|
const lockKey = `lock:${normalizeCacheKey(name)}`;
|
|
2948
|
-
const owner =
|
|
2949
|
-
const ttlMs = Math.max(1, seconds) *
|
|
1595
|
+
const owner = crypto.randomUUID();
|
|
1596
|
+
const ttlMs = Math.max(1, seconds) * 1000;
|
|
2950
1597
|
const client = this.client;
|
|
2951
1598
|
return {
|
|
2952
1599
|
async acquire() {
|
|
@@ -2975,13 +1622,7 @@ var RedisStore = class {
|
|
|
2975
1622
|
end
|
|
2976
1623
|
`;
|
|
2977
1624
|
const evalClient = client;
|
|
2978
|
-
const result = await evalClient.eval(
|
|
2979
|
-
luaScript,
|
|
2980
|
-
1,
|
|
2981
|
-
lockKey,
|
|
2982
|
-
owner,
|
|
2983
|
-
extensionSeconds.toString()
|
|
2984
|
-
);
|
|
1625
|
+
const result = await evalClient.eval(luaScript, 1, lockKey, owner, extensionSeconds.toString());
|
|
2985
1626
|
return result === 1;
|
|
2986
1627
|
},
|
|
2987
1628
|
async getRemainingTime() {
|
|
@@ -2991,7 +1632,7 @@ var RedisStore = class {
|
|
|
2991
1632
|
const retryInterval = options?.retryInterval ?? options?.sleepMillis ?? 100;
|
|
2992
1633
|
const maxRetries = options?.maxRetries ?? Number.POSITIVE_INFINITY;
|
|
2993
1634
|
const signal = options?.signal;
|
|
2994
|
-
const deadline = Date.now() + Math.max(0, secondsToWait) *
|
|
1635
|
+
const deadline = Date.now() + Math.max(0, secondsToWait) * 1000;
|
|
2995
1636
|
let attempt = 0;
|
|
2996
1637
|
while (Date.now() <= deadline && attempt < maxRetries) {
|
|
2997
1638
|
if (signal?.aborted) {
|
|
@@ -3005,25 +1646,19 @@ var RedisStore = class {
|
|
|
3005
1646
|
}
|
|
3006
1647
|
}
|
|
3007
1648
|
attempt++;
|
|
3008
|
-
const delay = Math.min(retryInterval * 1.5 ** Math.min(attempt, 10),
|
|
1649
|
+
const delay = Math.min(retryInterval * 1.5 ** Math.min(attempt, 10), 1000);
|
|
3009
1650
|
await sleep(delay);
|
|
3010
1651
|
}
|
|
3011
|
-
throw new LockTimeoutError(
|
|
3012
|
-
`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`
|
|
3013
|
-
);
|
|
1652
|
+
throw new LockTimeoutError(`Failed to acquire lock '${name}' within ${secondsToWait} seconds.`);
|
|
3014
1653
|
}
|
|
3015
1654
|
};
|
|
3016
1655
|
}
|
|
3017
|
-
}
|
|
1656
|
+
}
|
|
3018
1657
|
|
|
3019
1658
|
// src/stores/TieredStore.ts
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
*
|
|
3024
|
-
* @param local - The L1 cache store (usually MemoryStore).
|
|
3025
|
-
* @param remote - The L2 cache store (usually RedisStore or FileStore).
|
|
3026
|
-
*/
|
|
1659
|
+
class TieredStore {
|
|
1660
|
+
local;
|
|
1661
|
+
remote;
|
|
3027
1662
|
constructor(local, remote) {
|
|
3028
1663
|
this.local = local;
|
|
3029
1664
|
this.remote = remote;
|
|
@@ -3072,11 +1707,11 @@ var TieredStore = class {
|
|
|
3072
1707
|
}
|
|
3073
1708
|
return this.local.ttl ? this.local.ttl(key) : null;
|
|
3074
1709
|
}
|
|
3075
|
-
}
|
|
1710
|
+
}
|
|
3076
1711
|
|
|
3077
1712
|
// src/index.ts
|
|
3078
|
-
|
|
3079
|
-
store = new MemoryStore
|
|
1713
|
+
class MemoryCacheProvider {
|
|
1714
|
+
store = new MemoryStore;
|
|
3080
1715
|
async get(key) {
|
|
3081
1716
|
return this.store.get(key);
|
|
3082
1717
|
}
|
|
@@ -3089,7 +1724,7 @@ var MemoryCacheProvider = class {
|
|
|
3089
1724
|
async clear() {
|
|
3090
1725
|
await this.store.flush();
|
|
3091
1726
|
}
|
|
3092
|
-
}
|
|
1727
|
+
}
|
|
3093
1728
|
function resolveStoreConfig(core, options) {
|
|
3094
1729
|
if (options) {
|
|
3095
1730
|
return options;
|
|
@@ -3107,15 +1742,15 @@ function createStoreFactory(config) {
|
|
|
3107
1742
|
const hasExplicitStores = Object.keys(stores).length > 0;
|
|
3108
1743
|
if (!storeConfig) {
|
|
3109
1744
|
if (name === "memory") {
|
|
3110
|
-
return new MemoryStore
|
|
1745
|
+
return new MemoryStore;
|
|
3111
1746
|
}
|
|
3112
1747
|
if (name === "null") {
|
|
3113
|
-
return new NullStore
|
|
1748
|
+
return new NullStore;
|
|
3114
1749
|
}
|
|
3115
1750
|
if (hasExplicitStores) {
|
|
3116
1751
|
throw new Error(`Cache store '${name}' is not defined.`);
|
|
3117
1752
|
}
|
|
3118
|
-
return new MemoryStore
|
|
1753
|
+
return new MemoryStore;
|
|
3119
1754
|
}
|
|
3120
1755
|
if (storeConfig.driver === "memory") {
|
|
3121
1756
|
return new MemoryStore({ maxItems: storeConfig.maxItems });
|
|
@@ -3127,7 +1762,7 @@ function createStoreFactory(config) {
|
|
|
3127
1762
|
return new RedisStore({ connection: storeConfig.connection, prefix: storeConfig.prefix });
|
|
3128
1763
|
}
|
|
3129
1764
|
if (storeConfig.driver === "null") {
|
|
3130
|
-
return new NullStore
|
|
1765
|
+
return new NullStore;
|
|
3131
1766
|
}
|
|
3132
1767
|
if (storeConfig.driver === "custom") {
|
|
3133
1768
|
return storeConfig.store;
|
|
@@ -3180,7 +1815,7 @@ function createStoreFactory(config) {
|
|
|
3180
1815
|
if (storeConfig.driver === "circuit-breaker") {
|
|
3181
1816
|
const factory = createStoreFactory(config);
|
|
3182
1817
|
const primary = factory(storeConfig.primary);
|
|
3183
|
-
const fallback = storeConfig.fallback ? factory(storeConfig.fallback) :
|
|
1818
|
+
const fallback = storeConfig.fallback ? factory(storeConfig.fallback) : undefined;
|
|
3184
1819
|
return new CircuitBreakerStore(primary, {
|
|
3185
1820
|
maxFailures: storeConfig.maxFailures,
|
|
3186
1821
|
resetTimeout: storeConfig.resetTimeout,
|
|
@@ -3190,16 +1825,18 @@ function createStoreFactory(config) {
|
|
|
3190
1825
|
throw new Error(`Unsupported cache driver '${storeConfig.driver}'.`);
|
|
3191
1826
|
};
|
|
3192
1827
|
}
|
|
3193
|
-
|
|
1828
|
+
|
|
1829
|
+
class OrbitStasis {
|
|
1830
|
+
options;
|
|
1831
|
+
manager;
|
|
3194
1832
|
constructor(options) {
|
|
3195
1833
|
this.options = options;
|
|
3196
1834
|
}
|
|
3197
|
-
manager;
|
|
3198
1835
|
install(core) {
|
|
3199
1836
|
const resolvedConfig = resolveStoreConfig(core, this.options);
|
|
3200
1837
|
const exposeAs = resolvedConfig.exposeAs ?? "cache";
|
|
3201
1838
|
const defaultStore = resolvedConfig.default ?? (resolvedConfig.provider ? "default" : "memory");
|
|
3202
|
-
const defaultTtl = resolvedConfig.defaultTtl ?? (typeof resolvedConfig.defaultTTL === "number" ? resolvedConfig.defaultTTL :
|
|
1839
|
+
const defaultTtl = resolvedConfig.defaultTtl ?? (typeof resolvedConfig.defaultTTL === "number" ? resolvedConfig.defaultTTL : undefined) ?? 60;
|
|
3203
1840
|
const prefix = resolvedConfig.prefix ?? "";
|
|
3204
1841
|
const logger = core.logger;
|
|
3205
1842
|
logger.info(`[OrbitCache] Initializing Cache (Exposed as: ${exposeAs})`);
|
|
@@ -3214,21 +1851,16 @@ var OrbitStasis = class {
|
|
|
3214
1851
|
const key = payload.key ? ` (key: ${payload.key})` : "";
|
|
3215
1852
|
logger.error(`[OrbitCache] cache event '${event}' failed${key}`, error);
|
|
3216
1853
|
});
|
|
3217
|
-
const stores = resolvedConfig.stores ?? (resolvedConfig.provider ? { default: { driver: "provider", provider: resolvedConfig.provider } } :
|
|
3218
|
-
const manager = new CacheManager(
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
mode: resolvedConfig.eventsMode ?? "async",
|
|
3228
|
-
throwOnError: resolvedConfig.throwOnEventError,
|
|
3229
|
-
onError: onEventError
|
|
3230
|
-
}
|
|
3231
|
-
);
|
|
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
|
+
});
|
|
3232
1864
|
this.manager = manager;
|
|
3233
1865
|
core.adapter.use("*", async (c, next) => {
|
|
3234
1866
|
c.set(exposeAs, manager);
|
|
@@ -3243,33 +1875,12 @@ var OrbitStasis = class {
|
|
|
3243
1875
|
}
|
|
3244
1876
|
return this.manager;
|
|
3245
1877
|
}
|
|
3246
|
-
}
|
|
1878
|
+
}
|
|
3247
1879
|
function orbitCache(core, options = {}) {
|
|
3248
1880
|
const orbit = new OrbitStasis(options);
|
|
3249
1881
|
orbit.install(core);
|
|
3250
1882
|
return orbit.getCache();
|
|
3251
1883
|
}
|
|
3252
1884
|
var OrbitCache = OrbitStasis;
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
CacheManager,
|
|
3256
|
-
CacheRepository,
|
|
3257
|
-
CircuitBreakerStore,
|
|
3258
|
-
FileStore,
|
|
3259
|
-
LockTimeoutError,
|
|
3260
|
-
MarkovPredictor,
|
|
3261
|
-
MemoryCacheProvider,
|
|
3262
|
-
MemoryStore,
|
|
3263
|
-
NullStore,
|
|
3264
|
-
OrbitCache,
|
|
3265
|
-
OrbitStasis,
|
|
3266
|
-
PredictiveStore,
|
|
3267
|
-
RateLimiter,
|
|
3268
|
-
RedisStore,
|
|
3269
|
-
TieredStore,
|
|
3270
|
-
isExpired,
|
|
3271
|
-
isTaggableStore,
|
|
3272
|
-
normalizeCacheKey,
|
|
3273
|
-
sleep,
|
|
3274
|
-
ttlToExpiresAt
|
|
3275
|
-
});
|
|
1885
|
+
|
|
1886
|
+
//# debugId=66831AF7E477960E64756E2164756E21
|