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
|
@@ -73,8 +73,275 @@ var import_common = require("@nestjs/common");
|
|
|
73
73
|
|
|
74
74
|
// ../../src/CacheStack.ts
|
|
75
75
|
var import_node_crypto = require("crypto");
|
|
76
|
-
var import_node_fs = require("fs");
|
|
77
76
|
var import_node_events = require("events");
|
|
77
|
+
var import_node_fs = require("fs");
|
|
78
|
+
|
|
79
|
+
// ../../src/CacheNamespace.ts
|
|
80
|
+
var CacheNamespace = class _CacheNamespace {
|
|
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
|
+
/**
|
|
94
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
95
|
+
*/
|
|
96
|
+
async getOrThrow(key, fetcher, options) {
|
|
97
|
+
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
98
|
+
}
|
|
99
|
+
async has(key) {
|
|
100
|
+
return this.cache.has(this.qualify(key));
|
|
101
|
+
}
|
|
102
|
+
async ttl(key) {
|
|
103
|
+
return this.cache.ttl(this.qualify(key));
|
|
104
|
+
}
|
|
105
|
+
async set(key, value, options) {
|
|
106
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
107
|
+
}
|
|
108
|
+
async delete(key) {
|
|
109
|
+
await this.cache.delete(this.qualify(key));
|
|
110
|
+
}
|
|
111
|
+
async mdelete(keys) {
|
|
112
|
+
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
113
|
+
}
|
|
114
|
+
async clear() {
|
|
115
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
116
|
+
}
|
|
117
|
+
async mget(entries) {
|
|
118
|
+
return this.cache.mget(
|
|
119
|
+
entries.map((entry) => ({
|
|
120
|
+
...entry,
|
|
121
|
+
key: this.qualify(entry.key)
|
|
122
|
+
}))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
async mset(entries) {
|
|
126
|
+
await this.cache.mset(
|
|
127
|
+
entries.map((entry) => ({
|
|
128
|
+
...entry,
|
|
129
|
+
key: this.qualify(entry.key)
|
|
130
|
+
}))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
async invalidateByTag(tag) {
|
|
134
|
+
await this.cache.invalidateByTag(tag);
|
|
135
|
+
}
|
|
136
|
+
async invalidateByPattern(pattern) {
|
|
137
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
141
|
+
*/
|
|
142
|
+
async inspect(key) {
|
|
143
|
+
return this.cache.inspect(this.qualify(key));
|
|
144
|
+
}
|
|
145
|
+
wrap(keyPrefix, fetcher, options) {
|
|
146
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
147
|
+
}
|
|
148
|
+
warm(entries, options) {
|
|
149
|
+
return this.cache.warm(
|
|
150
|
+
entries.map((entry) => ({
|
|
151
|
+
...entry,
|
|
152
|
+
key: this.qualify(entry.key)
|
|
153
|
+
})),
|
|
154
|
+
options
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
getMetrics() {
|
|
158
|
+
return this.cache.getMetrics();
|
|
159
|
+
}
|
|
160
|
+
getHitRate() {
|
|
161
|
+
return this.cache.getHitRate();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
165
|
+
*
|
|
166
|
+
* ```ts
|
|
167
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
168
|
+
* const posts = tenant.namespace('posts')
|
|
169
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
namespace(childPrefix) {
|
|
173
|
+
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
174
|
+
}
|
|
175
|
+
qualify(key) {
|
|
176
|
+
return `${this.prefix}:${key}`;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// ../../src/internal/CircuitBreakerManager.ts
|
|
181
|
+
var CircuitBreakerManager = class {
|
|
182
|
+
breakers = /* @__PURE__ */ new Map();
|
|
183
|
+
maxEntries;
|
|
184
|
+
constructor(options) {
|
|
185
|
+
this.maxEntries = options.maxEntries;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Throws if the circuit is open for the given key.
|
|
189
|
+
* Automatically resets if the cooldown has elapsed.
|
|
190
|
+
*/
|
|
191
|
+
assertClosed(key, options) {
|
|
192
|
+
const state = this.breakers.get(key);
|
|
193
|
+
if (!state?.openUntil) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
if (state.openUntil <= now) {
|
|
198
|
+
state.openUntil = null;
|
|
199
|
+
state.failures = 0;
|
|
200
|
+
this.breakers.set(key, state);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const remainingMs = state.openUntil - now;
|
|
204
|
+
const remainingSecs = Math.ceil(remainingMs / 1e3);
|
|
205
|
+
throw new Error(`Circuit breaker is open for key "${key}" (resets in ${remainingSecs}s).`);
|
|
206
|
+
}
|
|
207
|
+
recordFailure(key, options) {
|
|
208
|
+
if (!options) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
212
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
213
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
214
|
+
state.failures += 1;
|
|
215
|
+
if (state.failures >= failureThreshold) {
|
|
216
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
217
|
+
}
|
|
218
|
+
this.breakers.set(key, state);
|
|
219
|
+
this.pruneIfNeeded();
|
|
220
|
+
}
|
|
221
|
+
recordSuccess(key) {
|
|
222
|
+
this.breakers.delete(key);
|
|
223
|
+
}
|
|
224
|
+
isOpen(key) {
|
|
225
|
+
const state = this.breakers.get(key);
|
|
226
|
+
if (!state?.openUntil) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (state.openUntil <= Date.now()) {
|
|
230
|
+
state.openUntil = null;
|
|
231
|
+
state.failures = 0;
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
delete(key) {
|
|
237
|
+
this.breakers.delete(key);
|
|
238
|
+
}
|
|
239
|
+
clear() {
|
|
240
|
+
this.breakers.clear();
|
|
241
|
+
}
|
|
242
|
+
tripCount() {
|
|
243
|
+
let count = 0;
|
|
244
|
+
for (const state of this.breakers.values()) {
|
|
245
|
+
if (state.openUntil !== null) {
|
|
246
|
+
count += 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return count;
|
|
250
|
+
}
|
|
251
|
+
pruneIfNeeded() {
|
|
252
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
for (const [key, state] of this.breakers.entries()) {
|
|
256
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
if (!state.openUntil || state.openUntil <= Date.now()) {
|
|
260
|
+
this.breakers.delete(key);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const key of this.breakers.keys()) {
|
|
264
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
this.breakers.delete(key);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// ../../src/internal/MetricsCollector.ts
|
|
273
|
+
var MetricsCollector = class {
|
|
274
|
+
data = this.empty();
|
|
275
|
+
get snapshot() {
|
|
276
|
+
return {
|
|
277
|
+
...this.data,
|
|
278
|
+
hitsByLayer: { ...this.data.hitsByLayer },
|
|
279
|
+
missesByLayer: { ...this.data.missesByLayer },
|
|
280
|
+
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
increment(field, amount = 1) {
|
|
284
|
+
;
|
|
285
|
+
this.data[field] += amount;
|
|
286
|
+
}
|
|
287
|
+
incrementLayer(map, layerName) {
|
|
288
|
+
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Records a read latency sample for the given layer.
|
|
292
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
293
|
+
*/
|
|
294
|
+
recordLatency(layerName, durationMs) {
|
|
295
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
296
|
+
if (!existing) {
|
|
297
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
existing.count += 1;
|
|
301
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
302
|
+
if (durationMs > existing.maxMs) {
|
|
303
|
+
existing.maxMs = durationMs;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
reset() {
|
|
307
|
+
this.data = this.empty();
|
|
308
|
+
}
|
|
309
|
+
hitRate() {
|
|
310
|
+
const total = this.data.hits + this.data.misses;
|
|
311
|
+
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
312
|
+
const byLayer = {};
|
|
313
|
+
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
314
|
+
for (const layer of allLayers) {
|
|
315
|
+
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
316
|
+
const m = this.data.missesByLayer[layer] ?? 0;
|
|
317
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
318
|
+
}
|
|
319
|
+
return { overall, byLayer };
|
|
320
|
+
}
|
|
321
|
+
empty() {
|
|
322
|
+
return {
|
|
323
|
+
hits: 0,
|
|
324
|
+
misses: 0,
|
|
325
|
+
fetches: 0,
|
|
326
|
+
sets: 0,
|
|
327
|
+
deletes: 0,
|
|
328
|
+
backfills: 0,
|
|
329
|
+
invalidations: 0,
|
|
330
|
+
staleHits: 0,
|
|
331
|
+
refreshes: 0,
|
|
332
|
+
refreshErrors: 0,
|
|
333
|
+
writeFailures: 0,
|
|
334
|
+
singleFlightWaits: 0,
|
|
335
|
+
negativeCacheHits: 0,
|
|
336
|
+
circuitBreakerTrips: 0,
|
|
337
|
+
degradedOperations: 0,
|
|
338
|
+
hitsByLayer: {},
|
|
339
|
+
missesByLayer: {},
|
|
340
|
+
latencyByLayer: {},
|
|
341
|
+
resetAt: Date.now()
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
};
|
|
78
345
|
|
|
79
346
|
// ../../src/internal/StoredValue.ts
|
|
80
347
|
function isStoredValueEnvelope(value) {
|
|
@@ -177,67 +444,129 @@ function normalizePositiveSeconds(value) {
|
|
|
177
444
|
return value;
|
|
178
445
|
}
|
|
179
446
|
|
|
180
|
-
// ../../src/
|
|
181
|
-
var
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
prefix;
|
|
188
|
-
async get(key, fetcher, options) {
|
|
189
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
190
|
-
}
|
|
191
|
-
async set(key, value, options) {
|
|
192
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
193
|
-
}
|
|
194
|
-
async delete(key) {
|
|
195
|
-
await this.cache.delete(this.qualify(key));
|
|
447
|
+
// ../../src/internal/TtlResolver.ts
|
|
448
|
+
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
449
|
+
var TtlResolver = class {
|
|
450
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
451
|
+
maxProfileEntries;
|
|
452
|
+
constructor(options) {
|
|
453
|
+
this.maxProfileEntries = options.maxProfileEntries;
|
|
196
454
|
}
|
|
197
|
-
|
|
198
|
-
|
|
455
|
+
recordAccess(key) {
|
|
456
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
457
|
+
profile.hits += 1;
|
|
458
|
+
profile.lastAccessAt = Date.now();
|
|
459
|
+
this.accessProfiles.set(key, profile);
|
|
460
|
+
this.pruneIfNeeded();
|
|
199
461
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
...entry,
|
|
203
|
-
key: this.qualify(entry.key)
|
|
204
|
-
})));
|
|
462
|
+
deleteProfile(key) {
|
|
463
|
+
this.accessProfiles.delete(key);
|
|
205
464
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
...entry,
|
|
209
|
-
key: this.qualify(entry.key)
|
|
210
|
-
})));
|
|
465
|
+
clearProfiles() {
|
|
466
|
+
this.accessProfiles.clear();
|
|
211
467
|
}
|
|
212
|
-
|
|
213
|
-
|
|
468
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
469
|
+
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
470
|
+
layerName,
|
|
471
|
+
options?.negativeTtl,
|
|
472
|
+
globalNegativeTtl,
|
|
473
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
474
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
475
|
+
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
476
|
+
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
477
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
214
478
|
}
|
|
215
|
-
|
|
216
|
-
|
|
479
|
+
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
480
|
+
if (override !== void 0) {
|
|
481
|
+
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
482
|
+
}
|
|
483
|
+
if (globalDefault !== void 0) {
|
|
484
|
+
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
485
|
+
}
|
|
486
|
+
return fallback;
|
|
217
487
|
}
|
|
218
|
-
|
|
219
|
-
|
|
488
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
489
|
+
if (!ttl || !adaptiveTtl) {
|
|
490
|
+
return ttl;
|
|
491
|
+
}
|
|
492
|
+
const profile = this.accessProfiles.get(key);
|
|
493
|
+
if (!profile) {
|
|
494
|
+
return ttl;
|
|
495
|
+
}
|
|
496
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
497
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
498
|
+
if (profile.hits < hotAfter) {
|
|
499
|
+
return ttl;
|
|
500
|
+
}
|
|
501
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
502
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
503
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
504
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
220
505
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
506
|
+
applyJitter(ttl, jitter) {
|
|
507
|
+
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
508
|
+
return ttl;
|
|
509
|
+
}
|
|
510
|
+
const delta = (Math.random() * 2 - 1) * jitter;
|
|
511
|
+
return Math.max(1, Math.round(ttl + delta));
|
|
226
512
|
}
|
|
227
|
-
|
|
228
|
-
|
|
513
|
+
readLayerNumber(layerName, value) {
|
|
514
|
+
if (typeof value === "number") {
|
|
515
|
+
return value;
|
|
516
|
+
}
|
|
517
|
+
return value[layerName];
|
|
229
518
|
}
|
|
230
|
-
|
|
231
|
-
|
|
519
|
+
pruneIfNeeded() {
|
|
520
|
+
if (this.accessProfiles.size <= this.maxProfileEntries) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
524
|
+
let removed = 0;
|
|
525
|
+
for (const key of this.accessProfiles.keys()) {
|
|
526
|
+
if (removed >= toRemove) {
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
this.accessProfiles.delete(key);
|
|
530
|
+
removed += 1;
|
|
531
|
+
}
|
|
232
532
|
}
|
|
233
533
|
};
|
|
234
534
|
|
|
235
535
|
// ../../src/invalidation/PatternMatcher.ts
|
|
236
|
-
var PatternMatcher = class {
|
|
536
|
+
var PatternMatcher = class _PatternMatcher {
|
|
537
|
+
/**
|
|
538
|
+
* Tests whether a glob-style pattern matches a value.
|
|
539
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
540
|
+
* Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
|
|
541
|
+
*/
|
|
237
542
|
static matches(pattern, value) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
543
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Linear-time glob matching using dynamic programming.
|
|
547
|
+
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
548
|
+
*/
|
|
549
|
+
static matchLinear(pattern, value) {
|
|
550
|
+
const m = pattern.length;
|
|
551
|
+
const n = value.length;
|
|
552
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
|
|
553
|
+
dp[0][0] = true;
|
|
554
|
+
for (let i = 1; i <= m; i++) {
|
|
555
|
+
if (pattern[i - 1] === "*") {
|
|
556
|
+
dp[i][0] = dp[i - 1]?.[0];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
for (let i = 1; i <= m; i++) {
|
|
560
|
+
for (let j = 1; j <= n; j++) {
|
|
561
|
+
const pc = pattern[i - 1];
|
|
562
|
+
if (pc === "*") {
|
|
563
|
+
dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
|
|
564
|
+
} else if (pc === "?" || pc === value[j - 1]) {
|
|
565
|
+
dp[i][j] = dp[i - 1]?.[j - 1];
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return dp[m]?.[n];
|
|
241
570
|
}
|
|
242
571
|
};
|
|
243
572
|
|
|
@@ -246,11 +575,17 @@ var TagIndex = class {
|
|
|
246
575
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
247
576
|
keyToTags = /* @__PURE__ */ new Map();
|
|
248
577
|
knownKeys = /* @__PURE__ */ new Set();
|
|
578
|
+
maxKnownKeys;
|
|
579
|
+
constructor(options = {}) {
|
|
580
|
+
this.maxKnownKeys = options.maxKnownKeys;
|
|
581
|
+
}
|
|
249
582
|
async touch(key) {
|
|
250
583
|
this.knownKeys.add(key);
|
|
584
|
+
this.pruneKnownKeysIfNeeded();
|
|
251
585
|
}
|
|
252
586
|
async track(key, tags) {
|
|
253
587
|
this.knownKeys.add(key);
|
|
588
|
+
this.pruneKnownKeysIfNeeded();
|
|
254
589
|
if (tags.length === 0) {
|
|
255
590
|
return;
|
|
256
591
|
}
|
|
@@ -289,6 +624,9 @@ var TagIndex = class {
|
|
|
289
624
|
async keysForTag(tag) {
|
|
290
625
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
291
626
|
}
|
|
627
|
+
async tagsForKey(key) {
|
|
628
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
629
|
+
}
|
|
292
630
|
async matchPattern(pattern) {
|
|
293
631
|
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
294
632
|
}
|
|
@@ -297,6 +635,21 @@ var TagIndex = class {
|
|
|
297
635
|
this.keyToTags.clear();
|
|
298
636
|
this.knownKeys.clear();
|
|
299
637
|
}
|
|
638
|
+
pruneKnownKeysIfNeeded() {
|
|
639
|
+
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
643
|
+
let removed = 0;
|
|
644
|
+
for (const key of this.knownKeys) {
|
|
645
|
+
if (removed >= toRemove) {
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
this.knownKeys.delete(key);
|
|
649
|
+
this.keyToTags.delete(key);
|
|
650
|
+
removed += 1;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
300
653
|
};
|
|
301
654
|
|
|
302
655
|
// ../../node_modules/async-mutex/index.mjs
|
|
@@ -499,31 +852,22 @@ var StampedeGuard = class {
|
|
|
499
852
|
}
|
|
500
853
|
};
|
|
501
854
|
|
|
855
|
+
// ../../src/types.ts
|
|
856
|
+
var CacheMissError = class extends Error {
|
|
857
|
+
key;
|
|
858
|
+
constructor(key) {
|
|
859
|
+
super(`Cache miss for key "${key}".`);
|
|
860
|
+
this.name = "CacheMissError";
|
|
861
|
+
this.key = key;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
502
865
|
// ../../src/CacheStack.ts
|
|
503
|
-
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
504
866
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
505
867
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
506
868
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
507
869
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
508
|
-
var
|
|
509
|
-
hits: 0,
|
|
510
|
-
misses: 0,
|
|
511
|
-
fetches: 0,
|
|
512
|
-
sets: 0,
|
|
513
|
-
deletes: 0,
|
|
514
|
-
backfills: 0,
|
|
515
|
-
invalidations: 0,
|
|
516
|
-
staleHits: 0,
|
|
517
|
-
refreshes: 0,
|
|
518
|
-
refreshErrors: 0,
|
|
519
|
-
writeFailures: 0,
|
|
520
|
-
singleFlightWaits: 0,
|
|
521
|
-
negativeCacheHits: 0,
|
|
522
|
-
circuitBreakerTrips: 0,
|
|
523
|
-
degradedOperations: 0,
|
|
524
|
-
hitsByLayer: {},
|
|
525
|
-
missesByLayer: {}
|
|
526
|
-
});
|
|
870
|
+
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
527
871
|
var DebugLogger = class {
|
|
528
872
|
enabled;
|
|
529
873
|
constructor(enabled) {
|
|
@@ -558,6 +902,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
558
902
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
559
903
|
}
|
|
560
904
|
this.validateConfiguration();
|
|
905
|
+
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
906
|
+
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
907
|
+
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
908
|
+
if (options.publishSetInvalidation !== void 0) {
|
|
909
|
+
console.warn(
|
|
910
|
+
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
911
|
+
);
|
|
912
|
+
}
|
|
561
913
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
562
914
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
563
915
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -566,36 +918,42 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
566
918
|
layers;
|
|
567
919
|
options;
|
|
568
920
|
stampedeGuard = new StampedeGuard();
|
|
569
|
-
|
|
921
|
+
metricsCollector = new MetricsCollector();
|
|
570
922
|
instanceId = (0, import_node_crypto.randomUUID)();
|
|
571
923
|
startup;
|
|
572
924
|
unsubscribeInvalidation;
|
|
573
925
|
logger;
|
|
574
926
|
tagIndex;
|
|
575
927
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
576
|
-
accessProfiles = /* @__PURE__ */ new Map();
|
|
577
928
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
578
|
-
|
|
929
|
+
ttlResolver;
|
|
930
|
+
circuitBreakerManager;
|
|
579
931
|
isDisconnecting = false;
|
|
580
932
|
disconnectPromise;
|
|
933
|
+
/**
|
|
934
|
+
* Read-through cache get.
|
|
935
|
+
* Returns the cached value if present and fresh, or invokes `fetcher` on a miss
|
|
936
|
+
* and stores the result across all layers. Returns `null` if the key is not found
|
|
937
|
+
* and no `fetcher` is provided.
|
|
938
|
+
*/
|
|
581
939
|
async get(key, fetcher, options) {
|
|
582
940
|
const normalizedKey = this.validateCacheKey(key);
|
|
583
941
|
this.validateWriteOptions(options);
|
|
584
942
|
await this.startup;
|
|
585
943
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
586
944
|
if (hit.found) {
|
|
587
|
-
this.recordAccess(normalizedKey);
|
|
945
|
+
this.ttlResolver.recordAccess(normalizedKey);
|
|
588
946
|
if (this.isNegativeStoredValue(hit.stored)) {
|
|
589
|
-
this.
|
|
947
|
+
this.metricsCollector.increment("negativeCacheHits");
|
|
590
948
|
}
|
|
591
949
|
if (hit.state === "fresh") {
|
|
592
|
-
this.
|
|
950
|
+
this.metricsCollector.increment("hits");
|
|
593
951
|
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
594
952
|
return hit.value;
|
|
595
953
|
}
|
|
596
954
|
if (hit.state === "stale-while-revalidate") {
|
|
597
|
-
this.
|
|
598
|
-
this.
|
|
955
|
+
this.metricsCollector.increment("hits");
|
|
956
|
+
this.metricsCollector.increment("staleHits");
|
|
599
957
|
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
600
958
|
if (fetcher) {
|
|
601
959
|
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
@@ -603,47 +961,148 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
603
961
|
return hit.value;
|
|
604
962
|
}
|
|
605
963
|
if (!fetcher) {
|
|
606
|
-
this.
|
|
607
|
-
this.
|
|
964
|
+
this.metricsCollector.increment("hits");
|
|
965
|
+
this.metricsCollector.increment("staleHits");
|
|
608
966
|
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
609
967
|
return hit.value;
|
|
610
968
|
}
|
|
611
969
|
try {
|
|
612
970
|
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
613
971
|
} catch (error) {
|
|
614
|
-
this.
|
|
615
|
-
this.
|
|
972
|
+
this.metricsCollector.increment("staleHits");
|
|
973
|
+
this.metricsCollector.increment("refreshErrors");
|
|
616
974
|
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
617
975
|
return hit.value;
|
|
618
976
|
}
|
|
619
977
|
}
|
|
620
|
-
this.
|
|
978
|
+
this.metricsCollector.increment("misses");
|
|
621
979
|
if (!fetcher) {
|
|
622
980
|
return null;
|
|
623
981
|
}
|
|
624
982
|
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
625
983
|
}
|
|
984
|
+
/**
|
|
985
|
+
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
986
|
+
* Fetches and caches the value if not already present.
|
|
987
|
+
*/
|
|
988
|
+
async getOrSet(key, fetcher, options) {
|
|
989
|
+
return this.get(key, fetcher, options);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
993
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
994
|
+
* return non-null.
|
|
995
|
+
*/
|
|
996
|
+
async getOrThrow(key, fetcher, options) {
|
|
997
|
+
const value = await this.get(key, fetcher, options);
|
|
998
|
+
if (value === null) {
|
|
999
|
+
throw new CacheMissError(key);
|
|
1000
|
+
}
|
|
1001
|
+
return value;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Returns true if the given key exists and is not expired in any layer.
|
|
1005
|
+
*/
|
|
1006
|
+
async has(key) {
|
|
1007
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
1008
|
+
await this.startup;
|
|
1009
|
+
for (const layer of this.layers) {
|
|
1010
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (layer.has) {
|
|
1014
|
+
try {
|
|
1015
|
+
const exists = await layer.has(normalizedKey);
|
|
1016
|
+
if (exists) {
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
} else {
|
|
1022
|
+
try {
|
|
1023
|
+
const value = await layer.get(normalizedKey);
|
|
1024
|
+
if (value !== null) {
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Returns the remaining TTL in seconds for the key in the fastest layer
|
|
1035
|
+
* that has it, or null if the key is not found / has no TTL.
|
|
1036
|
+
*/
|
|
1037
|
+
async ttl(key) {
|
|
1038
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
1039
|
+
await this.startup;
|
|
1040
|
+
for (const layer of this.layers) {
|
|
1041
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (layer.ttl) {
|
|
1045
|
+
try {
|
|
1046
|
+
const remaining = await layer.ttl(normalizedKey);
|
|
1047
|
+
if (remaining !== null) {
|
|
1048
|
+
return remaining;
|
|
1049
|
+
}
|
|
1050
|
+
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1058
|
+
*/
|
|
626
1059
|
async set(key, value, options) {
|
|
627
1060
|
const normalizedKey = this.validateCacheKey(key);
|
|
628
1061
|
this.validateWriteOptions(options);
|
|
629
1062
|
await this.startup;
|
|
630
1063
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
631
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Deletes the key from all layers and publishes an invalidation message.
|
|
1067
|
+
*/
|
|
632
1068
|
async delete(key) {
|
|
633
1069
|
const normalizedKey = this.validateCacheKey(key);
|
|
634
1070
|
await this.startup;
|
|
635
1071
|
await this.deleteKeys([normalizedKey]);
|
|
636
|
-
await this.publishInvalidation({
|
|
1072
|
+
await this.publishInvalidation({
|
|
1073
|
+
scope: "key",
|
|
1074
|
+
keys: [normalizedKey],
|
|
1075
|
+
sourceId: this.instanceId,
|
|
1076
|
+
operation: "delete"
|
|
1077
|
+
});
|
|
637
1078
|
}
|
|
638
1079
|
async clear() {
|
|
639
1080
|
await this.startup;
|
|
640
1081
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
641
1082
|
await this.tagIndex.clear();
|
|
642
|
-
this.
|
|
643
|
-
this.
|
|
1083
|
+
this.ttlResolver.clearProfiles();
|
|
1084
|
+
this.circuitBreakerManager.clear();
|
|
1085
|
+
this.metricsCollector.increment("invalidations");
|
|
644
1086
|
this.logger.debug?.("clear");
|
|
645
1087
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
646
1088
|
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
|
|
1091
|
+
*/
|
|
1092
|
+
async mdelete(keys) {
|
|
1093
|
+
if (keys.length === 0) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
await this.startup;
|
|
1097
|
+
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
1098
|
+
await this.deleteKeys(normalizedKeys);
|
|
1099
|
+
await this.publishInvalidation({
|
|
1100
|
+
scope: "keys",
|
|
1101
|
+
keys: normalizedKeys,
|
|
1102
|
+
sourceId: this.instanceId,
|
|
1103
|
+
operation: "delete"
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
647
1106
|
async mget(entries) {
|
|
648
1107
|
if (entries.length === 0) {
|
|
649
1108
|
return [];
|
|
@@ -681,7 +1140,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
681
1140
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
682
1141
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
683
1142
|
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
684
|
-
const
|
|
1143
|
+
const entry = normalizedEntries[index];
|
|
1144
|
+
if (!entry) continue;
|
|
1145
|
+
const key = entry.key;
|
|
685
1146
|
const indexes = indexesByKey.get(key) ?? [];
|
|
686
1147
|
indexes.push(index);
|
|
687
1148
|
indexesByKey.set(key, indexes);
|
|
@@ -689,6 +1150,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
689
1150
|
}
|
|
690
1151
|
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
691
1152
|
const layer = this.layers[layerIndex];
|
|
1153
|
+
if (!layer) continue;
|
|
692
1154
|
const keys = [...pending];
|
|
693
1155
|
if (keys.length === 0) {
|
|
694
1156
|
break;
|
|
@@ -697,7 +1159,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
697
1159
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
698
1160
|
const key = keys[offset];
|
|
699
1161
|
const stored = values[offset];
|
|
700
|
-
if (stored === null) {
|
|
1162
|
+
if (!key || stored === null) {
|
|
701
1163
|
continue;
|
|
702
1164
|
}
|
|
703
1165
|
const resolved = resolveStoredValue(stored);
|
|
@@ -709,13 +1171,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
709
1171
|
await this.backfill(key, stored, layerIndex - 1);
|
|
710
1172
|
resultsByKey.set(key, resolved.value);
|
|
711
1173
|
pending.delete(key);
|
|
712
|
-
this.
|
|
1174
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
713
1175
|
}
|
|
714
1176
|
}
|
|
715
1177
|
if (pending.size > 0) {
|
|
716
1178
|
for (const key of pending) {
|
|
717
1179
|
await this.tagIndex.remove(key);
|
|
718
|
-
this.
|
|
1180
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
719
1181
|
}
|
|
720
1182
|
}
|
|
721
1183
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
@@ -730,26 +1192,38 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
730
1192
|
}
|
|
731
1193
|
async warm(entries, options = {}) {
|
|
732
1194
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
1195
|
+
const total = entries.length;
|
|
1196
|
+
let completed = 0;
|
|
733
1197
|
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
734
|
-
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
1198
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length || 1) }, async () => {
|
|
735
1199
|
while (queue.length > 0) {
|
|
736
1200
|
const entry = queue.shift();
|
|
737
1201
|
if (!entry) {
|
|
738
1202
|
return;
|
|
739
1203
|
}
|
|
1204
|
+
let success = false;
|
|
740
1205
|
try {
|
|
741
1206
|
await this.get(entry.key, entry.fetcher, entry.options);
|
|
742
1207
|
this.emit("warm", { key: entry.key });
|
|
1208
|
+
success = true;
|
|
743
1209
|
} catch (error) {
|
|
744
1210
|
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
745
1211
|
if (!options.continueOnError) {
|
|
746
1212
|
throw error;
|
|
747
1213
|
}
|
|
1214
|
+
} finally {
|
|
1215
|
+
completed += 1;
|
|
1216
|
+
const progress = { completed, total, key: entry.key, success };
|
|
1217
|
+
options.onProgress?.(progress);
|
|
748
1218
|
}
|
|
749
1219
|
}
|
|
750
1220
|
});
|
|
751
1221
|
await Promise.all(workers);
|
|
752
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Returns a cached version of `fetcher`. The cache key is derived from
|
|
1225
|
+
* `prefix` plus the serialized arguments unless a `keyResolver` is provided.
|
|
1226
|
+
*/
|
|
753
1227
|
wrap(prefix, fetcher, options = {}) {
|
|
754
1228
|
return (...args) => {
|
|
755
1229
|
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
@@ -757,6 +1231,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
757
1231
|
return this.get(key, () => fetcher(...args), options);
|
|
758
1232
|
};
|
|
759
1233
|
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Creates a `CacheNamespace` that automatically prefixes all keys with
|
|
1236
|
+
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1237
|
+
*/
|
|
760
1238
|
namespace(prefix) {
|
|
761
1239
|
return new CacheNamespace(this, prefix);
|
|
762
1240
|
}
|
|
@@ -773,7 +1251,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
773
1251
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
774
1252
|
}
|
|
775
1253
|
getMetrics() {
|
|
776
|
-
return
|
|
1254
|
+
return this.metricsCollector.snapshot;
|
|
777
1255
|
}
|
|
778
1256
|
getStats() {
|
|
779
1257
|
return {
|
|
@@ -787,7 +1265,53 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
787
1265
|
};
|
|
788
1266
|
}
|
|
789
1267
|
resetMetrics() {
|
|
790
|
-
|
|
1268
|
+
this.metricsCollector.reset();
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Returns computed hit-rate statistics (overall and per-layer).
|
|
1272
|
+
*/
|
|
1273
|
+
getHitRate() {
|
|
1274
|
+
return this.metricsCollector.hitRate();
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1278
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1279
|
+
* Returns `null` if the key does not exist in any layer.
|
|
1280
|
+
*/
|
|
1281
|
+
async inspect(key) {
|
|
1282
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
1283
|
+
await this.startup;
|
|
1284
|
+
const foundInLayers = [];
|
|
1285
|
+
let freshTtlSeconds = null;
|
|
1286
|
+
let staleTtlSeconds = null;
|
|
1287
|
+
let errorTtlSeconds = null;
|
|
1288
|
+
let isStale = false;
|
|
1289
|
+
for (const layer of this.layers) {
|
|
1290
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
1294
|
+
if (stored === null) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const resolved = resolveStoredValue(stored);
|
|
1298
|
+
if (resolved.state === "expired") {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
foundInLayers.push(layer.name);
|
|
1302
|
+
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
1303
|
+
const now = Date.now();
|
|
1304
|
+
freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
|
|
1305
|
+
staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
|
|
1306
|
+
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
1307
|
+
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (foundInLayers.length === 0) {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
1314
|
+
return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
791
1315
|
}
|
|
792
1316
|
async exportState() {
|
|
793
1317
|
await this.startup;
|
|
@@ -816,10 +1340,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
816
1340
|
}
|
|
817
1341
|
async importState(entries) {
|
|
818
1342
|
await this.startup;
|
|
819
|
-
await Promise.all(
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1343
|
+
await Promise.all(
|
|
1344
|
+
entries.map(async (entry) => {
|
|
1345
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1346
|
+
await this.tagIndex.touch(entry.key);
|
|
1347
|
+
})
|
|
1348
|
+
);
|
|
823
1349
|
}
|
|
824
1350
|
async persistToFile(filePath) {
|
|
825
1351
|
const snapshot = await this.exportState();
|
|
@@ -827,11 +1353,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
827
1353
|
}
|
|
828
1354
|
async restoreFromFile(filePath) {
|
|
829
1355
|
const raw = await import_node_fs.promises.readFile(filePath, "utf8");
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1356
|
+
let parsed;
|
|
1357
|
+
try {
|
|
1358
|
+
parsed = JSON.parse(raw, (_key, value) => {
|
|
1359
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1360
|
+
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1361
|
+
}
|
|
1362
|
+
return value;
|
|
1363
|
+
});
|
|
1364
|
+
} catch (cause) {
|
|
1365
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
833
1366
|
}
|
|
834
|
-
|
|
1367
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1368
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1369
|
+
}
|
|
1370
|
+
await this.importState(parsed);
|
|
835
1371
|
}
|
|
836
1372
|
async disconnect() {
|
|
837
1373
|
if (!this.disconnectPromise) {
|
|
@@ -856,7 +1392,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
856
1392
|
const fetchTask = async () => {
|
|
857
1393
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
858
1394
|
if (secondHit.found) {
|
|
859
|
-
this.
|
|
1395
|
+
this.metricsCollector.increment("hits");
|
|
860
1396
|
return secondHit.value;
|
|
861
1397
|
}
|
|
862
1398
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
@@ -881,12 +1417,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
881
1417
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
882
1418
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
883
1419
|
const deadline = Date.now() + timeoutMs;
|
|
884
|
-
this.
|
|
1420
|
+
this.metricsCollector.increment("singleFlightWaits");
|
|
885
1421
|
this.emit("stampede-dedupe", { key });
|
|
886
1422
|
while (Date.now() < deadline) {
|
|
887
1423
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
888
1424
|
if (hit.found) {
|
|
889
|
-
this.
|
|
1425
|
+
this.metricsCollector.increment("hits");
|
|
890
1426
|
return hit.value;
|
|
891
1427
|
}
|
|
892
1428
|
await this.sleep(pollIntervalMs);
|
|
@@ -894,12 +1430,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
894
1430
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
895
1431
|
}
|
|
896
1432
|
async fetchAndPopulate(key, fetcher, options) {
|
|
897
|
-
this.
|
|
898
|
-
this.
|
|
1433
|
+
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1434
|
+
this.metricsCollector.increment("fetches");
|
|
1435
|
+
const fetchStart = Date.now();
|
|
899
1436
|
let fetched;
|
|
900
1437
|
try {
|
|
901
1438
|
fetched = await fetcher();
|
|
902
|
-
this.
|
|
1439
|
+
this.circuitBreakerManager.recordSuccess(key);
|
|
1440
|
+
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
903
1441
|
} catch (error) {
|
|
904
1442
|
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
905
1443
|
throw error;
|
|
@@ -911,6 +1449,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
911
1449
|
await this.storeEntry(key, "empty", null, options);
|
|
912
1450
|
return null;
|
|
913
1451
|
}
|
|
1452
|
+
if (options?.shouldCache && !options.shouldCache(fetched)) {
|
|
1453
|
+
return fetched;
|
|
1454
|
+
}
|
|
914
1455
|
await this.storeEntry(key, "value", fetched, options);
|
|
915
1456
|
return fetched;
|
|
916
1457
|
}
|
|
@@ -921,7 +1462,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
921
1462
|
} else {
|
|
922
1463
|
await this.tagIndex.touch(key);
|
|
923
1464
|
}
|
|
924
|
-
this.
|
|
1465
|
+
this.metricsCollector.increment("sets");
|
|
925
1466
|
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
926
1467
|
this.emit("set", { key, kind, tags: options?.tags });
|
|
927
1468
|
if (this.shouldBroadcastL1Invalidation()) {
|
|
@@ -932,9 +1473,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
932
1473
|
let sawRetainableValue = false;
|
|
933
1474
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
934
1475
|
const layer = this.layers[index];
|
|
1476
|
+
if (!layer) continue;
|
|
1477
|
+
const readStart = performance.now();
|
|
935
1478
|
const stored = await this.readLayerEntry(layer, key);
|
|
1479
|
+
const readDuration = performance.now() - readStart;
|
|
1480
|
+
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
936
1481
|
if (stored === null) {
|
|
937
|
-
this.
|
|
1482
|
+
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
938
1483
|
continue;
|
|
939
1484
|
}
|
|
940
1485
|
const resolved = resolveStoredValue(stored);
|
|
@@ -948,10 +1493,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
948
1493
|
}
|
|
949
1494
|
await this.tagIndex.touch(key);
|
|
950
1495
|
await this.backfill(key, stored, index - 1, options);
|
|
951
|
-
this.
|
|
1496
|
+
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
952
1497
|
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
953
1498
|
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
954
|
-
return {
|
|
1499
|
+
return {
|
|
1500
|
+
found: true,
|
|
1501
|
+
value: resolved.value,
|
|
1502
|
+
stored,
|
|
1503
|
+
state: resolved.state,
|
|
1504
|
+
layerIndex: index,
|
|
1505
|
+
layerName: layer.name
|
|
1506
|
+
};
|
|
955
1507
|
}
|
|
956
1508
|
if (!sawRetainableValue) {
|
|
957
1509
|
await this.tagIndex.remove(key);
|
|
@@ -983,7 +1535,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
983
1535
|
}
|
|
984
1536
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
985
1537
|
const layer = this.layers[index];
|
|
986
|
-
if (this.shouldSkipLayer(layer)) {
|
|
1538
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
987
1539
|
continue;
|
|
988
1540
|
}
|
|
989
1541
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
@@ -993,7 +1545,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
993
1545
|
await this.handleLayerFailure(layer, "backfill", error);
|
|
994
1546
|
continue;
|
|
995
1547
|
}
|
|
996
|
-
this.
|
|
1548
|
+
this.metricsCollector.increment("backfills");
|
|
997
1549
|
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
998
1550
|
this.emit("backfill", { key, layer: layer.name });
|
|
999
1551
|
}
|
|
@@ -1010,11 +1562,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1010
1562
|
options?.staleWhileRevalidate,
|
|
1011
1563
|
this.options.staleWhileRevalidate
|
|
1012
1564
|
);
|
|
1013
|
-
const staleIfError = this.resolveLayerSeconds(
|
|
1014
|
-
layer.name,
|
|
1015
|
-
options?.staleIfError,
|
|
1016
|
-
this.options.staleIfError
|
|
1017
|
-
);
|
|
1565
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
1018
1566
|
const payload = createStoredValueEnvelope({
|
|
1019
1567
|
kind,
|
|
1020
1568
|
value,
|
|
@@ -1042,7 +1590,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1042
1590
|
if (failures.length === 0) {
|
|
1043
1591
|
return;
|
|
1044
1592
|
}
|
|
1045
|
-
this.
|
|
1593
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
1046
1594
|
this.logger.debug?.("write-failure", {
|
|
1047
1595
|
...context,
|
|
1048
1596
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
@@ -1055,42 +1603,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1055
1603
|
}
|
|
1056
1604
|
}
|
|
1057
1605
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1058
|
-
|
|
1059
|
-
layerName,
|
|
1060
|
-
options?.negativeTtl,
|
|
1061
|
-
this.options.negativeTtl,
|
|
1062
|
-
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
1063
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
1064
|
-
const adaptiveTtl = this.applyAdaptiveTtl(
|
|
1065
|
-
key,
|
|
1066
|
-
layerName,
|
|
1067
|
-
baseTtl,
|
|
1068
|
-
options?.adaptiveTtl ?? this.options.adaptiveTtl
|
|
1069
|
-
);
|
|
1070
|
-
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
1071
|
-
return this.applyJitter(adaptiveTtl, jitter);
|
|
1606
|
+
return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
|
|
1072
1607
|
}
|
|
1073
1608
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1074
|
-
|
|
1075
|
-
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
1076
|
-
}
|
|
1077
|
-
if (globalDefault !== void 0) {
|
|
1078
|
-
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
1079
|
-
}
|
|
1080
|
-
return fallback;
|
|
1081
|
-
}
|
|
1082
|
-
readLayerNumber(layerName, value) {
|
|
1083
|
-
if (typeof value === "number") {
|
|
1084
|
-
return value;
|
|
1085
|
-
}
|
|
1086
|
-
return value[layerName];
|
|
1087
|
-
}
|
|
1088
|
-
applyJitter(ttl, jitter) {
|
|
1089
|
-
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
1090
|
-
return ttl;
|
|
1091
|
-
}
|
|
1092
|
-
const delta = (Math.random() * 2 - 1) * jitter;
|
|
1093
|
-
return Math.max(1, Math.round(ttl + delta));
|
|
1609
|
+
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
1094
1610
|
}
|
|
1095
1611
|
shouldNegativeCache(options) {
|
|
1096
1612
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
@@ -1100,11 +1616,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1100
1616
|
return;
|
|
1101
1617
|
}
|
|
1102
1618
|
const refresh = (async () => {
|
|
1103
|
-
this.
|
|
1619
|
+
this.metricsCollector.increment("refreshes");
|
|
1104
1620
|
try {
|
|
1105
1621
|
await this.fetchWithGuards(key, fetcher, options);
|
|
1106
1622
|
} catch (error) {
|
|
1107
|
-
this.
|
|
1623
|
+
this.metricsCollector.increment("refreshErrors");
|
|
1108
1624
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
1109
1625
|
} finally {
|
|
1110
1626
|
this.backgroundRefreshes.delete(key);
|
|
@@ -1126,10 +1642,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1126
1642
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
1127
1643
|
for (const key of keys) {
|
|
1128
1644
|
await this.tagIndex.remove(key);
|
|
1129
|
-
this.
|
|
1645
|
+
this.ttlResolver.deleteProfile(key);
|
|
1646
|
+
this.circuitBreakerManager.delete(key);
|
|
1130
1647
|
}
|
|
1131
|
-
this.
|
|
1132
|
-
this.
|
|
1648
|
+
this.metricsCollector.increment("deletes", keys.length);
|
|
1649
|
+
this.metricsCollector.increment("invalidations");
|
|
1133
1650
|
this.logger.debug?.("delete", { keys });
|
|
1134
1651
|
this.emit("delete", { keys });
|
|
1135
1652
|
}
|
|
@@ -1150,7 +1667,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1150
1667
|
if (message.scope === "clear") {
|
|
1151
1668
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
1152
1669
|
await this.tagIndex.clear();
|
|
1153
|
-
this.
|
|
1670
|
+
this.ttlResolver.clearProfiles();
|
|
1154
1671
|
return;
|
|
1155
1672
|
}
|
|
1156
1673
|
const keys = message.keys ?? [];
|
|
@@ -1158,10 +1675,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1158
1675
|
if (message.operation !== "write") {
|
|
1159
1676
|
for (const key of keys) {
|
|
1160
1677
|
await this.tagIndex.remove(key);
|
|
1161
|
-
this.
|
|
1678
|
+
this.ttlResolver.deleteProfile(key);
|
|
1162
1679
|
}
|
|
1163
1680
|
}
|
|
1164
1681
|
}
|
|
1682
|
+
async getTagsForKey(key) {
|
|
1683
|
+
if (this.tagIndex.tagsForKey) {
|
|
1684
|
+
return this.tagIndex.tagsForKey(key);
|
|
1685
|
+
}
|
|
1686
|
+
return [];
|
|
1687
|
+
}
|
|
1165
1688
|
formatError(error) {
|
|
1166
1689
|
if (error instanceof Error) {
|
|
1167
1690
|
return error.message;
|
|
@@ -1188,13 +1711,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1188
1711
|
}
|
|
1189
1712
|
return;
|
|
1190
1713
|
}
|
|
1191
|
-
await Promise.all(
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1714
|
+
await Promise.all(
|
|
1715
|
+
keys.map(async (key) => {
|
|
1716
|
+
try {
|
|
1717
|
+
await layer.delete(key);
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1720
|
+
}
|
|
1721
|
+
})
|
|
1722
|
+
);
|
|
1198
1723
|
})
|
|
1199
1724
|
);
|
|
1200
1725
|
}
|
|
@@ -1295,7 +1820,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1295
1820
|
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1296
1821
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1297
1822
|
const layer = this.layers[index];
|
|
1298
|
-
if (this.shouldSkipLayer(layer)) {
|
|
1823
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
1299
1824
|
continue;
|
|
1300
1825
|
}
|
|
1301
1826
|
try {
|
|
@@ -1309,33 +1834,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1309
1834
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1310
1835
|
}
|
|
1311
1836
|
}
|
|
1312
|
-
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
1313
|
-
if (!ttl || !adaptiveTtl) {
|
|
1314
|
-
return ttl;
|
|
1315
|
-
}
|
|
1316
|
-
const profile = this.accessProfiles.get(key);
|
|
1317
|
-
if (!profile) {
|
|
1318
|
-
return ttl;
|
|
1319
|
-
}
|
|
1320
|
-
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
1321
|
-
const hotAfter = config.hotAfter ?? 3;
|
|
1322
|
-
if (profile.hits < hotAfter) {
|
|
1323
|
-
return ttl;
|
|
1324
|
-
}
|
|
1325
|
-
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
1326
|
-
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
1327
|
-
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
1328
|
-
return Math.min(maxTtl, ttl + step * multiplier);
|
|
1329
|
-
}
|
|
1330
|
-
recordAccess(key) {
|
|
1331
|
-
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
1332
|
-
profile.hits += 1;
|
|
1333
|
-
profile.lastAccessAt = Date.now();
|
|
1334
|
-
this.accessProfiles.set(key, profile);
|
|
1335
|
-
}
|
|
1336
|
-
incrementMetricMap(target, key) {
|
|
1337
|
-
target[key] = (target[key] ?? 0) + 1;
|
|
1338
|
-
}
|
|
1339
1837
|
shouldSkipLayer(layer) {
|
|
1340
1838
|
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1341
1839
|
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
@@ -1346,7 +1844,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1346
1844
|
}
|
|
1347
1845
|
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1348
1846
|
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1349
|
-
this.
|
|
1847
|
+
this.metricsCollector.increment("degradedOperations");
|
|
1350
1848
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1351
1849
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1352
1850
|
return null;
|
|
@@ -1354,37 +1852,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1354
1852
|
isGracefulDegradationEnabled() {
|
|
1355
1853
|
return Boolean(this.options.gracefulDegradation);
|
|
1356
1854
|
}
|
|
1357
|
-
assertCircuitClosed(key, options) {
|
|
1358
|
-
const state = this.circuitBreakers.get(key);
|
|
1359
|
-
if (!state?.openUntil) {
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
if (state.openUntil <= Date.now()) {
|
|
1363
|
-
state.openUntil = null;
|
|
1364
|
-
state.failures = 0;
|
|
1365
|
-
this.circuitBreakers.set(key, state);
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
this.emitError("circuit-breaker-open", { key, openUntil: state.openUntil });
|
|
1369
|
-
throw new Error(`Circuit breaker is open for key "${key}".`);
|
|
1370
|
-
}
|
|
1371
1855
|
recordCircuitFailure(key, options, error) {
|
|
1372
1856
|
if (!options) {
|
|
1373
1857
|
return;
|
|
1374
1858
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
state.failures += 1;
|
|
1379
|
-
if (state.failures >= failureThreshold) {
|
|
1380
|
-
state.openUntil = Date.now() + cooldownMs;
|
|
1381
|
-
this.metrics.circuitBreakerTrips += 1;
|
|
1859
|
+
this.circuitBreakerManager.recordFailure(key, options);
|
|
1860
|
+
if (this.circuitBreakerManager.isOpen(key)) {
|
|
1861
|
+
this.metricsCollector.increment("circuitBreakerTrips");
|
|
1382
1862
|
}
|
|
1383
|
-
this.
|
|
1384
|
-
this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
|
|
1385
|
-
}
|
|
1386
|
-
resetCircuitBreaker(key) {
|
|
1387
|
-
this.circuitBreakers.delete(key);
|
|
1863
|
+
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
1388
1864
|
}
|
|
1389
1865
|
isNegativeStoredValue(stored) {
|
|
1390
1866
|
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
@@ -1439,6 +1915,22 @@ var CacheStackModule = class {
|
|
|
1439
1915
|
exports: [provider]
|
|
1440
1916
|
};
|
|
1441
1917
|
}
|
|
1918
|
+
static forRootAsync(options) {
|
|
1919
|
+
const provider = {
|
|
1920
|
+
provide: CACHE_STACK,
|
|
1921
|
+
inject: options.inject ?? [],
|
|
1922
|
+
useFactory: async (...args) => {
|
|
1923
|
+
const resolved = await options.useFactory(...args);
|
|
1924
|
+
return new CacheStack(resolved.layers, resolved.bridgeOptions);
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
return {
|
|
1928
|
+
global: true,
|
|
1929
|
+
module: CacheStackModule,
|
|
1930
|
+
providers: [provider],
|
|
1931
|
+
exports: [provider]
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1442
1934
|
};
|
|
1443
1935
|
CacheStackModule = __decorateClass([
|
|
1444
1936
|
(0, import_common.Global)(),
|