layercache 1.0.2 → 1.2.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 +97 -5
- package/benchmarks/latency.ts +1 -1
- package/benchmarks/stampede.ts +1 -4
- package/dist/{chunk-IILH5XTS.js → chunk-BWM4MU2X.js} +36 -4
- package/dist/cli.cjs +87 -9
- package/dist/cli.js +52 -6
- package/dist/index.cjs +1219 -272
- package/dist/index.d.cts +469 -13
- package/dist/index.d.ts +469 -13
- package/dist/index.js +1181 -271
- 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 +6 -1
- package/packages/nestjs/dist/index.cjs +712 -220
- package/packages/nestjs/dist/index.d.cts +243 -11
- package/packages/nestjs/dist/index.d.ts +243 -11
- package/packages/nestjs/dist/index.js +712 -220
|
@@ -47,8 +47,275 @@ import { Global, Inject, Module } from "@nestjs/common";
|
|
|
47
47
|
|
|
48
48
|
// ../../src/CacheStack.ts
|
|
49
49
|
import { randomUUID } from "crypto";
|
|
50
|
-
import { promises as fs } from "fs";
|
|
51
50
|
import { EventEmitter } from "events";
|
|
51
|
+
import { promises as fs } from "fs";
|
|
52
|
+
|
|
53
|
+
// ../../src/CacheNamespace.ts
|
|
54
|
+
var CacheNamespace = class _CacheNamespace {
|
|
55
|
+
constructor(cache, prefix) {
|
|
56
|
+
this.cache = cache;
|
|
57
|
+
this.prefix = prefix;
|
|
58
|
+
}
|
|
59
|
+
cache;
|
|
60
|
+
prefix;
|
|
61
|
+
async get(key, fetcher, options) {
|
|
62
|
+
return this.cache.get(this.qualify(key), fetcher, options);
|
|
63
|
+
}
|
|
64
|
+
async getOrSet(key, fetcher, options) {
|
|
65
|
+
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
69
|
+
*/
|
|
70
|
+
async getOrThrow(key, fetcher, options) {
|
|
71
|
+
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
72
|
+
}
|
|
73
|
+
async has(key) {
|
|
74
|
+
return this.cache.has(this.qualify(key));
|
|
75
|
+
}
|
|
76
|
+
async ttl(key) {
|
|
77
|
+
return this.cache.ttl(this.qualify(key));
|
|
78
|
+
}
|
|
79
|
+
async set(key, value, options) {
|
|
80
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
81
|
+
}
|
|
82
|
+
async delete(key) {
|
|
83
|
+
await this.cache.delete(this.qualify(key));
|
|
84
|
+
}
|
|
85
|
+
async mdelete(keys) {
|
|
86
|
+
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
87
|
+
}
|
|
88
|
+
async clear() {
|
|
89
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
90
|
+
}
|
|
91
|
+
async mget(entries) {
|
|
92
|
+
return this.cache.mget(
|
|
93
|
+
entries.map((entry) => ({
|
|
94
|
+
...entry,
|
|
95
|
+
key: this.qualify(entry.key)
|
|
96
|
+
}))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
async mset(entries) {
|
|
100
|
+
await this.cache.mset(
|
|
101
|
+
entries.map((entry) => ({
|
|
102
|
+
...entry,
|
|
103
|
+
key: this.qualify(entry.key)
|
|
104
|
+
}))
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
async invalidateByTag(tag) {
|
|
108
|
+
await this.cache.invalidateByTag(tag);
|
|
109
|
+
}
|
|
110
|
+
async invalidateByPattern(pattern) {
|
|
111
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
115
|
+
*/
|
|
116
|
+
async inspect(key) {
|
|
117
|
+
return this.cache.inspect(this.qualify(key));
|
|
118
|
+
}
|
|
119
|
+
wrap(keyPrefix, fetcher, options) {
|
|
120
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
121
|
+
}
|
|
122
|
+
warm(entries, options) {
|
|
123
|
+
return this.cache.warm(
|
|
124
|
+
entries.map((entry) => ({
|
|
125
|
+
...entry,
|
|
126
|
+
key: this.qualify(entry.key)
|
|
127
|
+
})),
|
|
128
|
+
options
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
getMetrics() {
|
|
132
|
+
return this.cache.getMetrics();
|
|
133
|
+
}
|
|
134
|
+
getHitRate() {
|
|
135
|
+
return this.cache.getHitRate();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
142
|
+
* const posts = tenant.namespace('posts')
|
|
143
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
namespace(childPrefix) {
|
|
147
|
+
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
148
|
+
}
|
|
149
|
+
qualify(key) {
|
|
150
|
+
return `${this.prefix}:${key}`;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ../../src/internal/CircuitBreakerManager.ts
|
|
155
|
+
var CircuitBreakerManager = class {
|
|
156
|
+
breakers = /* @__PURE__ */ new Map();
|
|
157
|
+
maxEntries;
|
|
158
|
+
constructor(options) {
|
|
159
|
+
this.maxEntries = options.maxEntries;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Throws if the circuit is open for the given key.
|
|
163
|
+
* Automatically resets if the cooldown has elapsed.
|
|
164
|
+
*/
|
|
165
|
+
assertClosed(key, options) {
|
|
166
|
+
const state = this.breakers.get(key);
|
|
167
|
+
if (!state?.openUntil) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
if (state.openUntil <= now) {
|
|
172
|
+
state.openUntil = null;
|
|
173
|
+
state.failures = 0;
|
|
174
|
+
this.breakers.set(key, state);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const remainingMs = state.openUntil - now;
|
|
178
|
+
const remainingSecs = Math.ceil(remainingMs / 1e3);
|
|
179
|
+
throw new Error(`Circuit breaker is open for key "${key}" (resets in ${remainingSecs}s).`);
|
|
180
|
+
}
|
|
181
|
+
recordFailure(key, options) {
|
|
182
|
+
if (!options) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
186
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
187
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
188
|
+
state.failures += 1;
|
|
189
|
+
if (state.failures >= failureThreshold) {
|
|
190
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
191
|
+
}
|
|
192
|
+
this.breakers.set(key, state);
|
|
193
|
+
this.pruneIfNeeded();
|
|
194
|
+
}
|
|
195
|
+
recordSuccess(key) {
|
|
196
|
+
this.breakers.delete(key);
|
|
197
|
+
}
|
|
198
|
+
isOpen(key) {
|
|
199
|
+
const state = this.breakers.get(key);
|
|
200
|
+
if (!state?.openUntil) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
if (state.openUntil <= Date.now()) {
|
|
204
|
+
state.openUntil = null;
|
|
205
|
+
state.failures = 0;
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
delete(key) {
|
|
211
|
+
this.breakers.delete(key);
|
|
212
|
+
}
|
|
213
|
+
clear() {
|
|
214
|
+
this.breakers.clear();
|
|
215
|
+
}
|
|
216
|
+
tripCount() {
|
|
217
|
+
let count = 0;
|
|
218
|
+
for (const state of this.breakers.values()) {
|
|
219
|
+
if (state.openUntil !== null) {
|
|
220
|
+
count += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return count;
|
|
224
|
+
}
|
|
225
|
+
pruneIfNeeded() {
|
|
226
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
for (const [key, state] of this.breakers.entries()) {
|
|
230
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (!state.openUntil || state.openUntil <= Date.now()) {
|
|
234
|
+
this.breakers.delete(key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const key of this.breakers.keys()) {
|
|
238
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
this.breakers.delete(key);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// ../../src/internal/MetricsCollector.ts
|
|
247
|
+
var MetricsCollector = class {
|
|
248
|
+
data = this.empty();
|
|
249
|
+
get snapshot() {
|
|
250
|
+
return {
|
|
251
|
+
...this.data,
|
|
252
|
+
hitsByLayer: { ...this.data.hitsByLayer },
|
|
253
|
+
missesByLayer: { ...this.data.missesByLayer },
|
|
254
|
+
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
increment(field, amount = 1) {
|
|
258
|
+
;
|
|
259
|
+
this.data[field] += amount;
|
|
260
|
+
}
|
|
261
|
+
incrementLayer(map, layerName) {
|
|
262
|
+
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Records a read latency sample for the given layer.
|
|
266
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
267
|
+
*/
|
|
268
|
+
recordLatency(layerName, durationMs) {
|
|
269
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
270
|
+
if (!existing) {
|
|
271
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
existing.count += 1;
|
|
275
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
276
|
+
if (durationMs > existing.maxMs) {
|
|
277
|
+
existing.maxMs = durationMs;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
reset() {
|
|
281
|
+
this.data = this.empty();
|
|
282
|
+
}
|
|
283
|
+
hitRate() {
|
|
284
|
+
const total = this.data.hits + this.data.misses;
|
|
285
|
+
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
286
|
+
const byLayer = {};
|
|
287
|
+
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
288
|
+
for (const layer of allLayers) {
|
|
289
|
+
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
290
|
+
const m = this.data.missesByLayer[layer] ?? 0;
|
|
291
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
292
|
+
}
|
|
293
|
+
return { overall, byLayer };
|
|
294
|
+
}
|
|
295
|
+
empty() {
|
|
296
|
+
return {
|
|
297
|
+
hits: 0,
|
|
298
|
+
misses: 0,
|
|
299
|
+
fetches: 0,
|
|
300
|
+
sets: 0,
|
|
301
|
+
deletes: 0,
|
|
302
|
+
backfills: 0,
|
|
303
|
+
invalidations: 0,
|
|
304
|
+
staleHits: 0,
|
|
305
|
+
refreshes: 0,
|
|
306
|
+
refreshErrors: 0,
|
|
307
|
+
writeFailures: 0,
|
|
308
|
+
singleFlightWaits: 0,
|
|
309
|
+
negativeCacheHits: 0,
|
|
310
|
+
circuitBreakerTrips: 0,
|
|
311
|
+
degradedOperations: 0,
|
|
312
|
+
hitsByLayer: {},
|
|
313
|
+
missesByLayer: {},
|
|
314
|
+
latencyByLayer: {},
|
|
315
|
+
resetAt: Date.now()
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
};
|
|
52
319
|
|
|
53
320
|
// ../../src/internal/StoredValue.ts
|
|
54
321
|
function isStoredValueEnvelope(value) {
|
|
@@ -151,67 +418,129 @@ function normalizePositiveSeconds(value) {
|
|
|
151
418
|
return value;
|
|
152
419
|
}
|
|
153
420
|
|
|
154
|
-
// ../../src/
|
|
155
|
-
var
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
prefix;
|
|
162
|
-
async get(key, fetcher, options) {
|
|
163
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
164
|
-
}
|
|
165
|
-
async set(key, value, options) {
|
|
166
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
167
|
-
}
|
|
168
|
-
async delete(key) {
|
|
169
|
-
await this.cache.delete(this.qualify(key));
|
|
421
|
+
// ../../src/internal/TtlResolver.ts
|
|
422
|
+
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
423
|
+
var TtlResolver = class {
|
|
424
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
425
|
+
maxProfileEntries;
|
|
426
|
+
constructor(options) {
|
|
427
|
+
this.maxProfileEntries = options.maxProfileEntries;
|
|
170
428
|
}
|
|
171
|
-
|
|
172
|
-
|
|
429
|
+
recordAccess(key) {
|
|
430
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
431
|
+
profile.hits += 1;
|
|
432
|
+
profile.lastAccessAt = Date.now();
|
|
433
|
+
this.accessProfiles.set(key, profile);
|
|
434
|
+
this.pruneIfNeeded();
|
|
173
435
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
...entry,
|
|
177
|
-
key: this.qualify(entry.key)
|
|
178
|
-
})));
|
|
436
|
+
deleteProfile(key) {
|
|
437
|
+
this.accessProfiles.delete(key);
|
|
179
438
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
...entry,
|
|
183
|
-
key: this.qualify(entry.key)
|
|
184
|
-
})));
|
|
439
|
+
clearProfiles() {
|
|
440
|
+
this.accessProfiles.clear();
|
|
185
441
|
}
|
|
186
|
-
|
|
187
|
-
|
|
442
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
443
|
+
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
444
|
+
layerName,
|
|
445
|
+
options?.negativeTtl,
|
|
446
|
+
globalNegativeTtl,
|
|
447
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
448
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
449
|
+
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
450
|
+
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
451
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
188
452
|
}
|
|
189
|
-
|
|
190
|
-
|
|
453
|
+
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
454
|
+
if (override !== void 0) {
|
|
455
|
+
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
456
|
+
}
|
|
457
|
+
if (globalDefault !== void 0) {
|
|
458
|
+
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
459
|
+
}
|
|
460
|
+
return fallback;
|
|
191
461
|
}
|
|
192
|
-
|
|
193
|
-
|
|
462
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
463
|
+
if (!ttl || !adaptiveTtl) {
|
|
464
|
+
return ttl;
|
|
465
|
+
}
|
|
466
|
+
const profile = this.accessProfiles.get(key);
|
|
467
|
+
if (!profile) {
|
|
468
|
+
return ttl;
|
|
469
|
+
}
|
|
470
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
471
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
472
|
+
if (profile.hits < hotAfter) {
|
|
473
|
+
return ttl;
|
|
474
|
+
}
|
|
475
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
476
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
477
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
478
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
194
479
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
480
|
+
applyJitter(ttl, jitter) {
|
|
481
|
+
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
482
|
+
return ttl;
|
|
483
|
+
}
|
|
484
|
+
const delta = (Math.random() * 2 - 1) * jitter;
|
|
485
|
+
return Math.max(1, Math.round(ttl + delta));
|
|
200
486
|
}
|
|
201
|
-
|
|
202
|
-
|
|
487
|
+
readLayerNumber(layerName, value) {
|
|
488
|
+
if (typeof value === "number") {
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
return value[layerName];
|
|
203
492
|
}
|
|
204
|
-
|
|
205
|
-
|
|
493
|
+
pruneIfNeeded() {
|
|
494
|
+
if (this.accessProfiles.size <= this.maxProfileEntries) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
498
|
+
let removed = 0;
|
|
499
|
+
for (const key of this.accessProfiles.keys()) {
|
|
500
|
+
if (removed >= toRemove) {
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
this.accessProfiles.delete(key);
|
|
504
|
+
removed += 1;
|
|
505
|
+
}
|
|
206
506
|
}
|
|
207
507
|
};
|
|
208
508
|
|
|
209
509
|
// ../../src/invalidation/PatternMatcher.ts
|
|
210
|
-
var PatternMatcher = class {
|
|
510
|
+
var PatternMatcher = class _PatternMatcher {
|
|
511
|
+
/**
|
|
512
|
+
* Tests whether a glob-style pattern matches a value.
|
|
513
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
514
|
+
* Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
|
|
515
|
+
*/
|
|
211
516
|
static matches(pattern, value) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
517
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Linear-time glob matching using dynamic programming.
|
|
521
|
+
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
522
|
+
*/
|
|
523
|
+
static matchLinear(pattern, value) {
|
|
524
|
+
const m = pattern.length;
|
|
525
|
+
const n = value.length;
|
|
526
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
|
|
527
|
+
dp[0][0] = true;
|
|
528
|
+
for (let i = 1; i <= m; i++) {
|
|
529
|
+
if (pattern[i - 1] === "*") {
|
|
530
|
+
dp[i][0] = dp[i - 1]?.[0];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (let i = 1; i <= m; i++) {
|
|
534
|
+
for (let j = 1; j <= n; j++) {
|
|
535
|
+
const pc = pattern[i - 1];
|
|
536
|
+
if (pc === "*") {
|
|
537
|
+
dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
|
|
538
|
+
} else if (pc === "?" || pc === value[j - 1]) {
|
|
539
|
+
dp[i][j] = dp[i - 1]?.[j - 1];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return dp[m]?.[n];
|
|
215
544
|
}
|
|
216
545
|
};
|
|
217
546
|
|
|
@@ -220,11 +549,17 @@ var TagIndex = class {
|
|
|
220
549
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
221
550
|
keyToTags = /* @__PURE__ */ new Map();
|
|
222
551
|
knownKeys = /* @__PURE__ */ new Set();
|
|
552
|
+
maxKnownKeys;
|
|
553
|
+
constructor(options = {}) {
|
|
554
|
+
this.maxKnownKeys = options.maxKnownKeys;
|
|
555
|
+
}
|
|
223
556
|
async touch(key) {
|
|
224
557
|
this.knownKeys.add(key);
|
|
558
|
+
this.pruneKnownKeysIfNeeded();
|
|
225
559
|
}
|
|
226
560
|
async track(key, tags) {
|
|
227
561
|
this.knownKeys.add(key);
|
|
562
|
+
this.pruneKnownKeysIfNeeded();
|
|
228
563
|
if (tags.length === 0) {
|
|
229
564
|
return;
|
|
230
565
|
}
|
|
@@ -263,6 +598,9 @@ var TagIndex = class {
|
|
|
263
598
|
async keysForTag(tag) {
|
|
264
599
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
265
600
|
}
|
|
601
|
+
async tagsForKey(key) {
|
|
602
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
603
|
+
}
|
|
266
604
|
async matchPattern(pattern) {
|
|
267
605
|
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
268
606
|
}
|
|
@@ -271,6 +609,21 @@ var TagIndex = class {
|
|
|
271
609
|
this.keyToTags.clear();
|
|
272
610
|
this.knownKeys.clear();
|
|
273
611
|
}
|
|
612
|
+
pruneKnownKeysIfNeeded() {
|
|
613
|
+
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
617
|
+
let removed = 0;
|
|
618
|
+
for (const key of this.knownKeys) {
|
|
619
|
+
if (removed >= toRemove) {
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
this.knownKeys.delete(key);
|
|
623
|
+
this.keyToTags.delete(key);
|
|
624
|
+
removed += 1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
274
627
|
};
|
|
275
628
|
|
|
276
629
|
// ../../node_modules/async-mutex/index.mjs
|
|
@@ -473,31 +826,22 @@ var StampedeGuard = class {
|
|
|
473
826
|
}
|
|
474
827
|
};
|
|
475
828
|
|
|
829
|
+
// ../../src/types.ts
|
|
830
|
+
var CacheMissError = class extends Error {
|
|
831
|
+
key;
|
|
832
|
+
constructor(key) {
|
|
833
|
+
super(`Cache miss for key "${key}".`);
|
|
834
|
+
this.name = "CacheMissError";
|
|
835
|
+
this.key = key;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
|
|
476
839
|
// ../../src/CacheStack.ts
|
|
477
|
-
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
478
840
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
479
841
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
480
842
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
481
843
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
482
|
-
var
|
|
483
|
-
hits: 0,
|
|
484
|
-
misses: 0,
|
|
485
|
-
fetches: 0,
|
|
486
|
-
sets: 0,
|
|
487
|
-
deletes: 0,
|
|
488
|
-
backfills: 0,
|
|
489
|
-
invalidations: 0,
|
|
490
|
-
staleHits: 0,
|
|
491
|
-
refreshes: 0,
|
|
492
|
-
refreshErrors: 0,
|
|
493
|
-
writeFailures: 0,
|
|
494
|
-
singleFlightWaits: 0,
|
|
495
|
-
negativeCacheHits: 0,
|
|
496
|
-
circuitBreakerTrips: 0,
|
|
497
|
-
degradedOperations: 0,
|
|
498
|
-
hitsByLayer: {},
|
|
499
|
-
missesByLayer: {}
|
|
500
|
-
});
|
|
844
|
+
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
501
845
|
var DebugLogger = class {
|
|
502
846
|
enabled;
|
|
503
847
|
constructor(enabled) {
|
|
@@ -532,6 +876,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
532
876
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
533
877
|
}
|
|
534
878
|
this.validateConfiguration();
|
|
879
|
+
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
880
|
+
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
881
|
+
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
882
|
+
if (options.publishSetInvalidation !== void 0) {
|
|
883
|
+
console.warn(
|
|
884
|
+
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
885
|
+
);
|
|
886
|
+
}
|
|
535
887
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
536
888
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
537
889
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -540,36 +892,42 @@ var CacheStack = class extends EventEmitter {
|
|
|
540
892
|
layers;
|
|
541
893
|
options;
|
|
542
894
|
stampedeGuard = new StampedeGuard();
|
|
543
|
-
|
|
895
|
+
metricsCollector = new MetricsCollector();
|
|
544
896
|
instanceId = randomUUID();
|
|
545
897
|
startup;
|
|
546
898
|
unsubscribeInvalidation;
|
|
547
899
|
logger;
|
|
548
900
|
tagIndex;
|
|
549
901
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
550
|
-
accessProfiles = /* @__PURE__ */ new Map();
|
|
551
902
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
552
|
-
|
|
903
|
+
ttlResolver;
|
|
904
|
+
circuitBreakerManager;
|
|
553
905
|
isDisconnecting = false;
|
|
554
906
|
disconnectPromise;
|
|
907
|
+
/**
|
|
908
|
+
* Read-through cache get.
|
|
909
|
+
* Returns the cached value if present and fresh, or invokes `fetcher` on a miss
|
|
910
|
+
* and stores the result across all layers. Returns `null` if the key is not found
|
|
911
|
+
* and no `fetcher` is provided.
|
|
912
|
+
*/
|
|
555
913
|
async get(key, fetcher, options) {
|
|
556
914
|
const normalizedKey = this.validateCacheKey(key);
|
|
557
915
|
this.validateWriteOptions(options);
|
|
558
916
|
await this.startup;
|
|
559
917
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
560
918
|
if (hit.found) {
|
|
561
|
-
this.recordAccess(normalizedKey);
|
|
919
|
+
this.ttlResolver.recordAccess(normalizedKey);
|
|
562
920
|
if (this.isNegativeStoredValue(hit.stored)) {
|
|
563
|
-
this.
|
|
921
|
+
this.metricsCollector.increment("negativeCacheHits");
|
|
564
922
|
}
|
|
565
923
|
if (hit.state === "fresh") {
|
|
566
|
-
this.
|
|
924
|
+
this.metricsCollector.increment("hits");
|
|
567
925
|
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
568
926
|
return hit.value;
|
|
569
927
|
}
|
|
570
928
|
if (hit.state === "stale-while-revalidate") {
|
|
571
|
-
this.
|
|
572
|
-
this.
|
|
929
|
+
this.metricsCollector.increment("hits");
|
|
930
|
+
this.metricsCollector.increment("staleHits");
|
|
573
931
|
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
574
932
|
if (fetcher) {
|
|
575
933
|
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
@@ -577,47 +935,148 @@ var CacheStack = class extends EventEmitter {
|
|
|
577
935
|
return hit.value;
|
|
578
936
|
}
|
|
579
937
|
if (!fetcher) {
|
|
580
|
-
this.
|
|
581
|
-
this.
|
|
938
|
+
this.metricsCollector.increment("hits");
|
|
939
|
+
this.metricsCollector.increment("staleHits");
|
|
582
940
|
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
583
941
|
return hit.value;
|
|
584
942
|
}
|
|
585
943
|
try {
|
|
586
944
|
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
587
945
|
} catch (error) {
|
|
588
|
-
this.
|
|
589
|
-
this.
|
|
946
|
+
this.metricsCollector.increment("staleHits");
|
|
947
|
+
this.metricsCollector.increment("refreshErrors");
|
|
590
948
|
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
591
949
|
return hit.value;
|
|
592
950
|
}
|
|
593
951
|
}
|
|
594
|
-
this.
|
|
952
|
+
this.metricsCollector.increment("misses");
|
|
595
953
|
if (!fetcher) {
|
|
596
954
|
return null;
|
|
597
955
|
}
|
|
598
956
|
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
599
957
|
}
|
|
958
|
+
/**
|
|
959
|
+
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
960
|
+
* Fetches and caches the value if not already present.
|
|
961
|
+
*/
|
|
962
|
+
async getOrSet(key, fetcher, options) {
|
|
963
|
+
return this.get(key, fetcher, options);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
967
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
968
|
+
* return non-null.
|
|
969
|
+
*/
|
|
970
|
+
async getOrThrow(key, fetcher, options) {
|
|
971
|
+
const value = await this.get(key, fetcher, options);
|
|
972
|
+
if (value === null) {
|
|
973
|
+
throw new CacheMissError(key);
|
|
974
|
+
}
|
|
975
|
+
return value;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Returns true if the given key exists and is not expired in any layer.
|
|
979
|
+
*/
|
|
980
|
+
async has(key) {
|
|
981
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
982
|
+
await this.startup;
|
|
983
|
+
for (const layer of this.layers) {
|
|
984
|
+
if (this.shouldSkipLayer(layer)) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (layer.has) {
|
|
988
|
+
try {
|
|
989
|
+
const exists = await layer.has(normalizedKey);
|
|
990
|
+
if (exists) {
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
} else {
|
|
996
|
+
try {
|
|
997
|
+
const value = await layer.get(normalizedKey);
|
|
998
|
+
if (value !== null) {
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Returns the remaining TTL in seconds for the key in the fastest layer
|
|
1009
|
+
* that has it, or null if the key is not found / has no TTL.
|
|
1010
|
+
*/
|
|
1011
|
+
async ttl(key) {
|
|
1012
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
1013
|
+
await this.startup;
|
|
1014
|
+
for (const layer of this.layers) {
|
|
1015
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (layer.ttl) {
|
|
1019
|
+
try {
|
|
1020
|
+
const remaining = await layer.ttl(normalizedKey);
|
|
1021
|
+
if (remaining !== null) {
|
|
1022
|
+
return remaining;
|
|
1023
|
+
}
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1032
|
+
*/
|
|
600
1033
|
async set(key, value, options) {
|
|
601
1034
|
const normalizedKey = this.validateCacheKey(key);
|
|
602
1035
|
this.validateWriteOptions(options);
|
|
603
1036
|
await this.startup;
|
|
604
1037
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
605
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Deletes the key from all layers and publishes an invalidation message.
|
|
1041
|
+
*/
|
|
606
1042
|
async delete(key) {
|
|
607
1043
|
const normalizedKey = this.validateCacheKey(key);
|
|
608
1044
|
await this.startup;
|
|
609
1045
|
await this.deleteKeys([normalizedKey]);
|
|
610
|
-
await this.publishInvalidation({
|
|
1046
|
+
await this.publishInvalidation({
|
|
1047
|
+
scope: "key",
|
|
1048
|
+
keys: [normalizedKey],
|
|
1049
|
+
sourceId: this.instanceId,
|
|
1050
|
+
operation: "delete"
|
|
1051
|
+
});
|
|
611
1052
|
}
|
|
612
1053
|
async clear() {
|
|
613
1054
|
await this.startup;
|
|
614
1055
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
615
1056
|
await this.tagIndex.clear();
|
|
616
|
-
this.
|
|
617
|
-
this.
|
|
1057
|
+
this.ttlResolver.clearProfiles();
|
|
1058
|
+
this.circuitBreakerManager.clear();
|
|
1059
|
+
this.metricsCollector.increment("invalidations");
|
|
618
1060
|
this.logger.debug?.("clear");
|
|
619
1061
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
620
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
|
|
1065
|
+
*/
|
|
1066
|
+
async mdelete(keys) {
|
|
1067
|
+
if (keys.length === 0) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
await this.startup;
|
|
1071
|
+
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
1072
|
+
await this.deleteKeys(normalizedKeys);
|
|
1073
|
+
await this.publishInvalidation({
|
|
1074
|
+
scope: "keys",
|
|
1075
|
+
keys: normalizedKeys,
|
|
1076
|
+
sourceId: this.instanceId,
|
|
1077
|
+
operation: "delete"
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
621
1080
|
async mget(entries) {
|
|
622
1081
|
if (entries.length === 0) {
|
|
623
1082
|
return [];
|
|
@@ -655,7 +1114,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
655
1114
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
656
1115
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
657
1116
|
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
658
|
-
const
|
|
1117
|
+
const entry = normalizedEntries[index];
|
|
1118
|
+
if (!entry) continue;
|
|
1119
|
+
const key = entry.key;
|
|
659
1120
|
const indexes = indexesByKey.get(key) ?? [];
|
|
660
1121
|
indexes.push(index);
|
|
661
1122
|
indexesByKey.set(key, indexes);
|
|
@@ -663,6 +1124,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
663
1124
|
}
|
|
664
1125
|
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
665
1126
|
const layer = this.layers[layerIndex];
|
|
1127
|
+
if (!layer) continue;
|
|
666
1128
|
const keys = [...pending];
|
|
667
1129
|
if (keys.length === 0) {
|
|
668
1130
|
break;
|
|
@@ -671,7 +1133,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
671
1133
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
672
1134
|
const key = keys[offset];
|
|
673
1135
|
const stored = values[offset];
|
|
674
|
-
if (stored === null) {
|
|
1136
|
+
if (!key || stored === null) {
|
|
675
1137
|
continue;
|
|
676
1138
|
}
|
|
677
1139
|
const resolved = resolveStoredValue(stored);
|
|
@@ -683,13 +1145,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
683
1145
|
await this.backfill(key, stored, layerIndex - 1);
|
|
684
1146
|
resultsByKey.set(key, resolved.value);
|
|
685
1147
|
pending.delete(key);
|
|
686
|
-
this.
|
|
1148
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
687
1149
|
}
|
|
688
1150
|
}
|
|
689
1151
|
if (pending.size > 0) {
|
|
690
1152
|
for (const key of pending) {
|
|
691
1153
|
await this.tagIndex.remove(key);
|
|
692
|
-
this.
|
|
1154
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
693
1155
|
}
|
|
694
1156
|
}
|
|
695
1157
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
@@ -704,26 +1166,38 @@ var CacheStack = class extends EventEmitter {
|
|
|
704
1166
|
}
|
|
705
1167
|
async warm(entries, options = {}) {
|
|
706
1168
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
1169
|
+
const total = entries.length;
|
|
1170
|
+
let completed = 0;
|
|
707
1171
|
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
708
|
-
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
1172
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length || 1) }, async () => {
|
|
709
1173
|
while (queue.length > 0) {
|
|
710
1174
|
const entry = queue.shift();
|
|
711
1175
|
if (!entry) {
|
|
712
1176
|
return;
|
|
713
1177
|
}
|
|
1178
|
+
let success = false;
|
|
714
1179
|
try {
|
|
715
1180
|
await this.get(entry.key, entry.fetcher, entry.options);
|
|
716
1181
|
this.emit("warm", { key: entry.key });
|
|
1182
|
+
success = true;
|
|
717
1183
|
} catch (error) {
|
|
718
1184
|
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
719
1185
|
if (!options.continueOnError) {
|
|
720
1186
|
throw error;
|
|
721
1187
|
}
|
|
1188
|
+
} finally {
|
|
1189
|
+
completed += 1;
|
|
1190
|
+
const progress = { completed, total, key: entry.key, success };
|
|
1191
|
+
options.onProgress?.(progress);
|
|
722
1192
|
}
|
|
723
1193
|
}
|
|
724
1194
|
});
|
|
725
1195
|
await Promise.all(workers);
|
|
726
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Returns a cached version of `fetcher`. The cache key is derived from
|
|
1199
|
+
* `prefix` plus the serialized arguments unless a `keyResolver` is provided.
|
|
1200
|
+
*/
|
|
727
1201
|
wrap(prefix, fetcher, options = {}) {
|
|
728
1202
|
return (...args) => {
|
|
729
1203
|
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
@@ -731,6 +1205,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
731
1205
|
return this.get(key, () => fetcher(...args), options);
|
|
732
1206
|
};
|
|
733
1207
|
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Creates a `CacheNamespace` that automatically prefixes all keys with
|
|
1210
|
+
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1211
|
+
*/
|
|
734
1212
|
namespace(prefix) {
|
|
735
1213
|
return new CacheNamespace(this, prefix);
|
|
736
1214
|
}
|
|
@@ -747,7 +1225,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
747
1225
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
748
1226
|
}
|
|
749
1227
|
getMetrics() {
|
|
750
|
-
return
|
|
1228
|
+
return this.metricsCollector.snapshot;
|
|
751
1229
|
}
|
|
752
1230
|
getStats() {
|
|
753
1231
|
return {
|
|
@@ -761,7 +1239,53 @@ var CacheStack = class extends EventEmitter {
|
|
|
761
1239
|
};
|
|
762
1240
|
}
|
|
763
1241
|
resetMetrics() {
|
|
764
|
-
|
|
1242
|
+
this.metricsCollector.reset();
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Returns computed hit-rate statistics (overall and per-layer).
|
|
1246
|
+
*/
|
|
1247
|
+
getHitRate() {
|
|
1248
|
+
return this.metricsCollector.hitRate();
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1252
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1253
|
+
* Returns `null` if the key does not exist in any layer.
|
|
1254
|
+
*/
|
|
1255
|
+
async inspect(key) {
|
|
1256
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
1257
|
+
await this.startup;
|
|
1258
|
+
const foundInLayers = [];
|
|
1259
|
+
let freshTtlSeconds = null;
|
|
1260
|
+
let staleTtlSeconds = null;
|
|
1261
|
+
let errorTtlSeconds = null;
|
|
1262
|
+
let isStale = false;
|
|
1263
|
+
for (const layer of this.layers) {
|
|
1264
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
1268
|
+
if (stored === null) {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
const resolved = resolveStoredValue(stored);
|
|
1272
|
+
if (resolved.state === "expired") {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
foundInLayers.push(layer.name);
|
|
1276
|
+
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
1277
|
+
const now = Date.now();
|
|
1278
|
+
freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
|
|
1279
|
+
staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
|
|
1280
|
+
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
1281
|
+
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (foundInLayers.length === 0) {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
1288
|
+
return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
765
1289
|
}
|
|
766
1290
|
async exportState() {
|
|
767
1291
|
await this.startup;
|
|
@@ -790,10 +1314,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
790
1314
|
}
|
|
791
1315
|
async importState(entries) {
|
|
792
1316
|
await this.startup;
|
|
793
|
-
await Promise.all(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1317
|
+
await Promise.all(
|
|
1318
|
+
entries.map(async (entry) => {
|
|
1319
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1320
|
+
await this.tagIndex.touch(entry.key);
|
|
1321
|
+
})
|
|
1322
|
+
);
|
|
797
1323
|
}
|
|
798
1324
|
async persistToFile(filePath) {
|
|
799
1325
|
const snapshot = await this.exportState();
|
|
@@ -801,11 +1327,21 @@ var CacheStack = class extends EventEmitter {
|
|
|
801
1327
|
}
|
|
802
1328
|
async restoreFromFile(filePath) {
|
|
803
1329
|
const raw = await fs.readFile(filePath, "utf8");
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1330
|
+
let parsed;
|
|
1331
|
+
try {
|
|
1332
|
+
parsed = JSON.parse(raw, (_key, value) => {
|
|
1333
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1334
|
+
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1335
|
+
}
|
|
1336
|
+
return value;
|
|
1337
|
+
});
|
|
1338
|
+
} catch (cause) {
|
|
1339
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
807
1340
|
}
|
|
808
|
-
|
|
1341
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1342
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1343
|
+
}
|
|
1344
|
+
await this.importState(parsed);
|
|
809
1345
|
}
|
|
810
1346
|
async disconnect() {
|
|
811
1347
|
if (!this.disconnectPromise) {
|
|
@@ -830,7 +1366,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
830
1366
|
const fetchTask = async () => {
|
|
831
1367
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
832
1368
|
if (secondHit.found) {
|
|
833
|
-
this.
|
|
1369
|
+
this.metricsCollector.increment("hits");
|
|
834
1370
|
return secondHit.value;
|
|
835
1371
|
}
|
|
836
1372
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
@@ -855,12 +1391,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
855
1391
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
856
1392
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
857
1393
|
const deadline = Date.now() + timeoutMs;
|
|
858
|
-
this.
|
|
1394
|
+
this.metricsCollector.increment("singleFlightWaits");
|
|
859
1395
|
this.emit("stampede-dedupe", { key });
|
|
860
1396
|
while (Date.now() < deadline) {
|
|
861
1397
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
862
1398
|
if (hit.found) {
|
|
863
|
-
this.
|
|
1399
|
+
this.metricsCollector.increment("hits");
|
|
864
1400
|
return hit.value;
|
|
865
1401
|
}
|
|
866
1402
|
await this.sleep(pollIntervalMs);
|
|
@@ -868,12 +1404,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
868
1404
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
869
1405
|
}
|
|
870
1406
|
async fetchAndPopulate(key, fetcher, options) {
|
|
871
|
-
this.
|
|
872
|
-
this.
|
|
1407
|
+
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1408
|
+
this.metricsCollector.increment("fetches");
|
|
1409
|
+
const fetchStart = Date.now();
|
|
873
1410
|
let fetched;
|
|
874
1411
|
try {
|
|
875
1412
|
fetched = await fetcher();
|
|
876
|
-
this.
|
|
1413
|
+
this.circuitBreakerManager.recordSuccess(key);
|
|
1414
|
+
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
877
1415
|
} catch (error) {
|
|
878
1416
|
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
879
1417
|
throw error;
|
|
@@ -885,6 +1423,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
885
1423
|
await this.storeEntry(key, "empty", null, options);
|
|
886
1424
|
return null;
|
|
887
1425
|
}
|
|
1426
|
+
if (options?.shouldCache && !options.shouldCache(fetched)) {
|
|
1427
|
+
return fetched;
|
|
1428
|
+
}
|
|
888
1429
|
await this.storeEntry(key, "value", fetched, options);
|
|
889
1430
|
return fetched;
|
|
890
1431
|
}
|
|
@@ -895,7 +1436,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
895
1436
|
} else {
|
|
896
1437
|
await this.tagIndex.touch(key);
|
|
897
1438
|
}
|
|
898
|
-
this.
|
|
1439
|
+
this.metricsCollector.increment("sets");
|
|
899
1440
|
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
900
1441
|
this.emit("set", { key, kind, tags: options?.tags });
|
|
901
1442
|
if (this.shouldBroadcastL1Invalidation()) {
|
|
@@ -906,9 +1447,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
906
1447
|
let sawRetainableValue = false;
|
|
907
1448
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
908
1449
|
const layer = this.layers[index];
|
|
1450
|
+
if (!layer) continue;
|
|
1451
|
+
const readStart = performance.now();
|
|
909
1452
|
const stored = await this.readLayerEntry(layer, key);
|
|
1453
|
+
const readDuration = performance.now() - readStart;
|
|
1454
|
+
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
910
1455
|
if (stored === null) {
|
|
911
|
-
this.
|
|
1456
|
+
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
912
1457
|
continue;
|
|
913
1458
|
}
|
|
914
1459
|
const resolved = resolveStoredValue(stored);
|
|
@@ -922,10 +1467,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
922
1467
|
}
|
|
923
1468
|
await this.tagIndex.touch(key);
|
|
924
1469
|
await this.backfill(key, stored, index - 1, options);
|
|
925
|
-
this.
|
|
1470
|
+
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
926
1471
|
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
927
1472
|
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
928
|
-
return {
|
|
1473
|
+
return {
|
|
1474
|
+
found: true,
|
|
1475
|
+
value: resolved.value,
|
|
1476
|
+
stored,
|
|
1477
|
+
state: resolved.state,
|
|
1478
|
+
layerIndex: index,
|
|
1479
|
+
layerName: layer.name
|
|
1480
|
+
};
|
|
929
1481
|
}
|
|
930
1482
|
if (!sawRetainableValue) {
|
|
931
1483
|
await this.tagIndex.remove(key);
|
|
@@ -957,7 +1509,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
957
1509
|
}
|
|
958
1510
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
959
1511
|
const layer = this.layers[index];
|
|
960
|
-
if (this.shouldSkipLayer(layer)) {
|
|
1512
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
961
1513
|
continue;
|
|
962
1514
|
}
|
|
963
1515
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
@@ -967,7 +1519,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
967
1519
|
await this.handleLayerFailure(layer, "backfill", error);
|
|
968
1520
|
continue;
|
|
969
1521
|
}
|
|
970
|
-
this.
|
|
1522
|
+
this.metricsCollector.increment("backfills");
|
|
971
1523
|
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
972
1524
|
this.emit("backfill", { key, layer: layer.name });
|
|
973
1525
|
}
|
|
@@ -984,11 +1536,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
984
1536
|
options?.staleWhileRevalidate,
|
|
985
1537
|
this.options.staleWhileRevalidate
|
|
986
1538
|
);
|
|
987
|
-
const staleIfError = this.resolveLayerSeconds(
|
|
988
|
-
layer.name,
|
|
989
|
-
options?.staleIfError,
|
|
990
|
-
this.options.staleIfError
|
|
991
|
-
);
|
|
1539
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
992
1540
|
const payload = createStoredValueEnvelope({
|
|
993
1541
|
kind,
|
|
994
1542
|
value,
|
|
@@ -1016,7 +1564,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1016
1564
|
if (failures.length === 0) {
|
|
1017
1565
|
return;
|
|
1018
1566
|
}
|
|
1019
|
-
this.
|
|
1567
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
1020
1568
|
this.logger.debug?.("write-failure", {
|
|
1021
1569
|
...context,
|
|
1022
1570
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
@@ -1029,42 +1577,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1029
1577
|
}
|
|
1030
1578
|
}
|
|
1031
1579
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1032
|
-
|
|
1033
|
-
layerName,
|
|
1034
|
-
options?.negativeTtl,
|
|
1035
|
-
this.options.negativeTtl,
|
|
1036
|
-
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
1037
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
1038
|
-
const adaptiveTtl = this.applyAdaptiveTtl(
|
|
1039
|
-
key,
|
|
1040
|
-
layerName,
|
|
1041
|
-
baseTtl,
|
|
1042
|
-
options?.adaptiveTtl ?? this.options.adaptiveTtl
|
|
1043
|
-
);
|
|
1044
|
-
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
1045
|
-
return this.applyJitter(adaptiveTtl, jitter);
|
|
1580
|
+
return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
|
|
1046
1581
|
}
|
|
1047
1582
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1048
|
-
|
|
1049
|
-
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
1050
|
-
}
|
|
1051
|
-
if (globalDefault !== void 0) {
|
|
1052
|
-
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
1053
|
-
}
|
|
1054
|
-
return fallback;
|
|
1055
|
-
}
|
|
1056
|
-
readLayerNumber(layerName, value) {
|
|
1057
|
-
if (typeof value === "number") {
|
|
1058
|
-
return value;
|
|
1059
|
-
}
|
|
1060
|
-
return value[layerName];
|
|
1061
|
-
}
|
|
1062
|
-
applyJitter(ttl, jitter) {
|
|
1063
|
-
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
1064
|
-
return ttl;
|
|
1065
|
-
}
|
|
1066
|
-
const delta = (Math.random() * 2 - 1) * jitter;
|
|
1067
|
-
return Math.max(1, Math.round(ttl + delta));
|
|
1583
|
+
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
1068
1584
|
}
|
|
1069
1585
|
shouldNegativeCache(options) {
|
|
1070
1586
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
@@ -1074,11 +1590,11 @@ var CacheStack = class extends EventEmitter {
|
|
|
1074
1590
|
return;
|
|
1075
1591
|
}
|
|
1076
1592
|
const refresh = (async () => {
|
|
1077
|
-
this.
|
|
1593
|
+
this.metricsCollector.increment("refreshes");
|
|
1078
1594
|
try {
|
|
1079
1595
|
await this.fetchWithGuards(key, fetcher, options);
|
|
1080
1596
|
} catch (error) {
|
|
1081
|
-
this.
|
|
1597
|
+
this.metricsCollector.increment("refreshErrors");
|
|
1082
1598
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
1083
1599
|
} finally {
|
|
1084
1600
|
this.backgroundRefreshes.delete(key);
|
|
@@ -1100,10 +1616,11 @@ var CacheStack = class extends EventEmitter {
|
|
|
1100
1616
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
1101
1617
|
for (const key of keys) {
|
|
1102
1618
|
await this.tagIndex.remove(key);
|
|
1103
|
-
this.
|
|
1619
|
+
this.ttlResolver.deleteProfile(key);
|
|
1620
|
+
this.circuitBreakerManager.delete(key);
|
|
1104
1621
|
}
|
|
1105
|
-
this.
|
|
1106
|
-
this.
|
|
1622
|
+
this.metricsCollector.increment("deletes", keys.length);
|
|
1623
|
+
this.metricsCollector.increment("invalidations");
|
|
1107
1624
|
this.logger.debug?.("delete", { keys });
|
|
1108
1625
|
this.emit("delete", { keys });
|
|
1109
1626
|
}
|
|
@@ -1124,7 +1641,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1124
1641
|
if (message.scope === "clear") {
|
|
1125
1642
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
1126
1643
|
await this.tagIndex.clear();
|
|
1127
|
-
this.
|
|
1644
|
+
this.ttlResolver.clearProfiles();
|
|
1128
1645
|
return;
|
|
1129
1646
|
}
|
|
1130
1647
|
const keys = message.keys ?? [];
|
|
@@ -1132,10 +1649,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1132
1649
|
if (message.operation !== "write") {
|
|
1133
1650
|
for (const key of keys) {
|
|
1134
1651
|
await this.tagIndex.remove(key);
|
|
1135
|
-
this.
|
|
1652
|
+
this.ttlResolver.deleteProfile(key);
|
|
1136
1653
|
}
|
|
1137
1654
|
}
|
|
1138
1655
|
}
|
|
1656
|
+
async getTagsForKey(key) {
|
|
1657
|
+
if (this.tagIndex.tagsForKey) {
|
|
1658
|
+
return this.tagIndex.tagsForKey(key);
|
|
1659
|
+
}
|
|
1660
|
+
return [];
|
|
1661
|
+
}
|
|
1139
1662
|
formatError(error) {
|
|
1140
1663
|
if (error instanceof Error) {
|
|
1141
1664
|
return error.message;
|
|
@@ -1162,13 +1685,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
1162
1685
|
}
|
|
1163
1686
|
return;
|
|
1164
1687
|
}
|
|
1165
|
-
await Promise.all(
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1688
|
+
await Promise.all(
|
|
1689
|
+
keys.map(async (key) => {
|
|
1690
|
+
try {
|
|
1691
|
+
await layer.delete(key);
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1694
|
+
}
|
|
1695
|
+
})
|
|
1696
|
+
);
|
|
1172
1697
|
})
|
|
1173
1698
|
);
|
|
1174
1699
|
}
|
|
@@ -1269,7 +1794,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1269
1794
|
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1270
1795
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1271
1796
|
const layer = this.layers[index];
|
|
1272
|
-
if (this.shouldSkipLayer(layer)) {
|
|
1797
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
1273
1798
|
continue;
|
|
1274
1799
|
}
|
|
1275
1800
|
try {
|
|
@@ -1283,33 +1808,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
1283
1808
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1284
1809
|
}
|
|
1285
1810
|
}
|
|
1286
|
-
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
1287
|
-
if (!ttl || !adaptiveTtl) {
|
|
1288
|
-
return ttl;
|
|
1289
|
-
}
|
|
1290
|
-
const profile = this.accessProfiles.get(key);
|
|
1291
|
-
if (!profile) {
|
|
1292
|
-
return ttl;
|
|
1293
|
-
}
|
|
1294
|
-
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
1295
|
-
const hotAfter = config.hotAfter ?? 3;
|
|
1296
|
-
if (profile.hits < hotAfter) {
|
|
1297
|
-
return ttl;
|
|
1298
|
-
}
|
|
1299
|
-
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
1300
|
-
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
1301
|
-
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
1302
|
-
return Math.min(maxTtl, ttl + step * multiplier);
|
|
1303
|
-
}
|
|
1304
|
-
recordAccess(key) {
|
|
1305
|
-
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
1306
|
-
profile.hits += 1;
|
|
1307
|
-
profile.lastAccessAt = Date.now();
|
|
1308
|
-
this.accessProfiles.set(key, profile);
|
|
1309
|
-
}
|
|
1310
|
-
incrementMetricMap(target, key) {
|
|
1311
|
-
target[key] = (target[key] ?? 0) + 1;
|
|
1312
|
-
}
|
|
1313
1811
|
shouldSkipLayer(layer) {
|
|
1314
1812
|
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1315
1813
|
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
@@ -1320,7 +1818,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1320
1818
|
}
|
|
1321
1819
|
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1322
1820
|
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1323
|
-
this.
|
|
1821
|
+
this.metricsCollector.increment("degradedOperations");
|
|
1324
1822
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1325
1823
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1326
1824
|
return null;
|
|
@@ -1328,37 +1826,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
1328
1826
|
isGracefulDegradationEnabled() {
|
|
1329
1827
|
return Boolean(this.options.gracefulDegradation);
|
|
1330
1828
|
}
|
|
1331
|
-
assertCircuitClosed(key, options) {
|
|
1332
|
-
const state = this.circuitBreakers.get(key);
|
|
1333
|
-
if (!state?.openUntil) {
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
if (state.openUntil <= Date.now()) {
|
|
1337
|
-
state.openUntil = null;
|
|
1338
|
-
state.failures = 0;
|
|
1339
|
-
this.circuitBreakers.set(key, state);
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
this.emitError("circuit-breaker-open", { key, openUntil: state.openUntil });
|
|
1343
|
-
throw new Error(`Circuit breaker is open for key "${key}".`);
|
|
1344
|
-
}
|
|
1345
1829
|
recordCircuitFailure(key, options, error) {
|
|
1346
1830
|
if (!options) {
|
|
1347
1831
|
return;
|
|
1348
1832
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
state.failures += 1;
|
|
1353
|
-
if (state.failures >= failureThreshold) {
|
|
1354
|
-
state.openUntil = Date.now() + cooldownMs;
|
|
1355
|
-
this.metrics.circuitBreakerTrips += 1;
|
|
1833
|
+
this.circuitBreakerManager.recordFailure(key, options);
|
|
1834
|
+
if (this.circuitBreakerManager.isOpen(key)) {
|
|
1835
|
+
this.metricsCollector.increment("circuitBreakerTrips");
|
|
1356
1836
|
}
|
|
1357
|
-
this.
|
|
1358
|
-
this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
|
|
1359
|
-
}
|
|
1360
|
-
resetCircuitBreaker(key) {
|
|
1361
|
-
this.circuitBreakers.delete(key);
|
|
1837
|
+
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
1362
1838
|
}
|
|
1363
1839
|
isNegativeStoredValue(stored) {
|
|
1364
1840
|
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
@@ -1413,6 +1889,22 @@ var CacheStackModule = class {
|
|
|
1413
1889
|
exports: [provider]
|
|
1414
1890
|
};
|
|
1415
1891
|
}
|
|
1892
|
+
static forRootAsync(options) {
|
|
1893
|
+
const provider = {
|
|
1894
|
+
provide: CACHE_STACK,
|
|
1895
|
+
inject: options.inject ?? [],
|
|
1896
|
+
useFactory: async (...args) => {
|
|
1897
|
+
const resolved = await options.useFactory(...args);
|
|
1898
|
+
return new CacheStack(resolved.layers, resolved.bridgeOptions);
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
return {
|
|
1902
|
+
global: true,
|
|
1903
|
+
module: CacheStackModule,
|
|
1904
|
+
providers: [provider],
|
|
1905
|
+
exports: [provider]
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1416
1908
|
};
|
|
1417
1909
|
CacheStackModule = __decorateClass([
|
|
1418
1910
|
Global(),
|