layercache 1.0.1 → 1.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 +286 -7
- package/benchmarks/latency.ts +1 -1
- package/benchmarks/stampede.ts +1 -4
- package/dist/chunk-QUB5VZFZ.js +132 -0
- package/dist/cli.cjs +296 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +135 -0
- package/dist/index.cjs +1576 -184
- package/dist/index.d.cts +465 -7
- package/dist/index.d.ts +465 -7
- package/dist/index.js +1526 -266
- package/examples/express-api/index.ts +12 -8
- package/examples/nestjs-module/app.module.ts +2 -5
- package/examples/nextjs-api-routes/route.ts +1 -4
- package/package.json +10 -2
- package/packages/nestjs/dist/index.cjs +1058 -155
- package/packages/nestjs/dist/index.d.cts +345 -2
- package/packages/nestjs/dist/index.d.ts +345 -2
- package/packages/nestjs/dist/index.js +1057 -155
package/dist/index.cjs
CHANGED
|
@@ -20,8 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
CacheNamespace: () => CacheNamespace,
|
|
23
24
|
CacheStack: () => CacheStack,
|
|
25
|
+
DiskLayer: () => DiskLayer,
|
|
24
26
|
JsonSerializer: () => JsonSerializer,
|
|
27
|
+
MemcachedLayer: () => MemcachedLayer,
|
|
25
28
|
MemoryLayer: () => MemoryLayer,
|
|
26
29
|
MsgpackSerializer: () => MsgpackSerializer,
|
|
27
30
|
PatternMatcher: () => PatternMatcher,
|
|
@@ -30,12 +33,241 @@ __export(index_exports, {
|
|
|
30
33
|
RedisSingleFlightCoordinator: () => RedisSingleFlightCoordinator,
|
|
31
34
|
RedisTagIndex: () => RedisTagIndex,
|
|
32
35
|
StampedeGuard: () => StampedeGuard,
|
|
33
|
-
TagIndex: () => TagIndex
|
|
36
|
+
TagIndex: () => TagIndex,
|
|
37
|
+
cacheGraphqlResolver: () => cacheGraphqlResolver,
|
|
38
|
+
createCacheStatsHandler: () => createCacheStatsHandler,
|
|
39
|
+
createCachedMethodDecorator: () => createCachedMethodDecorator,
|
|
40
|
+
createFastifyLayercachePlugin: () => createFastifyLayercachePlugin,
|
|
41
|
+
createPrometheusMetricsExporter: () => createPrometheusMetricsExporter,
|
|
42
|
+
createTrpcCacheMiddleware: () => createTrpcCacheMiddleware
|
|
34
43
|
});
|
|
35
44
|
module.exports = __toCommonJS(index_exports);
|
|
36
45
|
|
|
37
46
|
// src/CacheStack.ts
|
|
38
47
|
var import_node_crypto = require("crypto");
|
|
48
|
+
var import_node_events = require("events");
|
|
49
|
+
var import_node_fs = require("fs");
|
|
50
|
+
|
|
51
|
+
// src/CacheNamespace.ts
|
|
52
|
+
var CacheNamespace = class {
|
|
53
|
+
constructor(cache, prefix) {
|
|
54
|
+
this.cache = cache;
|
|
55
|
+
this.prefix = prefix;
|
|
56
|
+
}
|
|
57
|
+
cache;
|
|
58
|
+
prefix;
|
|
59
|
+
async get(key, fetcher, options) {
|
|
60
|
+
return this.cache.get(this.qualify(key), fetcher, options);
|
|
61
|
+
}
|
|
62
|
+
async getOrSet(key, fetcher, options) {
|
|
63
|
+
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
64
|
+
}
|
|
65
|
+
async has(key) {
|
|
66
|
+
return this.cache.has(this.qualify(key));
|
|
67
|
+
}
|
|
68
|
+
async ttl(key) {
|
|
69
|
+
return this.cache.ttl(this.qualify(key));
|
|
70
|
+
}
|
|
71
|
+
async set(key, value, options) {
|
|
72
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
73
|
+
}
|
|
74
|
+
async delete(key) {
|
|
75
|
+
await this.cache.delete(this.qualify(key));
|
|
76
|
+
}
|
|
77
|
+
async mdelete(keys) {
|
|
78
|
+
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
79
|
+
}
|
|
80
|
+
async clear() {
|
|
81
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
82
|
+
}
|
|
83
|
+
async mget(entries) {
|
|
84
|
+
return this.cache.mget(
|
|
85
|
+
entries.map((entry) => ({
|
|
86
|
+
...entry,
|
|
87
|
+
key: this.qualify(entry.key)
|
|
88
|
+
}))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
async mset(entries) {
|
|
92
|
+
await this.cache.mset(
|
|
93
|
+
entries.map((entry) => ({
|
|
94
|
+
...entry,
|
|
95
|
+
key: this.qualify(entry.key)
|
|
96
|
+
}))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
async invalidateByTag(tag) {
|
|
100
|
+
await this.cache.invalidateByTag(tag);
|
|
101
|
+
}
|
|
102
|
+
async invalidateByPattern(pattern) {
|
|
103
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
104
|
+
}
|
|
105
|
+
wrap(keyPrefix, fetcher, options) {
|
|
106
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
107
|
+
}
|
|
108
|
+
warm(entries, options) {
|
|
109
|
+
return this.cache.warm(
|
|
110
|
+
entries.map((entry) => ({
|
|
111
|
+
...entry,
|
|
112
|
+
key: this.qualify(entry.key)
|
|
113
|
+
})),
|
|
114
|
+
options
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
getMetrics() {
|
|
118
|
+
return this.cache.getMetrics();
|
|
119
|
+
}
|
|
120
|
+
getHitRate() {
|
|
121
|
+
return this.cache.getHitRate();
|
|
122
|
+
}
|
|
123
|
+
qualify(key) {
|
|
124
|
+
return `${this.prefix}:${key}`;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/internal/CircuitBreakerManager.ts
|
|
129
|
+
var CircuitBreakerManager = class {
|
|
130
|
+
breakers = /* @__PURE__ */ new Map();
|
|
131
|
+
maxEntries;
|
|
132
|
+
constructor(options) {
|
|
133
|
+
this.maxEntries = options.maxEntries;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Throws if the circuit is open for the given key.
|
|
137
|
+
* Automatically resets if the cooldown has elapsed.
|
|
138
|
+
*/
|
|
139
|
+
assertClosed(key, options) {
|
|
140
|
+
const state = this.breakers.get(key);
|
|
141
|
+
if (!state?.openUntil) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
if (state.openUntil <= now) {
|
|
146
|
+
state.openUntil = null;
|
|
147
|
+
state.failures = 0;
|
|
148
|
+
this.breakers.set(key, state);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const remainingMs = state.openUntil - now;
|
|
152
|
+
const remainingSecs = Math.ceil(remainingMs / 1e3);
|
|
153
|
+
throw new Error(`Circuit breaker is open for key "${key}" (resets in ${remainingSecs}s).`);
|
|
154
|
+
}
|
|
155
|
+
recordFailure(key, options) {
|
|
156
|
+
if (!options) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
160
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
161
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
162
|
+
state.failures += 1;
|
|
163
|
+
if (state.failures >= failureThreshold) {
|
|
164
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
165
|
+
}
|
|
166
|
+
this.breakers.set(key, state);
|
|
167
|
+
this.pruneIfNeeded();
|
|
168
|
+
}
|
|
169
|
+
recordSuccess(key) {
|
|
170
|
+
this.breakers.delete(key);
|
|
171
|
+
}
|
|
172
|
+
isOpen(key) {
|
|
173
|
+
const state = this.breakers.get(key);
|
|
174
|
+
if (!state?.openUntil) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (state.openUntil <= Date.now()) {
|
|
178
|
+
state.openUntil = null;
|
|
179
|
+
state.failures = 0;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
delete(key) {
|
|
185
|
+
this.breakers.delete(key);
|
|
186
|
+
}
|
|
187
|
+
clear() {
|
|
188
|
+
this.breakers.clear();
|
|
189
|
+
}
|
|
190
|
+
tripCount() {
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (const state of this.breakers.values()) {
|
|
193
|
+
if (state.openUntil !== null) {
|
|
194
|
+
count += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return count;
|
|
198
|
+
}
|
|
199
|
+
pruneIfNeeded() {
|
|
200
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const [key, state] of this.breakers.entries()) {
|
|
204
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (!state.openUntil || state.openUntil <= Date.now()) {
|
|
208
|
+
this.breakers.delete(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const key of this.breakers.keys()) {
|
|
212
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
this.breakers.delete(key);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// src/internal/MetricsCollector.ts
|
|
221
|
+
var MetricsCollector = class {
|
|
222
|
+
data = this.empty();
|
|
223
|
+
get snapshot() {
|
|
224
|
+
return { ...this.data };
|
|
225
|
+
}
|
|
226
|
+
increment(field, amount = 1) {
|
|
227
|
+
;
|
|
228
|
+
this.data[field] += amount;
|
|
229
|
+
}
|
|
230
|
+
incrementLayer(map, layerName) {
|
|
231
|
+
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
232
|
+
}
|
|
233
|
+
reset() {
|
|
234
|
+
this.data = this.empty();
|
|
235
|
+
}
|
|
236
|
+
hitRate() {
|
|
237
|
+
const total = this.data.hits + this.data.misses;
|
|
238
|
+
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
239
|
+
const byLayer = {};
|
|
240
|
+
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
241
|
+
for (const layer of allLayers) {
|
|
242
|
+
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
243
|
+
const m = this.data.missesByLayer[layer] ?? 0;
|
|
244
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
245
|
+
}
|
|
246
|
+
return { overall, byLayer };
|
|
247
|
+
}
|
|
248
|
+
empty() {
|
|
249
|
+
return {
|
|
250
|
+
hits: 0,
|
|
251
|
+
misses: 0,
|
|
252
|
+
fetches: 0,
|
|
253
|
+
sets: 0,
|
|
254
|
+
deletes: 0,
|
|
255
|
+
backfills: 0,
|
|
256
|
+
invalidations: 0,
|
|
257
|
+
staleHits: 0,
|
|
258
|
+
refreshes: 0,
|
|
259
|
+
refreshErrors: 0,
|
|
260
|
+
writeFailures: 0,
|
|
261
|
+
singleFlightWaits: 0,
|
|
262
|
+
negativeCacheHits: 0,
|
|
263
|
+
circuitBreakerTrips: 0,
|
|
264
|
+
degradedOperations: 0,
|
|
265
|
+
hitsByLayer: {},
|
|
266
|
+
missesByLayer: {},
|
|
267
|
+
resetAt: Date.now()
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
};
|
|
39
271
|
|
|
40
272
|
// src/internal/StoredValue.ts
|
|
41
273
|
function isStoredValueEnvelope(value) {
|
|
@@ -55,7 +287,10 @@ function createStoredValueEnvelope(options) {
|
|
|
55
287
|
value: options.value,
|
|
56
288
|
freshUntil,
|
|
57
289
|
staleUntil,
|
|
58
|
-
errorUntil
|
|
290
|
+
errorUntil,
|
|
291
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
292
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
293
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
59
294
|
};
|
|
60
295
|
}
|
|
61
296
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -96,6 +331,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
96
331
|
}
|
|
97
332
|
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
98
333
|
}
|
|
334
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
335
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
336
|
+
return void 0;
|
|
337
|
+
}
|
|
338
|
+
const remainingMs = stored.freshUntil - now;
|
|
339
|
+
if (remainingMs <= 0) {
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
343
|
+
}
|
|
344
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
345
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
346
|
+
return stored;
|
|
347
|
+
}
|
|
348
|
+
return createStoredValueEnvelope({
|
|
349
|
+
kind: stored.kind,
|
|
350
|
+
value: stored.value,
|
|
351
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
352
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
353
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
354
|
+
now
|
|
355
|
+
});
|
|
356
|
+
}
|
|
99
357
|
function maxExpiry(stored) {
|
|
100
358
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
101
359
|
(value) => value !== null
|
|
@@ -112,12 +370,129 @@ function normalizePositiveSeconds(value) {
|
|
|
112
370
|
return value;
|
|
113
371
|
}
|
|
114
372
|
|
|
373
|
+
// src/internal/TtlResolver.ts
|
|
374
|
+
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
375
|
+
var TtlResolver = class {
|
|
376
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
377
|
+
maxProfileEntries;
|
|
378
|
+
constructor(options) {
|
|
379
|
+
this.maxProfileEntries = options.maxProfileEntries;
|
|
380
|
+
}
|
|
381
|
+
recordAccess(key) {
|
|
382
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
383
|
+
profile.hits += 1;
|
|
384
|
+
profile.lastAccessAt = Date.now();
|
|
385
|
+
this.accessProfiles.set(key, profile);
|
|
386
|
+
this.pruneIfNeeded();
|
|
387
|
+
}
|
|
388
|
+
deleteProfile(key) {
|
|
389
|
+
this.accessProfiles.delete(key);
|
|
390
|
+
}
|
|
391
|
+
clearProfiles() {
|
|
392
|
+
this.accessProfiles.clear();
|
|
393
|
+
}
|
|
394
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
395
|
+
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
396
|
+
layerName,
|
|
397
|
+
options?.negativeTtl,
|
|
398
|
+
globalNegativeTtl,
|
|
399
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
400
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
401
|
+
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
402
|
+
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
403
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
404
|
+
}
|
|
405
|
+
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
406
|
+
if (override !== void 0) {
|
|
407
|
+
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
408
|
+
}
|
|
409
|
+
if (globalDefault !== void 0) {
|
|
410
|
+
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
411
|
+
}
|
|
412
|
+
return fallback;
|
|
413
|
+
}
|
|
414
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
415
|
+
if (!ttl || !adaptiveTtl) {
|
|
416
|
+
return ttl;
|
|
417
|
+
}
|
|
418
|
+
const profile = this.accessProfiles.get(key);
|
|
419
|
+
if (!profile) {
|
|
420
|
+
return ttl;
|
|
421
|
+
}
|
|
422
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
423
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
424
|
+
if (profile.hits < hotAfter) {
|
|
425
|
+
return ttl;
|
|
426
|
+
}
|
|
427
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
428
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
429
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
430
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
431
|
+
}
|
|
432
|
+
applyJitter(ttl, jitter) {
|
|
433
|
+
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
434
|
+
return ttl;
|
|
435
|
+
}
|
|
436
|
+
const delta = (Math.random() * 2 - 1) * jitter;
|
|
437
|
+
return Math.max(1, Math.round(ttl + delta));
|
|
438
|
+
}
|
|
439
|
+
readLayerNumber(layerName, value) {
|
|
440
|
+
if (typeof value === "number") {
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
return value[layerName];
|
|
444
|
+
}
|
|
445
|
+
pruneIfNeeded() {
|
|
446
|
+
if (this.accessProfiles.size <= this.maxProfileEntries) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
450
|
+
let removed = 0;
|
|
451
|
+
for (const key of this.accessProfiles.keys()) {
|
|
452
|
+
if (removed >= toRemove) {
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
this.accessProfiles.delete(key);
|
|
456
|
+
removed += 1;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
115
461
|
// src/invalidation/PatternMatcher.ts
|
|
116
|
-
var PatternMatcher = class {
|
|
462
|
+
var PatternMatcher = class _PatternMatcher {
|
|
463
|
+
/**
|
|
464
|
+
* Tests whether a glob-style pattern matches a value.
|
|
465
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
466
|
+
* Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
|
|
467
|
+
*/
|
|
117
468
|
static matches(pattern, value) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
469
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Linear-time glob matching using dynamic programming.
|
|
473
|
+
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
474
|
+
*/
|
|
475
|
+
static matchLinear(pattern, value) {
|
|
476
|
+
const m = pattern.length;
|
|
477
|
+
const n = value.length;
|
|
478
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
|
|
479
|
+
dp[0][0] = true;
|
|
480
|
+
for (let i = 1; i <= m; i++) {
|
|
481
|
+
if (pattern[i - 1] === "*") {
|
|
482
|
+
dp[i][0] = dp[i - 1]?.[0];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
for (let i = 1; i <= m; i++) {
|
|
486
|
+
for (let j = 1; j <= n; j++) {
|
|
487
|
+
const pc = pattern[i - 1];
|
|
488
|
+
if (pc === "*") {
|
|
489
|
+
dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
|
|
490
|
+
} else if (pc === "?" || pc === value[j - 1]) {
|
|
491
|
+
dp[i][j] = dp[i - 1]?.[j - 1];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return dp[m]?.[n];
|
|
121
496
|
}
|
|
122
497
|
};
|
|
123
498
|
|
|
@@ -184,64 +559,75 @@ var import_async_mutex = require("async-mutex");
|
|
|
184
559
|
var StampedeGuard = class {
|
|
185
560
|
mutexes = /* @__PURE__ */ new Map();
|
|
186
561
|
async execute(key, task) {
|
|
187
|
-
const
|
|
562
|
+
const entry = this.getMutexEntry(key);
|
|
188
563
|
try {
|
|
189
|
-
return await mutex.runExclusive(task);
|
|
564
|
+
return await entry.mutex.runExclusive(task);
|
|
190
565
|
} finally {
|
|
191
|
-
|
|
566
|
+
entry.references -= 1;
|
|
567
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
192
568
|
this.mutexes.delete(key);
|
|
193
569
|
}
|
|
194
570
|
}
|
|
195
571
|
}
|
|
196
|
-
|
|
197
|
-
let
|
|
198
|
-
if (!
|
|
199
|
-
|
|
200
|
-
this.mutexes.set(key,
|
|
572
|
+
getMutexEntry(key) {
|
|
573
|
+
let entry = this.mutexes.get(key);
|
|
574
|
+
if (!entry) {
|
|
575
|
+
entry = { mutex: new import_async_mutex.Mutex(), references: 0 };
|
|
576
|
+
this.mutexes.set(key, entry);
|
|
201
577
|
}
|
|
202
|
-
|
|
578
|
+
entry.references += 1;
|
|
579
|
+
return entry;
|
|
203
580
|
}
|
|
204
581
|
};
|
|
205
582
|
|
|
206
583
|
// src/CacheStack.ts
|
|
207
|
-
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
208
584
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
209
585
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
210
586
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
211
|
-
var
|
|
212
|
-
|
|
213
|
-
misses: 0,
|
|
214
|
-
fetches: 0,
|
|
215
|
-
sets: 0,
|
|
216
|
-
deletes: 0,
|
|
217
|
-
backfills: 0,
|
|
218
|
-
invalidations: 0,
|
|
219
|
-
staleHits: 0,
|
|
220
|
-
refreshes: 0,
|
|
221
|
-
refreshErrors: 0,
|
|
222
|
-
writeFailures: 0,
|
|
223
|
-
singleFlightWaits: 0
|
|
224
|
-
});
|
|
587
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
588
|
+
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
225
589
|
var DebugLogger = class {
|
|
226
590
|
enabled;
|
|
227
591
|
constructor(enabled) {
|
|
228
592
|
this.enabled = enabled;
|
|
229
593
|
}
|
|
230
594
|
debug(message, context) {
|
|
595
|
+
this.write("debug", message, context);
|
|
596
|
+
}
|
|
597
|
+
info(message, context) {
|
|
598
|
+
this.write("info", message, context);
|
|
599
|
+
}
|
|
600
|
+
warn(message, context) {
|
|
601
|
+
this.write("warn", message, context);
|
|
602
|
+
}
|
|
603
|
+
error(message, context) {
|
|
604
|
+
this.write("error", message, context);
|
|
605
|
+
}
|
|
606
|
+
write(level, message, context) {
|
|
231
607
|
if (!this.enabled) {
|
|
232
608
|
return;
|
|
233
609
|
}
|
|
234
610
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
235
|
-
console
|
|
611
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
236
612
|
}
|
|
237
613
|
};
|
|
238
|
-
var CacheStack = class {
|
|
614
|
+
var CacheStack = class extends import_node_events.EventEmitter {
|
|
239
615
|
constructor(layers, options = {}) {
|
|
616
|
+
super();
|
|
240
617
|
this.layers = layers;
|
|
241
618
|
this.options = options;
|
|
242
619
|
if (layers.length === 0) {
|
|
243
620
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
244
621
|
}
|
|
622
|
+
this.validateConfiguration();
|
|
623
|
+
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
624
|
+
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
625
|
+
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
626
|
+
if (options.publishSetInvalidation !== void 0) {
|
|
627
|
+
console.warn(
|
|
628
|
+
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
629
|
+
);
|
|
630
|
+
}
|
|
245
631
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
246
632
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
247
633
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -250,112 +636,313 @@ var CacheStack = class {
|
|
|
250
636
|
layers;
|
|
251
637
|
options;
|
|
252
638
|
stampedeGuard = new StampedeGuard();
|
|
253
|
-
|
|
639
|
+
metricsCollector = new MetricsCollector();
|
|
254
640
|
instanceId = (0, import_node_crypto.randomUUID)();
|
|
255
641
|
startup;
|
|
256
642
|
unsubscribeInvalidation;
|
|
257
643
|
logger;
|
|
258
644
|
tagIndex;
|
|
259
645
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
646
|
+
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
647
|
+
ttlResolver;
|
|
648
|
+
circuitBreakerManager;
|
|
649
|
+
isDisconnecting = false;
|
|
650
|
+
disconnectPromise;
|
|
651
|
+
/**
|
|
652
|
+
* Read-through cache get.
|
|
653
|
+
* Returns the cached value if present and fresh, or invokes `fetcher` on a miss
|
|
654
|
+
* and stores the result across all layers. Returns `null` if the key is not found
|
|
655
|
+
* and no `fetcher` is provided.
|
|
656
|
+
*/
|
|
260
657
|
async get(key, fetcher, options) {
|
|
658
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
659
|
+
this.validateWriteOptions(options);
|
|
261
660
|
await this.startup;
|
|
262
|
-
const hit = await this.readFromLayers(
|
|
661
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
263
662
|
if (hit.found) {
|
|
663
|
+
this.ttlResolver.recordAccess(normalizedKey);
|
|
664
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
665
|
+
this.metricsCollector.increment("negativeCacheHits");
|
|
666
|
+
}
|
|
264
667
|
if (hit.state === "fresh") {
|
|
265
|
-
this.
|
|
668
|
+
this.metricsCollector.increment("hits");
|
|
669
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
266
670
|
return hit.value;
|
|
267
671
|
}
|
|
268
672
|
if (hit.state === "stale-while-revalidate") {
|
|
269
|
-
this.
|
|
270
|
-
this.
|
|
673
|
+
this.metricsCollector.increment("hits");
|
|
674
|
+
this.metricsCollector.increment("staleHits");
|
|
675
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
271
676
|
if (fetcher) {
|
|
272
|
-
this.scheduleBackgroundRefresh(
|
|
677
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
273
678
|
}
|
|
274
679
|
return hit.value;
|
|
275
680
|
}
|
|
276
681
|
if (!fetcher) {
|
|
277
|
-
this.
|
|
278
|
-
this.
|
|
682
|
+
this.metricsCollector.increment("hits");
|
|
683
|
+
this.metricsCollector.increment("staleHits");
|
|
684
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
279
685
|
return hit.value;
|
|
280
686
|
}
|
|
281
687
|
try {
|
|
282
|
-
return await this.fetchWithGuards(
|
|
688
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
283
689
|
} catch (error) {
|
|
284
|
-
this.
|
|
285
|
-
this.
|
|
286
|
-
this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
|
|
690
|
+
this.metricsCollector.increment("staleHits");
|
|
691
|
+
this.metricsCollector.increment("refreshErrors");
|
|
692
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
287
693
|
return hit.value;
|
|
288
694
|
}
|
|
289
695
|
}
|
|
290
|
-
this.
|
|
696
|
+
this.metricsCollector.increment("misses");
|
|
291
697
|
if (!fetcher) {
|
|
292
698
|
return null;
|
|
293
699
|
}
|
|
294
|
-
return this.fetchWithGuards(
|
|
700
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
704
|
+
* Fetches and caches the value if not already present.
|
|
705
|
+
*/
|
|
706
|
+
async getOrSet(key, fetcher, options) {
|
|
707
|
+
return this.get(key, fetcher, options);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Returns true if the given key exists and is not expired in any layer.
|
|
711
|
+
*/
|
|
712
|
+
async has(key) {
|
|
713
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
714
|
+
await this.startup;
|
|
715
|
+
for (const layer of this.layers) {
|
|
716
|
+
if (this.shouldSkipLayer(layer)) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (layer.has) {
|
|
720
|
+
try {
|
|
721
|
+
const exists = await layer.has(normalizedKey);
|
|
722
|
+
if (exists) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
try {
|
|
729
|
+
const value = await layer.get(normalizedKey);
|
|
730
|
+
if (value !== null) {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Returns the remaining TTL in seconds for the key in the fastest layer
|
|
741
|
+
* that has it, or null if the key is not found / has no TTL.
|
|
742
|
+
*/
|
|
743
|
+
async ttl(key) {
|
|
744
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
745
|
+
await this.startup;
|
|
746
|
+
for (const layer of this.layers) {
|
|
747
|
+
if (this.shouldSkipLayer(layer)) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (layer.ttl) {
|
|
751
|
+
try {
|
|
752
|
+
const remaining = await layer.ttl(normalizedKey);
|
|
753
|
+
if (remaining !== null) {
|
|
754
|
+
return remaining;
|
|
755
|
+
}
|
|
756
|
+
} catch {
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
295
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Stores a value in all cache layers. Overwrites any existing value.
|
|
764
|
+
*/
|
|
296
765
|
async set(key, value, options) {
|
|
766
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
767
|
+
this.validateWriteOptions(options);
|
|
297
768
|
await this.startup;
|
|
298
|
-
await this.storeEntry(
|
|
769
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
299
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Deletes the key from all layers and publishes an invalidation message.
|
|
773
|
+
*/
|
|
300
774
|
async delete(key) {
|
|
775
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
301
776
|
await this.startup;
|
|
302
|
-
await this.deleteKeys([
|
|
303
|
-
await this.publishInvalidation({
|
|
777
|
+
await this.deleteKeys([normalizedKey]);
|
|
778
|
+
await this.publishInvalidation({
|
|
779
|
+
scope: "key",
|
|
780
|
+
keys: [normalizedKey],
|
|
781
|
+
sourceId: this.instanceId,
|
|
782
|
+
operation: "delete"
|
|
783
|
+
});
|
|
304
784
|
}
|
|
305
785
|
async clear() {
|
|
306
786
|
await this.startup;
|
|
307
787
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
308
788
|
await this.tagIndex.clear();
|
|
309
|
-
this.
|
|
310
|
-
this.
|
|
789
|
+
this.ttlResolver.clearProfiles();
|
|
790
|
+
this.circuitBreakerManager.clear();
|
|
791
|
+
this.metricsCollector.increment("invalidations");
|
|
792
|
+
this.logger.debug?.("clear");
|
|
311
793
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
312
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
|
|
797
|
+
*/
|
|
798
|
+
async mdelete(keys) {
|
|
799
|
+
if (keys.length === 0) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
await this.startup;
|
|
803
|
+
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
804
|
+
await this.deleteKeys(normalizedKeys);
|
|
805
|
+
await this.publishInvalidation({
|
|
806
|
+
scope: "keys",
|
|
807
|
+
keys: normalizedKeys,
|
|
808
|
+
sourceId: this.instanceId,
|
|
809
|
+
operation: "delete"
|
|
810
|
+
});
|
|
811
|
+
}
|
|
313
812
|
async mget(entries) {
|
|
314
813
|
if (entries.length === 0) {
|
|
315
814
|
return [];
|
|
316
815
|
}
|
|
317
|
-
const
|
|
816
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
817
|
+
...entry,
|
|
818
|
+
key: this.validateCacheKey(entry.key)
|
|
819
|
+
}));
|
|
820
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
821
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
318
822
|
if (!canFastPath) {
|
|
319
|
-
|
|
823
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
824
|
+
return Promise.all(
|
|
825
|
+
normalizedEntries.map((entry) => {
|
|
826
|
+
const optionsSignature = this.serializeOptions(entry.options);
|
|
827
|
+
const existing = pendingReads.get(entry.key);
|
|
828
|
+
if (!existing) {
|
|
829
|
+
const promise = this.get(entry.key, entry.fetch, entry.options);
|
|
830
|
+
pendingReads.set(entry.key, {
|
|
831
|
+
promise,
|
|
832
|
+
fetch: entry.fetch,
|
|
833
|
+
optionsSignature
|
|
834
|
+
});
|
|
835
|
+
return promise;
|
|
836
|
+
}
|
|
837
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
838
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
839
|
+
}
|
|
840
|
+
return existing.promise;
|
|
841
|
+
})
|
|
842
|
+
);
|
|
320
843
|
}
|
|
321
844
|
await this.startup;
|
|
322
|
-
const pending = new Set(
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
845
|
+
const pending = /* @__PURE__ */ new Set();
|
|
846
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
847
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
848
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
849
|
+
const entry = normalizedEntries[index];
|
|
850
|
+
if (!entry) continue;
|
|
851
|
+
const key = entry.key;
|
|
852
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
853
|
+
indexes.push(index);
|
|
854
|
+
indexesByKey.set(key, indexes);
|
|
855
|
+
pending.add(key);
|
|
856
|
+
}
|
|
857
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
858
|
+
const layer = this.layers[layerIndex];
|
|
859
|
+
if (!layer) continue;
|
|
860
|
+
const keys = [...pending];
|
|
861
|
+
if (keys.length === 0) {
|
|
327
862
|
break;
|
|
328
863
|
}
|
|
329
|
-
const keys = indexes.map((index) => entries[index].key);
|
|
330
864
|
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
331
865
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
332
|
-
const
|
|
866
|
+
const key = keys[offset];
|
|
333
867
|
const stored = values[offset];
|
|
334
|
-
if (stored === null) {
|
|
868
|
+
if (!key || stored === null) {
|
|
335
869
|
continue;
|
|
336
870
|
}
|
|
337
871
|
const resolved = resolveStoredValue(stored);
|
|
338
872
|
if (resolved.state === "expired") {
|
|
339
|
-
await layer.delete(
|
|
873
|
+
await layer.delete(key);
|
|
340
874
|
continue;
|
|
341
875
|
}
|
|
342
|
-
await this.tagIndex.touch(
|
|
343
|
-
await this.backfill(
|
|
344
|
-
|
|
345
|
-
pending.delete(
|
|
346
|
-
this.
|
|
876
|
+
await this.tagIndex.touch(key);
|
|
877
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
878
|
+
resultsByKey.set(key, resolved.value);
|
|
879
|
+
pending.delete(key);
|
|
880
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
347
881
|
}
|
|
348
882
|
}
|
|
349
883
|
if (pending.size > 0) {
|
|
350
|
-
for (const
|
|
351
|
-
await this.tagIndex.remove(
|
|
352
|
-
this.
|
|
884
|
+
for (const key of pending) {
|
|
885
|
+
await this.tagIndex.remove(key);
|
|
886
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
353
887
|
}
|
|
354
888
|
}
|
|
355
|
-
return
|
|
889
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
356
890
|
}
|
|
357
891
|
async mset(entries) {
|
|
358
|
-
|
|
892
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
893
|
+
...entry,
|
|
894
|
+
key: this.validateCacheKey(entry.key)
|
|
895
|
+
}));
|
|
896
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
897
|
+
await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
898
|
+
}
|
|
899
|
+
async warm(entries, options = {}) {
|
|
900
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
901
|
+
const total = entries.length;
|
|
902
|
+
let completed = 0;
|
|
903
|
+
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
904
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length || 1) }, async () => {
|
|
905
|
+
while (queue.length > 0) {
|
|
906
|
+
const entry = queue.shift();
|
|
907
|
+
if (!entry) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
let success = false;
|
|
911
|
+
try {
|
|
912
|
+
await this.get(entry.key, entry.fetcher, entry.options);
|
|
913
|
+
this.emit("warm", { key: entry.key });
|
|
914
|
+
success = true;
|
|
915
|
+
} catch (error) {
|
|
916
|
+
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
917
|
+
if (!options.continueOnError) {
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
} finally {
|
|
921
|
+
completed += 1;
|
|
922
|
+
const progress = { completed, total, key: entry.key, success };
|
|
923
|
+
options.onProgress?.(progress);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
await Promise.all(workers);
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Returns a cached version of `fetcher`. The cache key is derived from
|
|
931
|
+
* `prefix` plus the serialized arguments unless a `keyResolver` is provided.
|
|
932
|
+
*/
|
|
933
|
+
wrap(prefix, fetcher, options = {}) {
|
|
934
|
+
return (...args) => {
|
|
935
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
936
|
+
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
937
|
+
return this.get(key, () => fetcher(...args), options);
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Creates a `CacheNamespace` that automatically prefixes all keys with
|
|
942
|
+
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
943
|
+
*/
|
|
944
|
+
namespace(prefix) {
|
|
945
|
+
return new CacheNamespace(this, prefix);
|
|
359
946
|
}
|
|
360
947
|
async invalidateByTag(tag) {
|
|
361
948
|
await this.startup;
|
|
@@ -370,15 +957,94 @@ var CacheStack = class {
|
|
|
370
957
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
371
958
|
}
|
|
372
959
|
getMetrics() {
|
|
373
|
-
return
|
|
960
|
+
return this.metricsCollector.snapshot;
|
|
961
|
+
}
|
|
962
|
+
getStats() {
|
|
963
|
+
return {
|
|
964
|
+
metrics: this.getMetrics(),
|
|
965
|
+
layers: this.layers.map((layer) => ({
|
|
966
|
+
name: layer.name,
|
|
967
|
+
isLocal: Boolean(layer.isLocal),
|
|
968
|
+
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
969
|
+
})),
|
|
970
|
+
backgroundRefreshes: this.backgroundRefreshes.size
|
|
971
|
+
};
|
|
374
972
|
}
|
|
375
973
|
resetMetrics() {
|
|
376
|
-
|
|
974
|
+
this.metricsCollector.reset();
|
|
377
975
|
}
|
|
378
|
-
|
|
976
|
+
/**
|
|
977
|
+
* Returns computed hit-rate statistics (overall and per-layer).
|
|
978
|
+
*/
|
|
979
|
+
getHitRate() {
|
|
980
|
+
return this.metricsCollector.hitRate();
|
|
981
|
+
}
|
|
982
|
+
async exportState() {
|
|
983
|
+
await this.startup;
|
|
984
|
+
const exported = /* @__PURE__ */ new Map();
|
|
985
|
+
for (const layer of this.layers) {
|
|
986
|
+
if (!layer.keys) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const keys = await layer.keys();
|
|
990
|
+
for (const key of keys) {
|
|
991
|
+
if (exported.has(key)) {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
995
|
+
if (stored === null) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
exported.set(key, {
|
|
999
|
+
key,
|
|
1000
|
+
value: stored,
|
|
1001
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return [...exported.values()];
|
|
1006
|
+
}
|
|
1007
|
+
async importState(entries) {
|
|
379
1008
|
await this.startup;
|
|
380
|
-
await
|
|
381
|
-
|
|
1009
|
+
await Promise.all(
|
|
1010
|
+
entries.map(async (entry) => {
|
|
1011
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1012
|
+
await this.tagIndex.touch(entry.key);
|
|
1013
|
+
})
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
async persistToFile(filePath) {
|
|
1017
|
+
const snapshot = await this.exportState();
|
|
1018
|
+
await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1019
|
+
}
|
|
1020
|
+
async restoreFromFile(filePath) {
|
|
1021
|
+
const raw = await import_node_fs.promises.readFile(filePath, "utf8");
|
|
1022
|
+
let parsed;
|
|
1023
|
+
try {
|
|
1024
|
+
parsed = JSON.parse(raw, (_key, value) => {
|
|
1025
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1026
|
+
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1027
|
+
}
|
|
1028
|
+
return value;
|
|
1029
|
+
});
|
|
1030
|
+
} catch (cause) {
|
|
1031
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1032
|
+
}
|
|
1033
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1034
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1035
|
+
}
|
|
1036
|
+
await this.importState(parsed);
|
|
1037
|
+
}
|
|
1038
|
+
async disconnect() {
|
|
1039
|
+
if (!this.disconnectPromise) {
|
|
1040
|
+
this.isDisconnecting = true;
|
|
1041
|
+
this.disconnectPromise = (async () => {
|
|
1042
|
+
await this.startup;
|
|
1043
|
+
await this.unsubscribeInvalidation?.();
|
|
1044
|
+
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1045
|
+
})();
|
|
1046
|
+
}
|
|
1047
|
+
await this.disconnectPromise;
|
|
382
1048
|
}
|
|
383
1049
|
async initialize() {
|
|
384
1050
|
if (!this.options.invalidationBus) {
|
|
@@ -392,7 +1058,7 @@ var CacheStack = class {
|
|
|
392
1058
|
const fetchTask = async () => {
|
|
393
1059
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
394
1060
|
if (secondHit.found) {
|
|
395
|
-
this.
|
|
1061
|
+
this.metricsCollector.increment("hits");
|
|
396
1062
|
return secondHit.value;
|
|
397
1063
|
}
|
|
398
1064
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
@@ -417,11 +1083,12 @@ var CacheStack = class {
|
|
|
417
1083
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
418
1084
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
419
1085
|
const deadline = Date.now() + timeoutMs;
|
|
420
|
-
this.
|
|
1086
|
+
this.metricsCollector.increment("singleFlightWaits");
|
|
1087
|
+
this.emit("stampede-dedupe", { key });
|
|
421
1088
|
while (Date.now() < deadline) {
|
|
422
1089
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
423
1090
|
if (hit.found) {
|
|
424
|
-
this.
|
|
1091
|
+
this.metricsCollector.increment("hits");
|
|
425
1092
|
return hit.value;
|
|
426
1093
|
}
|
|
427
1094
|
await this.sleep(pollIntervalMs);
|
|
@@ -429,8 +1096,18 @@ var CacheStack = class {
|
|
|
429
1096
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
430
1097
|
}
|
|
431
1098
|
async fetchAndPopulate(key, fetcher, options) {
|
|
432
|
-
this.
|
|
433
|
-
|
|
1099
|
+
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1100
|
+
this.metricsCollector.increment("fetches");
|
|
1101
|
+
const fetchStart = Date.now();
|
|
1102
|
+
let fetched;
|
|
1103
|
+
try {
|
|
1104
|
+
fetched = await fetcher();
|
|
1105
|
+
this.circuitBreakerManager.recordSuccess(key);
|
|
1106
|
+
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
434
1111
|
if (fetched === null || fetched === void 0) {
|
|
435
1112
|
if (!this.shouldNegativeCache(options)) {
|
|
436
1113
|
return null;
|
|
@@ -448,9 +1125,10 @@ var CacheStack = class {
|
|
|
448
1125
|
} else {
|
|
449
1126
|
await this.tagIndex.touch(key);
|
|
450
1127
|
}
|
|
451
|
-
this.
|
|
452
|
-
this.logger.debug("set", { key, kind, tags: options?.tags });
|
|
453
|
-
|
|
1128
|
+
this.metricsCollector.increment("sets");
|
|
1129
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
1130
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
1131
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
454
1132
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
455
1133
|
}
|
|
456
1134
|
}
|
|
@@ -458,8 +1136,10 @@ var CacheStack = class {
|
|
|
458
1136
|
let sawRetainableValue = false;
|
|
459
1137
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
460
1138
|
const layer = this.layers[index];
|
|
1139
|
+
if (!layer) continue;
|
|
461
1140
|
const stored = await this.readLayerEntry(layer, key);
|
|
462
1141
|
if (stored === null) {
|
|
1142
|
+
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
463
1143
|
continue;
|
|
464
1144
|
}
|
|
465
1145
|
const resolved = resolveStoredValue(stored);
|
|
@@ -473,20 +1153,41 @@ var CacheStack = class {
|
|
|
473
1153
|
}
|
|
474
1154
|
await this.tagIndex.touch(key);
|
|
475
1155
|
await this.backfill(key, stored, index - 1, options);
|
|
476
|
-
this.
|
|
477
|
-
|
|
1156
|
+
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
1157
|
+
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
1158
|
+
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
1159
|
+
return {
|
|
1160
|
+
found: true,
|
|
1161
|
+
value: resolved.value,
|
|
1162
|
+
stored,
|
|
1163
|
+
state: resolved.state,
|
|
1164
|
+
layerIndex: index,
|
|
1165
|
+
layerName: layer.name
|
|
1166
|
+
};
|
|
478
1167
|
}
|
|
479
1168
|
if (!sawRetainableValue) {
|
|
480
1169
|
await this.tagIndex.remove(key);
|
|
481
1170
|
}
|
|
482
|
-
this.logger.debug("miss", { key, mode });
|
|
1171
|
+
this.logger.debug?.("miss", { key, mode });
|
|
1172
|
+
this.emit("miss", { key, mode });
|
|
483
1173
|
return { found: false, value: null, stored: null, state: "miss" };
|
|
484
1174
|
}
|
|
485
1175
|
async readLayerEntry(layer, key) {
|
|
1176
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
486
1179
|
if (layer.getEntry) {
|
|
487
|
-
|
|
1180
|
+
try {
|
|
1181
|
+
return await layer.getEntry(key);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
return await layer.get(key);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
488
1190
|
}
|
|
489
|
-
return layer.get(key);
|
|
490
1191
|
}
|
|
491
1192
|
async backfill(key, stored, upToIndex, options) {
|
|
492
1193
|
if (upToIndex < 0) {
|
|
@@ -494,26 +1195,34 @@ var CacheStack = class {
|
|
|
494
1195
|
}
|
|
495
1196
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
496
1197
|
const layer = this.layers[index];
|
|
1198
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
497
1201
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1202
|
+
try {
|
|
1203
|
+
await layer.set(key, stored, ttl);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
this.metricsCollector.increment("backfills");
|
|
1209
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1210
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
501
1211
|
}
|
|
502
1212
|
}
|
|
503
1213
|
async writeAcrossLayers(key, kind, value, options) {
|
|
504
1214
|
const now = Date.now();
|
|
505
1215
|
const operations = this.layers.map((layer) => async () => {
|
|
506
|
-
|
|
1216
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
507
1220
|
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
508
1221
|
layer.name,
|
|
509
1222
|
options?.staleWhileRevalidate,
|
|
510
1223
|
this.options.staleWhileRevalidate
|
|
511
1224
|
);
|
|
512
|
-
const staleIfError = this.resolveLayerSeconds(
|
|
513
|
-
layer.name,
|
|
514
|
-
options?.staleIfError,
|
|
515
|
-
this.options.staleIfError
|
|
516
|
-
);
|
|
1225
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
517
1226
|
const payload = createStoredValueEnvelope({
|
|
518
1227
|
kind,
|
|
519
1228
|
value,
|
|
@@ -523,7 +1232,11 @@ var CacheStack = class {
|
|
|
523
1232
|
now
|
|
524
1233
|
});
|
|
525
1234
|
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
526
|
-
|
|
1235
|
+
try {
|
|
1236
|
+
await layer.set(key, payload, ttl);
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1239
|
+
}
|
|
527
1240
|
});
|
|
528
1241
|
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
529
1242
|
}
|
|
@@ -537,8 +1250,8 @@ var CacheStack = class {
|
|
|
537
1250
|
if (failures.length === 0) {
|
|
538
1251
|
return;
|
|
539
1252
|
}
|
|
540
|
-
this.
|
|
541
|
-
this.logger.debug("write-failure", {
|
|
1253
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
1254
|
+
this.logger.debug?.("write-failure", {
|
|
542
1255
|
...context,
|
|
543
1256
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
544
1257
|
});
|
|
@@ -549,52 +1262,26 @@ var CacheStack = class {
|
|
|
549
1262
|
);
|
|
550
1263
|
}
|
|
551
1264
|
}
|
|
552
|
-
resolveFreshTtl(layerName, kind, options, fallbackTtl) {
|
|
553
|
-
|
|
554
|
-
layerName,
|
|
555
|
-
options?.negativeTtl,
|
|
556
|
-
this.options.negativeTtl,
|
|
557
|
-
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
558
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
559
|
-
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
560
|
-
return this.applyJitter(baseTtl, jitter);
|
|
1265
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1266
|
+
return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
|
|
561
1267
|
}
|
|
562
1268
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
563
|
-
|
|
564
|
-
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
565
|
-
}
|
|
566
|
-
if (globalDefault !== void 0) {
|
|
567
|
-
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
568
|
-
}
|
|
569
|
-
return fallback;
|
|
570
|
-
}
|
|
571
|
-
readLayerNumber(layerName, value) {
|
|
572
|
-
if (typeof value === "number") {
|
|
573
|
-
return value;
|
|
574
|
-
}
|
|
575
|
-
return value[layerName];
|
|
576
|
-
}
|
|
577
|
-
applyJitter(ttl, jitter) {
|
|
578
|
-
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
579
|
-
return ttl;
|
|
580
|
-
}
|
|
581
|
-
const delta = (Math.random() * 2 - 1) * jitter;
|
|
582
|
-
return Math.max(1, Math.round(ttl + delta));
|
|
1269
|
+
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
583
1270
|
}
|
|
584
1271
|
shouldNegativeCache(options) {
|
|
585
1272
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
586
1273
|
}
|
|
587
1274
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
588
|
-
if (this.backgroundRefreshes.has(key)) {
|
|
1275
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
589
1276
|
return;
|
|
590
1277
|
}
|
|
591
1278
|
const refresh = (async () => {
|
|
592
|
-
this.
|
|
1279
|
+
this.metricsCollector.increment("refreshes");
|
|
593
1280
|
try {
|
|
594
1281
|
await this.fetchWithGuards(key, fetcher, options);
|
|
595
1282
|
} catch (error) {
|
|
596
|
-
this.
|
|
597
|
-
this.logger.debug("refresh-error", { key, error: this.formatError(error) });
|
|
1283
|
+
this.metricsCollector.increment("refreshErrors");
|
|
1284
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
598
1285
|
} finally {
|
|
599
1286
|
this.backgroundRefreshes.delete(key);
|
|
600
1287
|
}
|
|
@@ -612,21 +1299,16 @@ var CacheStack = class {
|
|
|
612
1299
|
if (keys.length === 0) {
|
|
613
1300
|
return;
|
|
614
1301
|
}
|
|
615
|
-
await
|
|
616
|
-
this.layers.map(async (layer) => {
|
|
617
|
-
if (layer.deleteMany) {
|
|
618
|
-
await layer.deleteMany(keys);
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
622
|
-
})
|
|
623
|
-
);
|
|
1302
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
624
1303
|
for (const key of keys) {
|
|
625
1304
|
await this.tagIndex.remove(key);
|
|
1305
|
+
this.ttlResolver.deleteProfile(key);
|
|
1306
|
+
this.circuitBreakerManager.delete(key);
|
|
626
1307
|
}
|
|
627
|
-
this.
|
|
628
|
-
this.
|
|
629
|
-
this.logger.debug("delete", { keys });
|
|
1308
|
+
this.metricsCollector.increment("deletes", keys.length);
|
|
1309
|
+
this.metricsCollector.increment("invalidations");
|
|
1310
|
+
this.logger.debug?.("delete", { keys });
|
|
1311
|
+
this.emit("delete", { keys });
|
|
630
1312
|
}
|
|
631
1313
|
async publishInvalidation(message) {
|
|
632
1314
|
if (!this.options.invalidationBus) {
|
|
@@ -645,21 +1327,15 @@ var CacheStack = class {
|
|
|
645
1327
|
if (message.scope === "clear") {
|
|
646
1328
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
647
1329
|
await this.tagIndex.clear();
|
|
1330
|
+
this.ttlResolver.clearProfiles();
|
|
648
1331
|
return;
|
|
649
1332
|
}
|
|
650
1333
|
const keys = message.keys ?? [];
|
|
651
|
-
await
|
|
652
|
-
localLayers.map(async (layer) => {
|
|
653
|
-
if (layer.deleteMany) {
|
|
654
|
-
await layer.deleteMany(keys);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
658
|
-
})
|
|
659
|
-
);
|
|
1334
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
660
1335
|
if (message.operation !== "write") {
|
|
661
1336
|
for (const key of keys) {
|
|
662
1337
|
await this.tagIndex.remove(key);
|
|
1338
|
+
this.ttlResolver.deleteProfile(key);
|
|
663
1339
|
}
|
|
664
1340
|
}
|
|
665
1341
|
}
|
|
@@ -672,6 +1348,210 @@ var CacheStack = class {
|
|
|
672
1348
|
sleep(ms) {
|
|
673
1349
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
674
1350
|
}
|
|
1351
|
+
shouldBroadcastL1Invalidation() {
|
|
1352
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1353
|
+
}
|
|
1354
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
1355
|
+
await Promise.all(
|
|
1356
|
+
layers.map(async (layer) => {
|
|
1357
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (layer.deleteMany) {
|
|
1361
|
+
try {
|
|
1362
|
+
await layer.deleteMany(keys);
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1365
|
+
}
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
await Promise.all(
|
|
1369
|
+
keys.map(async (key) => {
|
|
1370
|
+
try {
|
|
1371
|
+
await layer.delete(key);
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1374
|
+
}
|
|
1375
|
+
})
|
|
1376
|
+
);
|
|
1377
|
+
})
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
validateConfiguration() {
|
|
1381
|
+
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
1382
|
+
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
1383
|
+
}
|
|
1384
|
+
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
1385
|
+
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
1386
|
+
}
|
|
1387
|
+
this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
1388
|
+
this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
1389
|
+
this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
1390
|
+
this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
1391
|
+
this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
1392
|
+
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1393
|
+
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1394
|
+
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1395
|
+
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1396
|
+
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1397
|
+
}
|
|
1398
|
+
validateWriteOptions(options) {
|
|
1399
|
+
if (!options) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
1403
|
+
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
1404
|
+
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
1405
|
+
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1406
|
+
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1407
|
+
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1408
|
+
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1409
|
+
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1410
|
+
}
|
|
1411
|
+
validateLayerNumberOption(name, value) {
|
|
1412
|
+
if (value === void 0) {
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (typeof value === "number") {
|
|
1416
|
+
this.validateNonNegativeNumber(name, value);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1420
|
+
if (layerValue === void 0) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
validatePositiveNumber(name, value) {
|
|
1427
|
+
if (value === void 0) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1431
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
validateNonNegativeNumber(name, value) {
|
|
1435
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1436
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
validateCacheKey(key) {
|
|
1440
|
+
if (key.length === 0) {
|
|
1441
|
+
throw new Error("Cache key must not be empty.");
|
|
1442
|
+
}
|
|
1443
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1444
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1445
|
+
}
|
|
1446
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1447
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1448
|
+
}
|
|
1449
|
+
return key;
|
|
1450
|
+
}
|
|
1451
|
+
serializeOptions(options) {
|
|
1452
|
+
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1453
|
+
}
|
|
1454
|
+
validateAdaptiveTtlOptions(options) {
|
|
1455
|
+
if (!options || options === true) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1459
|
+
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1460
|
+
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1461
|
+
}
|
|
1462
|
+
validateCircuitBreakerOptions(options) {
|
|
1463
|
+
if (!options) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1467
|
+
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1468
|
+
}
|
|
1469
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1470
|
+
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
1471
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
1472
|
+
if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
|
|
1473
|
+
const refreshed = refreshStoredEnvelope(hit.stored);
|
|
1474
|
+
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1475
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1476
|
+
const layer = this.layers[index];
|
|
1477
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
try {
|
|
1481
|
+
await layer.set(key, refreshed, ttl);
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
|
|
1488
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
shouldSkipLayer(layer) {
|
|
1492
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1493
|
+
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
1494
|
+
}
|
|
1495
|
+
async handleLayerFailure(layer, operation, error) {
|
|
1496
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
1497
|
+
throw error;
|
|
1498
|
+
}
|
|
1499
|
+
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1500
|
+
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1501
|
+
this.metricsCollector.increment("degradedOperations");
|
|
1502
|
+
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1503
|
+
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
isGracefulDegradationEnabled() {
|
|
1507
|
+
return Boolean(this.options.gracefulDegradation);
|
|
1508
|
+
}
|
|
1509
|
+
recordCircuitFailure(key, options, error) {
|
|
1510
|
+
if (!options) {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
this.circuitBreakerManager.recordFailure(key, options);
|
|
1514
|
+
if (this.circuitBreakerManager.isOpen(key)) {
|
|
1515
|
+
this.metricsCollector.increment("circuitBreakerTrips");
|
|
1516
|
+
}
|
|
1517
|
+
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
1518
|
+
}
|
|
1519
|
+
isNegativeStoredValue(stored) {
|
|
1520
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1521
|
+
}
|
|
1522
|
+
emitError(operation, context) {
|
|
1523
|
+
this.logger.error?.(operation, context);
|
|
1524
|
+
if (this.listenerCount("error") > 0) {
|
|
1525
|
+
this.emit("error", { operation, ...context });
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
serializeKeyPart(value) {
|
|
1529
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1530
|
+
return String(value);
|
|
1531
|
+
}
|
|
1532
|
+
return JSON.stringify(this.normalizeForSerialization(value));
|
|
1533
|
+
}
|
|
1534
|
+
isCacheSnapshotEntries(value) {
|
|
1535
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1536
|
+
if (!entry || typeof entry !== "object") {
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
const candidate = entry;
|
|
1540
|
+
return typeof candidate.key === "string";
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
normalizeForSerialization(value) {
|
|
1544
|
+
if (Array.isArray(value)) {
|
|
1545
|
+
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
1546
|
+
}
|
|
1547
|
+
if (value && typeof value === "object") {
|
|
1548
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
1549
|
+
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
1550
|
+
return normalized;
|
|
1551
|
+
}, {});
|
|
1552
|
+
}
|
|
1553
|
+
return value;
|
|
1554
|
+
}
|
|
675
1555
|
};
|
|
676
1556
|
|
|
677
1557
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -679,19 +1559,27 @@ var RedisInvalidationBus = class {
|
|
|
679
1559
|
channel;
|
|
680
1560
|
publisher;
|
|
681
1561
|
subscriber;
|
|
1562
|
+
activeListener;
|
|
682
1563
|
constructor(options) {
|
|
683
1564
|
this.publisher = options.publisher;
|
|
684
1565
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
685
1566
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
686
1567
|
}
|
|
687
1568
|
async subscribe(handler) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1569
|
+
if (this.activeListener) {
|
|
1570
|
+
throw new Error("RedisInvalidationBus already has an active subscription.");
|
|
1571
|
+
}
|
|
1572
|
+
const listener = (_channel, payload) => {
|
|
1573
|
+
void this.handleMessage(payload, handler);
|
|
691
1574
|
};
|
|
1575
|
+
this.activeListener = listener;
|
|
692
1576
|
this.subscriber.on("message", listener);
|
|
693
1577
|
await this.subscriber.subscribe(this.channel);
|
|
694
1578
|
return async () => {
|
|
1579
|
+
if (this.activeListener !== listener) {
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
this.activeListener = void 0;
|
|
695
1583
|
this.subscriber.off("message", listener);
|
|
696
1584
|
await this.subscriber.unsubscribe(this.channel);
|
|
697
1585
|
};
|
|
@@ -699,6 +1587,37 @@ var RedisInvalidationBus = class {
|
|
|
699
1587
|
async publish(message) {
|
|
700
1588
|
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
701
1589
|
}
|
|
1590
|
+
async handleMessage(payload, handler) {
|
|
1591
|
+
let message;
|
|
1592
|
+
try {
|
|
1593
|
+
const parsed = JSON.parse(payload);
|
|
1594
|
+
if (!this.isInvalidationMessage(parsed)) {
|
|
1595
|
+
throw new Error("Invalid invalidation payload shape.");
|
|
1596
|
+
}
|
|
1597
|
+
message = parsed;
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
this.reportError("invalid invalidation payload", error);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
try {
|
|
1603
|
+
await handler(message);
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
this.reportError("invalidation handler failed", error);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
isInvalidationMessage(value) {
|
|
1609
|
+
if (!value || typeof value !== "object") {
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
const candidate = value;
|
|
1613
|
+
const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
|
|
1614
|
+
const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "clear";
|
|
1615
|
+
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
1616
|
+
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
1617
|
+
}
|
|
1618
|
+
reportError(message, error) {
|
|
1619
|
+
console.error(`[layercache] ${message}`, error);
|
|
1620
|
+
}
|
|
702
1621
|
};
|
|
703
1622
|
|
|
704
1623
|
// src/invalidation/RedisTagIndex.ts
|
|
@@ -791,17 +1710,97 @@ var RedisTagIndex = class {
|
|
|
791
1710
|
}
|
|
792
1711
|
};
|
|
793
1712
|
|
|
1713
|
+
// src/http/createCacheStatsHandler.ts
|
|
1714
|
+
function createCacheStatsHandler(cache) {
|
|
1715
|
+
return async (_request, response) => {
|
|
1716
|
+
response.statusCode = 200;
|
|
1717
|
+
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
1718
|
+
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// src/decorators/createCachedMethodDecorator.ts
|
|
1723
|
+
function createCachedMethodDecorator(options) {
|
|
1724
|
+
const wrappedByInstance = /* @__PURE__ */ new WeakMap();
|
|
1725
|
+
return ((_, propertyKey, descriptor) => {
|
|
1726
|
+
const original = descriptor.value;
|
|
1727
|
+
if (typeof original !== "function") {
|
|
1728
|
+
throw new Error("createCachedMethodDecorator can only be applied to methods.");
|
|
1729
|
+
}
|
|
1730
|
+
descriptor.value = async function(...args) {
|
|
1731
|
+
const instance = this;
|
|
1732
|
+
let wrapped = wrappedByInstance.get(instance);
|
|
1733
|
+
if (!wrapped) {
|
|
1734
|
+
const cache = options.cache(instance);
|
|
1735
|
+
wrapped = cache.wrap(
|
|
1736
|
+
options.prefix ?? String(propertyKey),
|
|
1737
|
+
(...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
|
|
1738
|
+
options
|
|
1739
|
+
);
|
|
1740
|
+
wrappedByInstance.set(instance, wrapped);
|
|
1741
|
+
}
|
|
1742
|
+
return wrapped(...args);
|
|
1743
|
+
};
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/integrations/fastify.ts
|
|
1748
|
+
function createFastifyLayercachePlugin(cache, options = {}) {
|
|
1749
|
+
return async (fastify) => {
|
|
1750
|
+
fastify.decorate("cache", cache);
|
|
1751
|
+
if (options.exposeStatsRoute !== false && fastify.get) {
|
|
1752
|
+
fastify.get(options.statsPath ?? "/cache/stats", async () => cache.getStats());
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// src/integrations/graphql.ts
|
|
1758
|
+
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
1759
|
+
const wrapped = cache.wrap(prefix, resolver, {
|
|
1760
|
+
...options,
|
|
1761
|
+
keyResolver: options.keyResolver
|
|
1762
|
+
});
|
|
1763
|
+
return (...args) => wrapped(...args);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// src/integrations/trpc.ts
|
|
1767
|
+
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1768
|
+
return async (context) => {
|
|
1769
|
+
const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
|
|
1770
|
+
let didFetch = false;
|
|
1771
|
+
let fetchedResult = null;
|
|
1772
|
+
const cached = await cache.get(
|
|
1773
|
+
key,
|
|
1774
|
+
async () => {
|
|
1775
|
+
didFetch = true;
|
|
1776
|
+
fetchedResult = await context.next();
|
|
1777
|
+
return fetchedResult;
|
|
1778
|
+
},
|
|
1779
|
+
options
|
|
1780
|
+
);
|
|
1781
|
+
if (cached !== null) {
|
|
1782
|
+
return cached;
|
|
1783
|
+
}
|
|
1784
|
+
if (didFetch) {
|
|
1785
|
+
return fetchedResult;
|
|
1786
|
+
}
|
|
1787
|
+
return context.next();
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
794
1791
|
// src/layers/MemoryLayer.ts
|
|
795
1792
|
var MemoryLayer = class {
|
|
796
1793
|
name;
|
|
797
1794
|
defaultTtl;
|
|
798
1795
|
isLocal = true;
|
|
799
1796
|
maxSize;
|
|
1797
|
+
evictionPolicy;
|
|
800
1798
|
entries = /* @__PURE__ */ new Map();
|
|
801
1799
|
constructor(options = {}) {
|
|
802
1800
|
this.name = options.name ?? "memory";
|
|
803
1801
|
this.defaultTtl = options.ttl;
|
|
804
1802
|
this.maxSize = options.maxSize ?? 1e3;
|
|
1803
|
+
this.evictionPolicy = options.evictionPolicy ?? "lru";
|
|
805
1804
|
}
|
|
806
1805
|
async get(key) {
|
|
807
1806
|
const value = await this.getEntry(key);
|
|
@@ -816,8 +1815,13 @@ var MemoryLayer = class {
|
|
|
816
1815
|
this.entries.delete(key);
|
|
817
1816
|
return null;
|
|
818
1817
|
}
|
|
819
|
-
this.
|
|
820
|
-
|
|
1818
|
+
if (this.evictionPolicy === "lru") {
|
|
1819
|
+
this.entries.delete(key);
|
|
1820
|
+
entry.frequency += 1;
|
|
1821
|
+
this.entries.set(key, entry);
|
|
1822
|
+
} else {
|
|
1823
|
+
entry.frequency += 1;
|
|
1824
|
+
}
|
|
821
1825
|
return entry.value;
|
|
822
1826
|
}
|
|
823
1827
|
async getMany(keys) {
|
|
@@ -831,15 +1835,42 @@ var MemoryLayer = class {
|
|
|
831
1835
|
this.entries.delete(key);
|
|
832
1836
|
this.entries.set(key, {
|
|
833
1837
|
value,
|
|
834
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
1838
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
|
|
1839
|
+
frequency: 0,
|
|
1840
|
+
insertedAt: Date.now()
|
|
835
1841
|
});
|
|
836
1842
|
while (this.entries.size > this.maxSize) {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1843
|
+
this.evict();
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
async has(key) {
|
|
1847
|
+
const entry = this.entries.get(key);
|
|
1848
|
+
if (!entry) {
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
if (this.isExpired(entry)) {
|
|
1852
|
+
this.entries.delete(key);
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
return true;
|
|
1856
|
+
}
|
|
1857
|
+
async ttl(key) {
|
|
1858
|
+
const entry = this.entries.get(key);
|
|
1859
|
+
if (!entry) {
|
|
1860
|
+
return null;
|
|
842
1861
|
}
|
|
1862
|
+
if (this.isExpired(entry)) {
|
|
1863
|
+
this.entries.delete(key);
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
if (entry.expiresAt === null) {
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
|
|
1870
|
+
}
|
|
1871
|
+
async size() {
|
|
1872
|
+
this.pruneExpired();
|
|
1873
|
+
return this.entries.size;
|
|
843
1874
|
}
|
|
844
1875
|
async delete(key) {
|
|
845
1876
|
this.entries.delete(key);
|
|
@@ -856,6 +1887,52 @@ var MemoryLayer = class {
|
|
|
856
1887
|
this.pruneExpired();
|
|
857
1888
|
return [...this.entries.keys()];
|
|
858
1889
|
}
|
|
1890
|
+
exportState() {
|
|
1891
|
+
this.pruneExpired();
|
|
1892
|
+
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
1893
|
+
key,
|
|
1894
|
+
value: entry.value,
|
|
1895
|
+
expiresAt: entry.expiresAt
|
|
1896
|
+
}));
|
|
1897
|
+
}
|
|
1898
|
+
importState(entries) {
|
|
1899
|
+
for (const entry of entries) {
|
|
1900
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
this.entries.set(entry.key, {
|
|
1904
|
+
value: entry.value,
|
|
1905
|
+
expiresAt: entry.expiresAt,
|
|
1906
|
+
frequency: 0,
|
|
1907
|
+
insertedAt: Date.now()
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
while (this.entries.size > this.maxSize) {
|
|
1911
|
+
this.evict();
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
evict() {
|
|
1915
|
+
if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
|
|
1916
|
+
const oldestKey = this.entries.keys().next().value;
|
|
1917
|
+
if (oldestKey !== void 0) {
|
|
1918
|
+
this.entries.delete(oldestKey);
|
|
1919
|
+
}
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
let victimKey;
|
|
1923
|
+
let minFreq = Number.POSITIVE_INFINITY;
|
|
1924
|
+
let minInsertedAt = Number.POSITIVE_INFINITY;
|
|
1925
|
+
for (const [key, entry] of this.entries.entries()) {
|
|
1926
|
+
if (entry.frequency < minFreq || entry.frequency === minFreq && entry.insertedAt < minInsertedAt) {
|
|
1927
|
+
minFreq = entry.frequency;
|
|
1928
|
+
minInsertedAt = entry.insertedAt;
|
|
1929
|
+
victimKey = key;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (victimKey !== void 0) {
|
|
1933
|
+
this.entries.delete(victimKey);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
859
1936
|
pruneExpired() {
|
|
860
1937
|
for (const [key, entry] of this.entries.entries()) {
|
|
861
1938
|
if (this.isExpired(entry)) {
|
|
@@ -868,6 +1945,9 @@ var MemoryLayer = class {
|
|
|
868
1945
|
}
|
|
869
1946
|
};
|
|
870
1947
|
|
|
1948
|
+
// src/layers/RedisLayer.ts
|
|
1949
|
+
var import_node_zlib = require("zlib");
|
|
1950
|
+
|
|
871
1951
|
// src/serialization/JsonSerializer.ts
|
|
872
1952
|
var JsonSerializer = class {
|
|
873
1953
|
serialize(value) {
|
|
@@ -880,6 +1960,7 @@ var JsonSerializer = class {
|
|
|
880
1960
|
};
|
|
881
1961
|
|
|
882
1962
|
// src/layers/RedisLayer.ts
|
|
1963
|
+
var BATCH_DELETE_SIZE = 500;
|
|
883
1964
|
var RedisLayer = class {
|
|
884
1965
|
name;
|
|
885
1966
|
defaultTtl;
|
|
@@ -889,6 +1970,8 @@ var RedisLayer = class {
|
|
|
889
1970
|
prefix;
|
|
890
1971
|
allowUnprefixedClear;
|
|
891
1972
|
scanCount;
|
|
1973
|
+
compression;
|
|
1974
|
+
compressionThreshold;
|
|
892
1975
|
constructor(options) {
|
|
893
1976
|
this.client = options.client;
|
|
894
1977
|
this.defaultTtl = options.ttl;
|
|
@@ -897,6 +1980,8 @@ var RedisLayer = class {
|
|
|
897
1980
|
this.prefix = options.prefix ?? "";
|
|
898
1981
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
899
1982
|
this.scanCount = options.scanCount ?? 100;
|
|
1983
|
+
this.compression = options.compression;
|
|
1984
|
+
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
900
1985
|
}
|
|
901
1986
|
async get(key) {
|
|
902
1987
|
const payload = await this.getEntry(key);
|
|
@@ -907,7 +1992,7 @@ var RedisLayer = class {
|
|
|
907
1992
|
if (payload === null) {
|
|
908
1993
|
return null;
|
|
909
1994
|
}
|
|
910
|
-
return this.
|
|
1995
|
+
return this.deserializeOrDelete(key, payload);
|
|
911
1996
|
}
|
|
912
1997
|
async getMany(keys) {
|
|
913
1998
|
if (keys.length === 0) {
|
|
@@ -921,16 +2006,18 @@ var RedisLayer = class {
|
|
|
921
2006
|
if (results === null) {
|
|
922
2007
|
return keys.map(() => null);
|
|
923
2008
|
}
|
|
924
|
-
return
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
2009
|
+
return Promise.all(
|
|
2010
|
+
results.map(async (result, index) => {
|
|
2011
|
+
const [error, payload] = result;
|
|
2012
|
+
if (error || payload === null || !this.isSerializablePayload(payload)) {
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
return this.deserializeOrDelete(keys[index] ?? "", payload);
|
|
2016
|
+
})
|
|
2017
|
+
);
|
|
931
2018
|
}
|
|
932
2019
|
async set(key, value, ttl = this.defaultTtl) {
|
|
933
|
-
const payload = this.serializer.serialize(value);
|
|
2020
|
+
const payload = this.encodePayload(this.serializer.serialize(value));
|
|
934
2021
|
const normalizedKey = this.withPrefix(key);
|
|
935
2022
|
if (ttl && ttl > 0) {
|
|
936
2023
|
await this.client.set(normalizedKey, payload, "EX", ttl);
|
|
@@ -947,14 +2034,44 @@ var RedisLayer = class {
|
|
|
947
2034
|
}
|
|
948
2035
|
await this.client.del(...keys.map((key) => this.withPrefix(key)));
|
|
949
2036
|
}
|
|
950
|
-
async
|
|
951
|
-
|
|
952
|
-
|
|
2037
|
+
async has(key) {
|
|
2038
|
+
const exists = await this.client.exists(this.withPrefix(key));
|
|
2039
|
+
return exists > 0;
|
|
2040
|
+
}
|
|
2041
|
+
async ttl(key) {
|
|
2042
|
+
const remaining = await this.client.ttl(this.withPrefix(key));
|
|
2043
|
+
if (remaining < 0) {
|
|
2044
|
+
return null;
|
|
953
2045
|
}
|
|
2046
|
+
return remaining;
|
|
2047
|
+
}
|
|
2048
|
+
async size() {
|
|
954
2049
|
const keys = await this.keys();
|
|
955
|
-
|
|
956
|
-
|
|
2050
|
+
return keys.length;
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Deletes all keys matching the layer's prefix in batches to avoid
|
|
2054
|
+
* loading millions of keys into memory at once.
|
|
2055
|
+
*/
|
|
2056
|
+
async clear() {
|
|
2057
|
+
if (!this.prefix && !this.allowUnprefixedClear) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
"RedisLayer.clear() requires a prefix or allowUnprefixedClear=true to avoid deleting unrelated keys."
|
|
2060
|
+
);
|
|
957
2061
|
}
|
|
2062
|
+
const pattern = `${this.prefix}*`;
|
|
2063
|
+
let cursor = "0";
|
|
2064
|
+
do {
|
|
2065
|
+
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
2066
|
+
cursor = nextCursor;
|
|
2067
|
+
if (keys.length === 0) {
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
2071
|
+
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
2072
|
+
await this.client.del(...batch);
|
|
2073
|
+
}
|
|
2074
|
+
} while (cursor !== "0");
|
|
958
2075
|
}
|
|
959
2076
|
async keys() {
|
|
960
2077
|
const pattern = `${this.prefix}*`;
|
|
@@ -977,6 +2094,205 @@ var RedisLayer = class {
|
|
|
977
2094
|
withPrefix(key) {
|
|
978
2095
|
return `${this.prefix}${key}`;
|
|
979
2096
|
}
|
|
2097
|
+
async deserializeOrDelete(key, payload) {
|
|
2098
|
+
try {
|
|
2099
|
+
return this.serializer.deserialize(this.decodePayload(payload));
|
|
2100
|
+
} catch {
|
|
2101
|
+
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
2102
|
+
return null;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
isSerializablePayload(payload) {
|
|
2106
|
+
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
2107
|
+
}
|
|
2108
|
+
encodePayload(payload) {
|
|
2109
|
+
if (!this.compression) {
|
|
2110
|
+
return payload;
|
|
2111
|
+
}
|
|
2112
|
+
const source = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
2113
|
+
if (source.byteLength < this.compressionThreshold) {
|
|
2114
|
+
return payload;
|
|
2115
|
+
}
|
|
2116
|
+
const header = Buffer.from(`LCZ1:${this.compression}:`);
|
|
2117
|
+
const compressed = this.compression === "gzip" ? (0, import_node_zlib.gzipSync)(source) : (0, import_node_zlib.brotliCompressSync)(source);
|
|
2118
|
+
return Buffer.concat([header, compressed]);
|
|
2119
|
+
}
|
|
2120
|
+
decodePayload(payload) {
|
|
2121
|
+
if (!Buffer.isBuffer(payload)) {
|
|
2122
|
+
return payload;
|
|
2123
|
+
}
|
|
2124
|
+
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
2125
|
+
return (0, import_node_zlib.gunzipSync)(payload.subarray(10));
|
|
2126
|
+
}
|
|
2127
|
+
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
2128
|
+
return (0, import_node_zlib.brotliDecompressSync)(payload.subarray(12));
|
|
2129
|
+
}
|
|
2130
|
+
return payload;
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
|
|
2134
|
+
// src/layers/DiskLayer.ts
|
|
2135
|
+
var import_node_crypto2 = require("crypto");
|
|
2136
|
+
var import_node_fs2 = require("fs");
|
|
2137
|
+
var import_node_path = require("path");
|
|
2138
|
+
var DiskLayer = class {
|
|
2139
|
+
name;
|
|
2140
|
+
defaultTtl;
|
|
2141
|
+
isLocal = true;
|
|
2142
|
+
directory;
|
|
2143
|
+
serializer;
|
|
2144
|
+
constructor(options) {
|
|
2145
|
+
this.directory = options.directory;
|
|
2146
|
+
this.defaultTtl = options.ttl;
|
|
2147
|
+
this.name = options.name ?? "disk";
|
|
2148
|
+
this.serializer = options.serializer ?? new JsonSerializer();
|
|
2149
|
+
}
|
|
2150
|
+
async get(key) {
|
|
2151
|
+
return unwrapStoredValue(await this.getEntry(key));
|
|
2152
|
+
}
|
|
2153
|
+
async getEntry(key) {
|
|
2154
|
+
const filePath = this.keyToPath(key);
|
|
2155
|
+
let raw;
|
|
2156
|
+
try {
|
|
2157
|
+
raw = await import_node_fs2.promises.readFile(filePath);
|
|
2158
|
+
} catch {
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
let entry;
|
|
2162
|
+
try {
|
|
2163
|
+
entry = this.serializer.deserialize(raw);
|
|
2164
|
+
} catch {
|
|
2165
|
+
await this.safeDelete(filePath);
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
2169
|
+
await this.safeDelete(filePath);
|
|
2170
|
+
return null;
|
|
2171
|
+
}
|
|
2172
|
+
return entry.value;
|
|
2173
|
+
}
|
|
2174
|
+
async set(key, value, ttl = this.defaultTtl) {
|
|
2175
|
+
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
2176
|
+
const entry = {
|
|
2177
|
+
value,
|
|
2178
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
2179
|
+
};
|
|
2180
|
+
const payload = this.serializer.serialize(entry);
|
|
2181
|
+
await import_node_fs2.promises.writeFile(this.keyToPath(key), payload);
|
|
2182
|
+
}
|
|
2183
|
+
async has(key) {
|
|
2184
|
+
const value = await this.getEntry(key);
|
|
2185
|
+
return value !== null;
|
|
2186
|
+
}
|
|
2187
|
+
async ttl(key) {
|
|
2188
|
+
const filePath = this.keyToPath(key);
|
|
2189
|
+
let raw;
|
|
2190
|
+
try {
|
|
2191
|
+
raw = await import_node_fs2.promises.readFile(filePath);
|
|
2192
|
+
} catch {
|
|
2193
|
+
return null;
|
|
2194
|
+
}
|
|
2195
|
+
let entry;
|
|
2196
|
+
try {
|
|
2197
|
+
entry = this.serializer.deserialize(raw);
|
|
2198
|
+
} catch {
|
|
2199
|
+
return null;
|
|
2200
|
+
}
|
|
2201
|
+
if (entry.expiresAt === null) {
|
|
2202
|
+
return null;
|
|
2203
|
+
}
|
|
2204
|
+
const remaining = Math.ceil((entry.expiresAt - Date.now()) / 1e3);
|
|
2205
|
+
if (remaining <= 0) {
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
return remaining;
|
|
2209
|
+
}
|
|
2210
|
+
async delete(key) {
|
|
2211
|
+
await this.safeDelete(this.keyToPath(key));
|
|
2212
|
+
}
|
|
2213
|
+
async deleteMany(keys) {
|
|
2214
|
+
await Promise.all(keys.map((key) => this.delete(key)));
|
|
2215
|
+
}
|
|
2216
|
+
async clear() {
|
|
2217
|
+
let entries;
|
|
2218
|
+
try {
|
|
2219
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
2220
|
+
} catch {
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
await Promise.all(
|
|
2224
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete((0, import_node_path.join)(this.directory, name)))
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
async keys() {
|
|
2228
|
+
let entries;
|
|
2229
|
+
try {
|
|
2230
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
2231
|
+
} catch {
|
|
2232
|
+
return [];
|
|
2233
|
+
}
|
|
2234
|
+
return entries.filter((name) => name.endsWith(".lc")).map((name) => name.slice(0, -3));
|
|
2235
|
+
}
|
|
2236
|
+
async size() {
|
|
2237
|
+
const keys = await this.keys();
|
|
2238
|
+
return keys.length;
|
|
2239
|
+
}
|
|
2240
|
+
keyToPath(key) {
|
|
2241
|
+
const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
|
|
2242
|
+
return (0, import_node_path.join)(this.directory, `${hash}.lc`);
|
|
2243
|
+
}
|
|
2244
|
+
async safeDelete(filePath) {
|
|
2245
|
+
try {
|
|
2246
|
+
await import_node_fs2.promises.unlink(filePath);
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
// src/layers/MemcachedLayer.ts
|
|
2253
|
+
var MemcachedLayer = class {
|
|
2254
|
+
name;
|
|
2255
|
+
defaultTtl;
|
|
2256
|
+
isLocal = false;
|
|
2257
|
+
client;
|
|
2258
|
+
keyPrefix;
|
|
2259
|
+
constructor(options) {
|
|
2260
|
+
this.client = options.client;
|
|
2261
|
+
this.defaultTtl = options.ttl;
|
|
2262
|
+
this.name = options.name ?? "memcached";
|
|
2263
|
+
this.keyPrefix = options.keyPrefix ?? "";
|
|
2264
|
+
}
|
|
2265
|
+
async get(key) {
|
|
2266
|
+
const result = await this.client.get(this.withPrefix(key));
|
|
2267
|
+
if (!result || result.value === null) {
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
try {
|
|
2271
|
+
return JSON.parse(result.value.toString("utf8"));
|
|
2272
|
+
} catch {
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async set(key, value, ttl = this.defaultTtl) {
|
|
2277
|
+
const payload = JSON.stringify(value);
|
|
2278
|
+
await this.client.set(this.withPrefix(key), payload, {
|
|
2279
|
+
expires: ttl && ttl > 0 ? ttl : void 0
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
async delete(key) {
|
|
2283
|
+
await this.client.delete(this.withPrefix(key));
|
|
2284
|
+
}
|
|
2285
|
+
async deleteMany(keys) {
|
|
2286
|
+
await Promise.all(keys.map((key) => this.delete(key)));
|
|
2287
|
+
}
|
|
2288
|
+
async clear() {
|
|
2289
|
+
throw new Error(
|
|
2290
|
+
"MemcachedLayer.clear() is not supported. Use a key prefix and rotate it to effectively invalidate all keys."
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
withPrefix(key) {
|
|
2294
|
+
return `${this.keyPrefix}${key}`;
|
|
2295
|
+
}
|
|
980
2296
|
};
|
|
981
2297
|
|
|
982
2298
|
// src/serialization/MsgpackSerializer.ts
|
|
@@ -992,7 +2308,7 @@ var MsgpackSerializer = class {
|
|
|
992
2308
|
};
|
|
993
2309
|
|
|
994
2310
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
995
|
-
var
|
|
2311
|
+
var import_node_crypto3 = require("crypto");
|
|
996
2312
|
var RELEASE_SCRIPT = `
|
|
997
2313
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
998
2314
|
return redis.call("del", KEYS[1])
|
|
@@ -1008,7 +2324,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
1008
2324
|
}
|
|
1009
2325
|
async execute(key, options, worker, waiter) {
|
|
1010
2326
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
1011
|
-
const token = (0,
|
|
2327
|
+
const token = (0, import_node_crypto3.randomUUID)();
|
|
1012
2328
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
1013
2329
|
if (acquired === "OK") {
|
|
1014
2330
|
try {
|
|
@@ -1020,10 +2336,80 @@ var RedisSingleFlightCoordinator = class {
|
|
|
1020
2336
|
return waiter();
|
|
1021
2337
|
}
|
|
1022
2338
|
};
|
|
2339
|
+
|
|
2340
|
+
// src/metrics/PrometheusExporter.ts
|
|
2341
|
+
function createPrometheusMetricsExporter(stacks) {
|
|
2342
|
+
return () => {
|
|
2343
|
+
const entries = Array.isArray(stacks) ? stacks : [{ stack: stacks, name: "default" }];
|
|
2344
|
+
const lines = [];
|
|
2345
|
+
lines.push("# HELP layercache_hits_total Total number of cache hits");
|
|
2346
|
+
lines.push("# TYPE layercache_hits_total counter");
|
|
2347
|
+
lines.push("# HELP layercache_misses_total Total number of cache misses");
|
|
2348
|
+
lines.push("# TYPE layercache_misses_total counter");
|
|
2349
|
+
lines.push("# HELP layercache_fetches_total Total fetcher invocations (full misses)");
|
|
2350
|
+
lines.push("# TYPE layercache_fetches_total counter");
|
|
2351
|
+
lines.push("# HELP layercache_sets_total Total number of cache sets");
|
|
2352
|
+
lines.push("# TYPE layercache_sets_total counter");
|
|
2353
|
+
lines.push("# HELP layercache_deletes_total Total number of cache deletes");
|
|
2354
|
+
lines.push("# TYPE layercache_deletes_total counter");
|
|
2355
|
+
lines.push("# HELP layercache_backfills_total Total number of backfill operations");
|
|
2356
|
+
lines.push("# TYPE layercache_backfills_total counter");
|
|
2357
|
+
lines.push("# HELP layercache_stale_hits_total Total number of stale hits served");
|
|
2358
|
+
lines.push("# TYPE layercache_stale_hits_total counter");
|
|
2359
|
+
lines.push("# HELP layercache_refreshes_total Background refreshes triggered");
|
|
2360
|
+
lines.push("# TYPE layercache_refreshes_total counter");
|
|
2361
|
+
lines.push("# HELP layercache_refresh_errors_total Background refresh errors");
|
|
2362
|
+
lines.push("# TYPE layercache_refresh_errors_total counter");
|
|
2363
|
+
lines.push("# HELP layercache_negative_cache_hits_total Negative cache hits");
|
|
2364
|
+
lines.push("# TYPE layercache_negative_cache_hits_total counter");
|
|
2365
|
+
lines.push("# HELP layercache_circuit_breaker_trips_total Circuit breaker trips");
|
|
2366
|
+
lines.push("# TYPE layercache_circuit_breaker_trips_total counter");
|
|
2367
|
+
lines.push("# HELP layercache_degraded_operations_total Operations run in degraded mode");
|
|
2368
|
+
lines.push("# TYPE layercache_degraded_operations_total counter");
|
|
2369
|
+
lines.push("# HELP layercache_hit_rate Overall cache hit rate (0-1)");
|
|
2370
|
+
lines.push("# TYPE layercache_hit_rate gauge");
|
|
2371
|
+
lines.push("# HELP layercache_hits_by_layer_total Hits broken down by layer");
|
|
2372
|
+
lines.push("# TYPE layercache_hits_by_layer_total counter");
|
|
2373
|
+
lines.push("# HELP layercache_misses_by_layer_total Misses broken down by layer");
|
|
2374
|
+
lines.push("# TYPE layercache_misses_by_layer_total counter");
|
|
2375
|
+
for (const { stack, name } of entries) {
|
|
2376
|
+
const m = stack.getMetrics();
|
|
2377
|
+
const hr = stack.getHitRate();
|
|
2378
|
+
const label = `cache="${sanitizeLabel(name)}"`;
|
|
2379
|
+
lines.push(`layercache_hits_total{${label}} ${m.hits}`);
|
|
2380
|
+
lines.push(`layercache_misses_total{${label}} ${m.misses}`);
|
|
2381
|
+
lines.push(`layercache_fetches_total{${label}} ${m.fetches}`);
|
|
2382
|
+
lines.push(`layercache_sets_total{${label}} ${m.sets}`);
|
|
2383
|
+
lines.push(`layercache_deletes_total{${label}} ${m.deletes}`);
|
|
2384
|
+
lines.push(`layercache_backfills_total{${label}} ${m.backfills}`);
|
|
2385
|
+
lines.push(`layercache_stale_hits_total{${label}} ${m.staleHits}`);
|
|
2386
|
+
lines.push(`layercache_refreshes_total{${label}} ${m.refreshes}`);
|
|
2387
|
+
lines.push(`layercache_refresh_errors_total{${label}} ${m.refreshErrors}`);
|
|
2388
|
+
lines.push(`layercache_negative_cache_hits_total{${label}} ${m.negativeCacheHits}`);
|
|
2389
|
+
lines.push(`layercache_circuit_breaker_trips_total{${label}} ${m.circuitBreakerTrips}`);
|
|
2390
|
+
lines.push(`layercache_degraded_operations_total{${label}} ${m.degradedOperations}`);
|
|
2391
|
+
lines.push(`layercache_hit_rate{${label}} ${hr.overall.toFixed(6)}`);
|
|
2392
|
+
for (const [layerName, count] of Object.entries(m.hitsByLayer)) {
|
|
2393
|
+
lines.push(`layercache_hits_by_layer_total{${label},layer="${sanitizeLabel(layerName)}"} ${count}`);
|
|
2394
|
+
}
|
|
2395
|
+
for (const [layerName, count] of Object.entries(m.missesByLayer)) {
|
|
2396
|
+
lines.push(`layercache_misses_by_layer_total{${label},layer="${sanitizeLabel(layerName)}"} ${count}`);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
lines.push("");
|
|
2400
|
+
return lines.join("\n");
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
function sanitizeLabel(value) {
|
|
2404
|
+
return value.replace(/["\\\n]/g, "_");
|
|
2405
|
+
}
|
|
1023
2406
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1024
2407
|
0 && (module.exports = {
|
|
2408
|
+
CacheNamespace,
|
|
1025
2409
|
CacheStack,
|
|
2410
|
+
DiskLayer,
|
|
1026
2411
|
JsonSerializer,
|
|
2412
|
+
MemcachedLayer,
|
|
1027
2413
|
MemoryLayer,
|
|
1028
2414
|
MsgpackSerializer,
|
|
1029
2415
|
PatternMatcher,
|
|
@@ -1032,5 +2418,11 @@ var RedisSingleFlightCoordinator = class {
|
|
|
1032
2418
|
RedisSingleFlightCoordinator,
|
|
1033
2419
|
RedisTagIndex,
|
|
1034
2420
|
StampedeGuard,
|
|
1035
|
-
TagIndex
|
|
2421
|
+
TagIndex,
|
|
2422
|
+
cacheGraphqlResolver,
|
|
2423
|
+
createCacheStatsHandler,
|
|
2424
|
+
createCachedMethodDecorator,
|
|
2425
|
+
createFastifyLayercachePlugin,
|
|
2426
|
+
createPrometheusMetricsExporter,
|
|
2427
|
+
createTrpcCacheMiddleware
|
|
1036
2428
|
});
|