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
|
@@ -30,6 +30,7 @@ var index_exports = {};
|
|
|
30
30
|
__export(index_exports, {
|
|
31
31
|
CACHE_STACK: () => CACHE_STACK,
|
|
32
32
|
CacheStackModule: () => CacheStackModule,
|
|
33
|
+
Cacheable: () => Cacheable,
|
|
33
34
|
InjectCacheStack: () => InjectCacheStack
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -37,11 +38,264 @@ module.exports = __toCommonJS(index_exports);
|
|
|
37
38
|
// src/constants.ts
|
|
38
39
|
var CACHE_STACK = /* @__PURE__ */ Symbol("CACHE_STACK");
|
|
39
40
|
|
|
41
|
+
// ../../src/decorators/createCachedMethodDecorator.ts
|
|
42
|
+
function createCachedMethodDecorator(options) {
|
|
43
|
+
const wrappedByInstance = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
return ((_, propertyKey, descriptor) => {
|
|
45
|
+
const original = descriptor.value;
|
|
46
|
+
if (typeof original !== "function") {
|
|
47
|
+
throw new Error("createCachedMethodDecorator can only be applied to methods.");
|
|
48
|
+
}
|
|
49
|
+
descriptor.value = async function(...args) {
|
|
50
|
+
const instance = this;
|
|
51
|
+
let wrapped = wrappedByInstance.get(instance);
|
|
52
|
+
if (!wrapped) {
|
|
53
|
+
const cache = options.cache(instance);
|
|
54
|
+
wrapped = cache.wrap(
|
|
55
|
+
options.prefix ?? String(propertyKey),
|
|
56
|
+
(...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
|
|
57
|
+
options
|
|
58
|
+
);
|
|
59
|
+
wrappedByInstance.set(instance, wrapped);
|
|
60
|
+
}
|
|
61
|
+
return wrapped(...args);
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/decorators.ts
|
|
67
|
+
function Cacheable(options) {
|
|
68
|
+
return createCachedMethodDecorator(options);
|
|
69
|
+
}
|
|
70
|
+
|
|
40
71
|
// src/module.ts
|
|
41
72
|
var import_common = require("@nestjs/common");
|
|
42
73
|
|
|
43
74
|
// ../../src/CacheStack.ts
|
|
44
75
|
var import_node_crypto = require("crypto");
|
|
76
|
+
var import_node_events = require("events");
|
|
77
|
+
var import_node_fs = require("fs");
|
|
78
|
+
|
|
79
|
+
// ../../src/CacheNamespace.ts
|
|
80
|
+
var CacheNamespace = class {
|
|
81
|
+
constructor(cache, prefix) {
|
|
82
|
+
this.cache = cache;
|
|
83
|
+
this.prefix = prefix;
|
|
84
|
+
}
|
|
85
|
+
cache;
|
|
86
|
+
prefix;
|
|
87
|
+
async get(key, fetcher, options) {
|
|
88
|
+
return this.cache.get(this.qualify(key), fetcher, options);
|
|
89
|
+
}
|
|
90
|
+
async getOrSet(key, fetcher, options) {
|
|
91
|
+
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
92
|
+
}
|
|
93
|
+
async has(key) {
|
|
94
|
+
return this.cache.has(this.qualify(key));
|
|
95
|
+
}
|
|
96
|
+
async ttl(key) {
|
|
97
|
+
return this.cache.ttl(this.qualify(key));
|
|
98
|
+
}
|
|
99
|
+
async set(key, value, options) {
|
|
100
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
101
|
+
}
|
|
102
|
+
async delete(key) {
|
|
103
|
+
await this.cache.delete(this.qualify(key));
|
|
104
|
+
}
|
|
105
|
+
async mdelete(keys) {
|
|
106
|
+
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
107
|
+
}
|
|
108
|
+
async clear() {
|
|
109
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
110
|
+
}
|
|
111
|
+
async mget(entries) {
|
|
112
|
+
return this.cache.mget(
|
|
113
|
+
entries.map((entry) => ({
|
|
114
|
+
...entry,
|
|
115
|
+
key: this.qualify(entry.key)
|
|
116
|
+
}))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
async mset(entries) {
|
|
120
|
+
await this.cache.mset(
|
|
121
|
+
entries.map((entry) => ({
|
|
122
|
+
...entry,
|
|
123
|
+
key: this.qualify(entry.key)
|
|
124
|
+
}))
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
async invalidateByTag(tag) {
|
|
128
|
+
await this.cache.invalidateByTag(tag);
|
|
129
|
+
}
|
|
130
|
+
async invalidateByPattern(pattern) {
|
|
131
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
132
|
+
}
|
|
133
|
+
wrap(keyPrefix, fetcher, options) {
|
|
134
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
135
|
+
}
|
|
136
|
+
warm(entries, options) {
|
|
137
|
+
return this.cache.warm(
|
|
138
|
+
entries.map((entry) => ({
|
|
139
|
+
...entry,
|
|
140
|
+
key: this.qualify(entry.key)
|
|
141
|
+
})),
|
|
142
|
+
options
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
getMetrics() {
|
|
146
|
+
return this.cache.getMetrics();
|
|
147
|
+
}
|
|
148
|
+
getHitRate() {
|
|
149
|
+
return this.cache.getHitRate();
|
|
150
|
+
}
|
|
151
|
+
qualify(key) {
|
|
152
|
+
return `${this.prefix}:${key}`;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// ../../src/internal/CircuitBreakerManager.ts
|
|
157
|
+
var CircuitBreakerManager = class {
|
|
158
|
+
breakers = /* @__PURE__ */ new Map();
|
|
159
|
+
maxEntries;
|
|
160
|
+
constructor(options) {
|
|
161
|
+
this.maxEntries = options.maxEntries;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Throws if the circuit is open for the given key.
|
|
165
|
+
* Automatically resets if the cooldown has elapsed.
|
|
166
|
+
*/
|
|
167
|
+
assertClosed(key, options) {
|
|
168
|
+
const state = this.breakers.get(key);
|
|
169
|
+
if (!state?.openUntil) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
if (state.openUntil <= now) {
|
|
174
|
+
state.openUntil = null;
|
|
175
|
+
state.failures = 0;
|
|
176
|
+
this.breakers.set(key, state);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const remainingMs = state.openUntil - now;
|
|
180
|
+
const remainingSecs = Math.ceil(remainingMs / 1e3);
|
|
181
|
+
throw new Error(`Circuit breaker is open for key "${key}" (resets in ${remainingSecs}s).`);
|
|
182
|
+
}
|
|
183
|
+
recordFailure(key, options) {
|
|
184
|
+
if (!options) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
188
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
189
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
190
|
+
state.failures += 1;
|
|
191
|
+
if (state.failures >= failureThreshold) {
|
|
192
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
193
|
+
}
|
|
194
|
+
this.breakers.set(key, state);
|
|
195
|
+
this.pruneIfNeeded();
|
|
196
|
+
}
|
|
197
|
+
recordSuccess(key) {
|
|
198
|
+
this.breakers.delete(key);
|
|
199
|
+
}
|
|
200
|
+
isOpen(key) {
|
|
201
|
+
const state = this.breakers.get(key);
|
|
202
|
+
if (!state?.openUntil) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (state.openUntil <= Date.now()) {
|
|
206
|
+
state.openUntil = null;
|
|
207
|
+
state.failures = 0;
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
delete(key) {
|
|
213
|
+
this.breakers.delete(key);
|
|
214
|
+
}
|
|
215
|
+
clear() {
|
|
216
|
+
this.breakers.clear();
|
|
217
|
+
}
|
|
218
|
+
tripCount() {
|
|
219
|
+
let count = 0;
|
|
220
|
+
for (const state of this.breakers.values()) {
|
|
221
|
+
if (state.openUntil !== null) {
|
|
222
|
+
count += 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return count;
|
|
226
|
+
}
|
|
227
|
+
pruneIfNeeded() {
|
|
228
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const [key, state] of this.breakers.entries()) {
|
|
232
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (!state.openUntil || state.openUntil <= Date.now()) {
|
|
236
|
+
this.breakers.delete(key);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const key of this.breakers.keys()) {
|
|
240
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
this.breakers.delete(key);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// ../../src/internal/MetricsCollector.ts
|
|
249
|
+
var MetricsCollector = class {
|
|
250
|
+
data = this.empty();
|
|
251
|
+
get snapshot() {
|
|
252
|
+
return { ...this.data };
|
|
253
|
+
}
|
|
254
|
+
increment(field, amount = 1) {
|
|
255
|
+
;
|
|
256
|
+
this.data[field] += amount;
|
|
257
|
+
}
|
|
258
|
+
incrementLayer(map, layerName) {
|
|
259
|
+
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
260
|
+
}
|
|
261
|
+
reset() {
|
|
262
|
+
this.data = this.empty();
|
|
263
|
+
}
|
|
264
|
+
hitRate() {
|
|
265
|
+
const total = this.data.hits + this.data.misses;
|
|
266
|
+
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
267
|
+
const byLayer = {};
|
|
268
|
+
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
269
|
+
for (const layer of allLayers) {
|
|
270
|
+
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
271
|
+
const m = this.data.missesByLayer[layer] ?? 0;
|
|
272
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
273
|
+
}
|
|
274
|
+
return { overall, byLayer };
|
|
275
|
+
}
|
|
276
|
+
empty() {
|
|
277
|
+
return {
|
|
278
|
+
hits: 0,
|
|
279
|
+
misses: 0,
|
|
280
|
+
fetches: 0,
|
|
281
|
+
sets: 0,
|
|
282
|
+
deletes: 0,
|
|
283
|
+
backfills: 0,
|
|
284
|
+
invalidations: 0,
|
|
285
|
+
staleHits: 0,
|
|
286
|
+
refreshes: 0,
|
|
287
|
+
refreshErrors: 0,
|
|
288
|
+
writeFailures: 0,
|
|
289
|
+
singleFlightWaits: 0,
|
|
290
|
+
negativeCacheHits: 0,
|
|
291
|
+
circuitBreakerTrips: 0,
|
|
292
|
+
degradedOperations: 0,
|
|
293
|
+
hitsByLayer: {},
|
|
294
|
+
missesByLayer: {},
|
|
295
|
+
resetAt: Date.now()
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
};
|
|
45
299
|
|
|
46
300
|
// ../../src/internal/StoredValue.ts
|
|
47
301
|
function isStoredValueEnvelope(value) {
|
|
@@ -61,7 +315,10 @@ function createStoredValueEnvelope(options) {
|
|
|
61
315
|
value: options.value,
|
|
62
316
|
freshUntil,
|
|
63
317
|
staleUntil,
|
|
64
|
-
errorUntil
|
|
318
|
+
errorUntil,
|
|
319
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
320
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
321
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
65
322
|
};
|
|
66
323
|
}
|
|
67
324
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -102,6 +359,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
102
359
|
}
|
|
103
360
|
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
104
361
|
}
|
|
362
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
363
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
const remainingMs = stored.freshUntil - now;
|
|
367
|
+
if (remainingMs <= 0) {
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
371
|
+
}
|
|
372
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
373
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
374
|
+
return stored;
|
|
375
|
+
}
|
|
376
|
+
return createStoredValueEnvelope({
|
|
377
|
+
kind: stored.kind,
|
|
378
|
+
value: stored.value,
|
|
379
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
380
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
381
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
382
|
+
now
|
|
383
|
+
});
|
|
384
|
+
}
|
|
105
385
|
function maxExpiry(stored) {
|
|
106
386
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
107
387
|
(value) => value !== null
|
|
@@ -118,12 +398,129 @@ function normalizePositiveSeconds(value) {
|
|
|
118
398
|
return value;
|
|
119
399
|
}
|
|
120
400
|
|
|
401
|
+
// ../../src/internal/TtlResolver.ts
|
|
402
|
+
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
403
|
+
var TtlResolver = class {
|
|
404
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
405
|
+
maxProfileEntries;
|
|
406
|
+
constructor(options) {
|
|
407
|
+
this.maxProfileEntries = options.maxProfileEntries;
|
|
408
|
+
}
|
|
409
|
+
recordAccess(key) {
|
|
410
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
411
|
+
profile.hits += 1;
|
|
412
|
+
profile.lastAccessAt = Date.now();
|
|
413
|
+
this.accessProfiles.set(key, profile);
|
|
414
|
+
this.pruneIfNeeded();
|
|
415
|
+
}
|
|
416
|
+
deleteProfile(key) {
|
|
417
|
+
this.accessProfiles.delete(key);
|
|
418
|
+
}
|
|
419
|
+
clearProfiles() {
|
|
420
|
+
this.accessProfiles.clear();
|
|
421
|
+
}
|
|
422
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
423
|
+
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
424
|
+
layerName,
|
|
425
|
+
options?.negativeTtl,
|
|
426
|
+
globalNegativeTtl,
|
|
427
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
428
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
429
|
+
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
430
|
+
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
431
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
432
|
+
}
|
|
433
|
+
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
434
|
+
if (override !== void 0) {
|
|
435
|
+
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
436
|
+
}
|
|
437
|
+
if (globalDefault !== void 0) {
|
|
438
|
+
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
439
|
+
}
|
|
440
|
+
return fallback;
|
|
441
|
+
}
|
|
442
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
443
|
+
if (!ttl || !adaptiveTtl) {
|
|
444
|
+
return ttl;
|
|
445
|
+
}
|
|
446
|
+
const profile = this.accessProfiles.get(key);
|
|
447
|
+
if (!profile) {
|
|
448
|
+
return ttl;
|
|
449
|
+
}
|
|
450
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
451
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
452
|
+
if (profile.hits < hotAfter) {
|
|
453
|
+
return ttl;
|
|
454
|
+
}
|
|
455
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
456
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
457
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
458
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
459
|
+
}
|
|
460
|
+
applyJitter(ttl, jitter) {
|
|
461
|
+
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
462
|
+
return ttl;
|
|
463
|
+
}
|
|
464
|
+
const delta = (Math.random() * 2 - 1) * jitter;
|
|
465
|
+
return Math.max(1, Math.round(ttl + delta));
|
|
466
|
+
}
|
|
467
|
+
readLayerNumber(layerName, value) {
|
|
468
|
+
if (typeof value === "number") {
|
|
469
|
+
return value;
|
|
470
|
+
}
|
|
471
|
+
return value[layerName];
|
|
472
|
+
}
|
|
473
|
+
pruneIfNeeded() {
|
|
474
|
+
if (this.accessProfiles.size <= this.maxProfileEntries) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
478
|
+
let removed = 0;
|
|
479
|
+
for (const key of this.accessProfiles.keys()) {
|
|
480
|
+
if (removed >= toRemove) {
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
this.accessProfiles.delete(key);
|
|
484
|
+
removed += 1;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
121
489
|
// ../../src/invalidation/PatternMatcher.ts
|
|
122
|
-
var PatternMatcher = class {
|
|
490
|
+
var PatternMatcher = class _PatternMatcher {
|
|
491
|
+
/**
|
|
492
|
+
* Tests whether a glob-style pattern matches a value.
|
|
493
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
494
|
+
* Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
|
|
495
|
+
*/
|
|
123
496
|
static matches(pattern, value) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
497
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Linear-time glob matching using dynamic programming.
|
|
501
|
+
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
502
|
+
*/
|
|
503
|
+
static matchLinear(pattern, value) {
|
|
504
|
+
const m = pattern.length;
|
|
505
|
+
const n = value.length;
|
|
506
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
|
|
507
|
+
dp[0][0] = true;
|
|
508
|
+
for (let i = 1; i <= m; i++) {
|
|
509
|
+
if (pattern[i - 1] === "*") {
|
|
510
|
+
dp[i][0] = dp[i - 1]?.[0];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
for (let i = 1; i <= m; i++) {
|
|
514
|
+
for (let j = 1; j <= n; j++) {
|
|
515
|
+
const pc = pattern[i - 1];
|
|
516
|
+
if (pc === "*") {
|
|
517
|
+
dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
|
|
518
|
+
} else if (pc === "?" || pc === value[j - 1]) {
|
|
519
|
+
dp[i][j] = dp[i - 1]?.[j - 1];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return dp[m]?.[n];
|
|
127
524
|
}
|
|
128
525
|
};
|
|
129
526
|
|
|
@@ -364,64 +761,75 @@ var Mutex = class {
|
|
|
364
761
|
var StampedeGuard = class {
|
|
365
762
|
mutexes = /* @__PURE__ */ new Map();
|
|
366
763
|
async execute(key, task) {
|
|
367
|
-
const
|
|
764
|
+
const entry = this.getMutexEntry(key);
|
|
368
765
|
try {
|
|
369
|
-
return await mutex.runExclusive(task);
|
|
766
|
+
return await entry.mutex.runExclusive(task);
|
|
370
767
|
} finally {
|
|
371
|
-
|
|
768
|
+
entry.references -= 1;
|
|
769
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
372
770
|
this.mutexes.delete(key);
|
|
373
771
|
}
|
|
374
772
|
}
|
|
375
773
|
}
|
|
376
|
-
|
|
377
|
-
let
|
|
378
|
-
if (!
|
|
379
|
-
|
|
380
|
-
this.mutexes.set(key,
|
|
774
|
+
getMutexEntry(key) {
|
|
775
|
+
let entry = this.mutexes.get(key);
|
|
776
|
+
if (!entry) {
|
|
777
|
+
entry = { mutex: new Mutex(), references: 0 };
|
|
778
|
+
this.mutexes.set(key, entry);
|
|
381
779
|
}
|
|
382
|
-
|
|
780
|
+
entry.references += 1;
|
|
781
|
+
return entry;
|
|
383
782
|
}
|
|
384
783
|
};
|
|
385
784
|
|
|
386
785
|
// ../../src/CacheStack.ts
|
|
387
|
-
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
388
786
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
389
787
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
390
788
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
391
|
-
var
|
|
392
|
-
|
|
393
|
-
misses: 0,
|
|
394
|
-
fetches: 0,
|
|
395
|
-
sets: 0,
|
|
396
|
-
deletes: 0,
|
|
397
|
-
backfills: 0,
|
|
398
|
-
invalidations: 0,
|
|
399
|
-
staleHits: 0,
|
|
400
|
-
refreshes: 0,
|
|
401
|
-
refreshErrors: 0,
|
|
402
|
-
writeFailures: 0,
|
|
403
|
-
singleFlightWaits: 0
|
|
404
|
-
});
|
|
789
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
790
|
+
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
405
791
|
var DebugLogger = class {
|
|
406
792
|
enabled;
|
|
407
793
|
constructor(enabled) {
|
|
408
794
|
this.enabled = enabled;
|
|
409
795
|
}
|
|
410
796
|
debug(message, context) {
|
|
797
|
+
this.write("debug", message, context);
|
|
798
|
+
}
|
|
799
|
+
info(message, context) {
|
|
800
|
+
this.write("info", message, context);
|
|
801
|
+
}
|
|
802
|
+
warn(message, context) {
|
|
803
|
+
this.write("warn", message, context);
|
|
804
|
+
}
|
|
805
|
+
error(message, context) {
|
|
806
|
+
this.write("error", message, context);
|
|
807
|
+
}
|
|
808
|
+
write(level, message, context) {
|
|
411
809
|
if (!this.enabled) {
|
|
412
810
|
return;
|
|
413
811
|
}
|
|
414
812
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
415
|
-
console
|
|
813
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
416
814
|
}
|
|
417
815
|
};
|
|
418
|
-
var CacheStack = class {
|
|
816
|
+
var CacheStack = class extends import_node_events.EventEmitter {
|
|
419
817
|
constructor(layers, options = {}) {
|
|
818
|
+
super();
|
|
420
819
|
this.layers = layers;
|
|
421
820
|
this.options = options;
|
|
422
821
|
if (layers.length === 0) {
|
|
423
822
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
424
823
|
}
|
|
824
|
+
this.validateConfiguration();
|
|
825
|
+
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
826
|
+
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
827
|
+
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
828
|
+
if (options.publishSetInvalidation !== void 0) {
|
|
829
|
+
console.warn(
|
|
830
|
+
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
831
|
+
);
|
|
832
|
+
}
|
|
425
833
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
426
834
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
427
835
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -430,112 +838,313 @@ var CacheStack = class {
|
|
|
430
838
|
layers;
|
|
431
839
|
options;
|
|
432
840
|
stampedeGuard = new StampedeGuard();
|
|
433
|
-
|
|
841
|
+
metricsCollector = new MetricsCollector();
|
|
434
842
|
instanceId = (0, import_node_crypto.randomUUID)();
|
|
435
843
|
startup;
|
|
436
844
|
unsubscribeInvalidation;
|
|
437
845
|
logger;
|
|
438
846
|
tagIndex;
|
|
439
847
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
848
|
+
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
849
|
+
ttlResolver;
|
|
850
|
+
circuitBreakerManager;
|
|
851
|
+
isDisconnecting = false;
|
|
852
|
+
disconnectPromise;
|
|
853
|
+
/**
|
|
854
|
+
* Read-through cache get.
|
|
855
|
+
* Returns the cached value if present and fresh, or invokes `fetcher` on a miss
|
|
856
|
+
* and stores the result across all layers. Returns `null` if the key is not found
|
|
857
|
+
* and no `fetcher` is provided.
|
|
858
|
+
*/
|
|
440
859
|
async get(key, fetcher, options) {
|
|
860
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
861
|
+
this.validateWriteOptions(options);
|
|
441
862
|
await this.startup;
|
|
442
|
-
const hit = await this.readFromLayers(
|
|
863
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
443
864
|
if (hit.found) {
|
|
865
|
+
this.ttlResolver.recordAccess(normalizedKey);
|
|
866
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
867
|
+
this.metricsCollector.increment("negativeCacheHits");
|
|
868
|
+
}
|
|
444
869
|
if (hit.state === "fresh") {
|
|
445
|
-
this.
|
|
870
|
+
this.metricsCollector.increment("hits");
|
|
871
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
446
872
|
return hit.value;
|
|
447
873
|
}
|
|
448
874
|
if (hit.state === "stale-while-revalidate") {
|
|
449
|
-
this.
|
|
450
|
-
this.
|
|
875
|
+
this.metricsCollector.increment("hits");
|
|
876
|
+
this.metricsCollector.increment("staleHits");
|
|
877
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
451
878
|
if (fetcher) {
|
|
452
|
-
this.scheduleBackgroundRefresh(
|
|
879
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
453
880
|
}
|
|
454
881
|
return hit.value;
|
|
455
882
|
}
|
|
456
883
|
if (!fetcher) {
|
|
457
|
-
this.
|
|
458
|
-
this.
|
|
884
|
+
this.metricsCollector.increment("hits");
|
|
885
|
+
this.metricsCollector.increment("staleHits");
|
|
886
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
459
887
|
return hit.value;
|
|
460
888
|
}
|
|
461
889
|
try {
|
|
462
|
-
return await this.fetchWithGuards(
|
|
890
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
463
891
|
} catch (error) {
|
|
464
|
-
this.
|
|
465
|
-
this.
|
|
466
|
-
this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
|
|
892
|
+
this.metricsCollector.increment("staleHits");
|
|
893
|
+
this.metricsCollector.increment("refreshErrors");
|
|
894
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
467
895
|
return hit.value;
|
|
468
896
|
}
|
|
469
897
|
}
|
|
470
|
-
this.
|
|
898
|
+
this.metricsCollector.increment("misses");
|
|
471
899
|
if (!fetcher) {
|
|
472
900
|
return null;
|
|
473
901
|
}
|
|
474
|
-
return this.fetchWithGuards(
|
|
902
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
906
|
+
* Fetches and caches the value if not already present.
|
|
907
|
+
*/
|
|
908
|
+
async getOrSet(key, fetcher, options) {
|
|
909
|
+
return this.get(key, fetcher, options);
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Returns true if the given key exists and is not expired in any layer.
|
|
913
|
+
*/
|
|
914
|
+
async has(key) {
|
|
915
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
916
|
+
await this.startup;
|
|
917
|
+
for (const layer of this.layers) {
|
|
918
|
+
if (this.shouldSkipLayer(layer)) {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (layer.has) {
|
|
922
|
+
try {
|
|
923
|
+
const exists = await layer.has(normalizedKey);
|
|
924
|
+
if (exists) {
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
} catch {
|
|
928
|
+
}
|
|
929
|
+
} else {
|
|
930
|
+
try {
|
|
931
|
+
const value = await layer.get(normalizedKey);
|
|
932
|
+
if (value !== null) {
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Returns the remaining TTL in seconds for the key in the fastest layer
|
|
943
|
+
* that has it, or null if the key is not found / has no TTL.
|
|
944
|
+
*/
|
|
945
|
+
async ttl(key) {
|
|
946
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
947
|
+
await this.startup;
|
|
948
|
+
for (const layer of this.layers) {
|
|
949
|
+
if (this.shouldSkipLayer(layer)) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (layer.ttl) {
|
|
953
|
+
try {
|
|
954
|
+
const remaining = await layer.ttl(normalizedKey);
|
|
955
|
+
if (remaining !== null) {
|
|
956
|
+
return remaining;
|
|
957
|
+
}
|
|
958
|
+
} catch {
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return null;
|
|
475
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* Stores a value in all cache layers. Overwrites any existing value.
|
|
966
|
+
*/
|
|
476
967
|
async set(key, value, options) {
|
|
968
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
969
|
+
this.validateWriteOptions(options);
|
|
477
970
|
await this.startup;
|
|
478
|
-
await this.storeEntry(
|
|
971
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
479
972
|
}
|
|
973
|
+
/**
|
|
974
|
+
* Deletes the key from all layers and publishes an invalidation message.
|
|
975
|
+
*/
|
|
480
976
|
async delete(key) {
|
|
977
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
481
978
|
await this.startup;
|
|
482
|
-
await this.deleteKeys([
|
|
483
|
-
await this.publishInvalidation({
|
|
979
|
+
await this.deleteKeys([normalizedKey]);
|
|
980
|
+
await this.publishInvalidation({
|
|
981
|
+
scope: "key",
|
|
982
|
+
keys: [normalizedKey],
|
|
983
|
+
sourceId: this.instanceId,
|
|
984
|
+
operation: "delete"
|
|
985
|
+
});
|
|
484
986
|
}
|
|
485
987
|
async clear() {
|
|
486
988
|
await this.startup;
|
|
487
989
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
488
990
|
await this.tagIndex.clear();
|
|
489
|
-
this.
|
|
490
|
-
this.
|
|
991
|
+
this.ttlResolver.clearProfiles();
|
|
992
|
+
this.circuitBreakerManager.clear();
|
|
993
|
+
this.metricsCollector.increment("invalidations");
|
|
994
|
+
this.logger.debug?.("clear");
|
|
491
995
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
492
996
|
}
|
|
997
|
+
/**
|
|
998
|
+
* Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
|
|
999
|
+
*/
|
|
1000
|
+
async mdelete(keys) {
|
|
1001
|
+
if (keys.length === 0) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
await this.startup;
|
|
1005
|
+
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
1006
|
+
await this.deleteKeys(normalizedKeys);
|
|
1007
|
+
await this.publishInvalidation({
|
|
1008
|
+
scope: "keys",
|
|
1009
|
+
keys: normalizedKeys,
|
|
1010
|
+
sourceId: this.instanceId,
|
|
1011
|
+
operation: "delete"
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
493
1014
|
async mget(entries) {
|
|
494
1015
|
if (entries.length === 0) {
|
|
495
1016
|
return [];
|
|
496
1017
|
}
|
|
497
|
-
const
|
|
1018
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1019
|
+
...entry,
|
|
1020
|
+
key: this.validateCacheKey(entry.key)
|
|
1021
|
+
}));
|
|
1022
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1023
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
498
1024
|
if (!canFastPath) {
|
|
499
|
-
|
|
1025
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
1026
|
+
return Promise.all(
|
|
1027
|
+
normalizedEntries.map((entry) => {
|
|
1028
|
+
const optionsSignature = this.serializeOptions(entry.options);
|
|
1029
|
+
const existing = pendingReads.get(entry.key);
|
|
1030
|
+
if (!existing) {
|
|
1031
|
+
const promise = this.get(entry.key, entry.fetch, entry.options);
|
|
1032
|
+
pendingReads.set(entry.key, {
|
|
1033
|
+
promise,
|
|
1034
|
+
fetch: entry.fetch,
|
|
1035
|
+
optionsSignature
|
|
1036
|
+
});
|
|
1037
|
+
return promise;
|
|
1038
|
+
}
|
|
1039
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
1040
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
1041
|
+
}
|
|
1042
|
+
return existing.promise;
|
|
1043
|
+
})
|
|
1044
|
+
);
|
|
500
1045
|
}
|
|
501
1046
|
await this.startup;
|
|
502
|
-
const pending = new Set(
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1047
|
+
const pending = /* @__PURE__ */ new Set();
|
|
1048
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
1049
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
1050
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
1051
|
+
const entry = normalizedEntries[index];
|
|
1052
|
+
if (!entry) continue;
|
|
1053
|
+
const key = entry.key;
|
|
1054
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
1055
|
+
indexes.push(index);
|
|
1056
|
+
indexesByKey.set(key, indexes);
|
|
1057
|
+
pending.add(key);
|
|
1058
|
+
}
|
|
1059
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
1060
|
+
const layer = this.layers[layerIndex];
|
|
1061
|
+
if (!layer) continue;
|
|
1062
|
+
const keys = [...pending];
|
|
1063
|
+
if (keys.length === 0) {
|
|
507
1064
|
break;
|
|
508
1065
|
}
|
|
509
|
-
const keys = indexes.map((index) => entries[index].key);
|
|
510
1066
|
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
511
1067
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
512
|
-
const
|
|
1068
|
+
const key = keys[offset];
|
|
513
1069
|
const stored = values[offset];
|
|
514
|
-
if (stored === null) {
|
|
1070
|
+
if (!key || stored === null) {
|
|
515
1071
|
continue;
|
|
516
1072
|
}
|
|
517
1073
|
const resolved = resolveStoredValue(stored);
|
|
518
1074
|
if (resolved.state === "expired") {
|
|
519
|
-
await layer.delete(
|
|
1075
|
+
await layer.delete(key);
|
|
520
1076
|
continue;
|
|
521
1077
|
}
|
|
522
|
-
await this.tagIndex.touch(
|
|
523
|
-
await this.backfill(
|
|
524
|
-
|
|
525
|
-
pending.delete(
|
|
526
|
-
this.
|
|
1078
|
+
await this.tagIndex.touch(key);
|
|
1079
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
1080
|
+
resultsByKey.set(key, resolved.value);
|
|
1081
|
+
pending.delete(key);
|
|
1082
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
527
1083
|
}
|
|
528
1084
|
}
|
|
529
1085
|
if (pending.size > 0) {
|
|
530
|
-
for (const
|
|
531
|
-
await this.tagIndex.remove(
|
|
532
|
-
this.
|
|
1086
|
+
for (const key of pending) {
|
|
1087
|
+
await this.tagIndex.remove(key);
|
|
1088
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
533
1089
|
}
|
|
534
1090
|
}
|
|
535
|
-
return
|
|
1091
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
536
1092
|
}
|
|
537
1093
|
async mset(entries) {
|
|
538
|
-
|
|
1094
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1095
|
+
...entry,
|
|
1096
|
+
key: this.validateCacheKey(entry.key)
|
|
1097
|
+
}));
|
|
1098
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1099
|
+
await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
1100
|
+
}
|
|
1101
|
+
async warm(entries, options = {}) {
|
|
1102
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
1103
|
+
const total = entries.length;
|
|
1104
|
+
let completed = 0;
|
|
1105
|
+
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
1106
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length || 1) }, async () => {
|
|
1107
|
+
while (queue.length > 0) {
|
|
1108
|
+
const entry = queue.shift();
|
|
1109
|
+
if (!entry) {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
let success = false;
|
|
1113
|
+
try {
|
|
1114
|
+
await this.get(entry.key, entry.fetcher, entry.options);
|
|
1115
|
+
this.emit("warm", { key: entry.key });
|
|
1116
|
+
success = true;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
1119
|
+
if (!options.continueOnError) {
|
|
1120
|
+
throw error;
|
|
1121
|
+
}
|
|
1122
|
+
} finally {
|
|
1123
|
+
completed += 1;
|
|
1124
|
+
const progress = { completed, total, key: entry.key, success };
|
|
1125
|
+
options.onProgress?.(progress);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
await Promise.all(workers);
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Returns a cached version of `fetcher`. The cache key is derived from
|
|
1133
|
+
* `prefix` plus the serialized arguments unless a `keyResolver` is provided.
|
|
1134
|
+
*/
|
|
1135
|
+
wrap(prefix, fetcher, options = {}) {
|
|
1136
|
+
return (...args) => {
|
|
1137
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
1138
|
+
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1139
|
+
return this.get(key, () => fetcher(...args), options);
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Creates a `CacheNamespace` that automatically prefixes all keys with
|
|
1144
|
+
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1145
|
+
*/
|
|
1146
|
+
namespace(prefix) {
|
|
1147
|
+
return new CacheNamespace(this, prefix);
|
|
539
1148
|
}
|
|
540
1149
|
async invalidateByTag(tag) {
|
|
541
1150
|
await this.startup;
|
|
@@ -550,15 +1159,94 @@ var CacheStack = class {
|
|
|
550
1159
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
551
1160
|
}
|
|
552
1161
|
getMetrics() {
|
|
553
|
-
return
|
|
1162
|
+
return this.metricsCollector.snapshot;
|
|
1163
|
+
}
|
|
1164
|
+
getStats() {
|
|
1165
|
+
return {
|
|
1166
|
+
metrics: this.getMetrics(),
|
|
1167
|
+
layers: this.layers.map((layer) => ({
|
|
1168
|
+
name: layer.name,
|
|
1169
|
+
isLocal: Boolean(layer.isLocal),
|
|
1170
|
+
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
1171
|
+
})),
|
|
1172
|
+
backgroundRefreshes: this.backgroundRefreshes.size
|
|
1173
|
+
};
|
|
554
1174
|
}
|
|
555
1175
|
resetMetrics() {
|
|
556
|
-
|
|
1176
|
+
this.metricsCollector.reset();
|
|
557
1177
|
}
|
|
558
|
-
|
|
1178
|
+
/**
|
|
1179
|
+
* Returns computed hit-rate statistics (overall and per-layer).
|
|
1180
|
+
*/
|
|
1181
|
+
getHitRate() {
|
|
1182
|
+
return this.metricsCollector.hitRate();
|
|
1183
|
+
}
|
|
1184
|
+
async exportState() {
|
|
1185
|
+
await this.startup;
|
|
1186
|
+
const exported = /* @__PURE__ */ new Map();
|
|
1187
|
+
for (const layer of this.layers) {
|
|
1188
|
+
if (!layer.keys) {
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const keys = await layer.keys();
|
|
1192
|
+
for (const key of keys) {
|
|
1193
|
+
if (exported.has(key)) {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
1197
|
+
if (stored === null) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
exported.set(key, {
|
|
1201
|
+
key,
|
|
1202
|
+
value: stored,
|
|
1203
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return [...exported.values()];
|
|
1208
|
+
}
|
|
1209
|
+
async importState(entries) {
|
|
559
1210
|
await this.startup;
|
|
560
|
-
await
|
|
561
|
-
|
|
1211
|
+
await Promise.all(
|
|
1212
|
+
entries.map(async (entry) => {
|
|
1213
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1214
|
+
await this.tagIndex.touch(entry.key);
|
|
1215
|
+
})
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
async persistToFile(filePath) {
|
|
1219
|
+
const snapshot = await this.exportState();
|
|
1220
|
+
await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1221
|
+
}
|
|
1222
|
+
async restoreFromFile(filePath) {
|
|
1223
|
+
const raw = await import_node_fs.promises.readFile(filePath, "utf8");
|
|
1224
|
+
let parsed;
|
|
1225
|
+
try {
|
|
1226
|
+
parsed = JSON.parse(raw, (_key, value) => {
|
|
1227
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1228
|
+
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1229
|
+
}
|
|
1230
|
+
return value;
|
|
1231
|
+
});
|
|
1232
|
+
} catch (cause) {
|
|
1233
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1234
|
+
}
|
|
1235
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1236
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1237
|
+
}
|
|
1238
|
+
await this.importState(parsed);
|
|
1239
|
+
}
|
|
1240
|
+
async disconnect() {
|
|
1241
|
+
if (!this.disconnectPromise) {
|
|
1242
|
+
this.isDisconnecting = true;
|
|
1243
|
+
this.disconnectPromise = (async () => {
|
|
1244
|
+
await this.startup;
|
|
1245
|
+
await this.unsubscribeInvalidation?.();
|
|
1246
|
+
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1247
|
+
})();
|
|
1248
|
+
}
|
|
1249
|
+
await this.disconnectPromise;
|
|
562
1250
|
}
|
|
563
1251
|
async initialize() {
|
|
564
1252
|
if (!this.options.invalidationBus) {
|
|
@@ -572,7 +1260,7 @@ var CacheStack = class {
|
|
|
572
1260
|
const fetchTask = async () => {
|
|
573
1261
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
574
1262
|
if (secondHit.found) {
|
|
575
|
-
this.
|
|
1263
|
+
this.metricsCollector.increment("hits");
|
|
576
1264
|
return secondHit.value;
|
|
577
1265
|
}
|
|
578
1266
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
@@ -597,11 +1285,12 @@ var CacheStack = class {
|
|
|
597
1285
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
598
1286
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
599
1287
|
const deadline = Date.now() + timeoutMs;
|
|
600
|
-
this.
|
|
1288
|
+
this.metricsCollector.increment("singleFlightWaits");
|
|
1289
|
+
this.emit("stampede-dedupe", { key });
|
|
601
1290
|
while (Date.now() < deadline) {
|
|
602
1291
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
603
1292
|
if (hit.found) {
|
|
604
|
-
this.
|
|
1293
|
+
this.metricsCollector.increment("hits");
|
|
605
1294
|
return hit.value;
|
|
606
1295
|
}
|
|
607
1296
|
await this.sleep(pollIntervalMs);
|
|
@@ -609,8 +1298,18 @@ var CacheStack = class {
|
|
|
609
1298
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
610
1299
|
}
|
|
611
1300
|
async fetchAndPopulate(key, fetcher, options) {
|
|
612
|
-
this.
|
|
613
|
-
|
|
1301
|
+
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1302
|
+
this.metricsCollector.increment("fetches");
|
|
1303
|
+
const fetchStart = Date.now();
|
|
1304
|
+
let fetched;
|
|
1305
|
+
try {
|
|
1306
|
+
fetched = await fetcher();
|
|
1307
|
+
this.circuitBreakerManager.recordSuccess(key);
|
|
1308
|
+
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
614
1313
|
if (fetched === null || fetched === void 0) {
|
|
615
1314
|
if (!this.shouldNegativeCache(options)) {
|
|
616
1315
|
return null;
|
|
@@ -628,9 +1327,10 @@ var CacheStack = class {
|
|
|
628
1327
|
} else {
|
|
629
1328
|
await this.tagIndex.touch(key);
|
|
630
1329
|
}
|
|
631
|
-
this.
|
|
632
|
-
this.logger.debug("set", { key, kind, tags: options?.tags });
|
|
633
|
-
|
|
1330
|
+
this.metricsCollector.increment("sets");
|
|
1331
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
1332
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
1333
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
634
1334
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
635
1335
|
}
|
|
636
1336
|
}
|
|
@@ -638,8 +1338,10 @@ var CacheStack = class {
|
|
|
638
1338
|
let sawRetainableValue = false;
|
|
639
1339
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
640
1340
|
const layer = this.layers[index];
|
|
1341
|
+
if (!layer) continue;
|
|
641
1342
|
const stored = await this.readLayerEntry(layer, key);
|
|
642
1343
|
if (stored === null) {
|
|
1344
|
+
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
643
1345
|
continue;
|
|
644
1346
|
}
|
|
645
1347
|
const resolved = resolveStoredValue(stored);
|
|
@@ -653,20 +1355,41 @@ var CacheStack = class {
|
|
|
653
1355
|
}
|
|
654
1356
|
await this.tagIndex.touch(key);
|
|
655
1357
|
await this.backfill(key, stored, index - 1, options);
|
|
656
|
-
this.
|
|
657
|
-
|
|
1358
|
+
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
1359
|
+
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
1360
|
+
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
1361
|
+
return {
|
|
1362
|
+
found: true,
|
|
1363
|
+
value: resolved.value,
|
|
1364
|
+
stored,
|
|
1365
|
+
state: resolved.state,
|
|
1366
|
+
layerIndex: index,
|
|
1367
|
+
layerName: layer.name
|
|
1368
|
+
};
|
|
658
1369
|
}
|
|
659
1370
|
if (!sawRetainableValue) {
|
|
660
1371
|
await this.tagIndex.remove(key);
|
|
661
1372
|
}
|
|
662
|
-
this.logger.debug("miss", { key, mode });
|
|
1373
|
+
this.logger.debug?.("miss", { key, mode });
|
|
1374
|
+
this.emit("miss", { key, mode });
|
|
663
1375
|
return { found: false, value: null, stored: null, state: "miss" };
|
|
664
1376
|
}
|
|
665
1377
|
async readLayerEntry(layer, key) {
|
|
1378
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
666
1381
|
if (layer.getEntry) {
|
|
667
|
-
|
|
1382
|
+
try {
|
|
1383
|
+
return await layer.getEntry(key);
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
try {
|
|
1389
|
+
return await layer.get(key);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
668
1392
|
}
|
|
669
|
-
return layer.get(key);
|
|
670
1393
|
}
|
|
671
1394
|
async backfill(key, stored, upToIndex, options) {
|
|
672
1395
|
if (upToIndex < 0) {
|
|
@@ -674,26 +1397,34 @@ var CacheStack = class {
|
|
|
674
1397
|
}
|
|
675
1398
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
676
1399
|
const layer = this.layers[index];
|
|
1400
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
677
1403
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1404
|
+
try {
|
|
1405
|
+
await layer.set(key, stored, ttl);
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
this.metricsCollector.increment("backfills");
|
|
1411
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1412
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
681
1413
|
}
|
|
682
1414
|
}
|
|
683
1415
|
async writeAcrossLayers(key, kind, value, options) {
|
|
684
1416
|
const now = Date.now();
|
|
685
1417
|
const operations = this.layers.map((layer) => async () => {
|
|
686
|
-
|
|
1418
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
687
1422
|
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
688
1423
|
layer.name,
|
|
689
1424
|
options?.staleWhileRevalidate,
|
|
690
1425
|
this.options.staleWhileRevalidate
|
|
691
1426
|
);
|
|
692
|
-
const staleIfError = this.resolveLayerSeconds(
|
|
693
|
-
layer.name,
|
|
694
|
-
options?.staleIfError,
|
|
695
|
-
this.options.staleIfError
|
|
696
|
-
);
|
|
1427
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
697
1428
|
const payload = createStoredValueEnvelope({
|
|
698
1429
|
kind,
|
|
699
1430
|
value,
|
|
@@ -703,7 +1434,11 @@ var CacheStack = class {
|
|
|
703
1434
|
now
|
|
704
1435
|
});
|
|
705
1436
|
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
706
|
-
|
|
1437
|
+
try {
|
|
1438
|
+
await layer.set(key, payload, ttl);
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1441
|
+
}
|
|
707
1442
|
});
|
|
708
1443
|
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
709
1444
|
}
|
|
@@ -717,8 +1452,8 @@ var CacheStack = class {
|
|
|
717
1452
|
if (failures.length === 0) {
|
|
718
1453
|
return;
|
|
719
1454
|
}
|
|
720
|
-
this.
|
|
721
|
-
this.logger.debug("write-failure", {
|
|
1455
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
1456
|
+
this.logger.debug?.("write-failure", {
|
|
722
1457
|
...context,
|
|
723
1458
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
724
1459
|
});
|
|
@@ -729,52 +1464,26 @@ var CacheStack = class {
|
|
|
729
1464
|
);
|
|
730
1465
|
}
|
|
731
1466
|
}
|
|
732
|
-
resolveFreshTtl(layerName, kind, options, fallbackTtl) {
|
|
733
|
-
|
|
734
|
-
layerName,
|
|
735
|
-
options?.negativeTtl,
|
|
736
|
-
this.options.negativeTtl,
|
|
737
|
-
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
738
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
739
|
-
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
740
|
-
return this.applyJitter(baseTtl, jitter);
|
|
1467
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1468
|
+
return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
|
|
741
1469
|
}
|
|
742
1470
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
743
|
-
|
|
744
|
-
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
745
|
-
}
|
|
746
|
-
if (globalDefault !== void 0) {
|
|
747
|
-
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
748
|
-
}
|
|
749
|
-
return fallback;
|
|
750
|
-
}
|
|
751
|
-
readLayerNumber(layerName, value) {
|
|
752
|
-
if (typeof value === "number") {
|
|
753
|
-
return value;
|
|
754
|
-
}
|
|
755
|
-
return value[layerName];
|
|
756
|
-
}
|
|
757
|
-
applyJitter(ttl, jitter) {
|
|
758
|
-
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
759
|
-
return ttl;
|
|
760
|
-
}
|
|
761
|
-
const delta = (Math.random() * 2 - 1) * jitter;
|
|
762
|
-
return Math.max(1, Math.round(ttl + delta));
|
|
1471
|
+
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
763
1472
|
}
|
|
764
1473
|
shouldNegativeCache(options) {
|
|
765
1474
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
766
1475
|
}
|
|
767
1476
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
768
|
-
if (this.backgroundRefreshes.has(key)) {
|
|
1477
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
769
1478
|
return;
|
|
770
1479
|
}
|
|
771
1480
|
const refresh = (async () => {
|
|
772
|
-
this.
|
|
1481
|
+
this.metricsCollector.increment("refreshes");
|
|
773
1482
|
try {
|
|
774
1483
|
await this.fetchWithGuards(key, fetcher, options);
|
|
775
1484
|
} catch (error) {
|
|
776
|
-
this.
|
|
777
|
-
this.logger.debug("refresh-error", { key, error: this.formatError(error) });
|
|
1485
|
+
this.metricsCollector.increment("refreshErrors");
|
|
1486
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
778
1487
|
} finally {
|
|
779
1488
|
this.backgroundRefreshes.delete(key);
|
|
780
1489
|
}
|
|
@@ -792,21 +1501,16 @@ var CacheStack = class {
|
|
|
792
1501
|
if (keys.length === 0) {
|
|
793
1502
|
return;
|
|
794
1503
|
}
|
|
795
|
-
await
|
|
796
|
-
this.layers.map(async (layer) => {
|
|
797
|
-
if (layer.deleteMany) {
|
|
798
|
-
await layer.deleteMany(keys);
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
802
|
-
})
|
|
803
|
-
);
|
|
1504
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
804
1505
|
for (const key of keys) {
|
|
805
1506
|
await this.tagIndex.remove(key);
|
|
1507
|
+
this.ttlResolver.deleteProfile(key);
|
|
1508
|
+
this.circuitBreakerManager.delete(key);
|
|
806
1509
|
}
|
|
807
|
-
this.
|
|
808
|
-
this.
|
|
809
|
-
this.logger.debug("delete", { keys });
|
|
1510
|
+
this.metricsCollector.increment("deletes", keys.length);
|
|
1511
|
+
this.metricsCollector.increment("invalidations");
|
|
1512
|
+
this.logger.debug?.("delete", { keys });
|
|
1513
|
+
this.emit("delete", { keys });
|
|
810
1514
|
}
|
|
811
1515
|
async publishInvalidation(message) {
|
|
812
1516
|
if (!this.options.invalidationBus) {
|
|
@@ -825,21 +1529,15 @@ var CacheStack = class {
|
|
|
825
1529
|
if (message.scope === "clear") {
|
|
826
1530
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
827
1531
|
await this.tagIndex.clear();
|
|
1532
|
+
this.ttlResolver.clearProfiles();
|
|
828
1533
|
return;
|
|
829
1534
|
}
|
|
830
1535
|
const keys = message.keys ?? [];
|
|
831
|
-
await
|
|
832
|
-
localLayers.map(async (layer) => {
|
|
833
|
-
if (layer.deleteMany) {
|
|
834
|
-
await layer.deleteMany(keys);
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
838
|
-
})
|
|
839
|
-
);
|
|
1536
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
840
1537
|
if (message.operation !== "write") {
|
|
841
1538
|
for (const key of keys) {
|
|
842
1539
|
await this.tagIndex.remove(key);
|
|
1540
|
+
this.ttlResolver.deleteProfile(key);
|
|
843
1541
|
}
|
|
844
1542
|
}
|
|
845
1543
|
}
|
|
@@ -852,6 +1550,210 @@ var CacheStack = class {
|
|
|
852
1550
|
sleep(ms) {
|
|
853
1551
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
854
1552
|
}
|
|
1553
|
+
shouldBroadcastL1Invalidation() {
|
|
1554
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1555
|
+
}
|
|
1556
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
1557
|
+
await Promise.all(
|
|
1558
|
+
layers.map(async (layer) => {
|
|
1559
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (layer.deleteMany) {
|
|
1563
|
+
try {
|
|
1564
|
+
await layer.deleteMany(keys);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1567
|
+
}
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
await Promise.all(
|
|
1571
|
+
keys.map(async (key) => {
|
|
1572
|
+
try {
|
|
1573
|
+
await layer.delete(key);
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1576
|
+
}
|
|
1577
|
+
})
|
|
1578
|
+
);
|
|
1579
|
+
})
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
validateConfiguration() {
|
|
1583
|
+
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
1584
|
+
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
1585
|
+
}
|
|
1586
|
+
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
1587
|
+
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
1588
|
+
}
|
|
1589
|
+
this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
1590
|
+
this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
1591
|
+
this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
1592
|
+
this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
1593
|
+
this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
1594
|
+
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1595
|
+
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1596
|
+
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1597
|
+
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1598
|
+
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1599
|
+
}
|
|
1600
|
+
validateWriteOptions(options) {
|
|
1601
|
+
if (!options) {
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
1605
|
+
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
1606
|
+
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
1607
|
+
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1608
|
+
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1609
|
+
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1610
|
+
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1611
|
+
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1612
|
+
}
|
|
1613
|
+
validateLayerNumberOption(name, value) {
|
|
1614
|
+
if (value === void 0) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (typeof value === "number") {
|
|
1618
|
+
this.validateNonNegativeNumber(name, value);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1622
|
+
if (layerValue === void 0) {
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
validatePositiveNumber(name, value) {
|
|
1629
|
+
if (value === void 0) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1633
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
validateNonNegativeNumber(name, value) {
|
|
1637
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1638
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
validateCacheKey(key) {
|
|
1642
|
+
if (key.length === 0) {
|
|
1643
|
+
throw new Error("Cache key must not be empty.");
|
|
1644
|
+
}
|
|
1645
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1646
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1647
|
+
}
|
|
1648
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1649
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1650
|
+
}
|
|
1651
|
+
return key;
|
|
1652
|
+
}
|
|
1653
|
+
serializeOptions(options) {
|
|
1654
|
+
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1655
|
+
}
|
|
1656
|
+
validateAdaptiveTtlOptions(options) {
|
|
1657
|
+
if (!options || options === true) {
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1661
|
+
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1662
|
+
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1663
|
+
}
|
|
1664
|
+
validateCircuitBreakerOptions(options) {
|
|
1665
|
+
if (!options) {
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1669
|
+
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1670
|
+
}
|
|
1671
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1672
|
+
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
1673
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
1674
|
+
if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
|
|
1675
|
+
const refreshed = refreshStoredEnvelope(hit.stored);
|
|
1676
|
+
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1677
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1678
|
+
const layer = this.layers[index];
|
|
1679
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
await layer.set(key, refreshed, ttl);
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
|
|
1690
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
shouldSkipLayer(layer) {
|
|
1694
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1695
|
+
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
1696
|
+
}
|
|
1697
|
+
async handleLayerFailure(layer, operation, error) {
|
|
1698
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
1699
|
+
throw error;
|
|
1700
|
+
}
|
|
1701
|
+
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1702
|
+
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1703
|
+
this.metricsCollector.increment("degradedOperations");
|
|
1704
|
+
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1705
|
+
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
isGracefulDegradationEnabled() {
|
|
1709
|
+
return Boolean(this.options.gracefulDegradation);
|
|
1710
|
+
}
|
|
1711
|
+
recordCircuitFailure(key, options, error) {
|
|
1712
|
+
if (!options) {
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
this.circuitBreakerManager.recordFailure(key, options);
|
|
1716
|
+
if (this.circuitBreakerManager.isOpen(key)) {
|
|
1717
|
+
this.metricsCollector.increment("circuitBreakerTrips");
|
|
1718
|
+
}
|
|
1719
|
+
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
1720
|
+
}
|
|
1721
|
+
isNegativeStoredValue(stored) {
|
|
1722
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1723
|
+
}
|
|
1724
|
+
emitError(operation, context) {
|
|
1725
|
+
this.logger.error?.(operation, context);
|
|
1726
|
+
if (this.listenerCount("error") > 0) {
|
|
1727
|
+
this.emit("error", { operation, ...context });
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
serializeKeyPart(value) {
|
|
1731
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1732
|
+
return String(value);
|
|
1733
|
+
}
|
|
1734
|
+
return JSON.stringify(this.normalizeForSerialization(value));
|
|
1735
|
+
}
|
|
1736
|
+
isCacheSnapshotEntries(value) {
|
|
1737
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1738
|
+
if (!entry || typeof entry !== "object") {
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
const candidate = entry;
|
|
1742
|
+
return typeof candidate.key === "string";
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
normalizeForSerialization(value) {
|
|
1746
|
+
if (Array.isArray(value)) {
|
|
1747
|
+
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
1748
|
+
}
|
|
1749
|
+
if (value && typeof value === "object") {
|
|
1750
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
1751
|
+
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
1752
|
+
return normalized;
|
|
1753
|
+
}, {});
|
|
1754
|
+
}
|
|
1755
|
+
return value;
|
|
1756
|
+
}
|
|
855
1757
|
};
|
|
856
1758
|
|
|
857
1759
|
// src/module.ts
|
|
@@ -878,5 +1780,6 @@ CacheStackModule = __decorateClass([
|
|
|
878
1780
|
0 && (module.exports = {
|
|
879
1781
|
CACHE_STACK,
|
|
880
1782
|
CacheStackModule,
|
|
1783
|
+
Cacheable,
|
|
881
1784
|
InjectCacheStack
|
|
882
1785
|
});
|