layercache 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -5
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-BWM4MU2X.js → chunk-GF47Y3XR.js} +13 -38
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +121 -21
- package/dist/cli.js +57 -2
- package/dist/edge-C1sBhTfv.d.cts +667 -0
- package/dist/edge-C1sBhTfv.d.ts +667 -0
- package/dist/edge.cjs +399 -0
- package/dist/edge.d.cts +2 -0
- package/dist/edge.d.ts +2 -0
- package/dist/edge.js +14 -0
- package/dist/index.cjs +969 -195
- package/dist/index.d.cts +43 -567
- package/dist/index.d.ts +43 -567
- package/dist/index.js +849 -496
- package/package.json +7 -2
- package/packages/nestjs/dist/index.cjs +913 -375
- package/packages/nestjs/dist/index.d.cts +75 -0
- package/packages/nestjs/dist/index.d.ts +75 -0
- package/packages/nestjs/dist/index.js +901 -373
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -40,17 +50,18 @@ __export(index_exports, {
|
|
|
40
50
|
createCachedMethodDecorator: () => createCachedMethodDecorator,
|
|
41
51
|
createExpressCacheMiddleware: () => createExpressCacheMiddleware,
|
|
42
52
|
createFastifyLayercachePlugin: () => createFastifyLayercachePlugin,
|
|
53
|
+
createHonoCacheMiddleware: () => createHonoCacheMiddleware,
|
|
54
|
+
createOpenTelemetryPlugin: () => createOpenTelemetryPlugin,
|
|
43
55
|
createPrometheusMetricsExporter: () => createPrometheusMetricsExporter,
|
|
44
56
|
createTrpcCacheMiddleware: () => createTrpcCacheMiddleware
|
|
45
57
|
});
|
|
46
58
|
module.exports = __toCommonJS(index_exports);
|
|
47
59
|
|
|
48
60
|
// src/CacheStack.ts
|
|
49
|
-
var import_node_crypto = require("crypto");
|
|
50
61
|
var import_node_events = require("events");
|
|
51
|
-
var import_node_fs = require("fs");
|
|
52
62
|
|
|
53
63
|
// src/CacheNamespace.ts
|
|
64
|
+
var import_async_mutex = require("async-mutex");
|
|
54
65
|
var CacheNamespace = class _CacheNamespace {
|
|
55
66
|
constructor(cache, prefix) {
|
|
56
67
|
this.cache = cache;
|
|
@@ -58,57 +69,69 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
58
69
|
}
|
|
59
70
|
cache;
|
|
60
71
|
prefix;
|
|
72
|
+
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
73
|
+
metrics = emptyMetrics();
|
|
61
74
|
async get(key, fetcher, options) {
|
|
62
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
75
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
63
76
|
}
|
|
64
77
|
async getOrSet(key, fetcher, options) {
|
|
65
|
-
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
78
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
68
81
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
69
82
|
*/
|
|
70
83
|
async getOrThrow(key, fetcher, options) {
|
|
71
|
-
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
84
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
72
85
|
}
|
|
73
86
|
async has(key) {
|
|
74
|
-
return this.cache.has(this.qualify(key));
|
|
87
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
75
88
|
}
|
|
76
89
|
async ttl(key) {
|
|
77
|
-
return this.cache.ttl(this.qualify(key));
|
|
90
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
78
91
|
}
|
|
79
92
|
async set(key, value, options) {
|
|
80
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
93
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
81
94
|
}
|
|
82
95
|
async delete(key) {
|
|
83
|
-
await this.cache.delete(this.qualify(key));
|
|
96
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
84
97
|
}
|
|
85
98
|
async mdelete(keys) {
|
|
86
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
99
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
87
100
|
}
|
|
88
101
|
async clear() {
|
|
89
|
-
await this.cache.
|
|
102
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
90
103
|
}
|
|
91
104
|
async mget(entries) {
|
|
92
|
-
return this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
return this.trackMetrics(
|
|
106
|
+
() => this.cache.mget(
|
|
107
|
+
entries.map((entry) => ({
|
|
108
|
+
...entry,
|
|
109
|
+
key: this.qualify(entry.key)
|
|
110
|
+
}))
|
|
111
|
+
)
|
|
97
112
|
);
|
|
98
113
|
}
|
|
99
114
|
async mset(entries) {
|
|
100
|
-
await this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
await this.trackMetrics(
|
|
116
|
+
() => this.cache.mset(
|
|
117
|
+
entries.map((entry) => ({
|
|
118
|
+
...entry,
|
|
119
|
+
key: this.qualify(entry.key)
|
|
120
|
+
}))
|
|
121
|
+
)
|
|
105
122
|
);
|
|
106
123
|
}
|
|
107
124
|
async invalidateByTag(tag) {
|
|
108
|
-
await this.cache.invalidateByTag(tag);
|
|
125
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
126
|
+
}
|
|
127
|
+
async invalidateByTags(tags, mode = "any") {
|
|
128
|
+
await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
|
|
109
129
|
}
|
|
110
130
|
async invalidateByPattern(pattern) {
|
|
111
|
-
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
131
|
+
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
132
|
+
}
|
|
133
|
+
async invalidateByPrefix(prefix) {
|
|
134
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
112
135
|
}
|
|
113
136
|
/**
|
|
114
137
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
@@ -129,10 +152,19 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
129
152
|
);
|
|
130
153
|
}
|
|
131
154
|
getMetrics() {
|
|
132
|
-
return this.
|
|
155
|
+
return cloneMetrics(this.metrics);
|
|
133
156
|
}
|
|
134
157
|
getHitRate() {
|
|
135
|
-
|
|
158
|
+
const total = this.metrics.hits + this.metrics.misses;
|
|
159
|
+
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
160
|
+
const byLayer = {};
|
|
161
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
162
|
+
for (const layer of layers) {
|
|
163
|
+
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
164
|
+
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
165
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
166
|
+
}
|
|
167
|
+
return { overall, byLayer };
|
|
136
168
|
}
|
|
137
169
|
/**
|
|
138
170
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -149,7 +181,130 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
149
181
|
qualify(key) {
|
|
150
182
|
return `${this.prefix}:${key}`;
|
|
151
183
|
}
|
|
184
|
+
async trackMetrics(operation) {
|
|
185
|
+
return this.getMetricsMutex().runExclusive(async () => {
|
|
186
|
+
const before = this.cache.getMetrics();
|
|
187
|
+
const result = await operation();
|
|
188
|
+
const after = this.cache.getMetrics();
|
|
189
|
+
this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
|
|
190
|
+
return result;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
getMetricsMutex() {
|
|
194
|
+
const existing = _CacheNamespace.metricsMutexes.get(this.cache);
|
|
195
|
+
if (existing) {
|
|
196
|
+
return existing;
|
|
197
|
+
}
|
|
198
|
+
const mutex = new import_async_mutex.Mutex();
|
|
199
|
+
_CacheNamespace.metricsMutexes.set(this.cache, mutex);
|
|
200
|
+
return mutex;
|
|
201
|
+
}
|
|
152
202
|
};
|
|
203
|
+
function emptyMetrics() {
|
|
204
|
+
return {
|
|
205
|
+
hits: 0,
|
|
206
|
+
misses: 0,
|
|
207
|
+
fetches: 0,
|
|
208
|
+
sets: 0,
|
|
209
|
+
deletes: 0,
|
|
210
|
+
backfills: 0,
|
|
211
|
+
invalidations: 0,
|
|
212
|
+
staleHits: 0,
|
|
213
|
+
refreshes: 0,
|
|
214
|
+
refreshErrors: 0,
|
|
215
|
+
writeFailures: 0,
|
|
216
|
+
singleFlightWaits: 0,
|
|
217
|
+
negativeCacheHits: 0,
|
|
218
|
+
circuitBreakerTrips: 0,
|
|
219
|
+
degradedOperations: 0,
|
|
220
|
+
hitsByLayer: {},
|
|
221
|
+
missesByLayer: {},
|
|
222
|
+
latencyByLayer: {},
|
|
223
|
+
resetAt: Date.now()
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function cloneMetrics(metrics) {
|
|
227
|
+
return {
|
|
228
|
+
...metrics,
|
|
229
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
230
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
231
|
+
latencyByLayer: Object.fromEntries(
|
|
232
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
233
|
+
)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function diffMetrics(before, after) {
|
|
237
|
+
const latencyByLayer = Object.fromEntries(
|
|
238
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
239
|
+
layer,
|
|
240
|
+
{
|
|
241
|
+
avgMs: value.avgMs,
|
|
242
|
+
maxMs: value.maxMs,
|
|
243
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
244
|
+
}
|
|
245
|
+
])
|
|
246
|
+
);
|
|
247
|
+
return {
|
|
248
|
+
hits: after.hits - before.hits,
|
|
249
|
+
misses: after.misses - before.misses,
|
|
250
|
+
fetches: after.fetches - before.fetches,
|
|
251
|
+
sets: after.sets - before.sets,
|
|
252
|
+
deletes: after.deletes - before.deletes,
|
|
253
|
+
backfills: after.backfills - before.backfills,
|
|
254
|
+
invalidations: after.invalidations - before.invalidations,
|
|
255
|
+
staleHits: after.staleHits - before.staleHits,
|
|
256
|
+
refreshes: after.refreshes - before.refreshes,
|
|
257
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
258
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
259
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
260
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
261
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
262
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
263
|
+
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
264
|
+
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
265
|
+
latencyByLayer,
|
|
266
|
+
resetAt: after.resetAt
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function addMetrics(base, delta) {
|
|
270
|
+
return {
|
|
271
|
+
hits: base.hits + delta.hits,
|
|
272
|
+
misses: base.misses + delta.misses,
|
|
273
|
+
fetches: base.fetches + delta.fetches,
|
|
274
|
+
sets: base.sets + delta.sets,
|
|
275
|
+
deletes: base.deletes + delta.deletes,
|
|
276
|
+
backfills: base.backfills + delta.backfills,
|
|
277
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
278
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
279
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
280
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
281
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
282
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
283
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
284
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
285
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
286
|
+
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
287
|
+
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
288
|
+
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
289
|
+
resetAt: base.resetAt
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function diffMap(before, after) {
|
|
293
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
294
|
+
const result = {};
|
|
295
|
+
for (const key of keys) {
|
|
296
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
function addMap(base, delta) {
|
|
301
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
302
|
+
const result = {};
|
|
303
|
+
for (const key of keys) {
|
|
304
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
153
308
|
|
|
154
309
|
// src/internal/CircuitBreakerManager.ts
|
|
155
310
|
var CircuitBreakerManager = class {
|
|
@@ -243,6 +398,95 @@ var CircuitBreakerManager = class {
|
|
|
243
398
|
}
|
|
244
399
|
};
|
|
245
400
|
|
|
401
|
+
// src/internal/FetchRateLimiter.ts
|
|
402
|
+
var FetchRateLimiter = class {
|
|
403
|
+
active = 0;
|
|
404
|
+
queue = [];
|
|
405
|
+
startedAt = [];
|
|
406
|
+
drainTimer;
|
|
407
|
+
async schedule(options, task) {
|
|
408
|
+
if (!options) {
|
|
409
|
+
return task();
|
|
410
|
+
}
|
|
411
|
+
const normalized = this.normalize(options);
|
|
412
|
+
if (!normalized) {
|
|
413
|
+
return task();
|
|
414
|
+
}
|
|
415
|
+
return new Promise((resolve, reject) => {
|
|
416
|
+
this.queue.push({ options: normalized, task, resolve, reject });
|
|
417
|
+
this.drain();
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
normalize(options) {
|
|
421
|
+
const maxConcurrent = options.maxConcurrent;
|
|
422
|
+
const intervalMs = options.intervalMs;
|
|
423
|
+
const maxPerInterval = options.maxPerInterval;
|
|
424
|
+
if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
|
|
425
|
+
return void 0;
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
maxConcurrent,
|
|
429
|
+
intervalMs,
|
|
430
|
+
maxPerInterval
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
drain() {
|
|
434
|
+
if (this.drainTimer) {
|
|
435
|
+
clearTimeout(this.drainTimer);
|
|
436
|
+
this.drainTimer = void 0;
|
|
437
|
+
}
|
|
438
|
+
while (this.queue.length > 0) {
|
|
439
|
+
const next = this.queue[0];
|
|
440
|
+
if (!next) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const waitMs = this.waitTime(next.options);
|
|
444
|
+
if (waitMs > 0) {
|
|
445
|
+
this.drainTimer = setTimeout(() => {
|
|
446
|
+
this.drainTimer = void 0;
|
|
447
|
+
this.drain();
|
|
448
|
+
}, waitMs);
|
|
449
|
+
this.drainTimer.unref?.();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this.queue.shift();
|
|
453
|
+
this.active += 1;
|
|
454
|
+
this.startedAt.push(Date.now());
|
|
455
|
+
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
456
|
+
this.active -= 1;
|
|
457
|
+
this.drain();
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
waitTime(options) {
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
if (options.maxConcurrent && this.active >= options.maxConcurrent) {
|
|
464
|
+
return 1;
|
|
465
|
+
}
|
|
466
|
+
if (!options.intervalMs || !options.maxPerInterval) {
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
this.prune(now, options.intervalMs);
|
|
470
|
+
if (this.startedAt.length < options.maxPerInterval) {
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
const oldest = this.startedAt[0];
|
|
474
|
+
if (!oldest) {
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
return Math.max(1, options.intervalMs - (now - oldest));
|
|
478
|
+
}
|
|
479
|
+
prune(now, intervalMs) {
|
|
480
|
+
while (this.startedAt.length > 0) {
|
|
481
|
+
const startedAt = this.startedAt[0];
|
|
482
|
+
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
this.startedAt.shift();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
246
490
|
// src/internal/MetricsCollector.ts
|
|
247
491
|
var MetricsCollector = class {
|
|
248
492
|
data = this.empty();
|
|
@@ -439,13 +683,14 @@ var TtlResolver = class {
|
|
|
439
683
|
clearProfiles() {
|
|
440
684
|
this.accessProfiles.clear();
|
|
441
685
|
}
|
|
442
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
686
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
687
|
+
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
443
688
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
444
689
|
layerName,
|
|
445
690
|
options?.negativeTtl,
|
|
446
691
|
globalNegativeTtl,
|
|
447
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
448
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
692
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
693
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
449
694
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
450
695
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
451
696
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -484,6 +729,29 @@ var TtlResolver = class {
|
|
|
484
729
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
485
730
|
return Math.max(1, Math.round(ttl + delta));
|
|
486
731
|
}
|
|
732
|
+
resolvePolicyTtl(key, value, policy) {
|
|
733
|
+
if (!policy) {
|
|
734
|
+
return void 0;
|
|
735
|
+
}
|
|
736
|
+
if (typeof policy === "function") {
|
|
737
|
+
return policy({ key, value });
|
|
738
|
+
}
|
|
739
|
+
const now = /* @__PURE__ */ new Date();
|
|
740
|
+
if (policy === "until-midnight") {
|
|
741
|
+
const nextMidnight = new Date(now);
|
|
742
|
+
nextMidnight.setHours(24, 0, 0, 0);
|
|
743
|
+
return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
|
|
744
|
+
}
|
|
745
|
+
if (policy === "next-hour") {
|
|
746
|
+
const nextHour = new Date(now);
|
|
747
|
+
nextHour.setMinutes(60, 0, 0);
|
|
748
|
+
return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
|
|
749
|
+
}
|
|
750
|
+
const alignToSeconds = policy.alignTo;
|
|
751
|
+
const currentSeconds = Math.floor(Date.now() / 1e3);
|
|
752
|
+
const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
|
|
753
|
+
return Math.max(1, nextBoundary - currentSeconds);
|
|
754
|
+
}
|
|
487
755
|
readLayerNumber(layerName, value) {
|
|
488
756
|
if (typeof value === "number") {
|
|
489
757
|
return value;
|
|
@@ -511,36 +779,46 @@ var PatternMatcher = class _PatternMatcher {
|
|
|
511
779
|
/**
|
|
512
780
|
* Tests whether a glob-style pattern matches a value.
|
|
513
781
|
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
514
|
-
* Uses a
|
|
782
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
783
|
+
* quadratic memory usage on long patterns/keys.
|
|
515
784
|
*/
|
|
516
785
|
static matches(pattern, value) {
|
|
517
786
|
return _PatternMatcher.matchLinear(pattern, value);
|
|
518
787
|
}
|
|
519
788
|
/**
|
|
520
|
-
* Linear-time glob matching
|
|
521
|
-
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
789
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
522
790
|
*/
|
|
523
791
|
static matchLinear(pattern, value) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
792
|
+
let patternIndex = 0;
|
|
793
|
+
let valueIndex = 0;
|
|
794
|
+
let starIndex = -1;
|
|
795
|
+
let backtrackValueIndex = 0;
|
|
796
|
+
while (valueIndex < value.length) {
|
|
797
|
+
const patternChar = pattern[patternIndex];
|
|
798
|
+
const valueChar = value[valueIndex];
|
|
799
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
800
|
+
starIndex = patternIndex;
|
|
801
|
+
patternIndex += 1;
|
|
802
|
+
backtrackValueIndex = valueIndex;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
806
|
+
patternIndex += 1;
|
|
807
|
+
valueIndex += 1;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (starIndex !== -1) {
|
|
811
|
+
patternIndex = starIndex + 1;
|
|
812
|
+
backtrackValueIndex += 1;
|
|
813
|
+
valueIndex = backtrackValueIndex;
|
|
814
|
+
continue;
|
|
541
815
|
}
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
819
|
+
patternIndex += 1;
|
|
542
820
|
}
|
|
543
|
-
return
|
|
821
|
+
return patternIndex === pattern.length;
|
|
544
822
|
}
|
|
545
823
|
};
|
|
546
824
|
|
|
@@ -578,26 +856,14 @@ var TagIndex = class {
|
|
|
578
856
|
}
|
|
579
857
|
}
|
|
580
858
|
async remove(key) {
|
|
581
|
-
this.
|
|
582
|
-
const tags = this.keyToTags.get(key);
|
|
583
|
-
if (!tags) {
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
for (const tag of tags) {
|
|
587
|
-
const keys = this.tagToKeys.get(tag);
|
|
588
|
-
if (!keys) {
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
keys.delete(key);
|
|
592
|
-
if (keys.size === 0) {
|
|
593
|
-
this.tagToKeys.delete(tag);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
this.keyToTags.delete(key);
|
|
859
|
+
this.removeKey(key);
|
|
597
860
|
}
|
|
598
861
|
async keysForTag(tag) {
|
|
599
862
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
600
863
|
}
|
|
864
|
+
async keysForPrefix(prefix) {
|
|
865
|
+
return [...this.knownKeys].filter((key) => key.startsWith(prefix));
|
|
866
|
+
}
|
|
601
867
|
async tagsForKey(key) {
|
|
602
868
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
603
869
|
}
|
|
@@ -619,15 +885,32 @@ var TagIndex = class {
|
|
|
619
885
|
if (removed >= toRemove) {
|
|
620
886
|
break;
|
|
621
887
|
}
|
|
622
|
-
this.
|
|
623
|
-
this.keyToTags.delete(key);
|
|
888
|
+
this.removeKey(key);
|
|
624
889
|
removed += 1;
|
|
625
890
|
}
|
|
626
891
|
}
|
|
892
|
+
removeKey(key) {
|
|
893
|
+
this.knownKeys.delete(key);
|
|
894
|
+
const tags = this.keyToTags.get(key);
|
|
895
|
+
if (!tags) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
for (const tag of tags) {
|
|
899
|
+
const keys = this.tagToKeys.get(tag);
|
|
900
|
+
if (!keys) {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
keys.delete(key);
|
|
904
|
+
if (keys.size === 0) {
|
|
905
|
+
this.tagToKeys.delete(tag);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
this.keyToTags.delete(key);
|
|
909
|
+
}
|
|
627
910
|
};
|
|
628
911
|
|
|
629
912
|
// src/stampede/StampedeGuard.ts
|
|
630
|
-
var
|
|
913
|
+
var import_async_mutex2 = require("async-mutex");
|
|
631
914
|
var StampedeGuard = class {
|
|
632
915
|
mutexes = /* @__PURE__ */ new Map();
|
|
633
916
|
async execute(key, task) {
|
|
@@ -644,7 +927,7 @@ var StampedeGuard = class {
|
|
|
644
927
|
getMutexEntry(key) {
|
|
645
928
|
let entry = this.mutexes.get(key);
|
|
646
929
|
if (!entry) {
|
|
647
|
-
entry = { mutex: new
|
|
930
|
+
entry = { mutex: new import_async_mutex2.Mutex(), references: 0 };
|
|
648
931
|
this.mutexes.set(key, entry);
|
|
649
932
|
}
|
|
650
933
|
entry.references += 1;
|
|
@@ -705,6 +988,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
705
988
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
706
989
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
707
990
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
991
|
+
this.currentGeneration = options.generation;
|
|
708
992
|
if (options.publishSetInvalidation !== void 0) {
|
|
709
993
|
console.warn(
|
|
710
994
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -713,21 +997,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
713
997
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
714
998
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
715
999
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1000
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
716
1001
|
this.startup = this.initialize();
|
|
717
1002
|
}
|
|
718
1003
|
layers;
|
|
719
1004
|
options;
|
|
720
1005
|
stampedeGuard = new StampedeGuard();
|
|
721
1006
|
metricsCollector = new MetricsCollector();
|
|
722
|
-
instanceId = (
|
|
1007
|
+
instanceId = createInstanceId();
|
|
723
1008
|
startup;
|
|
724
1009
|
unsubscribeInvalidation;
|
|
725
1010
|
logger;
|
|
726
1011
|
tagIndex;
|
|
1012
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
727
1013
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
728
1014
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
729
1015
|
ttlResolver;
|
|
730
1016
|
circuitBreakerManager;
|
|
1017
|
+
currentGeneration;
|
|
1018
|
+
writeBehindQueue = [];
|
|
1019
|
+
writeBehindTimer;
|
|
1020
|
+
writeBehindFlushPromise;
|
|
731
1021
|
isDisconnecting = false;
|
|
732
1022
|
disconnectPromise;
|
|
733
1023
|
/**
|
|
@@ -737,9 +1027,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
737
1027
|
* and no `fetcher` is provided.
|
|
738
1028
|
*/
|
|
739
1029
|
async get(key, fetcher, options) {
|
|
740
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1030
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
741
1031
|
this.validateWriteOptions(options);
|
|
742
|
-
await this.
|
|
1032
|
+
await this.awaitStartup("get");
|
|
743
1033
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
744
1034
|
if (hit.found) {
|
|
745
1035
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -804,8 +1094,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
804
1094
|
* Returns true if the given key exists and is not expired in any layer.
|
|
805
1095
|
*/
|
|
806
1096
|
async has(key) {
|
|
807
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
808
|
-
await this.
|
|
1097
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1098
|
+
await this.awaitStartup("has");
|
|
809
1099
|
for (const layer of this.layers) {
|
|
810
1100
|
if (this.shouldSkipLayer(layer)) {
|
|
811
1101
|
continue;
|
|
@@ -835,8 +1125,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
835
1125
|
* that has it, or null if the key is not found / has no TTL.
|
|
836
1126
|
*/
|
|
837
1127
|
async ttl(key) {
|
|
838
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
839
|
-
await this.
|
|
1128
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1129
|
+
await this.awaitStartup("ttl");
|
|
840
1130
|
for (const layer of this.layers) {
|
|
841
1131
|
if (this.shouldSkipLayer(layer)) {
|
|
842
1132
|
continue;
|
|
@@ -857,17 +1147,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
857
1147
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
858
1148
|
*/
|
|
859
1149
|
async set(key, value, options) {
|
|
860
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1150
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
861
1151
|
this.validateWriteOptions(options);
|
|
862
|
-
await this.
|
|
1152
|
+
await this.awaitStartup("set");
|
|
863
1153
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
864
1154
|
}
|
|
865
1155
|
/**
|
|
866
1156
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
867
1157
|
*/
|
|
868
1158
|
async delete(key) {
|
|
869
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
870
|
-
await this.
|
|
1159
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1160
|
+
await this.awaitStartup("delete");
|
|
871
1161
|
await this.deleteKeys([normalizedKey]);
|
|
872
1162
|
await this.publishInvalidation({
|
|
873
1163
|
scope: "key",
|
|
@@ -877,7 +1167,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
877
1167
|
});
|
|
878
1168
|
}
|
|
879
1169
|
async clear() {
|
|
880
|
-
await this.
|
|
1170
|
+
await this.awaitStartup("clear");
|
|
881
1171
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
882
1172
|
await this.tagIndex.clear();
|
|
883
1173
|
this.ttlResolver.clearProfiles();
|
|
@@ -893,23 +1183,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
893
1183
|
if (keys.length === 0) {
|
|
894
1184
|
return;
|
|
895
1185
|
}
|
|
896
|
-
await this.
|
|
1186
|
+
await this.awaitStartup("mdelete");
|
|
897
1187
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
898
|
-
|
|
1188
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1189
|
+
await this.deleteKeys(cacheKeys);
|
|
899
1190
|
await this.publishInvalidation({
|
|
900
1191
|
scope: "keys",
|
|
901
|
-
keys:
|
|
1192
|
+
keys: cacheKeys,
|
|
902
1193
|
sourceId: this.instanceId,
|
|
903
1194
|
operation: "delete"
|
|
904
1195
|
});
|
|
905
1196
|
}
|
|
906
1197
|
async mget(entries) {
|
|
1198
|
+
this.assertActive("mget");
|
|
907
1199
|
if (entries.length === 0) {
|
|
908
1200
|
return [];
|
|
909
1201
|
}
|
|
910
1202
|
const normalizedEntries = entries.map((entry) => ({
|
|
911
1203
|
...entry,
|
|
912
|
-
key: this.validateCacheKey(entry.key)
|
|
1204
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
913
1205
|
}));
|
|
914
1206
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
915
1207
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -935,7 +1227,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
935
1227
|
})
|
|
936
1228
|
);
|
|
937
1229
|
}
|
|
938
|
-
await this.
|
|
1230
|
+
await this.awaitStartup("mget");
|
|
939
1231
|
const pending = /* @__PURE__ */ new Set();
|
|
940
1232
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
941
1233
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -983,14 +1275,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
983
1275
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
984
1276
|
}
|
|
985
1277
|
async mset(entries) {
|
|
1278
|
+
this.assertActive("mset");
|
|
986
1279
|
const normalizedEntries = entries.map((entry) => ({
|
|
987
1280
|
...entry,
|
|
988
|
-
key: this.validateCacheKey(entry.key)
|
|
1281
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
989
1282
|
}));
|
|
990
1283
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
991
|
-
await
|
|
1284
|
+
await this.awaitStartup("mset");
|
|
1285
|
+
await this.writeBatch(normalizedEntries);
|
|
992
1286
|
}
|
|
993
1287
|
async warm(entries, options = {}) {
|
|
1288
|
+
this.assertActive("warm");
|
|
994
1289
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
995
1290
|
const total = entries.length;
|
|
996
1291
|
let completed = 0;
|
|
@@ -1039,14 +1334,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1039
1334
|
return new CacheNamespace(this, prefix);
|
|
1040
1335
|
}
|
|
1041
1336
|
async invalidateByTag(tag) {
|
|
1042
|
-
await this.
|
|
1337
|
+
await this.awaitStartup("invalidateByTag");
|
|
1043
1338
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
1044
1339
|
await this.deleteKeys(keys);
|
|
1045
1340
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1046
1341
|
}
|
|
1342
|
+
async invalidateByTags(tags, mode = "any") {
|
|
1343
|
+
if (tags.length === 0) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
await this.awaitStartup("invalidateByTags");
|
|
1347
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
|
|
1348
|
+
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1349
|
+
await this.deleteKeys(keys);
|
|
1350
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1351
|
+
}
|
|
1047
1352
|
async invalidateByPattern(pattern) {
|
|
1048
|
-
await this.
|
|
1049
|
-
const keys = await this.tagIndex.matchPattern(pattern);
|
|
1353
|
+
await this.awaitStartup("invalidateByPattern");
|
|
1354
|
+
const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
|
|
1355
|
+
await this.deleteKeys(keys);
|
|
1356
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1357
|
+
}
|
|
1358
|
+
async invalidateByPrefix(prefix) {
|
|
1359
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
1360
|
+
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1361
|
+
const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
|
|
1050
1362
|
await this.deleteKeys(keys);
|
|
1051
1363
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1052
1364
|
}
|
|
@@ -1073,14 +1385,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1073
1385
|
getHitRate() {
|
|
1074
1386
|
return this.metricsCollector.hitRate();
|
|
1075
1387
|
}
|
|
1388
|
+
async healthCheck() {
|
|
1389
|
+
await this.startup;
|
|
1390
|
+
return Promise.all(
|
|
1391
|
+
this.layers.map(async (layer) => {
|
|
1392
|
+
const startedAt = performance.now();
|
|
1393
|
+
try {
|
|
1394
|
+
const healthy = layer.ping ? await layer.ping() : true;
|
|
1395
|
+
return {
|
|
1396
|
+
layer: layer.name,
|
|
1397
|
+
healthy,
|
|
1398
|
+
latencyMs: performance.now() - startedAt
|
|
1399
|
+
};
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
return {
|
|
1402
|
+
layer: layer.name,
|
|
1403
|
+
healthy: false,
|
|
1404
|
+
latencyMs: performance.now() - startedAt,
|
|
1405
|
+
error: this.formatError(error)
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
})
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
bumpGeneration(nextGeneration) {
|
|
1412
|
+
const current = this.currentGeneration ?? 0;
|
|
1413
|
+
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1414
|
+
return this.currentGeneration;
|
|
1415
|
+
}
|
|
1076
1416
|
/**
|
|
1077
1417
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1078
1418
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1079
1419
|
* Returns `null` if the key does not exist in any layer.
|
|
1080
1420
|
*/
|
|
1081
1421
|
async inspect(key) {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1422
|
+
const userKey = this.validateCacheKey(key);
|
|
1423
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1424
|
+
await this.awaitStartup("inspect");
|
|
1084
1425
|
const foundInLayers = [];
|
|
1085
1426
|
let freshTtlSeconds = null;
|
|
1086
1427
|
let staleTtlSeconds = null;
|
|
@@ -1111,10 +1452,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1111
1452
|
return null;
|
|
1112
1453
|
}
|
|
1113
1454
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
1114
|
-
return { key:
|
|
1455
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1115
1456
|
}
|
|
1116
1457
|
async exportState() {
|
|
1117
|
-
await this.
|
|
1458
|
+
await this.awaitStartup("exportState");
|
|
1118
1459
|
const exported = /* @__PURE__ */ new Map();
|
|
1119
1460
|
for (const layer of this.layers) {
|
|
1120
1461
|
if (!layer.keys) {
|
|
@@ -1122,15 +1463,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1122
1463
|
}
|
|
1123
1464
|
const keys = await layer.keys();
|
|
1124
1465
|
for (const key of keys) {
|
|
1125
|
-
|
|
1466
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1467
|
+
if (exported.has(exportedKey)) {
|
|
1126
1468
|
continue;
|
|
1127
1469
|
}
|
|
1128
1470
|
const stored = await this.readLayerEntry(layer, key);
|
|
1129
1471
|
if (stored === null) {
|
|
1130
1472
|
continue;
|
|
1131
1473
|
}
|
|
1132
|
-
exported.set(
|
|
1133
|
-
key,
|
|
1474
|
+
exported.set(exportedKey, {
|
|
1475
|
+
key: exportedKey,
|
|
1134
1476
|
value: stored,
|
|
1135
1477
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1136
1478
|
});
|
|
@@ -1139,20 +1481,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1139
1481
|
return [...exported.values()];
|
|
1140
1482
|
}
|
|
1141
1483
|
async importState(entries) {
|
|
1142
|
-
await this.
|
|
1484
|
+
await this.awaitStartup("importState");
|
|
1143
1485
|
await Promise.all(
|
|
1144
1486
|
entries.map(async (entry) => {
|
|
1145
|
-
|
|
1146
|
-
await this.
|
|
1487
|
+
const qualifiedKey = this.qualifyKey(entry.key);
|
|
1488
|
+
await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
|
|
1489
|
+
await this.tagIndex.touch(qualifiedKey);
|
|
1147
1490
|
})
|
|
1148
1491
|
);
|
|
1149
1492
|
}
|
|
1150
1493
|
async persistToFile(filePath) {
|
|
1494
|
+
this.assertActive("persistToFile");
|
|
1151
1495
|
const snapshot = await this.exportState();
|
|
1152
|
-
|
|
1496
|
+
const { promises: fs2 } = await import("fs");
|
|
1497
|
+
await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1153
1498
|
}
|
|
1154
1499
|
async restoreFromFile(filePath) {
|
|
1155
|
-
|
|
1500
|
+
this.assertActive("restoreFromFile");
|
|
1501
|
+
const { promises: fs2 } = await import("fs");
|
|
1502
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
1156
1503
|
let parsed;
|
|
1157
1504
|
try {
|
|
1158
1505
|
parsed = JSON.parse(raw, (_key, value) => {
|
|
@@ -1175,7 +1522,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1175
1522
|
this.disconnectPromise = (async () => {
|
|
1176
1523
|
await this.startup;
|
|
1177
1524
|
await this.unsubscribeInvalidation?.();
|
|
1525
|
+
await this.flushWriteBehindQueue();
|
|
1178
1526
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1527
|
+
if (this.writeBehindTimer) {
|
|
1528
|
+
clearInterval(this.writeBehindTimer);
|
|
1529
|
+
this.writeBehindTimer = void 0;
|
|
1530
|
+
}
|
|
1531
|
+
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1179
1532
|
})();
|
|
1180
1533
|
}
|
|
1181
1534
|
await this.disconnectPromise;
|
|
@@ -1235,7 +1588,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1235
1588
|
const fetchStart = Date.now();
|
|
1236
1589
|
let fetched;
|
|
1237
1590
|
try {
|
|
1238
|
-
fetched = await
|
|
1591
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1592
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1593
|
+
fetcher
|
|
1594
|
+
);
|
|
1239
1595
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1240
1596
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1241
1597
|
} catch (error) {
|
|
@@ -1269,6 +1625,61 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1269
1625
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1270
1626
|
}
|
|
1271
1627
|
}
|
|
1628
|
+
async writeBatch(entries) {
|
|
1629
|
+
const now = Date.now();
|
|
1630
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1631
|
+
const immediateOperations = [];
|
|
1632
|
+
const deferredOperations = [];
|
|
1633
|
+
for (const entry of entries) {
|
|
1634
|
+
for (const layer of this.layers) {
|
|
1635
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1639
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1640
|
+
bucket.push(layerEntry);
|
|
1641
|
+
entriesByLayer.set(layer, bucket);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1645
|
+
const operation = async () => {
|
|
1646
|
+
try {
|
|
1647
|
+
if (layer.setMany) {
|
|
1648
|
+
await layer.setMany(layerEntries);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1657
|
+
deferredOperations.push(operation);
|
|
1658
|
+
} else {
|
|
1659
|
+
immediateOperations.push(operation);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1663
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1664
|
+
for (const entry of entries) {
|
|
1665
|
+
if (entry.options?.tags) {
|
|
1666
|
+
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1667
|
+
} else {
|
|
1668
|
+
await this.tagIndex.touch(entry.key);
|
|
1669
|
+
}
|
|
1670
|
+
this.metricsCollector.increment("sets");
|
|
1671
|
+
this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1672
|
+
this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1673
|
+
}
|
|
1674
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
1675
|
+
await this.publishInvalidation({
|
|
1676
|
+
scope: "keys",
|
|
1677
|
+
keys: entries.map((entry) => entry.key),
|
|
1678
|
+
sourceId: this.instanceId,
|
|
1679
|
+
operation: "write"
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1272
1683
|
async readFromLayers(key, options, mode) {
|
|
1273
1684
|
let sawRetainableValue = false;
|
|
1274
1685
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
@@ -1352,33 +1763,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1352
1763
|
}
|
|
1353
1764
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1354
1765
|
const now = Date.now();
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
options
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
});
|
|
1374
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1375
|
-
try {
|
|
1376
|
-
await layer.set(key, payload, ttl);
|
|
1377
|
-
} catch (error) {
|
|
1378
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
1766
|
+
const immediateOperations = [];
|
|
1767
|
+
const deferredOperations = [];
|
|
1768
|
+
for (const layer of this.layers) {
|
|
1769
|
+
const operation = async () => {
|
|
1770
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
1774
|
+
try {
|
|
1775
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1781
|
+
deferredOperations.push(operation);
|
|
1782
|
+
} else {
|
|
1783
|
+
immediateOperations.push(operation);
|
|
1379
1784
|
}
|
|
1380
|
-
}
|
|
1381
|
-
await this.executeLayerOperations(
|
|
1785
|
+
}
|
|
1786
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1787
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1382
1788
|
}
|
|
1383
1789
|
async executeLayerOperations(operations, context) {
|
|
1384
1790
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1402,8 +1808,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1402
1808
|
);
|
|
1403
1809
|
}
|
|
1404
1810
|
}
|
|
1405
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1406
|
-
return this.ttlResolver.resolveFreshTtl(
|
|
1811
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
1812
|
+
return this.ttlResolver.resolveFreshTtl(
|
|
1813
|
+
key,
|
|
1814
|
+
layerName,
|
|
1815
|
+
kind,
|
|
1816
|
+
options,
|
|
1817
|
+
fallbackTtl,
|
|
1818
|
+
this.options.negativeTtl,
|
|
1819
|
+
void 0,
|
|
1820
|
+
value
|
|
1821
|
+
);
|
|
1407
1822
|
}
|
|
1408
1823
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1409
1824
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1497,6 +1912,105 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1497
1912
|
shouldBroadcastL1Invalidation() {
|
|
1498
1913
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1499
1914
|
}
|
|
1915
|
+
initializeWriteBehind(options) {
|
|
1916
|
+
if (this.options.writeStrategy !== "write-behind") {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1920
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1924
|
+
void this.flushWriteBehindQueue();
|
|
1925
|
+
}, flushIntervalMs);
|
|
1926
|
+
this.writeBehindTimer.unref?.();
|
|
1927
|
+
}
|
|
1928
|
+
shouldWriteBehind(layer) {
|
|
1929
|
+
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
1930
|
+
}
|
|
1931
|
+
async enqueueWriteBehind(operation) {
|
|
1932
|
+
this.writeBehindQueue.push(operation);
|
|
1933
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1934
|
+
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
1935
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1936
|
+
await this.flushWriteBehindQueue();
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1940
|
+
await this.flushWriteBehindQueue();
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
async flushWriteBehindQueue() {
|
|
1944
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1945
|
+
await this.writeBehindFlushPromise;
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1949
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1950
|
+
this.writeBehindFlushPromise = (async () => {
|
|
1951
|
+
await Promise.allSettled(batch.map((operation) => operation()));
|
|
1952
|
+
})();
|
|
1953
|
+
await this.writeBehindFlushPromise;
|
|
1954
|
+
this.writeBehindFlushPromise = void 0;
|
|
1955
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1956
|
+
await this.flushWriteBehindQueue();
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
1960
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
1961
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
1962
|
+
layer.name,
|
|
1963
|
+
options?.staleWhileRevalidate,
|
|
1964
|
+
this.options.staleWhileRevalidate
|
|
1965
|
+
);
|
|
1966
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
1967
|
+
const payload = createStoredValueEnvelope({
|
|
1968
|
+
kind,
|
|
1969
|
+
value,
|
|
1970
|
+
freshTtlSeconds: freshTtl,
|
|
1971
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1972
|
+
staleIfErrorSeconds: staleIfError,
|
|
1973
|
+
now
|
|
1974
|
+
});
|
|
1975
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1976
|
+
return {
|
|
1977
|
+
key,
|
|
1978
|
+
value: payload,
|
|
1979
|
+
ttl
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
intersectKeys(groups) {
|
|
1983
|
+
if (groups.length === 0) {
|
|
1984
|
+
return [];
|
|
1985
|
+
}
|
|
1986
|
+
const [firstGroup, ...rest] = groups;
|
|
1987
|
+
if (!firstGroup) {
|
|
1988
|
+
return [];
|
|
1989
|
+
}
|
|
1990
|
+
const restSets = rest.map((group) => new Set(group));
|
|
1991
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
1992
|
+
}
|
|
1993
|
+
qualifyKey(key) {
|
|
1994
|
+
const prefix = this.generationPrefix();
|
|
1995
|
+
return prefix ? `${prefix}${key}` : key;
|
|
1996
|
+
}
|
|
1997
|
+
qualifyPattern(pattern) {
|
|
1998
|
+
const prefix = this.generationPrefix();
|
|
1999
|
+
return prefix ? `${prefix}${pattern}` : pattern;
|
|
2000
|
+
}
|
|
2001
|
+
stripQualifiedKey(key) {
|
|
2002
|
+
const prefix = this.generationPrefix();
|
|
2003
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
2004
|
+
return key;
|
|
2005
|
+
}
|
|
2006
|
+
return key.slice(prefix.length);
|
|
2007
|
+
}
|
|
2008
|
+
generationPrefix() {
|
|
2009
|
+
if (this.currentGeneration === void 0) {
|
|
2010
|
+
return "";
|
|
2011
|
+
}
|
|
2012
|
+
return `v${this.currentGeneration}:`;
|
|
2013
|
+
}
|
|
1500
2014
|
async deleteKeysFromLayers(layers, keys) {
|
|
1501
2015
|
await Promise.all(
|
|
1502
2016
|
layers.map(async (layer) => {
|
|
@@ -1540,6 +2054,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1540
2054
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1541
2055
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1542
2056
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2057
|
+
if (this.options.generation !== void 0) {
|
|
2058
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2059
|
+
}
|
|
1543
2060
|
}
|
|
1544
2061
|
validateWriteOptions(options) {
|
|
1545
2062
|
if (!options) {
|
|
@@ -1551,6 +2068,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1551
2068
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1552
2069
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1553
2070
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2071
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1554
2072
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1555
2073
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1556
2074
|
}
|
|
@@ -1594,6 +2112,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1594
2112
|
}
|
|
1595
2113
|
return key;
|
|
1596
2114
|
}
|
|
2115
|
+
validateTtlPolicy(name, policy) {
|
|
2116
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if ("alignTo" in policy) {
|
|
2120
|
+
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
throw new Error(`${name} is invalid.`);
|
|
2124
|
+
}
|
|
2125
|
+
assertActive(operation) {
|
|
2126
|
+
if (this.isDisconnecting) {
|
|
2127
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
async awaitStartup(operation) {
|
|
2131
|
+
this.assertActive(operation);
|
|
2132
|
+
await this.startup;
|
|
2133
|
+
this.assertActive(operation);
|
|
2134
|
+
}
|
|
1597
2135
|
serializeOptions(options) {
|
|
1598
2136
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1599
2137
|
}
|
|
@@ -1699,18 +2237,23 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1699
2237
|
return value;
|
|
1700
2238
|
}
|
|
1701
2239
|
};
|
|
2240
|
+
function createInstanceId() {
|
|
2241
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2242
|
+
}
|
|
1702
2243
|
|
|
1703
2244
|
// src/invalidation/RedisInvalidationBus.ts
|
|
1704
2245
|
var RedisInvalidationBus = class {
|
|
1705
2246
|
channel;
|
|
1706
2247
|
publisher;
|
|
1707
2248
|
subscriber;
|
|
2249
|
+
logger;
|
|
1708
2250
|
handlers = /* @__PURE__ */ new Set();
|
|
1709
2251
|
sharedListener;
|
|
1710
2252
|
constructor(options) {
|
|
1711
2253
|
this.publisher = options.publisher;
|
|
1712
2254
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
1713
2255
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
2256
|
+
this.logger = options.logger;
|
|
1714
2257
|
}
|
|
1715
2258
|
async subscribe(handler) {
|
|
1716
2259
|
if (this.handlers.size === 0) {
|
|
@@ -1767,6 +2310,10 @@ var RedisInvalidationBus = class {
|
|
|
1767
2310
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
1768
2311
|
}
|
|
1769
2312
|
reportError(message, error) {
|
|
2313
|
+
if (this.logger?.error) {
|
|
2314
|
+
this.logger.error(message, { error });
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
1770
2317
|
console.error(`[layercache] ${message}`, error);
|
|
1771
2318
|
}
|
|
1772
2319
|
};
|
|
@@ -1815,6 +2362,16 @@ var RedisTagIndex = class {
|
|
|
1815
2362
|
async keysForTag(tag) {
|
|
1816
2363
|
return this.client.smembers(this.tagKeysKey(tag));
|
|
1817
2364
|
}
|
|
2365
|
+
async keysForPrefix(prefix) {
|
|
2366
|
+
const matches = [];
|
|
2367
|
+
let cursor = "0";
|
|
2368
|
+
do {
|
|
2369
|
+
const [nextCursor, keys] = await this.client.sscan(this.knownKeysKey(), cursor, "COUNT", this.scanCount);
|
|
2370
|
+
cursor = nextCursor;
|
|
2371
|
+
matches.push(...keys.filter((key) => key.startsWith(prefix)));
|
|
2372
|
+
} while (cursor !== "0");
|
|
2373
|
+
return matches;
|
|
2374
|
+
}
|
|
1818
2375
|
async tagsForKey(key) {
|
|
1819
2376
|
return this.client.smembers(this.keyTagsKey(key));
|
|
1820
2377
|
}
|
|
@@ -1912,32 +2469,36 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
1912
2469
|
function createExpressCacheMiddleware(cache, options = {}) {
|
|
1913
2470
|
const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
|
|
1914
2471
|
return async (req, res, next) => {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
1921
|
-
const cached = await cache.get(key, void 0, options);
|
|
1922
|
-
if (cached !== null) {
|
|
1923
|
-
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
1924
|
-
res.setHeader?.("x-cache", "HIT");
|
|
1925
|
-
if (res.json) {
|
|
1926
|
-
res.json(cached);
|
|
1927
|
-
} else {
|
|
1928
|
-
res.end?.(JSON.stringify(cached));
|
|
2472
|
+
try {
|
|
2473
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2474
|
+
if (!allowedMethods.has(method)) {
|
|
2475
|
+
next();
|
|
2476
|
+
return;
|
|
1929
2477
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
res.
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2478
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
2479
|
+
const cached = await cache.get(key, void 0, options);
|
|
2480
|
+
if (cached !== null) {
|
|
2481
|
+
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
2482
|
+
res.setHeader?.("x-cache", "HIT");
|
|
2483
|
+
if (res.json) {
|
|
2484
|
+
res.json(cached);
|
|
2485
|
+
} else {
|
|
2486
|
+
res.end?.(JSON.stringify(cached));
|
|
2487
|
+
}
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
const originalJson = res.json?.bind(res);
|
|
2491
|
+
if (originalJson) {
|
|
2492
|
+
res.json = (body) => {
|
|
2493
|
+
res.setHeader?.("x-cache", "MISS");
|
|
2494
|
+
void cache.set(key, body, options);
|
|
2495
|
+
return originalJson(body);
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
next();
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
next(error);
|
|
1939
2501
|
}
|
|
1940
|
-
next();
|
|
1941
2502
|
};
|
|
1942
2503
|
}
|
|
1943
2504
|
|
|
@@ -1950,6 +2511,95 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
1950
2511
|
return (...args) => wrapped(...args);
|
|
1951
2512
|
}
|
|
1952
2513
|
|
|
2514
|
+
// src/integrations/hono.ts
|
|
2515
|
+
function createHonoCacheMiddleware(cache, options = {}) {
|
|
2516
|
+
const allowedMethods = new Set((options.methods ?? ["GET"]).map((method) => method.toUpperCase()));
|
|
2517
|
+
return async (context, next) => {
|
|
2518
|
+
const method = (context.req.method ?? "GET").toUpperCase();
|
|
2519
|
+
if (!allowedMethods.has(method)) {
|
|
2520
|
+
await next();
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${context.req.path ?? context.req.url ?? "/"}`;
|
|
2524
|
+
const cached = await cache.get(key, void 0, options);
|
|
2525
|
+
if (cached !== null) {
|
|
2526
|
+
context.header?.("x-cache", "HIT");
|
|
2527
|
+
context.header?.("content-type", "application/json; charset=utf-8");
|
|
2528
|
+
context.json(cached);
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
const originalJson = context.json.bind(context);
|
|
2532
|
+
context.json = (body, status) => {
|
|
2533
|
+
context.header?.("x-cache", "MISS");
|
|
2534
|
+
void cache.set(key, body, options);
|
|
2535
|
+
return originalJson(body, status);
|
|
2536
|
+
};
|
|
2537
|
+
await next();
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// src/integrations/opentelemetry.ts
|
|
2542
|
+
function createOpenTelemetryPlugin(cache, tracer) {
|
|
2543
|
+
const originals = {
|
|
2544
|
+
get: cache.get.bind(cache),
|
|
2545
|
+
set: cache.set.bind(cache),
|
|
2546
|
+
delete: cache.delete.bind(cache),
|
|
2547
|
+
mget: cache.mget.bind(cache),
|
|
2548
|
+
mset: cache.mset.bind(cache),
|
|
2549
|
+
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
2550
|
+
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
2551
|
+
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
2552
|
+
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
2553
|
+
};
|
|
2554
|
+
cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
|
|
2555
|
+
"layercache.key": String(args[0] ?? "")
|
|
2556
|
+
}));
|
|
2557
|
+
cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
|
|
2558
|
+
"layercache.key": String(args[0] ?? "")
|
|
2559
|
+
}));
|
|
2560
|
+
cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
|
|
2561
|
+
"layercache.key": String(args[0] ?? "")
|
|
2562
|
+
}));
|
|
2563
|
+
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
2564
|
+
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
2565
|
+
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
2566
|
+
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
2567
|
+
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
2568
|
+
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
2569
|
+
return {
|
|
2570
|
+
uninstall() {
|
|
2571
|
+
cache.get = originals.get;
|
|
2572
|
+
cache.set = originals.set;
|
|
2573
|
+
cache.delete = originals.delete;
|
|
2574
|
+
cache.mget = originals.mget;
|
|
2575
|
+
cache.mset = originals.mset;
|
|
2576
|
+
cache.invalidateByTag = originals.invalidateByTag;
|
|
2577
|
+
cache.invalidateByTags = originals.invalidateByTags;
|
|
2578
|
+
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
2579
|
+
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
function instrument(name, tracer, method, attributes) {
|
|
2584
|
+
return (async (...args) => {
|
|
2585
|
+
const span = tracer.startSpan(name, { attributes: attributes?.(args) });
|
|
2586
|
+
try {
|
|
2587
|
+
const result = await method(...args);
|
|
2588
|
+
span.setAttribute?.("layercache.success", true);
|
|
2589
|
+
if (result === null) {
|
|
2590
|
+
span.setAttribute?.("layercache.result", "null");
|
|
2591
|
+
}
|
|
2592
|
+
return result;
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
span.setAttribute?.("layercache.success", false);
|
|
2595
|
+
span.recordException?.(error);
|
|
2596
|
+
throw error;
|
|
2597
|
+
} finally {
|
|
2598
|
+
span.end();
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
|
|
1953
2603
|
// src/integrations/trpc.ts
|
|
1954
2604
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1955
2605
|
return async (context) => {
|
|
@@ -1982,12 +2632,21 @@ var MemoryLayer = class {
|
|
|
1982
2632
|
isLocal = true;
|
|
1983
2633
|
maxSize;
|
|
1984
2634
|
evictionPolicy;
|
|
2635
|
+
onEvict;
|
|
1985
2636
|
entries = /* @__PURE__ */ new Map();
|
|
2637
|
+
cleanupTimer;
|
|
1986
2638
|
constructor(options = {}) {
|
|
1987
2639
|
this.name = options.name ?? "memory";
|
|
1988
2640
|
this.defaultTtl = options.ttl;
|
|
1989
2641
|
this.maxSize = options.maxSize ?? 1e3;
|
|
1990
2642
|
this.evictionPolicy = options.evictionPolicy ?? "lru";
|
|
2643
|
+
this.onEvict = options.onEvict;
|
|
2644
|
+
if (options.cleanupIntervalMs && options.cleanupIntervalMs > 0) {
|
|
2645
|
+
this.cleanupTimer = setInterval(() => {
|
|
2646
|
+
this.pruneExpired();
|
|
2647
|
+
}, options.cleanupIntervalMs);
|
|
2648
|
+
this.cleanupTimer.unref?.();
|
|
2649
|
+
}
|
|
1991
2650
|
}
|
|
1992
2651
|
async get(key) {
|
|
1993
2652
|
const value = await this.getEntry(key);
|
|
@@ -2018,6 +2677,11 @@ var MemoryLayer = class {
|
|
|
2018
2677
|
}
|
|
2019
2678
|
return values;
|
|
2020
2679
|
}
|
|
2680
|
+
async setMany(entries) {
|
|
2681
|
+
for (const entry of entries) {
|
|
2682
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2021
2685
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2022
2686
|
this.entries.delete(key);
|
|
2023
2687
|
this.entries.set(key, {
|
|
@@ -2070,6 +2734,15 @@ var MemoryLayer = class {
|
|
|
2070
2734
|
async clear() {
|
|
2071
2735
|
this.entries.clear();
|
|
2072
2736
|
}
|
|
2737
|
+
async ping() {
|
|
2738
|
+
return true;
|
|
2739
|
+
}
|
|
2740
|
+
async dispose() {
|
|
2741
|
+
if (this.cleanupTimer) {
|
|
2742
|
+
clearInterval(this.cleanupTimer);
|
|
2743
|
+
this.cleanupTimer = void 0;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2073
2746
|
async keys() {
|
|
2074
2747
|
this.pruneExpired();
|
|
2075
2748
|
return [...this.entries.keys()];
|
|
@@ -2102,7 +2775,11 @@ var MemoryLayer = class {
|
|
|
2102
2775
|
if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
|
|
2103
2776
|
const oldestKey = this.entries.keys().next().value;
|
|
2104
2777
|
if (oldestKey !== void 0) {
|
|
2778
|
+
const entry = this.entries.get(oldestKey);
|
|
2105
2779
|
this.entries.delete(oldestKey);
|
|
2780
|
+
if (entry) {
|
|
2781
|
+
this.onEvict?.(oldestKey, unwrapStoredValue(entry.value));
|
|
2782
|
+
}
|
|
2106
2783
|
}
|
|
2107
2784
|
return;
|
|
2108
2785
|
}
|
|
@@ -2117,7 +2794,11 @@ var MemoryLayer = class {
|
|
|
2117
2794
|
}
|
|
2118
2795
|
}
|
|
2119
2796
|
if (victimKey !== void 0) {
|
|
2797
|
+
const victim = this.entries.get(victimKey);
|
|
2120
2798
|
this.entries.delete(victimKey);
|
|
2799
|
+
if (victim) {
|
|
2800
|
+
this.onEvict?.(victimKey, unwrapStoredValue(victim.value));
|
|
2801
|
+
}
|
|
2121
2802
|
}
|
|
2122
2803
|
}
|
|
2123
2804
|
pruneExpired() {
|
|
@@ -2158,22 +2839,24 @@ var RedisLayer = class {
|
|
|
2158
2839
|
defaultTtl;
|
|
2159
2840
|
isLocal = false;
|
|
2160
2841
|
client;
|
|
2161
|
-
|
|
2842
|
+
serializers;
|
|
2162
2843
|
prefix;
|
|
2163
2844
|
allowUnprefixedClear;
|
|
2164
2845
|
scanCount;
|
|
2165
2846
|
compression;
|
|
2166
2847
|
compressionThreshold;
|
|
2848
|
+
disconnectOnDispose;
|
|
2167
2849
|
constructor(options) {
|
|
2168
2850
|
this.client = options.client;
|
|
2169
2851
|
this.defaultTtl = options.ttl;
|
|
2170
2852
|
this.name = options.name ?? "redis";
|
|
2171
|
-
this.
|
|
2853
|
+
this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
|
|
2172
2854
|
this.prefix = options.prefix ?? "";
|
|
2173
2855
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
2174
2856
|
this.scanCount = options.scanCount ?? 100;
|
|
2175
2857
|
this.compression = options.compression;
|
|
2176
2858
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
2859
|
+
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
2177
2860
|
}
|
|
2178
2861
|
async get(key) {
|
|
2179
2862
|
const payload = await this.getEntry(key);
|
|
@@ -2208,8 +2891,25 @@ var RedisLayer = class {
|
|
|
2208
2891
|
})
|
|
2209
2892
|
);
|
|
2210
2893
|
}
|
|
2894
|
+
async setMany(entries) {
|
|
2895
|
+
if (entries.length === 0) {
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
const pipeline = this.client.pipeline();
|
|
2899
|
+
for (const entry of entries) {
|
|
2900
|
+
const serialized = this.primarySerializer().serialize(entry.value);
|
|
2901
|
+
const payload = await this.encodePayload(serialized);
|
|
2902
|
+
const normalizedKey = this.withPrefix(entry.key);
|
|
2903
|
+
if (entry.ttl && entry.ttl > 0) {
|
|
2904
|
+
pipeline.set(normalizedKey, payload, "EX", entry.ttl);
|
|
2905
|
+
} else {
|
|
2906
|
+
pipeline.set(normalizedKey, payload);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
await pipeline.exec();
|
|
2910
|
+
}
|
|
2211
2911
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2212
|
-
const serialized = this.
|
|
2912
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
2213
2913
|
const payload = await this.encodePayload(serialized);
|
|
2214
2914
|
const normalizedKey = this.withPrefix(key);
|
|
2215
2915
|
if (ttl && ttl > 0) {
|
|
@@ -2242,6 +2942,18 @@ var RedisLayer = class {
|
|
|
2242
2942
|
const keys = await this.keys();
|
|
2243
2943
|
return keys.length;
|
|
2244
2944
|
}
|
|
2945
|
+
async ping() {
|
|
2946
|
+
try {
|
|
2947
|
+
return await this.client.ping() === "PONG";
|
|
2948
|
+
} catch {
|
|
2949
|
+
return false;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
async dispose() {
|
|
2953
|
+
if (this.disconnectOnDispose) {
|
|
2954
|
+
this.client.disconnect();
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2245
2957
|
/**
|
|
2246
2958
|
* Deletes all keys matching the layer's prefix in batches to avoid
|
|
2247
2959
|
* loading millions of keys into memory at once.
|
|
@@ -2288,12 +3000,39 @@ var RedisLayer = class {
|
|
|
2288
3000
|
return `${this.prefix}${key}`;
|
|
2289
3001
|
}
|
|
2290
3002
|
async deserializeOrDelete(key, payload) {
|
|
3003
|
+
const decodedPayload = await this.decodePayload(payload);
|
|
3004
|
+
for (const serializer of this.serializers) {
|
|
3005
|
+
try {
|
|
3006
|
+
const value = serializer.deserialize(decodedPayload);
|
|
3007
|
+
if (serializer !== this.primarySerializer()) {
|
|
3008
|
+
await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
|
|
3009
|
+
}
|
|
3010
|
+
return value;
|
|
3011
|
+
} catch {
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
2291
3014
|
try {
|
|
2292
|
-
return this.serializer.deserialize(await this.decodePayload(payload));
|
|
2293
|
-
} catch {
|
|
2294
3015
|
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
2295
|
-
|
|
3016
|
+
} catch {
|
|
3017
|
+
}
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
async rewriteWithPrimarySerializer(key, value) {
|
|
3021
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
3022
|
+
const payload = await this.encodePayload(serialized);
|
|
3023
|
+
const ttl = await this.client.ttl(this.withPrefix(key));
|
|
3024
|
+
if (ttl > 0) {
|
|
3025
|
+
await this.client.set(this.withPrefix(key), payload, "EX", ttl);
|
|
3026
|
+
return;
|
|
2296
3027
|
}
|
|
3028
|
+
await this.client.set(this.withPrefix(key), payload);
|
|
3029
|
+
}
|
|
3030
|
+
primarySerializer() {
|
|
3031
|
+
const serializer = this.serializers[0];
|
|
3032
|
+
if (!serializer) {
|
|
3033
|
+
throw new Error("RedisLayer requires at least one serializer.");
|
|
3034
|
+
}
|
|
3035
|
+
return serializer;
|
|
2297
3036
|
}
|
|
2298
3037
|
isSerializablePayload(payload) {
|
|
2299
3038
|
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
@@ -2332,8 +3071,8 @@ var RedisLayer = class {
|
|
|
2332
3071
|
};
|
|
2333
3072
|
|
|
2334
3073
|
// src/layers/DiskLayer.ts
|
|
2335
|
-
var
|
|
2336
|
-
var
|
|
3074
|
+
var import_node_crypto = require("crypto");
|
|
3075
|
+
var import_node_fs = require("fs");
|
|
2337
3076
|
var import_node_path = require("path");
|
|
2338
3077
|
var DiskLayer = class {
|
|
2339
3078
|
name;
|
|
@@ -2342,6 +3081,7 @@ var DiskLayer = class {
|
|
|
2342
3081
|
directory;
|
|
2343
3082
|
serializer;
|
|
2344
3083
|
maxFiles;
|
|
3084
|
+
writeQueue = Promise.resolve();
|
|
2345
3085
|
constructor(options) {
|
|
2346
3086
|
this.directory = options.directory;
|
|
2347
3087
|
this.defaultTtl = options.ttl;
|
|
@@ -2356,7 +3096,7 @@ var DiskLayer = class {
|
|
|
2356
3096
|
const filePath = this.keyToPath(key);
|
|
2357
3097
|
let raw;
|
|
2358
3098
|
try {
|
|
2359
|
-
raw = await
|
|
3099
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
2360
3100
|
} catch {
|
|
2361
3101
|
return null;
|
|
2362
3102
|
}
|
|
@@ -2374,16 +3114,29 @@ var DiskLayer = class {
|
|
|
2374
3114
|
return entry.value;
|
|
2375
3115
|
}
|
|
2376
3116
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2377
|
-
await
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
3117
|
+
await this.enqueueWrite(async () => {
|
|
3118
|
+
await import_node_fs.promises.mkdir(this.directory, { recursive: true });
|
|
3119
|
+
const entry = {
|
|
3120
|
+
key,
|
|
3121
|
+
value,
|
|
3122
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
3123
|
+
};
|
|
3124
|
+
const payload = this.serializer.serialize(entry);
|
|
3125
|
+
const targetPath = this.keyToPath(key);
|
|
3126
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3127
|
+
await import_node_fs.promises.writeFile(tempPath, payload);
|
|
3128
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
3129
|
+
if (this.maxFiles !== void 0) {
|
|
3130
|
+
await this.enforceMaxFiles();
|
|
3131
|
+
}
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
async getMany(keys) {
|
|
3135
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
3136
|
+
}
|
|
3137
|
+
async setMany(entries) {
|
|
3138
|
+
for (const entry of entries) {
|
|
3139
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
2387
3140
|
}
|
|
2388
3141
|
}
|
|
2389
3142
|
async has(key) {
|
|
@@ -2394,7 +3147,7 @@ var DiskLayer = class {
|
|
|
2394
3147
|
const filePath = this.keyToPath(key);
|
|
2395
3148
|
let raw;
|
|
2396
3149
|
try {
|
|
2397
|
-
raw = await
|
|
3150
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
2398
3151
|
} catch {
|
|
2399
3152
|
return null;
|
|
2400
3153
|
}
|
|
@@ -2414,21 +3167,25 @@ var DiskLayer = class {
|
|
|
2414
3167
|
return remaining;
|
|
2415
3168
|
}
|
|
2416
3169
|
async delete(key) {
|
|
2417
|
-
await this.safeDelete(this.keyToPath(key));
|
|
3170
|
+
await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
|
|
2418
3171
|
}
|
|
2419
3172
|
async deleteMany(keys) {
|
|
2420
|
-
await
|
|
3173
|
+
await this.enqueueWrite(async () => {
|
|
3174
|
+
await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
|
|
3175
|
+
});
|
|
2421
3176
|
}
|
|
2422
3177
|
async clear() {
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
3178
|
+
await this.enqueueWrite(async () => {
|
|
3179
|
+
let entries;
|
|
3180
|
+
try {
|
|
3181
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
3182
|
+
} catch {
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
await Promise.all(
|
|
3186
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete((0, import_node_path.join)(this.directory, name)))
|
|
3187
|
+
);
|
|
3188
|
+
});
|
|
2432
3189
|
}
|
|
2433
3190
|
/**
|
|
2434
3191
|
* Returns the original cache key strings stored on disk.
|
|
@@ -2437,7 +3194,7 @@ var DiskLayer = class {
|
|
|
2437
3194
|
async keys() {
|
|
2438
3195
|
let entries;
|
|
2439
3196
|
try {
|
|
2440
|
-
entries = await
|
|
3197
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
2441
3198
|
} catch {
|
|
2442
3199
|
return [];
|
|
2443
3200
|
}
|
|
@@ -2448,7 +3205,7 @@ var DiskLayer = class {
|
|
|
2448
3205
|
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
2449
3206
|
let raw;
|
|
2450
3207
|
try {
|
|
2451
|
-
raw = await
|
|
3208
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
2452
3209
|
} catch {
|
|
2453
3210
|
return;
|
|
2454
3211
|
}
|
|
@@ -2472,16 +3229,31 @@ var DiskLayer = class {
|
|
|
2472
3229
|
const keys = await this.keys();
|
|
2473
3230
|
return keys.length;
|
|
2474
3231
|
}
|
|
3232
|
+
async ping() {
|
|
3233
|
+
try {
|
|
3234
|
+
await import_node_fs.promises.mkdir(this.directory, { recursive: true });
|
|
3235
|
+
return true;
|
|
3236
|
+
} catch {
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
async dispose() {
|
|
3241
|
+
}
|
|
2475
3242
|
keyToPath(key) {
|
|
2476
|
-
const hash = (0,
|
|
3243
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
|
|
2477
3244
|
return (0, import_node_path.join)(this.directory, `${hash}.lc`);
|
|
2478
3245
|
}
|
|
2479
3246
|
async safeDelete(filePath) {
|
|
2480
3247
|
try {
|
|
2481
|
-
await
|
|
3248
|
+
await import_node_fs.promises.unlink(filePath);
|
|
2482
3249
|
} catch {
|
|
2483
3250
|
}
|
|
2484
3251
|
}
|
|
3252
|
+
enqueueWrite(operation) {
|
|
3253
|
+
const next = this.writeQueue.then(operation, operation);
|
|
3254
|
+
this.writeQueue = next.catch(() => void 0);
|
|
3255
|
+
return next;
|
|
3256
|
+
}
|
|
2485
3257
|
/**
|
|
2486
3258
|
* Removes the oldest files (by mtime) when the directory exceeds maxFiles.
|
|
2487
3259
|
*/
|
|
@@ -2491,7 +3263,7 @@ var DiskLayer = class {
|
|
|
2491
3263
|
}
|
|
2492
3264
|
let entries;
|
|
2493
3265
|
try {
|
|
2494
|
-
entries = await
|
|
3266
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
2495
3267
|
} catch {
|
|
2496
3268
|
return;
|
|
2497
3269
|
}
|
|
@@ -2503,7 +3275,7 @@ var DiskLayer = class {
|
|
|
2503
3275
|
lcFiles.map(async (name) => {
|
|
2504
3276
|
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
2505
3277
|
try {
|
|
2506
|
-
const stat = await
|
|
3278
|
+
const stat = await import_node_fs.promises.stat(filePath);
|
|
2507
3279
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
2508
3280
|
} catch {
|
|
2509
3281
|
return { filePath, mtimeMs: 0 };
|
|
@@ -2587,7 +3359,7 @@ var MsgpackSerializer = class {
|
|
|
2587
3359
|
};
|
|
2588
3360
|
|
|
2589
3361
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
2590
|
-
var
|
|
3362
|
+
var import_node_crypto2 = require("crypto");
|
|
2591
3363
|
var RELEASE_SCRIPT = `
|
|
2592
3364
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
2593
3365
|
return redis.call("del", KEYS[1])
|
|
@@ -2603,7 +3375,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
2603
3375
|
}
|
|
2604
3376
|
async execute(key, options, worker, waiter) {
|
|
2605
3377
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
2606
|
-
const token = (0,
|
|
3378
|
+
const token = (0, import_node_crypto2.randomUUID)();
|
|
2607
3379
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
2608
3380
|
if (acquired === "OK") {
|
|
2609
3381
|
try {
|
|
@@ -2716,6 +3488,8 @@ function sanitizeLabel(value) {
|
|
|
2716
3488
|
createCachedMethodDecorator,
|
|
2717
3489
|
createExpressCacheMiddleware,
|
|
2718
3490
|
createFastifyLayercachePlugin,
|
|
3491
|
+
createHonoCacheMiddleware,
|
|
3492
|
+
createOpenTelemetryPlugin,
|
|
2719
3493
|
createPrometheusMetricsExporter,
|
|
2720
3494
|
createTrpcCacheMiddleware
|
|
2721
3495
|
});
|