@parsrun/cache 0.1.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 +135 -0
- package/dist/adapters/cloudflare-kv.d.ts +51 -0
- package/dist/adapters/cloudflare-kv.js +197 -0
- package/dist/adapters/cloudflare-kv.js.map +1 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +783 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +59 -0
- package/dist/adapters/memory.js +179 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/redis.d.ts +72 -0
- package/dist/adapters/redis.js +242 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/upstash.d.ts +50 -0
- package/dist/adapters/upstash.js +229 -0
- package/dist/adapters/upstash.js.map +1 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +978 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +234 -0
- package/dist/types.js +43 -0
- package/dist/types.js.map +1 -0
- package/package.json +80 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import {
|
|
3
|
+
type,
|
|
4
|
+
cacheSetOptions,
|
|
5
|
+
cacheGetResult,
|
|
6
|
+
cacheStats,
|
|
7
|
+
memoryCacheConfig,
|
|
8
|
+
redisCacheConfig,
|
|
9
|
+
upstashCacheConfig,
|
|
10
|
+
cloudflareKvConfig,
|
|
11
|
+
multiTierCacheConfig,
|
|
12
|
+
cacheConfig
|
|
13
|
+
} from "@parsrun/types";
|
|
14
|
+
var CacheError = class extends Error {
|
|
15
|
+
constructor(message, code, cause) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.cause = cause;
|
|
19
|
+
this.name = "CacheError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var CacheErrorCodes = {
|
|
23
|
+
CONNECTION_FAILED: "CONNECTION_FAILED",
|
|
24
|
+
OPERATION_FAILED: "OPERATION_FAILED",
|
|
25
|
+
SERIALIZATION_ERROR: "SERIALIZATION_ERROR",
|
|
26
|
+
INVALID_CONFIG: "INVALID_CONFIG",
|
|
27
|
+
NOT_IMPLEMENTED: "NOT_IMPLEMENTED"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/adapters/memory.ts
|
|
31
|
+
var MemoryCacheAdapter = class {
|
|
32
|
+
type = "memory";
|
|
33
|
+
cache = /* @__PURE__ */ new Map();
|
|
34
|
+
tagIndex = /* @__PURE__ */ new Map();
|
|
35
|
+
maxEntries;
|
|
36
|
+
cleanupInterval;
|
|
37
|
+
cleanupTimer = null;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.maxEntries = config?.maxEntries ?? 1e4;
|
|
40
|
+
this.cleanupInterval = config?.cleanupInterval ?? 6e4;
|
|
41
|
+
if (this.cleanupInterval > 0) {
|
|
42
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), this.cleanupInterval);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async get(key, options) {
|
|
46
|
+
const entry = this.cache.get(key);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
51
|
+
this.cache.delete(key);
|
|
52
|
+
this.removeFromTagIndex(key, entry.tags);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (options?.refresh && entry.expiresAt && entry.ttlSeconds) {
|
|
56
|
+
entry.expiresAt = Date.now() + entry.ttlSeconds * 1e3;
|
|
57
|
+
}
|
|
58
|
+
return entry.value;
|
|
59
|
+
}
|
|
60
|
+
async set(key, value, options) {
|
|
61
|
+
if (this.cache.size >= this.maxEntries && !this.cache.has(key)) {
|
|
62
|
+
const oldestKey = this.cache.keys().next().value;
|
|
63
|
+
if (oldestKey) {
|
|
64
|
+
const oldEntry = this.cache.get(oldestKey);
|
|
65
|
+
this.cache.delete(oldestKey);
|
|
66
|
+
if (oldEntry?.tags) {
|
|
67
|
+
this.removeFromTagIndex(oldestKey, oldEntry.tags);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const entry = {
|
|
72
|
+
value,
|
|
73
|
+
metadata: options?.metadata
|
|
74
|
+
};
|
|
75
|
+
if (options?.ttl) {
|
|
76
|
+
entry.expiresAt = Date.now() + options.ttl * 1e3;
|
|
77
|
+
entry.ttlSeconds = options.ttl;
|
|
78
|
+
}
|
|
79
|
+
if (options?.tags && options.tags.length > 0) {
|
|
80
|
+
entry.tags = options.tags;
|
|
81
|
+
this.addToTagIndex(key, options.tags);
|
|
82
|
+
}
|
|
83
|
+
this.cache.set(key, entry);
|
|
84
|
+
}
|
|
85
|
+
async delete(key) {
|
|
86
|
+
const entry = this.cache.get(key);
|
|
87
|
+
if (entry?.tags) {
|
|
88
|
+
this.removeFromTagIndex(key, entry.tags);
|
|
89
|
+
}
|
|
90
|
+
this.cache.delete(key);
|
|
91
|
+
}
|
|
92
|
+
async has(key) {
|
|
93
|
+
const entry = this.cache.get(key);
|
|
94
|
+
if (!entry) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
98
|
+
this.cache.delete(key);
|
|
99
|
+
this.removeFromTagIndex(key, entry.tags);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
async clear() {
|
|
105
|
+
this.cache.clear();
|
|
106
|
+
this.tagIndex.clear();
|
|
107
|
+
}
|
|
108
|
+
async getMany(keys) {
|
|
109
|
+
const result = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
result.set(key, await this.get(key));
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
async setMany(entries, options) {
|
|
116
|
+
for (const [key, value] of entries) {
|
|
117
|
+
await this.set(key, value, options);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async deleteMany(keys) {
|
|
121
|
+
for (const key of keys) {
|
|
122
|
+
await this.delete(key);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async invalidateByTags(tags) {
|
|
126
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
127
|
+
for (const tag of tags) {
|
|
128
|
+
const keys = this.tagIndex.get(tag);
|
|
129
|
+
if (keys) {
|
|
130
|
+
for (const key of keys) {
|
|
131
|
+
keysToDelete.add(key);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const key of keysToDelete) {
|
|
136
|
+
await this.delete(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async ttl(key) {
|
|
140
|
+
const entry = this.cache.get(key);
|
|
141
|
+
if (!entry) {
|
|
142
|
+
return -2;
|
|
143
|
+
}
|
|
144
|
+
if (!entry.expiresAt) {
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
const remaining = entry.expiresAt - Date.now();
|
|
148
|
+
if (remaining <= 0) {
|
|
149
|
+
return -2;
|
|
150
|
+
}
|
|
151
|
+
return Math.ceil(remaining / 1e3);
|
|
152
|
+
}
|
|
153
|
+
async close() {
|
|
154
|
+
if (this.cleanupTimer) {
|
|
155
|
+
clearInterval(this.cleanupTimer);
|
|
156
|
+
this.cleanupTimer = null;
|
|
157
|
+
}
|
|
158
|
+
await this.clear();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get cache statistics
|
|
162
|
+
*/
|
|
163
|
+
getStats() {
|
|
164
|
+
return {
|
|
165
|
+
size: this.cache.size,
|
|
166
|
+
maxEntries: this.maxEntries
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
addToTagIndex(key, tags) {
|
|
170
|
+
for (const tag of tags) {
|
|
171
|
+
let keys = this.tagIndex.get(tag);
|
|
172
|
+
if (!keys) {
|
|
173
|
+
keys = /* @__PURE__ */ new Set();
|
|
174
|
+
this.tagIndex.set(tag, keys);
|
|
175
|
+
}
|
|
176
|
+
keys.add(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
removeFromTagIndex(key, tags) {
|
|
180
|
+
if (!tags) return;
|
|
181
|
+
for (const tag of tags) {
|
|
182
|
+
const keys = this.tagIndex.get(tag);
|
|
183
|
+
if (keys) {
|
|
184
|
+
keys.delete(key);
|
|
185
|
+
if (keys.size === 0) {
|
|
186
|
+
this.tagIndex.delete(tag);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
cleanup() {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
for (const [key, entry] of this.cache) {
|
|
194
|
+
if (entry.expiresAt && now > entry.expiresAt) {
|
|
195
|
+
this.removeFromTagIndex(key, entry.tags);
|
|
196
|
+
this.cache.delete(key);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
function createMemoryCacheAdapter(config) {
|
|
202
|
+
return new MemoryCacheAdapter(config);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/adapters/redis.ts
|
|
206
|
+
var RedisCacheAdapter = class {
|
|
207
|
+
type = "redis";
|
|
208
|
+
client;
|
|
209
|
+
keyPrefix;
|
|
210
|
+
isExternalClient;
|
|
211
|
+
constructor(config) {
|
|
212
|
+
this.client = config.client;
|
|
213
|
+
this.keyPrefix = config.keyPrefix ?? "";
|
|
214
|
+
this.isExternalClient = true;
|
|
215
|
+
}
|
|
216
|
+
prefixKey(key) {
|
|
217
|
+
return this.keyPrefix ? `${this.keyPrefix}:${key}` : key;
|
|
218
|
+
}
|
|
219
|
+
async get(key, _options) {
|
|
220
|
+
try {
|
|
221
|
+
const data = await this.client.get(this.prefixKey(key));
|
|
222
|
+
if (!data) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const parsed = JSON.parse(data);
|
|
226
|
+
return parsed.value;
|
|
227
|
+
} catch (err) {
|
|
228
|
+
throw new CacheError(
|
|
229
|
+
`Redis get failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
230
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
231
|
+
err
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async set(key, value, options) {
|
|
236
|
+
try {
|
|
237
|
+
const data = JSON.stringify({
|
|
238
|
+
value,
|
|
239
|
+
tags: options?.tags,
|
|
240
|
+
metadata: options?.metadata
|
|
241
|
+
});
|
|
242
|
+
const prefixedKey = this.prefixKey(key);
|
|
243
|
+
if (options?.ttl) {
|
|
244
|
+
await this.client.set(prefixedKey, data, "EX", options.ttl);
|
|
245
|
+
} else {
|
|
246
|
+
await this.client.set(prefixedKey, data);
|
|
247
|
+
}
|
|
248
|
+
if (options?.tags && options.tags.length > 0) {
|
|
249
|
+
const pipeline = this.client.pipeline();
|
|
250
|
+
for (const tag of options.tags) {
|
|
251
|
+
const tagKey = this.prefixKey(`__tag:${tag}`);
|
|
252
|
+
pipeline.set(tagKey, JSON.stringify([...await this.getTagKeys(tag), key]));
|
|
253
|
+
}
|
|
254
|
+
await pipeline.exec();
|
|
255
|
+
}
|
|
256
|
+
} catch (err) {
|
|
257
|
+
throw new CacheError(
|
|
258
|
+
`Redis set failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
259
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
260
|
+
err
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async delete(key) {
|
|
265
|
+
try {
|
|
266
|
+
await this.client.del(this.prefixKey(key));
|
|
267
|
+
} catch (err) {
|
|
268
|
+
throw new CacheError(
|
|
269
|
+
`Redis delete failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
270
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
271
|
+
err
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async has(key) {
|
|
276
|
+
try {
|
|
277
|
+
const exists = await this.client.exists(this.prefixKey(key));
|
|
278
|
+
return exists > 0;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
throw new CacheError(
|
|
281
|
+
`Redis exists failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
282
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
283
|
+
err
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async clear() {
|
|
288
|
+
try {
|
|
289
|
+
if (this.keyPrefix) {
|
|
290
|
+
const keys = await this.client.keys(`${this.keyPrefix}:*`);
|
|
291
|
+
if (keys.length > 0) {
|
|
292
|
+
await this.client.del(...keys);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
await this.client.flushdb();
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
throw new CacheError(
|
|
299
|
+
`Redis clear failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
300
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
301
|
+
err
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async getMany(keys) {
|
|
306
|
+
try {
|
|
307
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
308
|
+
const results = await this.client.mget(...prefixedKeys);
|
|
309
|
+
const map = /* @__PURE__ */ new Map();
|
|
310
|
+
for (let i = 0; i < keys.length; i++) {
|
|
311
|
+
const data = results[i];
|
|
312
|
+
if (data) {
|
|
313
|
+
const parsed = JSON.parse(data);
|
|
314
|
+
map.set(keys[i] ?? "", parsed.value);
|
|
315
|
+
} else {
|
|
316
|
+
map.set(keys[i] ?? "", null);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return map;
|
|
320
|
+
} catch (err) {
|
|
321
|
+
throw new CacheError(
|
|
322
|
+
`Redis mget failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
323
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
324
|
+
err
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async setMany(entries, options) {
|
|
329
|
+
try {
|
|
330
|
+
const pipeline = this.client.pipeline();
|
|
331
|
+
for (const [key, value] of entries) {
|
|
332
|
+
const data = JSON.stringify({
|
|
333
|
+
value,
|
|
334
|
+
tags: options?.tags,
|
|
335
|
+
metadata: options?.metadata
|
|
336
|
+
});
|
|
337
|
+
const prefixedKey = this.prefixKey(key);
|
|
338
|
+
if (options?.ttl) {
|
|
339
|
+
pipeline.set(prefixedKey, data, "EX", options.ttl);
|
|
340
|
+
} else {
|
|
341
|
+
pipeline.set(prefixedKey, data);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
await pipeline.exec();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
throw new CacheError(
|
|
347
|
+
`Redis mset failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
348
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
349
|
+
err
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async deleteMany(keys) {
|
|
354
|
+
try {
|
|
355
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
356
|
+
await this.client.del(...prefixedKeys);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
throw new CacheError(
|
|
359
|
+
`Redis mdel failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
360
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
361
|
+
err
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async invalidateByTags(tags) {
|
|
366
|
+
try {
|
|
367
|
+
const keysToDelete = [];
|
|
368
|
+
for (const tag of tags) {
|
|
369
|
+
const keys = await this.getTagKeys(tag);
|
|
370
|
+
keysToDelete.push(...keys);
|
|
371
|
+
}
|
|
372
|
+
if (keysToDelete.length > 0) {
|
|
373
|
+
await this.deleteMany(keysToDelete);
|
|
374
|
+
}
|
|
375
|
+
const tagKeys = tags.map((tag) => this.prefixKey(`__tag:${tag}`));
|
|
376
|
+
await this.client.del(...tagKeys);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
throw new CacheError(
|
|
379
|
+
`Redis invalidate by tags failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
380
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
381
|
+
err
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async ttl(key) {
|
|
386
|
+
try {
|
|
387
|
+
return await this.client.ttl(this.prefixKey(key));
|
|
388
|
+
} catch (err) {
|
|
389
|
+
throw new CacheError(
|
|
390
|
+
`Redis ttl failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
391
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
392
|
+
err
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async close() {
|
|
397
|
+
if (!this.isExternalClient) {
|
|
398
|
+
await this.client.quit();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async getTagKeys(tag) {
|
|
402
|
+
const tagKey = this.prefixKey(`__tag:${tag}`);
|
|
403
|
+
const data = await this.client.get(tagKey);
|
|
404
|
+
if (data) {
|
|
405
|
+
return JSON.parse(data);
|
|
406
|
+
}
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
function createRedisCacheAdapter(config) {
|
|
411
|
+
return new RedisCacheAdapter(config);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/adapters/upstash.ts
|
|
415
|
+
var UpstashCacheAdapter = class {
|
|
416
|
+
type = "upstash";
|
|
417
|
+
url;
|
|
418
|
+
token;
|
|
419
|
+
keyPrefix;
|
|
420
|
+
constructor(config) {
|
|
421
|
+
this.url = config.url.replace(/\/$/, "");
|
|
422
|
+
this.token = config.token;
|
|
423
|
+
this.keyPrefix = config.keyPrefix ?? "";
|
|
424
|
+
}
|
|
425
|
+
prefixKey(key) {
|
|
426
|
+
return this.keyPrefix ? `${this.keyPrefix}:${key}` : key;
|
|
427
|
+
}
|
|
428
|
+
async command(...args) {
|
|
429
|
+
try {
|
|
430
|
+
const response = await fetch(this.url, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: {
|
|
433
|
+
Authorization: `Bearer ${this.token}`,
|
|
434
|
+
"Content-Type": "application/json"
|
|
435
|
+
},
|
|
436
|
+
body: JSON.stringify(args)
|
|
437
|
+
});
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
const error = await response.text();
|
|
440
|
+
throw new Error(`Upstash API error: ${error}`);
|
|
441
|
+
}
|
|
442
|
+
const data = await response.json();
|
|
443
|
+
if (data.error) {
|
|
444
|
+
throw new Error(data.error);
|
|
445
|
+
}
|
|
446
|
+
return data.result;
|
|
447
|
+
} catch (err) {
|
|
448
|
+
throw new CacheError(
|
|
449
|
+
`Upstash command failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
450
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
451
|
+
err
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async pipeline(commands) {
|
|
456
|
+
try {
|
|
457
|
+
const response = await fetch(`${this.url}/pipeline`, {
|
|
458
|
+
method: "POST",
|
|
459
|
+
headers: {
|
|
460
|
+
Authorization: `Bearer ${this.token}`,
|
|
461
|
+
"Content-Type": "application/json"
|
|
462
|
+
},
|
|
463
|
+
body: JSON.stringify(commands)
|
|
464
|
+
});
|
|
465
|
+
if (!response.ok) {
|
|
466
|
+
const error = await response.text();
|
|
467
|
+
throw new Error(`Upstash API error: ${error}`);
|
|
468
|
+
}
|
|
469
|
+
const data = await response.json();
|
|
470
|
+
return data.map((item) => {
|
|
471
|
+
if (item.error) {
|
|
472
|
+
throw new Error(item.error);
|
|
473
|
+
}
|
|
474
|
+
return item.result;
|
|
475
|
+
});
|
|
476
|
+
} catch (err) {
|
|
477
|
+
throw new CacheError(
|
|
478
|
+
`Upstash pipeline failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
479
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
480
|
+
err
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async get(key, _options) {
|
|
485
|
+
const data = await this.command("GET", this.prefixKey(key));
|
|
486
|
+
if (!data) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const parsed = JSON.parse(data);
|
|
491
|
+
return parsed.value;
|
|
492
|
+
} catch {
|
|
493
|
+
return data;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async set(key, value, options) {
|
|
497
|
+
const data = JSON.stringify({
|
|
498
|
+
value,
|
|
499
|
+
tags: options?.tags,
|
|
500
|
+
metadata: options?.metadata
|
|
501
|
+
});
|
|
502
|
+
const prefixedKey = this.prefixKey(key);
|
|
503
|
+
if (options?.ttl) {
|
|
504
|
+
await this.command("SET", prefixedKey, data, "EX", options.ttl);
|
|
505
|
+
} else {
|
|
506
|
+
await this.command("SET", prefixedKey, data);
|
|
507
|
+
}
|
|
508
|
+
if (options?.tags && options.tags.length > 0) {
|
|
509
|
+
const commands = [];
|
|
510
|
+
for (const tag of options.tags) {
|
|
511
|
+
commands.push(["SADD", this.prefixKey(`__tag:${tag}`), key]);
|
|
512
|
+
}
|
|
513
|
+
await this.pipeline(commands);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async delete(key) {
|
|
517
|
+
await this.command("DEL", this.prefixKey(key));
|
|
518
|
+
}
|
|
519
|
+
async has(key) {
|
|
520
|
+
const exists = await this.command("EXISTS", this.prefixKey(key));
|
|
521
|
+
return exists > 0;
|
|
522
|
+
}
|
|
523
|
+
async clear() {
|
|
524
|
+
if (this.keyPrefix) {
|
|
525
|
+
let cursor = "0";
|
|
526
|
+
do {
|
|
527
|
+
const result = await this.command(
|
|
528
|
+
"SCAN",
|
|
529
|
+
cursor,
|
|
530
|
+
"MATCH",
|
|
531
|
+
`${this.keyPrefix}:*`,
|
|
532
|
+
"COUNT",
|
|
533
|
+
100
|
|
534
|
+
);
|
|
535
|
+
cursor = result[0];
|
|
536
|
+
const keys = result[1];
|
|
537
|
+
if (keys.length > 0) {
|
|
538
|
+
await this.command("DEL", ...keys);
|
|
539
|
+
}
|
|
540
|
+
} while (cursor !== "0");
|
|
541
|
+
} else {
|
|
542
|
+
await this.command("FLUSHDB");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async getMany(keys) {
|
|
546
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
547
|
+
const results = await this.command("MGET", ...prefixedKeys);
|
|
548
|
+
const map = /* @__PURE__ */ new Map();
|
|
549
|
+
for (let i = 0; i < keys.length; i++) {
|
|
550
|
+
const data = results[i];
|
|
551
|
+
const key = keys[i];
|
|
552
|
+
if (key !== void 0) {
|
|
553
|
+
if (data) {
|
|
554
|
+
try {
|
|
555
|
+
const parsed = JSON.parse(data);
|
|
556
|
+
map.set(key, parsed.value);
|
|
557
|
+
} catch {
|
|
558
|
+
map.set(key, data);
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
map.set(key, null);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return map;
|
|
566
|
+
}
|
|
567
|
+
async setMany(entries, options) {
|
|
568
|
+
const commands = [];
|
|
569
|
+
for (const [key, value] of entries) {
|
|
570
|
+
const data = JSON.stringify({
|
|
571
|
+
value,
|
|
572
|
+
tags: options?.tags,
|
|
573
|
+
metadata: options?.metadata
|
|
574
|
+
});
|
|
575
|
+
const prefixedKey = this.prefixKey(key);
|
|
576
|
+
if (options?.ttl) {
|
|
577
|
+
commands.push(["SET", prefixedKey, data, "EX", options.ttl]);
|
|
578
|
+
} else {
|
|
579
|
+
commands.push(["SET", prefixedKey, data]);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
await this.pipeline(commands);
|
|
583
|
+
}
|
|
584
|
+
async deleteMany(keys) {
|
|
585
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
586
|
+
await this.command("DEL", ...prefixedKeys);
|
|
587
|
+
}
|
|
588
|
+
async invalidateByTags(tags) {
|
|
589
|
+
const keysToDelete = [];
|
|
590
|
+
for (const tag of tags) {
|
|
591
|
+
const keys = await this.command("SMEMBERS", this.prefixKey(`__tag:${tag}`));
|
|
592
|
+
keysToDelete.push(...keys);
|
|
593
|
+
}
|
|
594
|
+
if (keysToDelete.length > 0) {
|
|
595
|
+
await this.deleteMany(keysToDelete);
|
|
596
|
+
}
|
|
597
|
+
const tagKeys = tags.map((tag) => this.prefixKey(`__tag:${tag}`));
|
|
598
|
+
await this.command("DEL", ...tagKeys);
|
|
599
|
+
}
|
|
600
|
+
async ttl(key) {
|
|
601
|
+
return await this.command("TTL", this.prefixKey(key));
|
|
602
|
+
}
|
|
603
|
+
async close() {
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
function createUpstashCacheAdapter(config) {
|
|
607
|
+
return new UpstashCacheAdapter(config);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/adapters/cloudflare-kv.ts
|
|
611
|
+
var CloudflareKVCacheAdapter = class {
|
|
612
|
+
type = "cloudflare-kv";
|
|
613
|
+
kv;
|
|
614
|
+
keyPrefix;
|
|
615
|
+
constructor(config) {
|
|
616
|
+
this.kv = config.namespace;
|
|
617
|
+
this.keyPrefix = config.keyPrefix ?? "";
|
|
618
|
+
}
|
|
619
|
+
prefixKey(key) {
|
|
620
|
+
return this.keyPrefix ? `${this.keyPrefix}:${key}` : key;
|
|
621
|
+
}
|
|
622
|
+
async get(key, _options) {
|
|
623
|
+
try {
|
|
624
|
+
const data = await this.kv.get(this.prefixKey(key), { type: "json" });
|
|
625
|
+
if (!data) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
return data.value;
|
|
629
|
+
} catch (err) {
|
|
630
|
+
throw new CacheError(
|
|
631
|
+
`Cloudflare KV get failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
632
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
633
|
+
err
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async set(key, value, options) {
|
|
638
|
+
try {
|
|
639
|
+
const data = {
|
|
640
|
+
value,
|
|
641
|
+
tags: options?.tags,
|
|
642
|
+
metadata: options?.metadata
|
|
643
|
+
};
|
|
644
|
+
const putOptions = {};
|
|
645
|
+
if (options?.ttl) {
|
|
646
|
+
putOptions.expirationTtl = options.ttl;
|
|
647
|
+
}
|
|
648
|
+
if (options?.tags || options?.metadata) {
|
|
649
|
+
putOptions.metadata = {
|
|
650
|
+
tags: options.tags,
|
|
651
|
+
...options.metadata
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
await this.kv.put(this.prefixKey(key), JSON.stringify(data), putOptions);
|
|
655
|
+
if (options?.tags && options.tags.length > 0) {
|
|
656
|
+
for (const tag of options.tags) {
|
|
657
|
+
const tagKey = this.prefixKey(`__tag:${tag}`);
|
|
658
|
+
const existing = await this.kv.get(tagKey, { type: "json" });
|
|
659
|
+
const keys = existing ? [.../* @__PURE__ */ new Set([...existing, key])] : [key];
|
|
660
|
+
await this.kv.put(tagKey, JSON.stringify(keys));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} catch (err) {
|
|
664
|
+
throw new CacheError(
|
|
665
|
+
`Cloudflare KV set failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
666
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
667
|
+
err
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async delete(key) {
|
|
672
|
+
try {
|
|
673
|
+
await this.kv.delete(this.prefixKey(key));
|
|
674
|
+
} catch (err) {
|
|
675
|
+
throw new CacheError(
|
|
676
|
+
`Cloudflare KV delete failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
677
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
678
|
+
err
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async has(key) {
|
|
683
|
+
try {
|
|
684
|
+
const value = await this.kv.get(this.prefixKey(key));
|
|
685
|
+
return value !== null;
|
|
686
|
+
} catch (err) {
|
|
687
|
+
throw new CacheError(
|
|
688
|
+
`Cloudflare KV has failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
689
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
690
|
+
err
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async clear() {
|
|
695
|
+
try {
|
|
696
|
+
const prefix = this.keyPrefix ? `${this.keyPrefix}:` : "";
|
|
697
|
+
let cursor;
|
|
698
|
+
do {
|
|
699
|
+
const result = await this.kv.list({
|
|
700
|
+
prefix,
|
|
701
|
+
limit: 1e3,
|
|
702
|
+
cursor
|
|
703
|
+
});
|
|
704
|
+
for (const key of result.keys) {
|
|
705
|
+
await this.kv.delete(key.name);
|
|
706
|
+
}
|
|
707
|
+
cursor = result.list_complete ? void 0 : result.cursor;
|
|
708
|
+
} while (cursor);
|
|
709
|
+
} catch (err) {
|
|
710
|
+
throw new CacheError(
|
|
711
|
+
`Cloudflare KV clear failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
712
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
713
|
+
err
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async getMany(keys) {
|
|
718
|
+
const map = /* @__PURE__ */ new Map();
|
|
719
|
+
const promises = keys.map(async (key) => {
|
|
720
|
+
const value = await this.get(key);
|
|
721
|
+
return { key, value };
|
|
722
|
+
});
|
|
723
|
+
const results = await Promise.all(promises);
|
|
724
|
+
for (const { key, value } of results) {
|
|
725
|
+
map.set(key, value);
|
|
726
|
+
}
|
|
727
|
+
return map;
|
|
728
|
+
}
|
|
729
|
+
async setMany(entries, options) {
|
|
730
|
+
const promises = [];
|
|
731
|
+
for (const [key, value] of entries) {
|
|
732
|
+
promises.push(this.set(key, value, options));
|
|
733
|
+
}
|
|
734
|
+
await Promise.all(promises);
|
|
735
|
+
}
|
|
736
|
+
async deleteMany(keys) {
|
|
737
|
+
const promises = keys.map((key) => this.delete(key));
|
|
738
|
+
await Promise.all(promises);
|
|
739
|
+
}
|
|
740
|
+
async invalidateByTags(tags) {
|
|
741
|
+
try {
|
|
742
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
743
|
+
for (const tag of tags) {
|
|
744
|
+
const tagKey = this.prefixKey(`__tag:${tag}`);
|
|
745
|
+
const keys = await this.kv.get(tagKey, { type: "json" });
|
|
746
|
+
if (keys) {
|
|
747
|
+
for (const key of keys) {
|
|
748
|
+
keysToDelete.add(key);
|
|
749
|
+
}
|
|
750
|
+
await this.kv.delete(tagKey);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (keysToDelete.size > 0) {
|
|
754
|
+
await this.deleteMany([...keysToDelete]);
|
|
755
|
+
}
|
|
756
|
+
} catch (err) {
|
|
757
|
+
throw new CacheError(
|
|
758
|
+
`Cloudflare KV invalidate by tags failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
759
|
+
CacheErrorCodes.OPERATION_FAILED,
|
|
760
|
+
err
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async ttl(_key) {
|
|
765
|
+
return -1;
|
|
766
|
+
}
|
|
767
|
+
async close() {
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
function createCloudflareKVCacheAdapter(config) {
|
|
771
|
+
return new CloudflareKVCacheAdapter(config);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/index.ts
|
|
775
|
+
var CacheService = class {
|
|
776
|
+
adapter;
|
|
777
|
+
defaultTtl;
|
|
778
|
+
keyPrefix;
|
|
779
|
+
debug;
|
|
780
|
+
constructor(config) {
|
|
781
|
+
this.adapter = config.adapter;
|
|
782
|
+
this.defaultTtl = config.defaultTtl;
|
|
783
|
+
this.keyPrefix = config.keyPrefix ?? "";
|
|
784
|
+
this.debug = config.debug ?? false;
|
|
785
|
+
}
|
|
786
|
+
prefixKey(key) {
|
|
787
|
+
return this.keyPrefix ? `${this.keyPrefix}:${key}` : key;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Get adapter type
|
|
791
|
+
*/
|
|
792
|
+
get adapterType() {
|
|
793
|
+
return this.adapter.type;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Get a value from cache
|
|
797
|
+
*/
|
|
798
|
+
async get(key, options) {
|
|
799
|
+
const prefixedKey = this.prefixKey(key);
|
|
800
|
+
if (this.debug) {
|
|
801
|
+
console.log(`[Cache] GET ${prefixedKey}`);
|
|
802
|
+
}
|
|
803
|
+
return this.adapter.get(prefixedKey, options);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Set a value in cache
|
|
807
|
+
*/
|
|
808
|
+
async set(key, value, options) {
|
|
809
|
+
const prefixedKey = this.prefixKey(key);
|
|
810
|
+
const ttl = options?.ttl ?? this.defaultTtl;
|
|
811
|
+
if (this.debug) {
|
|
812
|
+
console.log(`[Cache] SET ${prefixedKey} (ttl: ${ttl ?? "none"})`);
|
|
813
|
+
}
|
|
814
|
+
await this.adapter.set(prefixedKey, value, { ...options, ttl });
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Delete a value from cache
|
|
818
|
+
*/
|
|
819
|
+
async delete(key) {
|
|
820
|
+
const prefixedKey = this.prefixKey(key);
|
|
821
|
+
if (this.debug) {
|
|
822
|
+
console.log(`[Cache] DELETE ${prefixedKey}`);
|
|
823
|
+
}
|
|
824
|
+
await this.adapter.delete(prefixedKey);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Check if a key exists in cache
|
|
828
|
+
*/
|
|
829
|
+
async has(key) {
|
|
830
|
+
return this.adapter.has(this.prefixKey(key));
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Clear all entries from cache
|
|
834
|
+
*/
|
|
835
|
+
async clear() {
|
|
836
|
+
if (this.adapter.clear) {
|
|
837
|
+
await this.adapter.clear();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Get multiple values at once
|
|
842
|
+
*/
|
|
843
|
+
async getMany(keys) {
|
|
844
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
845
|
+
if (this.adapter.getMany) {
|
|
846
|
+
const results2 = await this.adapter.getMany(prefixedKeys);
|
|
847
|
+
const unprefixed = /* @__PURE__ */ new Map();
|
|
848
|
+
let i = 0;
|
|
849
|
+
for (const key of keys) {
|
|
850
|
+
const prefixedKey = prefixedKeys[i];
|
|
851
|
+
if (prefixedKey) {
|
|
852
|
+
unprefixed.set(key, results2.get(prefixedKey) ?? null);
|
|
853
|
+
}
|
|
854
|
+
i++;
|
|
855
|
+
}
|
|
856
|
+
return unprefixed;
|
|
857
|
+
}
|
|
858
|
+
const results = /* @__PURE__ */ new Map();
|
|
859
|
+
for (const key of keys) {
|
|
860
|
+
results.set(key, await this.get(key));
|
|
861
|
+
}
|
|
862
|
+
return results;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Set multiple values at once
|
|
866
|
+
*/
|
|
867
|
+
async setMany(entries, options) {
|
|
868
|
+
const prefixedEntries = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const [key, value] of entries) {
|
|
870
|
+
prefixedEntries.set(this.prefixKey(key), value);
|
|
871
|
+
}
|
|
872
|
+
const ttl = options?.ttl ?? this.defaultTtl;
|
|
873
|
+
const opts = { ...options, ttl };
|
|
874
|
+
if (this.adapter.setMany) {
|
|
875
|
+
await this.adapter.setMany(prefixedEntries, opts);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
for (const [key, value] of entries) {
|
|
879
|
+
await this.set(key, value, opts);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Delete multiple keys
|
|
884
|
+
*/
|
|
885
|
+
async deleteMany(keys) {
|
|
886
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
887
|
+
if (this.adapter.deleteMany) {
|
|
888
|
+
await this.adapter.deleteMany(prefixedKeys);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
for (const key of keys) {
|
|
892
|
+
await this.delete(key);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Invalidate entries by tags
|
|
897
|
+
*/
|
|
898
|
+
async invalidateByTags(tags) {
|
|
899
|
+
if (this.adapter.invalidateByTags) {
|
|
900
|
+
await this.adapter.invalidateByTags(tags);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Get TTL for a key (in seconds)
|
|
905
|
+
* @returns TTL in seconds, -1 if no expiry, -2 if key doesn't exist
|
|
906
|
+
*/
|
|
907
|
+
async ttl(key) {
|
|
908
|
+
if (this.adapter.ttl) {
|
|
909
|
+
return this.adapter.ttl(this.prefixKey(key));
|
|
910
|
+
}
|
|
911
|
+
return -1;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Get or set a value with callback
|
|
915
|
+
* @param key Cache key
|
|
916
|
+
* @param fn Function to compute value if not cached
|
|
917
|
+
* @param options Set options (applied if value is computed)
|
|
918
|
+
*/
|
|
919
|
+
async getOrSet(key, fn, options) {
|
|
920
|
+
const cached = await this.get(key);
|
|
921
|
+
if (cached !== null) {
|
|
922
|
+
return cached;
|
|
923
|
+
}
|
|
924
|
+
const value = await fn();
|
|
925
|
+
await this.set(key, value, options);
|
|
926
|
+
return value;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Wrap a function with caching
|
|
930
|
+
*/
|
|
931
|
+
wrap(fn, keyFn, options) {
|
|
932
|
+
return async (...args) => {
|
|
933
|
+
const key = keyFn(...args);
|
|
934
|
+
return this.getOrSet(key, () => fn(...args), options);
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Close/cleanup cache resources
|
|
939
|
+
*/
|
|
940
|
+
async close() {
|
|
941
|
+
if (this.adapter.close) {
|
|
942
|
+
await this.adapter.close();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
function createCacheService(config) {
|
|
947
|
+
return new CacheService(config);
|
|
948
|
+
}
|
|
949
|
+
var index_default = {
|
|
950
|
+
CacheService,
|
|
951
|
+
createCacheService
|
|
952
|
+
};
|
|
953
|
+
export {
|
|
954
|
+
CacheError,
|
|
955
|
+
CacheErrorCodes,
|
|
956
|
+
CacheService,
|
|
957
|
+
CloudflareKVCacheAdapter,
|
|
958
|
+
MemoryCacheAdapter,
|
|
959
|
+
RedisCacheAdapter,
|
|
960
|
+
UpstashCacheAdapter,
|
|
961
|
+
cacheConfig,
|
|
962
|
+
cacheGetResult,
|
|
963
|
+
cacheStats,
|
|
964
|
+
cloudflareKvConfig,
|
|
965
|
+
createCacheService,
|
|
966
|
+
createCloudflareKVCacheAdapter,
|
|
967
|
+
createMemoryCacheAdapter,
|
|
968
|
+
createRedisCacheAdapter,
|
|
969
|
+
createUpstashCacheAdapter,
|
|
970
|
+
index_default as default,
|
|
971
|
+
memoryCacheConfig,
|
|
972
|
+
multiTierCacheConfig,
|
|
973
|
+
cacheSetOptions as parsCacheSetOptions,
|
|
974
|
+
redisCacheConfig,
|
|
975
|
+
type,
|
|
976
|
+
upstashCacheConfig
|
|
977
|
+
};
|
|
978
|
+
//# sourceMappingURL=index.js.map
|