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.js
CHANGED
|
@@ -1,5 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PatternMatcher,
|
|
3
|
+
RedisTagIndex
|
|
4
|
+
} from "./chunk-QUB5VZFZ.js";
|
|
5
|
+
|
|
1
6
|
// src/CacheStack.ts
|
|
2
7
|
import { randomUUID } from "crypto";
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import { promises as fs } from "fs";
|
|
10
|
+
|
|
11
|
+
// src/CacheNamespace.ts
|
|
12
|
+
var CacheNamespace = class {
|
|
13
|
+
constructor(cache, prefix) {
|
|
14
|
+
this.cache = cache;
|
|
15
|
+
this.prefix = prefix;
|
|
16
|
+
}
|
|
17
|
+
cache;
|
|
18
|
+
prefix;
|
|
19
|
+
async get(key, fetcher, options) {
|
|
20
|
+
return this.cache.get(this.qualify(key), fetcher, options);
|
|
21
|
+
}
|
|
22
|
+
async getOrSet(key, fetcher, options) {
|
|
23
|
+
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
24
|
+
}
|
|
25
|
+
async has(key) {
|
|
26
|
+
return this.cache.has(this.qualify(key));
|
|
27
|
+
}
|
|
28
|
+
async ttl(key) {
|
|
29
|
+
return this.cache.ttl(this.qualify(key));
|
|
30
|
+
}
|
|
31
|
+
async set(key, value, options) {
|
|
32
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
33
|
+
}
|
|
34
|
+
async delete(key) {
|
|
35
|
+
await this.cache.delete(this.qualify(key));
|
|
36
|
+
}
|
|
37
|
+
async mdelete(keys) {
|
|
38
|
+
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
39
|
+
}
|
|
40
|
+
async clear() {
|
|
41
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
42
|
+
}
|
|
43
|
+
async mget(entries) {
|
|
44
|
+
return this.cache.mget(
|
|
45
|
+
entries.map((entry) => ({
|
|
46
|
+
...entry,
|
|
47
|
+
key: this.qualify(entry.key)
|
|
48
|
+
}))
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
async mset(entries) {
|
|
52
|
+
await this.cache.mset(
|
|
53
|
+
entries.map((entry) => ({
|
|
54
|
+
...entry,
|
|
55
|
+
key: this.qualify(entry.key)
|
|
56
|
+
}))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
async invalidateByTag(tag) {
|
|
60
|
+
await this.cache.invalidateByTag(tag);
|
|
61
|
+
}
|
|
62
|
+
async invalidateByPattern(pattern) {
|
|
63
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
64
|
+
}
|
|
65
|
+
wrap(keyPrefix, fetcher, options) {
|
|
66
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
67
|
+
}
|
|
68
|
+
warm(entries, options) {
|
|
69
|
+
return this.cache.warm(
|
|
70
|
+
entries.map((entry) => ({
|
|
71
|
+
...entry,
|
|
72
|
+
key: this.qualify(entry.key)
|
|
73
|
+
})),
|
|
74
|
+
options
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
getMetrics() {
|
|
78
|
+
return this.cache.getMetrics();
|
|
79
|
+
}
|
|
80
|
+
getHitRate() {
|
|
81
|
+
return this.cache.getHitRate();
|
|
82
|
+
}
|
|
83
|
+
qualify(key) {
|
|
84
|
+
return `${this.prefix}:${key}`;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/internal/CircuitBreakerManager.ts
|
|
89
|
+
var CircuitBreakerManager = class {
|
|
90
|
+
breakers = /* @__PURE__ */ new Map();
|
|
91
|
+
maxEntries;
|
|
92
|
+
constructor(options) {
|
|
93
|
+
this.maxEntries = options.maxEntries;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Throws if the circuit is open for the given key.
|
|
97
|
+
* Automatically resets if the cooldown has elapsed.
|
|
98
|
+
*/
|
|
99
|
+
assertClosed(key, options) {
|
|
100
|
+
const state = this.breakers.get(key);
|
|
101
|
+
if (!state?.openUntil) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
if (state.openUntil <= now) {
|
|
106
|
+
state.openUntil = null;
|
|
107
|
+
state.failures = 0;
|
|
108
|
+
this.breakers.set(key, state);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const remainingMs = state.openUntil - now;
|
|
112
|
+
const remainingSecs = Math.ceil(remainingMs / 1e3);
|
|
113
|
+
throw new Error(`Circuit breaker is open for key "${key}" (resets in ${remainingSecs}s).`);
|
|
114
|
+
}
|
|
115
|
+
recordFailure(key, options) {
|
|
116
|
+
if (!options) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
120
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
121
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
122
|
+
state.failures += 1;
|
|
123
|
+
if (state.failures >= failureThreshold) {
|
|
124
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
125
|
+
}
|
|
126
|
+
this.breakers.set(key, state);
|
|
127
|
+
this.pruneIfNeeded();
|
|
128
|
+
}
|
|
129
|
+
recordSuccess(key) {
|
|
130
|
+
this.breakers.delete(key);
|
|
131
|
+
}
|
|
132
|
+
isOpen(key) {
|
|
133
|
+
const state = this.breakers.get(key);
|
|
134
|
+
if (!state?.openUntil) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (state.openUntil <= Date.now()) {
|
|
138
|
+
state.openUntil = null;
|
|
139
|
+
state.failures = 0;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
delete(key) {
|
|
145
|
+
this.breakers.delete(key);
|
|
146
|
+
}
|
|
147
|
+
clear() {
|
|
148
|
+
this.breakers.clear();
|
|
149
|
+
}
|
|
150
|
+
tripCount() {
|
|
151
|
+
let count = 0;
|
|
152
|
+
for (const state of this.breakers.values()) {
|
|
153
|
+
if (state.openUntil !== null) {
|
|
154
|
+
count += 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return count;
|
|
158
|
+
}
|
|
159
|
+
pruneIfNeeded() {
|
|
160
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const [key, state] of this.breakers.entries()) {
|
|
164
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (!state.openUntil || state.openUntil <= Date.now()) {
|
|
168
|
+
this.breakers.delete(key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const key of this.breakers.keys()) {
|
|
172
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
this.breakers.delete(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/internal/MetricsCollector.ts
|
|
181
|
+
var MetricsCollector = class {
|
|
182
|
+
data = this.empty();
|
|
183
|
+
get snapshot() {
|
|
184
|
+
return { ...this.data };
|
|
185
|
+
}
|
|
186
|
+
increment(field, amount = 1) {
|
|
187
|
+
;
|
|
188
|
+
this.data[field] += amount;
|
|
189
|
+
}
|
|
190
|
+
incrementLayer(map, layerName) {
|
|
191
|
+
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
192
|
+
}
|
|
193
|
+
reset() {
|
|
194
|
+
this.data = this.empty();
|
|
195
|
+
}
|
|
196
|
+
hitRate() {
|
|
197
|
+
const total = this.data.hits + this.data.misses;
|
|
198
|
+
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
199
|
+
const byLayer = {};
|
|
200
|
+
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
201
|
+
for (const layer of allLayers) {
|
|
202
|
+
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
203
|
+
const m = this.data.missesByLayer[layer] ?? 0;
|
|
204
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
205
|
+
}
|
|
206
|
+
return { overall, byLayer };
|
|
207
|
+
}
|
|
208
|
+
empty() {
|
|
209
|
+
return {
|
|
210
|
+
hits: 0,
|
|
211
|
+
misses: 0,
|
|
212
|
+
fetches: 0,
|
|
213
|
+
sets: 0,
|
|
214
|
+
deletes: 0,
|
|
215
|
+
backfills: 0,
|
|
216
|
+
invalidations: 0,
|
|
217
|
+
staleHits: 0,
|
|
218
|
+
refreshes: 0,
|
|
219
|
+
refreshErrors: 0,
|
|
220
|
+
writeFailures: 0,
|
|
221
|
+
singleFlightWaits: 0,
|
|
222
|
+
negativeCacheHits: 0,
|
|
223
|
+
circuitBreakerTrips: 0,
|
|
224
|
+
degradedOperations: 0,
|
|
225
|
+
hitsByLayer: {},
|
|
226
|
+
missesByLayer: {},
|
|
227
|
+
resetAt: Date.now()
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
};
|
|
3
231
|
|
|
4
232
|
// src/internal/StoredValue.ts
|
|
5
233
|
function isStoredValueEnvelope(value) {
|
|
@@ -19,7 +247,10 @@ function createStoredValueEnvelope(options) {
|
|
|
19
247
|
value: options.value,
|
|
20
248
|
freshUntil,
|
|
21
249
|
staleUntil,
|
|
22
|
-
errorUntil
|
|
250
|
+
errorUntil,
|
|
251
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
252
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
253
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
23
254
|
};
|
|
24
255
|
}
|
|
25
256
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -60,6 +291,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
60
291
|
}
|
|
61
292
|
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
62
293
|
}
|
|
294
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
295
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
const remainingMs = stored.freshUntil - now;
|
|
299
|
+
if (remainingMs <= 0) {
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
303
|
+
}
|
|
304
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
305
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
306
|
+
return stored;
|
|
307
|
+
}
|
|
308
|
+
return createStoredValueEnvelope({
|
|
309
|
+
kind: stored.kind,
|
|
310
|
+
value: stored.value,
|
|
311
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
312
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
313
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
314
|
+
now
|
|
315
|
+
});
|
|
316
|
+
}
|
|
63
317
|
function maxExpiry(stored) {
|
|
64
318
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
65
319
|
(value) => value !== null
|
|
@@ -76,12 +330,91 @@ function normalizePositiveSeconds(value) {
|
|
|
76
330
|
return value;
|
|
77
331
|
}
|
|
78
332
|
|
|
79
|
-
// src/
|
|
80
|
-
var
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
333
|
+
// src/internal/TtlResolver.ts
|
|
334
|
+
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
335
|
+
var TtlResolver = class {
|
|
336
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
337
|
+
maxProfileEntries;
|
|
338
|
+
constructor(options) {
|
|
339
|
+
this.maxProfileEntries = options.maxProfileEntries;
|
|
340
|
+
}
|
|
341
|
+
recordAccess(key) {
|
|
342
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
343
|
+
profile.hits += 1;
|
|
344
|
+
profile.lastAccessAt = Date.now();
|
|
345
|
+
this.accessProfiles.set(key, profile);
|
|
346
|
+
this.pruneIfNeeded();
|
|
347
|
+
}
|
|
348
|
+
deleteProfile(key) {
|
|
349
|
+
this.accessProfiles.delete(key);
|
|
350
|
+
}
|
|
351
|
+
clearProfiles() {
|
|
352
|
+
this.accessProfiles.clear();
|
|
353
|
+
}
|
|
354
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
355
|
+
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
356
|
+
layerName,
|
|
357
|
+
options?.negativeTtl,
|
|
358
|
+
globalNegativeTtl,
|
|
359
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
360
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
361
|
+
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
362
|
+
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
363
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
364
|
+
}
|
|
365
|
+
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
366
|
+
if (override !== void 0) {
|
|
367
|
+
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
368
|
+
}
|
|
369
|
+
if (globalDefault !== void 0) {
|
|
370
|
+
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
371
|
+
}
|
|
372
|
+
return fallback;
|
|
373
|
+
}
|
|
374
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
375
|
+
if (!ttl || !adaptiveTtl) {
|
|
376
|
+
return ttl;
|
|
377
|
+
}
|
|
378
|
+
const profile = this.accessProfiles.get(key);
|
|
379
|
+
if (!profile) {
|
|
380
|
+
return ttl;
|
|
381
|
+
}
|
|
382
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
383
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
384
|
+
if (profile.hits < hotAfter) {
|
|
385
|
+
return ttl;
|
|
386
|
+
}
|
|
387
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
388
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
389
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
390
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
391
|
+
}
|
|
392
|
+
applyJitter(ttl, jitter) {
|
|
393
|
+
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
394
|
+
return ttl;
|
|
395
|
+
}
|
|
396
|
+
const delta = (Math.random() * 2 - 1) * jitter;
|
|
397
|
+
return Math.max(1, Math.round(ttl + delta));
|
|
398
|
+
}
|
|
399
|
+
readLayerNumber(layerName, value) {
|
|
400
|
+
if (typeof value === "number") {
|
|
401
|
+
return value;
|
|
402
|
+
}
|
|
403
|
+
return value[layerName];
|
|
404
|
+
}
|
|
405
|
+
pruneIfNeeded() {
|
|
406
|
+
if (this.accessProfiles.size <= this.maxProfileEntries) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
410
|
+
let removed = 0;
|
|
411
|
+
for (const key of this.accessProfiles.keys()) {
|
|
412
|
+
if (removed >= toRemove) {
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
this.accessProfiles.delete(key);
|
|
416
|
+
removed += 1;
|
|
417
|
+
}
|
|
85
418
|
}
|
|
86
419
|
};
|
|
87
420
|
|
|
@@ -148,64 +481,75 @@ import { Mutex } from "async-mutex";
|
|
|
148
481
|
var StampedeGuard = class {
|
|
149
482
|
mutexes = /* @__PURE__ */ new Map();
|
|
150
483
|
async execute(key, task) {
|
|
151
|
-
const
|
|
484
|
+
const entry = this.getMutexEntry(key);
|
|
152
485
|
try {
|
|
153
|
-
return await mutex.runExclusive(task);
|
|
486
|
+
return await entry.mutex.runExclusive(task);
|
|
154
487
|
} finally {
|
|
155
|
-
|
|
488
|
+
entry.references -= 1;
|
|
489
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
156
490
|
this.mutexes.delete(key);
|
|
157
491
|
}
|
|
158
492
|
}
|
|
159
493
|
}
|
|
160
|
-
|
|
161
|
-
let
|
|
162
|
-
if (!
|
|
163
|
-
|
|
164
|
-
this.mutexes.set(key,
|
|
494
|
+
getMutexEntry(key) {
|
|
495
|
+
let entry = this.mutexes.get(key);
|
|
496
|
+
if (!entry) {
|
|
497
|
+
entry = { mutex: new Mutex(), references: 0 };
|
|
498
|
+
this.mutexes.set(key, entry);
|
|
165
499
|
}
|
|
166
|
-
|
|
500
|
+
entry.references += 1;
|
|
501
|
+
return entry;
|
|
167
502
|
}
|
|
168
503
|
};
|
|
169
504
|
|
|
170
505
|
// src/CacheStack.ts
|
|
171
|
-
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
172
506
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
173
507
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
174
508
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
175
|
-
var
|
|
176
|
-
|
|
177
|
-
misses: 0,
|
|
178
|
-
fetches: 0,
|
|
179
|
-
sets: 0,
|
|
180
|
-
deletes: 0,
|
|
181
|
-
backfills: 0,
|
|
182
|
-
invalidations: 0,
|
|
183
|
-
staleHits: 0,
|
|
184
|
-
refreshes: 0,
|
|
185
|
-
refreshErrors: 0,
|
|
186
|
-
writeFailures: 0,
|
|
187
|
-
singleFlightWaits: 0
|
|
188
|
-
});
|
|
509
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
510
|
+
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
189
511
|
var DebugLogger = class {
|
|
190
512
|
enabled;
|
|
191
513
|
constructor(enabled) {
|
|
192
514
|
this.enabled = enabled;
|
|
193
515
|
}
|
|
194
516
|
debug(message, context) {
|
|
517
|
+
this.write("debug", message, context);
|
|
518
|
+
}
|
|
519
|
+
info(message, context) {
|
|
520
|
+
this.write("info", message, context);
|
|
521
|
+
}
|
|
522
|
+
warn(message, context) {
|
|
523
|
+
this.write("warn", message, context);
|
|
524
|
+
}
|
|
525
|
+
error(message, context) {
|
|
526
|
+
this.write("error", message, context);
|
|
527
|
+
}
|
|
528
|
+
write(level, message, context) {
|
|
195
529
|
if (!this.enabled) {
|
|
196
530
|
return;
|
|
197
531
|
}
|
|
198
532
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
199
|
-
console
|
|
533
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
200
534
|
}
|
|
201
535
|
};
|
|
202
|
-
var CacheStack = class {
|
|
536
|
+
var CacheStack = class extends EventEmitter {
|
|
203
537
|
constructor(layers, options = {}) {
|
|
538
|
+
super();
|
|
204
539
|
this.layers = layers;
|
|
205
540
|
this.options = options;
|
|
206
541
|
if (layers.length === 0) {
|
|
207
542
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
208
543
|
}
|
|
544
|
+
this.validateConfiguration();
|
|
545
|
+
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
546
|
+
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
547
|
+
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
548
|
+
if (options.publishSetInvalidation !== void 0) {
|
|
549
|
+
console.warn(
|
|
550
|
+
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
551
|
+
);
|
|
552
|
+
}
|
|
209
553
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
210
554
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
211
555
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -214,112 +558,313 @@ var CacheStack = class {
|
|
|
214
558
|
layers;
|
|
215
559
|
options;
|
|
216
560
|
stampedeGuard = new StampedeGuard();
|
|
217
|
-
|
|
561
|
+
metricsCollector = new MetricsCollector();
|
|
218
562
|
instanceId = randomUUID();
|
|
219
563
|
startup;
|
|
220
564
|
unsubscribeInvalidation;
|
|
221
565
|
logger;
|
|
222
566
|
tagIndex;
|
|
223
567
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
568
|
+
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
569
|
+
ttlResolver;
|
|
570
|
+
circuitBreakerManager;
|
|
571
|
+
isDisconnecting = false;
|
|
572
|
+
disconnectPromise;
|
|
573
|
+
/**
|
|
574
|
+
* Read-through cache get.
|
|
575
|
+
* Returns the cached value if present and fresh, or invokes `fetcher` on a miss
|
|
576
|
+
* and stores the result across all layers. Returns `null` if the key is not found
|
|
577
|
+
* and no `fetcher` is provided.
|
|
578
|
+
*/
|
|
224
579
|
async get(key, fetcher, options) {
|
|
580
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
581
|
+
this.validateWriteOptions(options);
|
|
225
582
|
await this.startup;
|
|
226
|
-
const hit = await this.readFromLayers(
|
|
583
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
227
584
|
if (hit.found) {
|
|
585
|
+
this.ttlResolver.recordAccess(normalizedKey);
|
|
586
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
587
|
+
this.metricsCollector.increment("negativeCacheHits");
|
|
588
|
+
}
|
|
228
589
|
if (hit.state === "fresh") {
|
|
229
|
-
this.
|
|
590
|
+
this.metricsCollector.increment("hits");
|
|
591
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
230
592
|
return hit.value;
|
|
231
593
|
}
|
|
232
594
|
if (hit.state === "stale-while-revalidate") {
|
|
233
|
-
this.
|
|
234
|
-
this.
|
|
595
|
+
this.metricsCollector.increment("hits");
|
|
596
|
+
this.metricsCollector.increment("staleHits");
|
|
597
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
235
598
|
if (fetcher) {
|
|
236
|
-
this.scheduleBackgroundRefresh(
|
|
599
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
237
600
|
}
|
|
238
601
|
return hit.value;
|
|
239
602
|
}
|
|
240
603
|
if (!fetcher) {
|
|
241
|
-
this.
|
|
242
|
-
this.
|
|
604
|
+
this.metricsCollector.increment("hits");
|
|
605
|
+
this.metricsCollector.increment("staleHits");
|
|
606
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
243
607
|
return hit.value;
|
|
244
608
|
}
|
|
245
609
|
try {
|
|
246
|
-
return await this.fetchWithGuards(
|
|
610
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
247
611
|
} catch (error) {
|
|
248
|
-
this.
|
|
249
|
-
this.
|
|
250
|
-
this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
|
|
612
|
+
this.metricsCollector.increment("staleHits");
|
|
613
|
+
this.metricsCollector.increment("refreshErrors");
|
|
614
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
251
615
|
return hit.value;
|
|
252
616
|
}
|
|
253
617
|
}
|
|
254
|
-
this.
|
|
618
|
+
this.metricsCollector.increment("misses");
|
|
255
619
|
if (!fetcher) {
|
|
256
620
|
return null;
|
|
257
621
|
}
|
|
258
|
-
return this.fetchWithGuards(
|
|
622
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
626
|
+
* Fetches and caches the value if not already present.
|
|
627
|
+
*/
|
|
628
|
+
async getOrSet(key, fetcher, options) {
|
|
629
|
+
return this.get(key, fetcher, options);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Returns true if the given key exists and is not expired in any layer.
|
|
633
|
+
*/
|
|
634
|
+
async has(key) {
|
|
635
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
636
|
+
await this.startup;
|
|
637
|
+
for (const layer of this.layers) {
|
|
638
|
+
if (this.shouldSkipLayer(layer)) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (layer.has) {
|
|
642
|
+
try {
|
|
643
|
+
const exists = await layer.has(normalizedKey);
|
|
644
|
+
if (exists) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
try {
|
|
651
|
+
const value = await layer.get(normalizedKey);
|
|
652
|
+
if (value !== null) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Returns the remaining TTL in seconds for the key in the fastest layer
|
|
663
|
+
* that has it, or null if the key is not found / has no TTL.
|
|
664
|
+
*/
|
|
665
|
+
async ttl(key) {
|
|
666
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
667
|
+
await this.startup;
|
|
668
|
+
for (const layer of this.layers) {
|
|
669
|
+
if (this.shouldSkipLayer(layer)) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (layer.ttl) {
|
|
673
|
+
try {
|
|
674
|
+
const remaining = await layer.ttl(normalizedKey);
|
|
675
|
+
if (remaining !== null) {
|
|
676
|
+
return remaining;
|
|
677
|
+
}
|
|
678
|
+
} catch {
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
259
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* Stores a value in all cache layers. Overwrites any existing value.
|
|
686
|
+
*/
|
|
260
687
|
async set(key, value, options) {
|
|
688
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
689
|
+
this.validateWriteOptions(options);
|
|
261
690
|
await this.startup;
|
|
262
|
-
await this.storeEntry(
|
|
691
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
263
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Deletes the key from all layers and publishes an invalidation message.
|
|
695
|
+
*/
|
|
264
696
|
async delete(key) {
|
|
697
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
265
698
|
await this.startup;
|
|
266
|
-
await this.deleteKeys([
|
|
267
|
-
await this.publishInvalidation({
|
|
699
|
+
await this.deleteKeys([normalizedKey]);
|
|
700
|
+
await this.publishInvalidation({
|
|
701
|
+
scope: "key",
|
|
702
|
+
keys: [normalizedKey],
|
|
703
|
+
sourceId: this.instanceId,
|
|
704
|
+
operation: "delete"
|
|
705
|
+
});
|
|
268
706
|
}
|
|
269
707
|
async clear() {
|
|
270
708
|
await this.startup;
|
|
271
709
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
272
710
|
await this.tagIndex.clear();
|
|
273
|
-
this.
|
|
274
|
-
this.
|
|
711
|
+
this.ttlResolver.clearProfiles();
|
|
712
|
+
this.circuitBreakerManager.clear();
|
|
713
|
+
this.metricsCollector.increment("invalidations");
|
|
714
|
+
this.logger.debug?.("clear");
|
|
275
715
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
276
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
|
|
719
|
+
*/
|
|
720
|
+
async mdelete(keys) {
|
|
721
|
+
if (keys.length === 0) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
await this.startup;
|
|
725
|
+
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
726
|
+
await this.deleteKeys(normalizedKeys);
|
|
727
|
+
await this.publishInvalidation({
|
|
728
|
+
scope: "keys",
|
|
729
|
+
keys: normalizedKeys,
|
|
730
|
+
sourceId: this.instanceId,
|
|
731
|
+
operation: "delete"
|
|
732
|
+
});
|
|
733
|
+
}
|
|
277
734
|
async mget(entries) {
|
|
278
735
|
if (entries.length === 0) {
|
|
279
736
|
return [];
|
|
280
737
|
}
|
|
281
|
-
const
|
|
738
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
739
|
+
...entry,
|
|
740
|
+
key: this.validateCacheKey(entry.key)
|
|
741
|
+
}));
|
|
742
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
743
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
282
744
|
if (!canFastPath) {
|
|
283
|
-
|
|
745
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
746
|
+
return Promise.all(
|
|
747
|
+
normalizedEntries.map((entry) => {
|
|
748
|
+
const optionsSignature = this.serializeOptions(entry.options);
|
|
749
|
+
const existing = pendingReads.get(entry.key);
|
|
750
|
+
if (!existing) {
|
|
751
|
+
const promise = this.get(entry.key, entry.fetch, entry.options);
|
|
752
|
+
pendingReads.set(entry.key, {
|
|
753
|
+
promise,
|
|
754
|
+
fetch: entry.fetch,
|
|
755
|
+
optionsSignature
|
|
756
|
+
});
|
|
757
|
+
return promise;
|
|
758
|
+
}
|
|
759
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
760
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
761
|
+
}
|
|
762
|
+
return existing.promise;
|
|
763
|
+
})
|
|
764
|
+
);
|
|
284
765
|
}
|
|
285
766
|
await this.startup;
|
|
286
|
-
const pending = new Set(
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
767
|
+
const pending = /* @__PURE__ */ new Set();
|
|
768
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
769
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
770
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
771
|
+
const entry = normalizedEntries[index];
|
|
772
|
+
if (!entry) continue;
|
|
773
|
+
const key = entry.key;
|
|
774
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
775
|
+
indexes.push(index);
|
|
776
|
+
indexesByKey.set(key, indexes);
|
|
777
|
+
pending.add(key);
|
|
778
|
+
}
|
|
779
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
780
|
+
const layer = this.layers[layerIndex];
|
|
781
|
+
if (!layer) continue;
|
|
782
|
+
const keys = [...pending];
|
|
783
|
+
if (keys.length === 0) {
|
|
291
784
|
break;
|
|
292
785
|
}
|
|
293
|
-
const keys = indexes.map((index) => entries[index].key);
|
|
294
786
|
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
295
787
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
296
|
-
const
|
|
788
|
+
const key = keys[offset];
|
|
297
789
|
const stored = values[offset];
|
|
298
|
-
if (stored === null) {
|
|
790
|
+
if (!key || stored === null) {
|
|
299
791
|
continue;
|
|
300
792
|
}
|
|
301
793
|
const resolved = resolveStoredValue(stored);
|
|
302
794
|
if (resolved.state === "expired") {
|
|
303
|
-
await layer.delete(
|
|
795
|
+
await layer.delete(key);
|
|
304
796
|
continue;
|
|
305
797
|
}
|
|
306
|
-
await this.tagIndex.touch(
|
|
307
|
-
await this.backfill(
|
|
308
|
-
|
|
309
|
-
pending.delete(
|
|
310
|
-
this.
|
|
798
|
+
await this.tagIndex.touch(key);
|
|
799
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
800
|
+
resultsByKey.set(key, resolved.value);
|
|
801
|
+
pending.delete(key);
|
|
802
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
311
803
|
}
|
|
312
804
|
}
|
|
313
805
|
if (pending.size > 0) {
|
|
314
|
-
for (const
|
|
315
|
-
await this.tagIndex.remove(
|
|
316
|
-
this.
|
|
806
|
+
for (const key of pending) {
|
|
807
|
+
await this.tagIndex.remove(key);
|
|
808
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
317
809
|
}
|
|
318
810
|
}
|
|
319
|
-
return
|
|
811
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
320
812
|
}
|
|
321
813
|
async mset(entries) {
|
|
322
|
-
|
|
814
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
815
|
+
...entry,
|
|
816
|
+
key: this.validateCacheKey(entry.key)
|
|
817
|
+
}));
|
|
818
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
819
|
+
await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
820
|
+
}
|
|
821
|
+
async warm(entries, options = {}) {
|
|
822
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
823
|
+
const total = entries.length;
|
|
824
|
+
let completed = 0;
|
|
825
|
+
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
826
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length || 1) }, async () => {
|
|
827
|
+
while (queue.length > 0) {
|
|
828
|
+
const entry = queue.shift();
|
|
829
|
+
if (!entry) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
let success = false;
|
|
833
|
+
try {
|
|
834
|
+
await this.get(entry.key, entry.fetcher, entry.options);
|
|
835
|
+
this.emit("warm", { key: entry.key });
|
|
836
|
+
success = true;
|
|
837
|
+
} catch (error) {
|
|
838
|
+
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
839
|
+
if (!options.continueOnError) {
|
|
840
|
+
throw error;
|
|
841
|
+
}
|
|
842
|
+
} finally {
|
|
843
|
+
completed += 1;
|
|
844
|
+
const progress = { completed, total, key: entry.key, success };
|
|
845
|
+
options.onProgress?.(progress);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
await Promise.all(workers);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Returns a cached version of `fetcher`. The cache key is derived from
|
|
853
|
+
* `prefix` plus the serialized arguments unless a `keyResolver` is provided.
|
|
854
|
+
*/
|
|
855
|
+
wrap(prefix, fetcher, options = {}) {
|
|
856
|
+
return (...args) => {
|
|
857
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
858
|
+
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
859
|
+
return this.get(key, () => fetcher(...args), options);
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Creates a `CacheNamespace` that automatically prefixes all keys with
|
|
864
|
+
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
865
|
+
*/
|
|
866
|
+
namespace(prefix) {
|
|
867
|
+
return new CacheNamespace(this, prefix);
|
|
323
868
|
}
|
|
324
869
|
async invalidateByTag(tag) {
|
|
325
870
|
await this.startup;
|
|
@@ -334,15 +879,94 @@ var CacheStack = class {
|
|
|
334
879
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
335
880
|
}
|
|
336
881
|
getMetrics() {
|
|
337
|
-
return
|
|
882
|
+
return this.metricsCollector.snapshot;
|
|
883
|
+
}
|
|
884
|
+
getStats() {
|
|
885
|
+
return {
|
|
886
|
+
metrics: this.getMetrics(),
|
|
887
|
+
layers: this.layers.map((layer) => ({
|
|
888
|
+
name: layer.name,
|
|
889
|
+
isLocal: Boolean(layer.isLocal),
|
|
890
|
+
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
891
|
+
})),
|
|
892
|
+
backgroundRefreshes: this.backgroundRefreshes.size
|
|
893
|
+
};
|
|
338
894
|
}
|
|
339
895
|
resetMetrics() {
|
|
340
|
-
|
|
896
|
+
this.metricsCollector.reset();
|
|
341
897
|
}
|
|
342
|
-
|
|
898
|
+
/**
|
|
899
|
+
* Returns computed hit-rate statistics (overall and per-layer).
|
|
900
|
+
*/
|
|
901
|
+
getHitRate() {
|
|
902
|
+
return this.metricsCollector.hitRate();
|
|
903
|
+
}
|
|
904
|
+
async exportState() {
|
|
905
|
+
await this.startup;
|
|
906
|
+
const exported = /* @__PURE__ */ new Map();
|
|
907
|
+
for (const layer of this.layers) {
|
|
908
|
+
if (!layer.keys) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const keys = await layer.keys();
|
|
912
|
+
for (const key of keys) {
|
|
913
|
+
if (exported.has(key)) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
917
|
+
if (stored === null) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
exported.set(key, {
|
|
921
|
+
key,
|
|
922
|
+
value: stored,
|
|
923
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return [...exported.values()];
|
|
928
|
+
}
|
|
929
|
+
async importState(entries) {
|
|
343
930
|
await this.startup;
|
|
344
|
-
await
|
|
345
|
-
|
|
931
|
+
await Promise.all(
|
|
932
|
+
entries.map(async (entry) => {
|
|
933
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
934
|
+
await this.tagIndex.touch(entry.key);
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
async persistToFile(filePath) {
|
|
939
|
+
const snapshot = await this.exportState();
|
|
940
|
+
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
941
|
+
}
|
|
942
|
+
async restoreFromFile(filePath) {
|
|
943
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
944
|
+
let parsed;
|
|
945
|
+
try {
|
|
946
|
+
parsed = JSON.parse(raw, (_key, value) => {
|
|
947
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
948
|
+
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
949
|
+
}
|
|
950
|
+
return value;
|
|
951
|
+
});
|
|
952
|
+
} catch (cause) {
|
|
953
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
954
|
+
}
|
|
955
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
956
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
957
|
+
}
|
|
958
|
+
await this.importState(parsed);
|
|
959
|
+
}
|
|
960
|
+
async disconnect() {
|
|
961
|
+
if (!this.disconnectPromise) {
|
|
962
|
+
this.isDisconnecting = true;
|
|
963
|
+
this.disconnectPromise = (async () => {
|
|
964
|
+
await this.startup;
|
|
965
|
+
await this.unsubscribeInvalidation?.();
|
|
966
|
+
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
967
|
+
})();
|
|
968
|
+
}
|
|
969
|
+
await this.disconnectPromise;
|
|
346
970
|
}
|
|
347
971
|
async initialize() {
|
|
348
972
|
if (!this.options.invalidationBus) {
|
|
@@ -356,7 +980,7 @@ var CacheStack = class {
|
|
|
356
980
|
const fetchTask = async () => {
|
|
357
981
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
358
982
|
if (secondHit.found) {
|
|
359
|
-
this.
|
|
983
|
+
this.metricsCollector.increment("hits");
|
|
360
984
|
return secondHit.value;
|
|
361
985
|
}
|
|
362
986
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
@@ -381,11 +1005,12 @@ var CacheStack = class {
|
|
|
381
1005
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
382
1006
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
383
1007
|
const deadline = Date.now() + timeoutMs;
|
|
384
|
-
this.
|
|
1008
|
+
this.metricsCollector.increment("singleFlightWaits");
|
|
1009
|
+
this.emit("stampede-dedupe", { key });
|
|
385
1010
|
while (Date.now() < deadline) {
|
|
386
1011
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
387
1012
|
if (hit.found) {
|
|
388
|
-
this.
|
|
1013
|
+
this.metricsCollector.increment("hits");
|
|
389
1014
|
return hit.value;
|
|
390
1015
|
}
|
|
391
1016
|
await this.sleep(pollIntervalMs);
|
|
@@ -393,8 +1018,18 @@ var CacheStack = class {
|
|
|
393
1018
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
394
1019
|
}
|
|
395
1020
|
async fetchAndPopulate(key, fetcher, options) {
|
|
396
|
-
this.
|
|
397
|
-
|
|
1021
|
+
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1022
|
+
this.metricsCollector.increment("fetches");
|
|
1023
|
+
const fetchStart = Date.now();
|
|
1024
|
+
let fetched;
|
|
1025
|
+
try {
|
|
1026
|
+
fetched = await fetcher();
|
|
1027
|
+
this.circuitBreakerManager.recordSuccess(key);
|
|
1028
|
+
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
398
1033
|
if (fetched === null || fetched === void 0) {
|
|
399
1034
|
if (!this.shouldNegativeCache(options)) {
|
|
400
1035
|
return null;
|
|
@@ -412,9 +1047,10 @@ var CacheStack = class {
|
|
|
412
1047
|
} else {
|
|
413
1048
|
await this.tagIndex.touch(key);
|
|
414
1049
|
}
|
|
415
|
-
this.
|
|
416
|
-
this.logger.debug("set", { key, kind, tags: options?.tags });
|
|
417
|
-
|
|
1050
|
+
this.metricsCollector.increment("sets");
|
|
1051
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
1052
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
1053
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
418
1054
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
419
1055
|
}
|
|
420
1056
|
}
|
|
@@ -422,8 +1058,10 @@ var CacheStack = class {
|
|
|
422
1058
|
let sawRetainableValue = false;
|
|
423
1059
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
424
1060
|
const layer = this.layers[index];
|
|
1061
|
+
if (!layer) continue;
|
|
425
1062
|
const stored = await this.readLayerEntry(layer, key);
|
|
426
1063
|
if (stored === null) {
|
|
1064
|
+
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
427
1065
|
continue;
|
|
428
1066
|
}
|
|
429
1067
|
const resolved = resolveStoredValue(stored);
|
|
@@ -437,20 +1075,41 @@ var CacheStack = class {
|
|
|
437
1075
|
}
|
|
438
1076
|
await this.tagIndex.touch(key);
|
|
439
1077
|
await this.backfill(key, stored, index - 1, options);
|
|
440
|
-
this.
|
|
441
|
-
|
|
1078
|
+
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
1079
|
+
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
1080
|
+
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
1081
|
+
return {
|
|
1082
|
+
found: true,
|
|
1083
|
+
value: resolved.value,
|
|
1084
|
+
stored,
|
|
1085
|
+
state: resolved.state,
|
|
1086
|
+
layerIndex: index,
|
|
1087
|
+
layerName: layer.name
|
|
1088
|
+
};
|
|
442
1089
|
}
|
|
443
1090
|
if (!sawRetainableValue) {
|
|
444
1091
|
await this.tagIndex.remove(key);
|
|
445
1092
|
}
|
|
446
|
-
this.logger.debug("miss", { key, mode });
|
|
1093
|
+
this.logger.debug?.("miss", { key, mode });
|
|
1094
|
+
this.emit("miss", { key, mode });
|
|
447
1095
|
return { found: false, value: null, stored: null, state: "miss" };
|
|
448
1096
|
}
|
|
449
1097
|
async readLayerEntry(layer, key) {
|
|
1098
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
450
1101
|
if (layer.getEntry) {
|
|
451
|
-
|
|
1102
|
+
try {
|
|
1103
|
+
return await layer.getEntry(key);
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
try {
|
|
1109
|
+
return await layer.get(key);
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
452
1112
|
}
|
|
453
|
-
return layer.get(key);
|
|
454
1113
|
}
|
|
455
1114
|
async backfill(key, stored, upToIndex, options) {
|
|
456
1115
|
if (upToIndex < 0) {
|
|
@@ -458,26 +1117,34 @@ var CacheStack = class {
|
|
|
458
1117
|
}
|
|
459
1118
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
460
1119
|
const layer = this.layers[index];
|
|
1120
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
461
1123
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1124
|
+
try {
|
|
1125
|
+
await layer.set(key, stored, ttl);
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
this.metricsCollector.increment("backfills");
|
|
1131
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1132
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
465
1133
|
}
|
|
466
1134
|
}
|
|
467
1135
|
async writeAcrossLayers(key, kind, value, options) {
|
|
468
1136
|
const now = Date.now();
|
|
469
1137
|
const operations = this.layers.map((layer) => async () => {
|
|
470
|
-
|
|
1138
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
471
1142
|
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
472
1143
|
layer.name,
|
|
473
1144
|
options?.staleWhileRevalidate,
|
|
474
1145
|
this.options.staleWhileRevalidate
|
|
475
1146
|
);
|
|
476
|
-
const staleIfError = this.resolveLayerSeconds(
|
|
477
|
-
layer.name,
|
|
478
|
-
options?.staleIfError,
|
|
479
|
-
this.options.staleIfError
|
|
480
|
-
);
|
|
1147
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
481
1148
|
const payload = createStoredValueEnvelope({
|
|
482
1149
|
kind,
|
|
483
1150
|
value,
|
|
@@ -487,7 +1154,11 @@ var CacheStack = class {
|
|
|
487
1154
|
now
|
|
488
1155
|
});
|
|
489
1156
|
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
490
|
-
|
|
1157
|
+
try {
|
|
1158
|
+
await layer.set(key, payload, ttl);
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1161
|
+
}
|
|
491
1162
|
});
|
|
492
1163
|
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
493
1164
|
}
|
|
@@ -501,8 +1172,8 @@ var CacheStack = class {
|
|
|
501
1172
|
if (failures.length === 0) {
|
|
502
1173
|
return;
|
|
503
1174
|
}
|
|
504
|
-
this.
|
|
505
|
-
this.logger.debug("write-failure", {
|
|
1175
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
1176
|
+
this.logger.debug?.("write-failure", {
|
|
506
1177
|
...context,
|
|
507
1178
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
508
1179
|
});
|
|
@@ -513,52 +1184,26 @@ var CacheStack = class {
|
|
|
513
1184
|
);
|
|
514
1185
|
}
|
|
515
1186
|
}
|
|
516
|
-
resolveFreshTtl(layerName, kind, options, fallbackTtl) {
|
|
517
|
-
|
|
518
|
-
layerName,
|
|
519
|
-
options?.negativeTtl,
|
|
520
|
-
this.options.negativeTtl,
|
|
521
|
-
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
522
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
523
|
-
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
524
|
-
return this.applyJitter(baseTtl, jitter);
|
|
1187
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1188
|
+
return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
|
|
525
1189
|
}
|
|
526
1190
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
527
|
-
|
|
528
|
-
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
529
|
-
}
|
|
530
|
-
if (globalDefault !== void 0) {
|
|
531
|
-
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
532
|
-
}
|
|
533
|
-
return fallback;
|
|
534
|
-
}
|
|
535
|
-
readLayerNumber(layerName, value) {
|
|
536
|
-
if (typeof value === "number") {
|
|
537
|
-
return value;
|
|
538
|
-
}
|
|
539
|
-
return value[layerName];
|
|
540
|
-
}
|
|
541
|
-
applyJitter(ttl, jitter) {
|
|
542
|
-
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
543
|
-
return ttl;
|
|
544
|
-
}
|
|
545
|
-
const delta = (Math.random() * 2 - 1) * jitter;
|
|
546
|
-
return Math.max(1, Math.round(ttl + delta));
|
|
1191
|
+
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
547
1192
|
}
|
|
548
1193
|
shouldNegativeCache(options) {
|
|
549
1194
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
550
1195
|
}
|
|
551
1196
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
552
|
-
if (this.backgroundRefreshes.has(key)) {
|
|
1197
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
553
1198
|
return;
|
|
554
1199
|
}
|
|
555
1200
|
const refresh = (async () => {
|
|
556
|
-
this.
|
|
1201
|
+
this.metricsCollector.increment("refreshes");
|
|
557
1202
|
try {
|
|
558
1203
|
await this.fetchWithGuards(key, fetcher, options);
|
|
559
1204
|
} catch (error) {
|
|
560
|
-
this.
|
|
561
|
-
this.logger.debug("refresh-error", { key, error: this.formatError(error) });
|
|
1205
|
+
this.metricsCollector.increment("refreshErrors");
|
|
1206
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
562
1207
|
} finally {
|
|
563
1208
|
this.backgroundRefreshes.delete(key);
|
|
564
1209
|
}
|
|
@@ -576,21 +1221,16 @@ var CacheStack = class {
|
|
|
576
1221
|
if (keys.length === 0) {
|
|
577
1222
|
return;
|
|
578
1223
|
}
|
|
579
|
-
await
|
|
580
|
-
this.layers.map(async (layer) => {
|
|
581
|
-
if (layer.deleteMany) {
|
|
582
|
-
await layer.deleteMany(keys);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
586
|
-
})
|
|
587
|
-
);
|
|
1224
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
588
1225
|
for (const key of keys) {
|
|
589
1226
|
await this.tagIndex.remove(key);
|
|
1227
|
+
this.ttlResolver.deleteProfile(key);
|
|
1228
|
+
this.circuitBreakerManager.delete(key);
|
|
590
1229
|
}
|
|
591
|
-
this.
|
|
592
|
-
this.
|
|
593
|
-
this.logger.debug("delete", { keys });
|
|
1230
|
+
this.metricsCollector.increment("deletes", keys.length);
|
|
1231
|
+
this.metricsCollector.increment("invalidations");
|
|
1232
|
+
this.logger.debug?.("delete", { keys });
|
|
1233
|
+
this.emit("delete", { keys });
|
|
594
1234
|
}
|
|
595
1235
|
async publishInvalidation(message) {
|
|
596
1236
|
if (!this.options.invalidationBus) {
|
|
@@ -609,21 +1249,15 @@ var CacheStack = class {
|
|
|
609
1249
|
if (message.scope === "clear") {
|
|
610
1250
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
611
1251
|
await this.tagIndex.clear();
|
|
1252
|
+
this.ttlResolver.clearProfiles();
|
|
612
1253
|
return;
|
|
613
1254
|
}
|
|
614
1255
|
const keys = message.keys ?? [];
|
|
615
|
-
await
|
|
616
|
-
localLayers.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
|
-
);
|
|
1256
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
624
1257
|
if (message.operation !== "write") {
|
|
625
1258
|
for (const key of keys) {
|
|
626
1259
|
await this.tagIndex.remove(key);
|
|
1260
|
+
this.ttlResolver.deleteProfile(key);
|
|
627
1261
|
}
|
|
628
1262
|
}
|
|
629
1263
|
}
|
|
@@ -636,6 +1270,210 @@ var CacheStack = class {
|
|
|
636
1270
|
sleep(ms) {
|
|
637
1271
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
638
1272
|
}
|
|
1273
|
+
shouldBroadcastL1Invalidation() {
|
|
1274
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1275
|
+
}
|
|
1276
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
1277
|
+
await Promise.all(
|
|
1278
|
+
layers.map(async (layer) => {
|
|
1279
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (layer.deleteMany) {
|
|
1283
|
+
try {
|
|
1284
|
+
await layer.deleteMany(keys);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
await Promise.all(
|
|
1291
|
+
keys.map(async (key) => {
|
|
1292
|
+
try {
|
|
1293
|
+
await layer.delete(key);
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1296
|
+
}
|
|
1297
|
+
})
|
|
1298
|
+
);
|
|
1299
|
+
})
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
validateConfiguration() {
|
|
1303
|
+
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
1304
|
+
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
1305
|
+
}
|
|
1306
|
+
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
1307
|
+
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
1308
|
+
}
|
|
1309
|
+
this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
1310
|
+
this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
1311
|
+
this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
1312
|
+
this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
1313
|
+
this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
1314
|
+
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1315
|
+
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1316
|
+
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1317
|
+
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1318
|
+
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1319
|
+
}
|
|
1320
|
+
validateWriteOptions(options) {
|
|
1321
|
+
if (!options) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
1325
|
+
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
1326
|
+
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
1327
|
+
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1328
|
+
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1329
|
+
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1330
|
+
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1331
|
+
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1332
|
+
}
|
|
1333
|
+
validateLayerNumberOption(name, value) {
|
|
1334
|
+
if (value === void 0) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (typeof value === "number") {
|
|
1338
|
+
this.validateNonNegativeNumber(name, value);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1342
|
+
if (layerValue === void 0) {
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
validatePositiveNumber(name, value) {
|
|
1349
|
+
if (value === void 0) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1353
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
validateNonNegativeNumber(name, value) {
|
|
1357
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1358
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
validateCacheKey(key) {
|
|
1362
|
+
if (key.length === 0) {
|
|
1363
|
+
throw new Error("Cache key must not be empty.");
|
|
1364
|
+
}
|
|
1365
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1366
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1367
|
+
}
|
|
1368
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1369
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1370
|
+
}
|
|
1371
|
+
return key;
|
|
1372
|
+
}
|
|
1373
|
+
serializeOptions(options) {
|
|
1374
|
+
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1375
|
+
}
|
|
1376
|
+
validateAdaptiveTtlOptions(options) {
|
|
1377
|
+
if (!options || options === true) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1381
|
+
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1382
|
+
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1383
|
+
}
|
|
1384
|
+
validateCircuitBreakerOptions(options) {
|
|
1385
|
+
if (!options) {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1389
|
+
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1390
|
+
}
|
|
1391
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1392
|
+
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
1393
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
1394
|
+
if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
|
|
1395
|
+
const refreshed = refreshStoredEnvelope(hit.stored);
|
|
1396
|
+
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1397
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1398
|
+
const layer = this.layers[index];
|
|
1399
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
try {
|
|
1403
|
+
await layer.set(key, refreshed, ttl);
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
|
|
1410
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
shouldSkipLayer(layer) {
|
|
1414
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1415
|
+
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
1416
|
+
}
|
|
1417
|
+
async handleLayerFailure(layer, operation, error) {
|
|
1418
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
1419
|
+
throw error;
|
|
1420
|
+
}
|
|
1421
|
+
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1422
|
+
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1423
|
+
this.metricsCollector.increment("degradedOperations");
|
|
1424
|
+
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1425
|
+
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
isGracefulDegradationEnabled() {
|
|
1429
|
+
return Boolean(this.options.gracefulDegradation);
|
|
1430
|
+
}
|
|
1431
|
+
recordCircuitFailure(key, options, error) {
|
|
1432
|
+
if (!options) {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
this.circuitBreakerManager.recordFailure(key, options);
|
|
1436
|
+
if (this.circuitBreakerManager.isOpen(key)) {
|
|
1437
|
+
this.metricsCollector.increment("circuitBreakerTrips");
|
|
1438
|
+
}
|
|
1439
|
+
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
1440
|
+
}
|
|
1441
|
+
isNegativeStoredValue(stored) {
|
|
1442
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1443
|
+
}
|
|
1444
|
+
emitError(operation, context) {
|
|
1445
|
+
this.logger.error?.(operation, context);
|
|
1446
|
+
if (this.listenerCount("error") > 0) {
|
|
1447
|
+
this.emit("error", { operation, ...context });
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
serializeKeyPart(value) {
|
|
1451
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1452
|
+
return String(value);
|
|
1453
|
+
}
|
|
1454
|
+
return JSON.stringify(this.normalizeForSerialization(value));
|
|
1455
|
+
}
|
|
1456
|
+
isCacheSnapshotEntries(value) {
|
|
1457
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1458
|
+
if (!entry || typeof entry !== "object") {
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
const candidate = entry;
|
|
1462
|
+
return typeof candidate.key === "string";
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
normalizeForSerialization(value) {
|
|
1466
|
+
if (Array.isArray(value)) {
|
|
1467
|
+
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
1468
|
+
}
|
|
1469
|
+
if (value && typeof value === "object") {
|
|
1470
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
1471
|
+
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
1472
|
+
return normalized;
|
|
1473
|
+
}, {});
|
|
1474
|
+
}
|
|
1475
|
+
return value;
|
|
1476
|
+
}
|
|
639
1477
|
};
|
|
640
1478
|
|
|
641
1479
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -643,19 +1481,27 @@ var RedisInvalidationBus = class {
|
|
|
643
1481
|
channel;
|
|
644
1482
|
publisher;
|
|
645
1483
|
subscriber;
|
|
1484
|
+
activeListener;
|
|
646
1485
|
constructor(options) {
|
|
647
1486
|
this.publisher = options.publisher;
|
|
648
1487
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
649
1488
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
650
1489
|
}
|
|
651
1490
|
async subscribe(handler) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1491
|
+
if (this.activeListener) {
|
|
1492
|
+
throw new Error("RedisInvalidationBus already has an active subscription.");
|
|
1493
|
+
}
|
|
1494
|
+
const listener = (_channel, payload) => {
|
|
1495
|
+
void this.handleMessage(payload, handler);
|
|
655
1496
|
};
|
|
1497
|
+
this.activeListener = listener;
|
|
656
1498
|
this.subscriber.on("message", listener);
|
|
657
1499
|
await this.subscriber.subscribe(this.channel);
|
|
658
1500
|
return async () => {
|
|
1501
|
+
if (this.activeListener !== listener) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
this.activeListener = void 0;
|
|
659
1505
|
this.subscriber.off("message", listener);
|
|
660
1506
|
await this.subscriber.unsubscribe(this.channel);
|
|
661
1507
|
};
|
|
@@ -663,109 +1509,130 @@ var RedisInvalidationBus = class {
|
|
|
663
1509
|
async publish(message) {
|
|
664
1510
|
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
665
1511
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
scanCount;
|
|
673
|
-
constructor(options) {
|
|
674
|
-
this.client = options.client;
|
|
675
|
-
this.prefix = options.prefix ?? "layercache:tag-index";
|
|
676
|
-
this.scanCount = options.scanCount ?? 100;
|
|
677
|
-
}
|
|
678
|
-
async touch(key) {
|
|
679
|
-
await this.client.sadd(this.knownKeysKey(), key);
|
|
680
|
-
}
|
|
681
|
-
async track(key, tags) {
|
|
682
|
-
const keyTagsKey = this.keyTagsKey(key);
|
|
683
|
-
const existingTags = await this.client.smembers(keyTagsKey);
|
|
684
|
-
const pipeline = this.client.pipeline();
|
|
685
|
-
pipeline.sadd(this.knownKeysKey(), key);
|
|
686
|
-
for (const tag of existingTags) {
|
|
687
|
-
pipeline.srem(this.tagKeysKey(tag), key);
|
|
688
|
-
}
|
|
689
|
-
pipeline.del(keyTagsKey);
|
|
690
|
-
if (tags.length > 0) {
|
|
691
|
-
pipeline.sadd(keyTagsKey, ...tags);
|
|
692
|
-
for (const tag of new Set(tags)) {
|
|
693
|
-
pipeline.sadd(this.tagKeysKey(tag), key);
|
|
1512
|
+
async handleMessage(payload, handler) {
|
|
1513
|
+
let message;
|
|
1514
|
+
try {
|
|
1515
|
+
const parsed = JSON.parse(payload);
|
|
1516
|
+
if (!this.isInvalidationMessage(parsed)) {
|
|
1517
|
+
throw new Error("Invalid invalidation payload shape.");
|
|
694
1518
|
}
|
|
1519
|
+
message = parsed;
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
this.reportError("invalid invalidation payload", error);
|
|
1522
|
+
return;
|
|
695
1523
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const existingTags = await this.client.smembers(keyTagsKey);
|
|
701
|
-
const pipeline = this.client.pipeline();
|
|
702
|
-
pipeline.srem(this.knownKeysKey(), key);
|
|
703
|
-
pipeline.del(keyTagsKey);
|
|
704
|
-
for (const tag of existingTags) {
|
|
705
|
-
pipeline.srem(this.tagKeysKey(tag), key);
|
|
1524
|
+
try {
|
|
1525
|
+
await handler(message);
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
this.reportError("invalidation handler failed", error);
|
|
706
1528
|
}
|
|
707
|
-
await pipeline.exec();
|
|
708
|
-
}
|
|
709
|
-
async keysForTag(tag) {
|
|
710
|
-
return this.client.smembers(this.tagKeysKey(tag));
|
|
711
|
-
}
|
|
712
|
-
async matchPattern(pattern) {
|
|
713
|
-
const matches = [];
|
|
714
|
-
let cursor = "0";
|
|
715
|
-
do {
|
|
716
|
-
const [nextCursor, keys] = await this.client.sscan(
|
|
717
|
-
this.knownKeysKey(),
|
|
718
|
-
cursor,
|
|
719
|
-
"MATCH",
|
|
720
|
-
pattern,
|
|
721
|
-
"COUNT",
|
|
722
|
-
this.scanCount
|
|
723
|
-
);
|
|
724
|
-
cursor = nextCursor;
|
|
725
|
-
matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
|
|
726
|
-
} while (cursor !== "0");
|
|
727
|
-
return matches;
|
|
728
1529
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
return;
|
|
1530
|
+
isInvalidationMessage(value) {
|
|
1531
|
+
if (!value || typeof value !== "object") {
|
|
1532
|
+
return false;
|
|
733
1533
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
const pattern = `${this.prefix}:*`;
|
|
740
|
-
do {
|
|
741
|
-
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
742
|
-
cursor = nextCursor;
|
|
743
|
-
matches.push(...keys);
|
|
744
|
-
} while (cursor !== "0");
|
|
745
|
-
return matches;
|
|
746
|
-
}
|
|
747
|
-
knownKeysKey() {
|
|
748
|
-
return `${this.prefix}:keys`;
|
|
749
|
-
}
|
|
750
|
-
keyTagsKey(key) {
|
|
751
|
-
return `${this.prefix}:key:${encodeURIComponent(key)}`;
|
|
1534
|
+
const candidate = value;
|
|
1535
|
+
const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
|
|
1536
|
+
const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "clear";
|
|
1537
|
+
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
1538
|
+
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
752
1539
|
}
|
|
753
|
-
|
|
754
|
-
|
|
1540
|
+
reportError(message, error) {
|
|
1541
|
+
console.error(`[layercache] ${message}`, error);
|
|
755
1542
|
}
|
|
756
1543
|
};
|
|
757
1544
|
|
|
1545
|
+
// src/http/createCacheStatsHandler.ts
|
|
1546
|
+
function createCacheStatsHandler(cache) {
|
|
1547
|
+
return async (_request, response) => {
|
|
1548
|
+
response.statusCode = 200;
|
|
1549
|
+
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
1550
|
+
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/decorators/createCachedMethodDecorator.ts
|
|
1555
|
+
function createCachedMethodDecorator(options) {
|
|
1556
|
+
const wrappedByInstance = /* @__PURE__ */ new WeakMap();
|
|
1557
|
+
return ((_, propertyKey, descriptor) => {
|
|
1558
|
+
const original = descriptor.value;
|
|
1559
|
+
if (typeof original !== "function") {
|
|
1560
|
+
throw new Error("createCachedMethodDecorator can only be applied to methods.");
|
|
1561
|
+
}
|
|
1562
|
+
descriptor.value = async function(...args) {
|
|
1563
|
+
const instance = this;
|
|
1564
|
+
let wrapped = wrappedByInstance.get(instance);
|
|
1565
|
+
if (!wrapped) {
|
|
1566
|
+
const cache = options.cache(instance);
|
|
1567
|
+
wrapped = cache.wrap(
|
|
1568
|
+
options.prefix ?? String(propertyKey),
|
|
1569
|
+
(...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
|
|
1570
|
+
options
|
|
1571
|
+
);
|
|
1572
|
+
wrappedByInstance.set(instance, wrapped);
|
|
1573
|
+
}
|
|
1574
|
+
return wrapped(...args);
|
|
1575
|
+
};
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/integrations/fastify.ts
|
|
1580
|
+
function createFastifyLayercachePlugin(cache, options = {}) {
|
|
1581
|
+
return async (fastify) => {
|
|
1582
|
+
fastify.decorate("cache", cache);
|
|
1583
|
+
if (options.exposeStatsRoute !== false && fastify.get) {
|
|
1584
|
+
fastify.get(options.statsPath ?? "/cache/stats", async () => cache.getStats());
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/integrations/graphql.ts
|
|
1590
|
+
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
1591
|
+
const wrapped = cache.wrap(prefix, resolver, {
|
|
1592
|
+
...options,
|
|
1593
|
+
keyResolver: options.keyResolver
|
|
1594
|
+
});
|
|
1595
|
+
return (...args) => wrapped(...args);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/integrations/trpc.ts
|
|
1599
|
+
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1600
|
+
return async (context) => {
|
|
1601
|
+
const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
|
|
1602
|
+
let didFetch = false;
|
|
1603
|
+
let fetchedResult = null;
|
|
1604
|
+
const cached = await cache.get(
|
|
1605
|
+
key,
|
|
1606
|
+
async () => {
|
|
1607
|
+
didFetch = true;
|
|
1608
|
+
fetchedResult = await context.next();
|
|
1609
|
+
return fetchedResult;
|
|
1610
|
+
},
|
|
1611
|
+
options
|
|
1612
|
+
);
|
|
1613
|
+
if (cached !== null) {
|
|
1614
|
+
return cached;
|
|
1615
|
+
}
|
|
1616
|
+
if (didFetch) {
|
|
1617
|
+
return fetchedResult;
|
|
1618
|
+
}
|
|
1619
|
+
return context.next();
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
758
1623
|
// src/layers/MemoryLayer.ts
|
|
759
1624
|
var MemoryLayer = class {
|
|
760
1625
|
name;
|
|
761
1626
|
defaultTtl;
|
|
762
1627
|
isLocal = true;
|
|
763
1628
|
maxSize;
|
|
1629
|
+
evictionPolicy;
|
|
764
1630
|
entries = /* @__PURE__ */ new Map();
|
|
765
1631
|
constructor(options = {}) {
|
|
766
1632
|
this.name = options.name ?? "memory";
|
|
767
1633
|
this.defaultTtl = options.ttl;
|
|
768
1634
|
this.maxSize = options.maxSize ?? 1e3;
|
|
1635
|
+
this.evictionPolicy = options.evictionPolicy ?? "lru";
|
|
769
1636
|
}
|
|
770
1637
|
async get(key) {
|
|
771
1638
|
const value = await this.getEntry(key);
|
|
@@ -780,8 +1647,13 @@ var MemoryLayer = class {
|
|
|
780
1647
|
this.entries.delete(key);
|
|
781
1648
|
return null;
|
|
782
1649
|
}
|
|
783
|
-
this.
|
|
784
|
-
|
|
1650
|
+
if (this.evictionPolicy === "lru") {
|
|
1651
|
+
this.entries.delete(key);
|
|
1652
|
+
entry.frequency += 1;
|
|
1653
|
+
this.entries.set(key, entry);
|
|
1654
|
+
} else {
|
|
1655
|
+
entry.frequency += 1;
|
|
1656
|
+
}
|
|
785
1657
|
return entry.value;
|
|
786
1658
|
}
|
|
787
1659
|
async getMany(keys) {
|
|
@@ -795,15 +1667,42 @@ var MemoryLayer = class {
|
|
|
795
1667
|
this.entries.delete(key);
|
|
796
1668
|
this.entries.set(key, {
|
|
797
1669
|
value,
|
|
798
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
1670
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
|
|
1671
|
+
frequency: 0,
|
|
1672
|
+
insertedAt: Date.now()
|
|
799
1673
|
});
|
|
800
1674
|
while (this.entries.size > this.maxSize) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1675
|
+
this.evict();
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
async has(key) {
|
|
1679
|
+
const entry = this.entries.get(key);
|
|
1680
|
+
if (!entry) {
|
|
1681
|
+
return false;
|
|
1682
|
+
}
|
|
1683
|
+
if (this.isExpired(entry)) {
|
|
1684
|
+
this.entries.delete(key);
|
|
1685
|
+
return false;
|
|
806
1686
|
}
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
async ttl(key) {
|
|
1690
|
+
const entry = this.entries.get(key);
|
|
1691
|
+
if (!entry) {
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
if (this.isExpired(entry)) {
|
|
1695
|
+
this.entries.delete(key);
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
if (entry.expiresAt === null) {
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
|
|
1702
|
+
}
|
|
1703
|
+
async size() {
|
|
1704
|
+
this.pruneExpired();
|
|
1705
|
+
return this.entries.size;
|
|
807
1706
|
}
|
|
808
1707
|
async delete(key) {
|
|
809
1708
|
this.entries.delete(key);
|
|
@@ -820,6 +1719,52 @@ var MemoryLayer = class {
|
|
|
820
1719
|
this.pruneExpired();
|
|
821
1720
|
return [...this.entries.keys()];
|
|
822
1721
|
}
|
|
1722
|
+
exportState() {
|
|
1723
|
+
this.pruneExpired();
|
|
1724
|
+
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
1725
|
+
key,
|
|
1726
|
+
value: entry.value,
|
|
1727
|
+
expiresAt: entry.expiresAt
|
|
1728
|
+
}));
|
|
1729
|
+
}
|
|
1730
|
+
importState(entries) {
|
|
1731
|
+
for (const entry of entries) {
|
|
1732
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
this.entries.set(entry.key, {
|
|
1736
|
+
value: entry.value,
|
|
1737
|
+
expiresAt: entry.expiresAt,
|
|
1738
|
+
frequency: 0,
|
|
1739
|
+
insertedAt: Date.now()
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
while (this.entries.size > this.maxSize) {
|
|
1743
|
+
this.evict();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
evict() {
|
|
1747
|
+
if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
|
|
1748
|
+
const oldestKey = this.entries.keys().next().value;
|
|
1749
|
+
if (oldestKey !== void 0) {
|
|
1750
|
+
this.entries.delete(oldestKey);
|
|
1751
|
+
}
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
let victimKey;
|
|
1755
|
+
let minFreq = Number.POSITIVE_INFINITY;
|
|
1756
|
+
let minInsertedAt = Number.POSITIVE_INFINITY;
|
|
1757
|
+
for (const [key, entry] of this.entries.entries()) {
|
|
1758
|
+
if (entry.frequency < minFreq || entry.frequency === minFreq && entry.insertedAt < minInsertedAt) {
|
|
1759
|
+
minFreq = entry.frequency;
|
|
1760
|
+
minInsertedAt = entry.insertedAt;
|
|
1761
|
+
victimKey = key;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (victimKey !== void 0) {
|
|
1765
|
+
this.entries.delete(victimKey);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
823
1768
|
pruneExpired() {
|
|
824
1769
|
for (const [key, entry] of this.entries.entries()) {
|
|
825
1770
|
if (this.isExpired(entry)) {
|
|
@@ -832,6 +1777,9 @@ var MemoryLayer = class {
|
|
|
832
1777
|
}
|
|
833
1778
|
};
|
|
834
1779
|
|
|
1780
|
+
// src/layers/RedisLayer.ts
|
|
1781
|
+
import { brotliCompressSync, brotliDecompressSync, gunzipSync, gzipSync } from "zlib";
|
|
1782
|
+
|
|
835
1783
|
// src/serialization/JsonSerializer.ts
|
|
836
1784
|
var JsonSerializer = class {
|
|
837
1785
|
serialize(value) {
|
|
@@ -844,6 +1792,7 @@ var JsonSerializer = class {
|
|
|
844
1792
|
};
|
|
845
1793
|
|
|
846
1794
|
// src/layers/RedisLayer.ts
|
|
1795
|
+
var BATCH_DELETE_SIZE = 500;
|
|
847
1796
|
var RedisLayer = class {
|
|
848
1797
|
name;
|
|
849
1798
|
defaultTtl;
|
|
@@ -853,6 +1802,8 @@ var RedisLayer = class {
|
|
|
853
1802
|
prefix;
|
|
854
1803
|
allowUnprefixedClear;
|
|
855
1804
|
scanCount;
|
|
1805
|
+
compression;
|
|
1806
|
+
compressionThreshold;
|
|
856
1807
|
constructor(options) {
|
|
857
1808
|
this.client = options.client;
|
|
858
1809
|
this.defaultTtl = options.ttl;
|
|
@@ -861,6 +1812,8 @@ var RedisLayer = class {
|
|
|
861
1812
|
this.prefix = options.prefix ?? "";
|
|
862
1813
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
863
1814
|
this.scanCount = options.scanCount ?? 100;
|
|
1815
|
+
this.compression = options.compression;
|
|
1816
|
+
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
864
1817
|
}
|
|
865
1818
|
async get(key) {
|
|
866
1819
|
const payload = await this.getEntry(key);
|
|
@@ -871,7 +1824,7 @@ var RedisLayer = class {
|
|
|
871
1824
|
if (payload === null) {
|
|
872
1825
|
return null;
|
|
873
1826
|
}
|
|
874
|
-
return this.
|
|
1827
|
+
return this.deserializeOrDelete(key, payload);
|
|
875
1828
|
}
|
|
876
1829
|
async getMany(keys) {
|
|
877
1830
|
if (keys.length === 0) {
|
|
@@ -885,16 +1838,18 @@ var RedisLayer = class {
|
|
|
885
1838
|
if (results === null) {
|
|
886
1839
|
return keys.map(() => null);
|
|
887
1840
|
}
|
|
888
|
-
return
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1841
|
+
return Promise.all(
|
|
1842
|
+
results.map(async (result, index) => {
|
|
1843
|
+
const [error, payload] = result;
|
|
1844
|
+
if (error || payload === null || !this.isSerializablePayload(payload)) {
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
return this.deserializeOrDelete(keys[index] ?? "", payload);
|
|
1848
|
+
})
|
|
1849
|
+
);
|
|
895
1850
|
}
|
|
896
1851
|
async set(key, value, ttl = this.defaultTtl) {
|
|
897
|
-
const payload = this.serializer.serialize(value);
|
|
1852
|
+
const payload = this.encodePayload(this.serializer.serialize(value));
|
|
898
1853
|
const normalizedKey = this.withPrefix(key);
|
|
899
1854
|
if (ttl && ttl > 0) {
|
|
900
1855
|
await this.client.set(normalizedKey, payload, "EX", ttl);
|
|
@@ -911,14 +1866,44 @@ var RedisLayer = class {
|
|
|
911
1866
|
}
|
|
912
1867
|
await this.client.del(...keys.map((key) => this.withPrefix(key)));
|
|
913
1868
|
}
|
|
914
|
-
async
|
|
915
|
-
|
|
916
|
-
|
|
1869
|
+
async has(key) {
|
|
1870
|
+
const exists = await this.client.exists(this.withPrefix(key));
|
|
1871
|
+
return exists > 0;
|
|
1872
|
+
}
|
|
1873
|
+
async ttl(key) {
|
|
1874
|
+
const remaining = await this.client.ttl(this.withPrefix(key));
|
|
1875
|
+
if (remaining < 0) {
|
|
1876
|
+
return null;
|
|
917
1877
|
}
|
|
1878
|
+
return remaining;
|
|
1879
|
+
}
|
|
1880
|
+
async size() {
|
|
918
1881
|
const keys = await this.keys();
|
|
919
|
-
|
|
920
|
-
|
|
1882
|
+
return keys.length;
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Deletes all keys matching the layer's prefix in batches to avoid
|
|
1886
|
+
* loading millions of keys into memory at once.
|
|
1887
|
+
*/
|
|
1888
|
+
async clear() {
|
|
1889
|
+
if (!this.prefix && !this.allowUnprefixedClear) {
|
|
1890
|
+
throw new Error(
|
|
1891
|
+
"RedisLayer.clear() requires a prefix or allowUnprefixedClear=true to avoid deleting unrelated keys."
|
|
1892
|
+
);
|
|
921
1893
|
}
|
|
1894
|
+
const pattern = `${this.prefix}*`;
|
|
1895
|
+
let cursor = "0";
|
|
1896
|
+
do {
|
|
1897
|
+
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
1898
|
+
cursor = nextCursor;
|
|
1899
|
+
if (keys.length === 0) {
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
1903
|
+
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
1904
|
+
await this.client.del(...batch);
|
|
1905
|
+
}
|
|
1906
|
+
} while (cursor !== "0");
|
|
922
1907
|
}
|
|
923
1908
|
async keys() {
|
|
924
1909
|
const pattern = `${this.prefix}*`;
|
|
@@ -941,6 +1926,205 @@ var RedisLayer = class {
|
|
|
941
1926
|
withPrefix(key) {
|
|
942
1927
|
return `${this.prefix}${key}`;
|
|
943
1928
|
}
|
|
1929
|
+
async deserializeOrDelete(key, payload) {
|
|
1930
|
+
try {
|
|
1931
|
+
return this.serializer.deserialize(this.decodePayload(payload));
|
|
1932
|
+
} catch {
|
|
1933
|
+
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
isSerializablePayload(payload) {
|
|
1938
|
+
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
1939
|
+
}
|
|
1940
|
+
encodePayload(payload) {
|
|
1941
|
+
if (!this.compression) {
|
|
1942
|
+
return payload;
|
|
1943
|
+
}
|
|
1944
|
+
const source = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
1945
|
+
if (source.byteLength < this.compressionThreshold) {
|
|
1946
|
+
return payload;
|
|
1947
|
+
}
|
|
1948
|
+
const header = Buffer.from(`LCZ1:${this.compression}:`);
|
|
1949
|
+
const compressed = this.compression === "gzip" ? gzipSync(source) : brotliCompressSync(source);
|
|
1950
|
+
return Buffer.concat([header, compressed]);
|
|
1951
|
+
}
|
|
1952
|
+
decodePayload(payload) {
|
|
1953
|
+
if (!Buffer.isBuffer(payload)) {
|
|
1954
|
+
return payload;
|
|
1955
|
+
}
|
|
1956
|
+
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
1957
|
+
return gunzipSync(payload.subarray(10));
|
|
1958
|
+
}
|
|
1959
|
+
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
1960
|
+
return brotliDecompressSync(payload.subarray(12));
|
|
1961
|
+
}
|
|
1962
|
+
return payload;
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
// src/layers/DiskLayer.ts
|
|
1967
|
+
import { createHash } from "crypto";
|
|
1968
|
+
import { promises as fs2 } from "fs";
|
|
1969
|
+
import { join } from "path";
|
|
1970
|
+
var DiskLayer = class {
|
|
1971
|
+
name;
|
|
1972
|
+
defaultTtl;
|
|
1973
|
+
isLocal = true;
|
|
1974
|
+
directory;
|
|
1975
|
+
serializer;
|
|
1976
|
+
constructor(options) {
|
|
1977
|
+
this.directory = options.directory;
|
|
1978
|
+
this.defaultTtl = options.ttl;
|
|
1979
|
+
this.name = options.name ?? "disk";
|
|
1980
|
+
this.serializer = options.serializer ?? new JsonSerializer();
|
|
1981
|
+
}
|
|
1982
|
+
async get(key) {
|
|
1983
|
+
return unwrapStoredValue(await this.getEntry(key));
|
|
1984
|
+
}
|
|
1985
|
+
async getEntry(key) {
|
|
1986
|
+
const filePath = this.keyToPath(key);
|
|
1987
|
+
let raw;
|
|
1988
|
+
try {
|
|
1989
|
+
raw = await fs2.readFile(filePath);
|
|
1990
|
+
} catch {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
let entry;
|
|
1994
|
+
try {
|
|
1995
|
+
entry = this.serializer.deserialize(raw);
|
|
1996
|
+
} catch {
|
|
1997
|
+
await this.safeDelete(filePath);
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
2001
|
+
await this.safeDelete(filePath);
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
return entry.value;
|
|
2005
|
+
}
|
|
2006
|
+
async set(key, value, ttl = this.defaultTtl) {
|
|
2007
|
+
await fs2.mkdir(this.directory, { recursive: true });
|
|
2008
|
+
const entry = {
|
|
2009
|
+
value,
|
|
2010
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
2011
|
+
};
|
|
2012
|
+
const payload = this.serializer.serialize(entry);
|
|
2013
|
+
await fs2.writeFile(this.keyToPath(key), payload);
|
|
2014
|
+
}
|
|
2015
|
+
async has(key) {
|
|
2016
|
+
const value = await this.getEntry(key);
|
|
2017
|
+
return value !== null;
|
|
2018
|
+
}
|
|
2019
|
+
async ttl(key) {
|
|
2020
|
+
const filePath = this.keyToPath(key);
|
|
2021
|
+
let raw;
|
|
2022
|
+
try {
|
|
2023
|
+
raw = await fs2.readFile(filePath);
|
|
2024
|
+
} catch {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
let entry;
|
|
2028
|
+
try {
|
|
2029
|
+
entry = this.serializer.deserialize(raw);
|
|
2030
|
+
} catch {
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
if (entry.expiresAt === null) {
|
|
2034
|
+
return null;
|
|
2035
|
+
}
|
|
2036
|
+
const remaining = Math.ceil((entry.expiresAt - Date.now()) / 1e3);
|
|
2037
|
+
if (remaining <= 0) {
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
return remaining;
|
|
2041
|
+
}
|
|
2042
|
+
async delete(key) {
|
|
2043
|
+
await this.safeDelete(this.keyToPath(key));
|
|
2044
|
+
}
|
|
2045
|
+
async deleteMany(keys) {
|
|
2046
|
+
await Promise.all(keys.map((key) => this.delete(key)));
|
|
2047
|
+
}
|
|
2048
|
+
async clear() {
|
|
2049
|
+
let entries;
|
|
2050
|
+
try {
|
|
2051
|
+
entries = await fs2.readdir(this.directory);
|
|
2052
|
+
} catch {
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
await Promise.all(
|
|
2056
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete(join(this.directory, name)))
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
async keys() {
|
|
2060
|
+
let entries;
|
|
2061
|
+
try {
|
|
2062
|
+
entries = await fs2.readdir(this.directory);
|
|
2063
|
+
} catch {
|
|
2064
|
+
return [];
|
|
2065
|
+
}
|
|
2066
|
+
return entries.filter((name) => name.endsWith(".lc")).map((name) => name.slice(0, -3));
|
|
2067
|
+
}
|
|
2068
|
+
async size() {
|
|
2069
|
+
const keys = await this.keys();
|
|
2070
|
+
return keys.length;
|
|
2071
|
+
}
|
|
2072
|
+
keyToPath(key) {
|
|
2073
|
+
const hash = createHash("sha256").update(key).digest("hex");
|
|
2074
|
+
return join(this.directory, `${hash}.lc`);
|
|
2075
|
+
}
|
|
2076
|
+
async safeDelete(filePath) {
|
|
2077
|
+
try {
|
|
2078
|
+
await fs2.unlink(filePath);
|
|
2079
|
+
} catch {
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
|
|
2084
|
+
// src/layers/MemcachedLayer.ts
|
|
2085
|
+
var MemcachedLayer = class {
|
|
2086
|
+
name;
|
|
2087
|
+
defaultTtl;
|
|
2088
|
+
isLocal = false;
|
|
2089
|
+
client;
|
|
2090
|
+
keyPrefix;
|
|
2091
|
+
constructor(options) {
|
|
2092
|
+
this.client = options.client;
|
|
2093
|
+
this.defaultTtl = options.ttl;
|
|
2094
|
+
this.name = options.name ?? "memcached";
|
|
2095
|
+
this.keyPrefix = options.keyPrefix ?? "";
|
|
2096
|
+
}
|
|
2097
|
+
async get(key) {
|
|
2098
|
+
const result = await this.client.get(this.withPrefix(key));
|
|
2099
|
+
if (!result || result.value === null) {
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
try {
|
|
2103
|
+
return JSON.parse(result.value.toString("utf8"));
|
|
2104
|
+
} catch {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
async set(key, value, ttl = this.defaultTtl) {
|
|
2109
|
+
const payload = JSON.stringify(value);
|
|
2110
|
+
await this.client.set(this.withPrefix(key), payload, {
|
|
2111
|
+
expires: ttl && ttl > 0 ? ttl : void 0
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
async delete(key) {
|
|
2115
|
+
await this.client.delete(this.withPrefix(key));
|
|
2116
|
+
}
|
|
2117
|
+
async deleteMany(keys) {
|
|
2118
|
+
await Promise.all(keys.map((key) => this.delete(key)));
|
|
2119
|
+
}
|
|
2120
|
+
async clear() {
|
|
2121
|
+
throw new Error(
|
|
2122
|
+
"MemcachedLayer.clear() is not supported. Use a key prefix and rotate it to effectively invalidate all keys."
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
withPrefix(key) {
|
|
2126
|
+
return `${this.keyPrefix}${key}`;
|
|
2127
|
+
}
|
|
944
2128
|
};
|
|
945
2129
|
|
|
946
2130
|
// src/serialization/MsgpackSerializer.ts
|
|
@@ -984,9 +2168,79 @@ var RedisSingleFlightCoordinator = class {
|
|
|
984
2168
|
return waiter();
|
|
985
2169
|
}
|
|
986
2170
|
};
|
|
2171
|
+
|
|
2172
|
+
// src/metrics/PrometheusExporter.ts
|
|
2173
|
+
function createPrometheusMetricsExporter(stacks) {
|
|
2174
|
+
return () => {
|
|
2175
|
+
const entries = Array.isArray(stacks) ? stacks : [{ stack: stacks, name: "default" }];
|
|
2176
|
+
const lines = [];
|
|
2177
|
+
lines.push("# HELP layercache_hits_total Total number of cache hits");
|
|
2178
|
+
lines.push("# TYPE layercache_hits_total counter");
|
|
2179
|
+
lines.push("# HELP layercache_misses_total Total number of cache misses");
|
|
2180
|
+
lines.push("# TYPE layercache_misses_total counter");
|
|
2181
|
+
lines.push("# HELP layercache_fetches_total Total fetcher invocations (full misses)");
|
|
2182
|
+
lines.push("# TYPE layercache_fetches_total counter");
|
|
2183
|
+
lines.push("# HELP layercache_sets_total Total number of cache sets");
|
|
2184
|
+
lines.push("# TYPE layercache_sets_total counter");
|
|
2185
|
+
lines.push("# HELP layercache_deletes_total Total number of cache deletes");
|
|
2186
|
+
lines.push("# TYPE layercache_deletes_total counter");
|
|
2187
|
+
lines.push("# HELP layercache_backfills_total Total number of backfill operations");
|
|
2188
|
+
lines.push("# TYPE layercache_backfills_total counter");
|
|
2189
|
+
lines.push("# HELP layercache_stale_hits_total Total number of stale hits served");
|
|
2190
|
+
lines.push("# TYPE layercache_stale_hits_total counter");
|
|
2191
|
+
lines.push("# HELP layercache_refreshes_total Background refreshes triggered");
|
|
2192
|
+
lines.push("# TYPE layercache_refreshes_total counter");
|
|
2193
|
+
lines.push("# HELP layercache_refresh_errors_total Background refresh errors");
|
|
2194
|
+
lines.push("# TYPE layercache_refresh_errors_total counter");
|
|
2195
|
+
lines.push("# HELP layercache_negative_cache_hits_total Negative cache hits");
|
|
2196
|
+
lines.push("# TYPE layercache_negative_cache_hits_total counter");
|
|
2197
|
+
lines.push("# HELP layercache_circuit_breaker_trips_total Circuit breaker trips");
|
|
2198
|
+
lines.push("# TYPE layercache_circuit_breaker_trips_total counter");
|
|
2199
|
+
lines.push("# HELP layercache_degraded_operations_total Operations run in degraded mode");
|
|
2200
|
+
lines.push("# TYPE layercache_degraded_operations_total counter");
|
|
2201
|
+
lines.push("# HELP layercache_hit_rate Overall cache hit rate (0-1)");
|
|
2202
|
+
lines.push("# TYPE layercache_hit_rate gauge");
|
|
2203
|
+
lines.push("# HELP layercache_hits_by_layer_total Hits broken down by layer");
|
|
2204
|
+
lines.push("# TYPE layercache_hits_by_layer_total counter");
|
|
2205
|
+
lines.push("# HELP layercache_misses_by_layer_total Misses broken down by layer");
|
|
2206
|
+
lines.push("# TYPE layercache_misses_by_layer_total counter");
|
|
2207
|
+
for (const { stack, name } of entries) {
|
|
2208
|
+
const m = stack.getMetrics();
|
|
2209
|
+
const hr = stack.getHitRate();
|
|
2210
|
+
const label = `cache="${sanitizeLabel(name)}"`;
|
|
2211
|
+
lines.push(`layercache_hits_total{${label}} ${m.hits}`);
|
|
2212
|
+
lines.push(`layercache_misses_total{${label}} ${m.misses}`);
|
|
2213
|
+
lines.push(`layercache_fetches_total{${label}} ${m.fetches}`);
|
|
2214
|
+
lines.push(`layercache_sets_total{${label}} ${m.sets}`);
|
|
2215
|
+
lines.push(`layercache_deletes_total{${label}} ${m.deletes}`);
|
|
2216
|
+
lines.push(`layercache_backfills_total{${label}} ${m.backfills}`);
|
|
2217
|
+
lines.push(`layercache_stale_hits_total{${label}} ${m.staleHits}`);
|
|
2218
|
+
lines.push(`layercache_refreshes_total{${label}} ${m.refreshes}`);
|
|
2219
|
+
lines.push(`layercache_refresh_errors_total{${label}} ${m.refreshErrors}`);
|
|
2220
|
+
lines.push(`layercache_negative_cache_hits_total{${label}} ${m.negativeCacheHits}`);
|
|
2221
|
+
lines.push(`layercache_circuit_breaker_trips_total{${label}} ${m.circuitBreakerTrips}`);
|
|
2222
|
+
lines.push(`layercache_degraded_operations_total{${label}} ${m.degradedOperations}`);
|
|
2223
|
+
lines.push(`layercache_hit_rate{${label}} ${hr.overall.toFixed(6)}`);
|
|
2224
|
+
for (const [layerName, count] of Object.entries(m.hitsByLayer)) {
|
|
2225
|
+
lines.push(`layercache_hits_by_layer_total{${label},layer="${sanitizeLabel(layerName)}"} ${count}`);
|
|
2226
|
+
}
|
|
2227
|
+
for (const [layerName, count] of Object.entries(m.missesByLayer)) {
|
|
2228
|
+
lines.push(`layercache_misses_by_layer_total{${label},layer="${sanitizeLabel(layerName)}"} ${count}`);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
lines.push("");
|
|
2232
|
+
return lines.join("\n");
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function sanitizeLabel(value) {
|
|
2236
|
+
return value.replace(/["\\\n]/g, "_");
|
|
2237
|
+
}
|
|
987
2238
|
export {
|
|
2239
|
+
CacheNamespace,
|
|
988
2240
|
CacheStack,
|
|
2241
|
+
DiskLayer,
|
|
989
2242
|
JsonSerializer,
|
|
2243
|
+
MemcachedLayer,
|
|
990
2244
|
MemoryLayer,
|
|
991
2245
|
MsgpackSerializer,
|
|
992
2246
|
PatternMatcher,
|
|
@@ -995,5 +2249,11 @@ export {
|
|
|
995
2249
|
RedisSingleFlightCoordinator,
|
|
996
2250
|
RedisTagIndex,
|
|
997
2251
|
StampedeGuard,
|
|
998
|
-
TagIndex
|
|
2252
|
+
TagIndex,
|
|
2253
|
+
cacheGraphqlResolver,
|
|
2254
|
+
createCacheStatsHandler,
|
|
2255
|
+
createCachedMethodDecorator,
|
|
2256
|
+
createFastifyLayercachePlugin,
|
|
2257
|
+
createPrometheusMetricsExporter,
|
|
2258
|
+
createTrpcCacheMiddleware
|
|
999
2259
|
};
|