layercache 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -7
- package/dist/chunk-IILH5XTS.js +103 -0
- package/dist/cli.cjs +228 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +96 -0
- package/dist/index.cjs +833 -95
- package/dist/index.d.cts +182 -4
- package/dist/index.d.ts +182 -4
- package/dist/index.js +821 -183
- package/package.json +5 -2
- package/packages/nestjs/dist/index.cjs +652 -81
- package/packages/nestjs/dist/index.d.cts +204 -2
- package/packages/nestjs/dist/index.d.ts +204 -2
- package/packages/nestjs/dist/index.js +651 -81
|
@@ -30,6 +30,7 @@ var index_exports = {};
|
|
|
30
30
|
__export(index_exports, {
|
|
31
31
|
CACHE_STACK: () => CACHE_STACK,
|
|
32
32
|
CacheStackModule: () => CacheStackModule,
|
|
33
|
+
Cacheable: () => Cacheable,
|
|
33
34
|
InjectCacheStack: () => InjectCacheStack
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -37,11 +38,43 @@ module.exports = __toCommonJS(index_exports);
|
|
|
37
38
|
// src/constants.ts
|
|
38
39
|
var CACHE_STACK = /* @__PURE__ */ Symbol("CACHE_STACK");
|
|
39
40
|
|
|
41
|
+
// ../../src/decorators/createCachedMethodDecorator.ts
|
|
42
|
+
function createCachedMethodDecorator(options) {
|
|
43
|
+
const wrappedByInstance = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
return ((_, propertyKey, descriptor) => {
|
|
45
|
+
const original = descriptor.value;
|
|
46
|
+
if (typeof original !== "function") {
|
|
47
|
+
throw new Error("createCachedMethodDecorator can only be applied to methods.");
|
|
48
|
+
}
|
|
49
|
+
descriptor.value = async function(...args) {
|
|
50
|
+
const instance = this;
|
|
51
|
+
let wrapped = wrappedByInstance.get(instance);
|
|
52
|
+
if (!wrapped) {
|
|
53
|
+
const cache = options.cache(instance);
|
|
54
|
+
wrapped = cache.wrap(
|
|
55
|
+
options.prefix ?? String(propertyKey),
|
|
56
|
+
(...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
|
|
57
|
+
options
|
|
58
|
+
);
|
|
59
|
+
wrappedByInstance.set(instance, wrapped);
|
|
60
|
+
}
|
|
61
|
+
return wrapped(...args);
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/decorators.ts
|
|
67
|
+
function Cacheable(options) {
|
|
68
|
+
return createCachedMethodDecorator(options);
|
|
69
|
+
}
|
|
70
|
+
|
|
40
71
|
// src/module.ts
|
|
41
72
|
var import_common = require("@nestjs/common");
|
|
42
73
|
|
|
43
74
|
// ../../src/CacheStack.ts
|
|
44
75
|
var import_node_crypto = require("crypto");
|
|
76
|
+
var import_node_fs = require("fs");
|
|
77
|
+
var import_node_events = require("events");
|
|
45
78
|
|
|
46
79
|
// ../../src/internal/StoredValue.ts
|
|
47
80
|
function isStoredValueEnvelope(value) {
|
|
@@ -61,7 +94,10 @@ function createStoredValueEnvelope(options) {
|
|
|
61
94
|
value: options.value,
|
|
62
95
|
freshUntil,
|
|
63
96
|
staleUntil,
|
|
64
|
-
errorUntil
|
|
97
|
+
errorUntil,
|
|
98
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
99
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
100
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
65
101
|
};
|
|
66
102
|
}
|
|
67
103
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -102,6 +138,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
102
138
|
}
|
|
103
139
|
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
104
140
|
}
|
|
141
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
142
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
const remainingMs = stored.freshUntil - now;
|
|
146
|
+
if (remainingMs <= 0) {
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
150
|
+
}
|
|
151
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
152
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
153
|
+
return stored;
|
|
154
|
+
}
|
|
155
|
+
return createStoredValueEnvelope({
|
|
156
|
+
kind: stored.kind,
|
|
157
|
+
value: stored.value,
|
|
158
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
159
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
160
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
161
|
+
now
|
|
162
|
+
});
|
|
163
|
+
}
|
|
105
164
|
function maxExpiry(stored) {
|
|
106
165
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
107
166
|
(value) => value !== null
|
|
@@ -118,6 +177,61 @@ function normalizePositiveSeconds(value) {
|
|
|
118
177
|
return value;
|
|
119
178
|
}
|
|
120
179
|
|
|
180
|
+
// ../../src/CacheNamespace.ts
|
|
181
|
+
var CacheNamespace = class {
|
|
182
|
+
constructor(cache, prefix) {
|
|
183
|
+
this.cache = cache;
|
|
184
|
+
this.prefix = prefix;
|
|
185
|
+
}
|
|
186
|
+
cache;
|
|
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));
|
|
196
|
+
}
|
|
197
|
+
async clear() {
|
|
198
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
199
|
+
}
|
|
200
|
+
async mget(entries) {
|
|
201
|
+
return this.cache.mget(entries.map((entry) => ({
|
|
202
|
+
...entry,
|
|
203
|
+
key: this.qualify(entry.key)
|
|
204
|
+
})));
|
|
205
|
+
}
|
|
206
|
+
async mset(entries) {
|
|
207
|
+
await this.cache.mset(entries.map((entry) => ({
|
|
208
|
+
...entry,
|
|
209
|
+
key: this.qualify(entry.key)
|
|
210
|
+
})));
|
|
211
|
+
}
|
|
212
|
+
async invalidateByTag(tag) {
|
|
213
|
+
await this.cache.invalidateByTag(tag);
|
|
214
|
+
}
|
|
215
|
+
async invalidateByPattern(pattern) {
|
|
216
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
217
|
+
}
|
|
218
|
+
wrap(keyPrefix, fetcher, options) {
|
|
219
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
220
|
+
}
|
|
221
|
+
warm(entries, options) {
|
|
222
|
+
return this.cache.warm(entries.map((entry) => ({
|
|
223
|
+
...entry,
|
|
224
|
+
key: this.qualify(entry.key)
|
|
225
|
+
})), options);
|
|
226
|
+
}
|
|
227
|
+
getMetrics() {
|
|
228
|
+
return this.cache.getMetrics();
|
|
229
|
+
}
|
|
230
|
+
qualify(key) {
|
|
231
|
+
return `${this.prefix}:${key}`;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
121
235
|
// ../../src/invalidation/PatternMatcher.ts
|
|
122
236
|
var PatternMatcher = class {
|
|
123
237
|
static matches(pattern, value) {
|
|
@@ -364,22 +478,24 @@ var Mutex = class {
|
|
|
364
478
|
var StampedeGuard = class {
|
|
365
479
|
mutexes = /* @__PURE__ */ new Map();
|
|
366
480
|
async execute(key, task) {
|
|
367
|
-
const
|
|
481
|
+
const entry = this.getMutexEntry(key);
|
|
368
482
|
try {
|
|
369
|
-
return await mutex.runExclusive(task);
|
|
483
|
+
return await entry.mutex.runExclusive(task);
|
|
370
484
|
} finally {
|
|
371
|
-
|
|
485
|
+
entry.references -= 1;
|
|
486
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
372
487
|
this.mutexes.delete(key);
|
|
373
488
|
}
|
|
374
489
|
}
|
|
375
490
|
}
|
|
376
|
-
|
|
377
|
-
let
|
|
378
|
-
if (!
|
|
379
|
-
|
|
380
|
-
this.mutexes.set(key,
|
|
491
|
+
getMutexEntry(key) {
|
|
492
|
+
let entry = this.mutexes.get(key);
|
|
493
|
+
if (!entry) {
|
|
494
|
+
entry = { mutex: new Mutex(), references: 0 };
|
|
495
|
+
this.mutexes.set(key, entry);
|
|
381
496
|
}
|
|
382
|
-
|
|
497
|
+
entry.references += 1;
|
|
498
|
+
return entry;
|
|
383
499
|
}
|
|
384
500
|
};
|
|
385
501
|
|
|
@@ -388,6 +504,7 @@ var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
|
388
504
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
389
505
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
390
506
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
507
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
391
508
|
var EMPTY_METRICS = () => ({
|
|
392
509
|
hits: 0,
|
|
393
510
|
misses: 0,
|
|
@@ -400,7 +517,12 @@ var EMPTY_METRICS = () => ({
|
|
|
400
517
|
refreshes: 0,
|
|
401
518
|
refreshErrors: 0,
|
|
402
519
|
writeFailures: 0,
|
|
403
|
-
singleFlightWaits: 0
|
|
520
|
+
singleFlightWaits: 0,
|
|
521
|
+
negativeCacheHits: 0,
|
|
522
|
+
circuitBreakerTrips: 0,
|
|
523
|
+
degradedOperations: 0,
|
|
524
|
+
hitsByLayer: {},
|
|
525
|
+
missesByLayer: {}
|
|
404
526
|
});
|
|
405
527
|
var DebugLogger = class {
|
|
406
528
|
enabled;
|
|
@@ -408,20 +530,34 @@ var DebugLogger = class {
|
|
|
408
530
|
this.enabled = enabled;
|
|
409
531
|
}
|
|
410
532
|
debug(message, context) {
|
|
533
|
+
this.write("debug", message, context);
|
|
534
|
+
}
|
|
535
|
+
info(message, context) {
|
|
536
|
+
this.write("info", message, context);
|
|
537
|
+
}
|
|
538
|
+
warn(message, context) {
|
|
539
|
+
this.write("warn", message, context);
|
|
540
|
+
}
|
|
541
|
+
error(message, context) {
|
|
542
|
+
this.write("error", message, context);
|
|
543
|
+
}
|
|
544
|
+
write(level, message, context) {
|
|
411
545
|
if (!this.enabled) {
|
|
412
546
|
return;
|
|
413
547
|
}
|
|
414
548
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
415
|
-
console
|
|
549
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
416
550
|
}
|
|
417
551
|
};
|
|
418
|
-
var CacheStack = class {
|
|
552
|
+
var CacheStack = class extends import_node_events.EventEmitter {
|
|
419
553
|
constructor(layers, options = {}) {
|
|
554
|
+
super();
|
|
420
555
|
this.layers = layers;
|
|
421
556
|
this.options = options;
|
|
422
557
|
if (layers.length === 0) {
|
|
423
558
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
424
559
|
}
|
|
560
|
+
this.validateConfiguration();
|
|
425
561
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
426
562
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
427
563
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -437,33 +573,47 @@ var CacheStack = class {
|
|
|
437
573
|
logger;
|
|
438
574
|
tagIndex;
|
|
439
575
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
576
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
577
|
+
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
578
|
+
circuitBreakers = /* @__PURE__ */ new Map();
|
|
579
|
+
isDisconnecting = false;
|
|
580
|
+
disconnectPromise;
|
|
440
581
|
async get(key, fetcher, options) {
|
|
582
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
583
|
+
this.validateWriteOptions(options);
|
|
441
584
|
await this.startup;
|
|
442
|
-
const hit = await this.readFromLayers(
|
|
585
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
443
586
|
if (hit.found) {
|
|
587
|
+
this.recordAccess(normalizedKey);
|
|
588
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
589
|
+
this.metrics.negativeCacheHits += 1;
|
|
590
|
+
}
|
|
444
591
|
if (hit.state === "fresh") {
|
|
445
592
|
this.metrics.hits += 1;
|
|
593
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
446
594
|
return hit.value;
|
|
447
595
|
}
|
|
448
596
|
if (hit.state === "stale-while-revalidate") {
|
|
449
597
|
this.metrics.hits += 1;
|
|
450
598
|
this.metrics.staleHits += 1;
|
|
599
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
451
600
|
if (fetcher) {
|
|
452
|
-
this.scheduleBackgroundRefresh(
|
|
601
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
453
602
|
}
|
|
454
603
|
return hit.value;
|
|
455
604
|
}
|
|
456
605
|
if (!fetcher) {
|
|
457
606
|
this.metrics.hits += 1;
|
|
458
607
|
this.metrics.staleHits += 1;
|
|
608
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
459
609
|
return hit.value;
|
|
460
610
|
}
|
|
461
611
|
try {
|
|
462
|
-
return await this.fetchWithGuards(
|
|
612
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
463
613
|
} catch (error) {
|
|
464
614
|
this.metrics.staleHits += 1;
|
|
465
615
|
this.metrics.refreshErrors += 1;
|
|
466
|
-
this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
|
|
616
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
467
617
|
return hit.value;
|
|
468
618
|
}
|
|
469
619
|
}
|
|
@@ -471,71 +621,144 @@ var CacheStack = class {
|
|
|
471
621
|
if (!fetcher) {
|
|
472
622
|
return null;
|
|
473
623
|
}
|
|
474
|
-
return this.fetchWithGuards(
|
|
624
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
475
625
|
}
|
|
476
626
|
async set(key, value, options) {
|
|
627
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
628
|
+
this.validateWriteOptions(options);
|
|
477
629
|
await this.startup;
|
|
478
|
-
await this.storeEntry(
|
|
630
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
479
631
|
}
|
|
480
632
|
async delete(key) {
|
|
633
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
481
634
|
await this.startup;
|
|
482
|
-
await this.deleteKeys([
|
|
483
|
-
await this.publishInvalidation({ scope: "key", keys: [
|
|
635
|
+
await this.deleteKeys([normalizedKey]);
|
|
636
|
+
await this.publishInvalidation({ scope: "key", keys: [normalizedKey], sourceId: this.instanceId, operation: "delete" });
|
|
484
637
|
}
|
|
485
638
|
async clear() {
|
|
486
639
|
await this.startup;
|
|
487
640
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
488
641
|
await this.tagIndex.clear();
|
|
642
|
+
this.accessProfiles.clear();
|
|
489
643
|
this.metrics.invalidations += 1;
|
|
490
|
-
this.logger.debug("clear");
|
|
644
|
+
this.logger.debug?.("clear");
|
|
491
645
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
492
646
|
}
|
|
493
647
|
async mget(entries) {
|
|
494
648
|
if (entries.length === 0) {
|
|
495
649
|
return [];
|
|
496
650
|
}
|
|
497
|
-
const
|
|
651
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
652
|
+
...entry,
|
|
653
|
+
key: this.validateCacheKey(entry.key)
|
|
654
|
+
}));
|
|
655
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
656
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
498
657
|
if (!canFastPath) {
|
|
499
|
-
|
|
658
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
659
|
+
return Promise.all(
|
|
660
|
+
normalizedEntries.map((entry) => {
|
|
661
|
+
const optionsSignature = this.serializeOptions(entry.options);
|
|
662
|
+
const existing = pendingReads.get(entry.key);
|
|
663
|
+
if (!existing) {
|
|
664
|
+
const promise = this.get(entry.key, entry.fetch, entry.options);
|
|
665
|
+
pendingReads.set(entry.key, {
|
|
666
|
+
promise,
|
|
667
|
+
fetch: entry.fetch,
|
|
668
|
+
optionsSignature
|
|
669
|
+
});
|
|
670
|
+
return promise;
|
|
671
|
+
}
|
|
672
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
673
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
674
|
+
}
|
|
675
|
+
return existing.promise;
|
|
676
|
+
})
|
|
677
|
+
);
|
|
500
678
|
}
|
|
501
679
|
await this.startup;
|
|
502
|
-
const pending = new Set(
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
680
|
+
const pending = /* @__PURE__ */ new Set();
|
|
681
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
682
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
683
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
684
|
+
const key = normalizedEntries[index].key;
|
|
685
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
686
|
+
indexes.push(index);
|
|
687
|
+
indexesByKey.set(key, indexes);
|
|
688
|
+
pending.add(key);
|
|
689
|
+
}
|
|
690
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
691
|
+
const layer = this.layers[layerIndex];
|
|
692
|
+
const keys = [...pending];
|
|
693
|
+
if (keys.length === 0) {
|
|
507
694
|
break;
|
|
508
695
|
}
|
|
509
|
-
const keys = indexes.map((index) => entries[index].key);
|
|
510
696
|
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
511
697
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
512
|
-
const
|
|
698
|
+
const key = keys[offset];
|
|
513
699
|
const stored = values[offset];
|
|
514
700
|
if (stored === null) {
|
|
515
701
|
continue;
|
|
516
702
|
}
|
|
517
703
|
const resolved = resolveStoredValue(stored);
|
|
518
704
|
if (resolved.state === "expired") {
|
|
519
|
-
await layer.delete(
|
|
705
|
+
await layer.delete(key);
|
|
520
706
|
continue;
|
|
521
707
|
}
|
|
522
|
-
await this.tagIndex.touch(
|
|
523
|
-
await this.backfill(
|
|
524
|
-
|
|
525
|
-
pending.delete(
|
|
526
|
-
this.metrics.hits += 1;
|
|
708
|
+
await this.tagIndex.touch(key);
|
|
709
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
710
|
+
resultsByKey.set(key, resolved.value);
|
|
711
|
+
pending.delete(key);
|
|
712
|
+
this.metrics.hits += indexesByKey.get(key)?.length ?? 1;
|
|
527
713
|
}
|
|
528
714
|
}
|
|
529
715
|
if (pending.size > 0) {
|
|
530
|
-
for (const
|
|
531
|
-
await this.tagIndex.remove(
|
|
532
|
-
this.metrics.misses += 1;
|
|
716
|
+
for (const key of pending) {
|
|
717
|
+
await this.tagIndex.remove(key);
|
|
718
|
+
this.metrics.misses += indexesByKey.get(key)?.length ?? 1;
|
|
533
719
|
}
|
|
534
720
|
}
|
|
535
|
-
return
|
|
721
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
536
722
|
}
|
|
537
723
|
async mset(entries) {
|
|
538
|
-
|
|
724
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
725
|
+
...entry,
|
|
726
|
+
key: this.validateCacheKey(entry.key)
|
|
727
|
+
}));
|
|
728
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
729
|
+
await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
730
|
+
}
|
|
731
|
+
async warm(entries, options = {}) {
|
|
732
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
733
|
+
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
734
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
735
|
+
while (queue.length > 0) {
|
|
736
|
+
const entry = queue.shift();
|
|
737
|
+
if (!entry) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
await this.get(entry.key, entry.fetcher, entry.options);
|
|
742
|
+
this.emit("warm", { key: entry.key });
|
|
743
|
+
} catch (error) {
|
|
744
|
+
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
745
|
+
if (!options.continueOnError) {
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
await Promise.all(workers);
|
|
752
|
+
}
|
|
753
|
+
wrap(prefix, fetcher, options = {}) {
|
|
754
|
+
return (...args) => {
|
|
755
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
756
|
+
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
757
|
+
return this.get(key, () => fetcher(...args), options);
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
namespace(prefix) {
|
|
761
|
+
return new CacheNamespace(this, prefix);
|
|
539
762
|
}
|
|
540
763
|
async invalidateByTag(tag) {
|
|
541
764
|
await this.startup;
|
|
@@ -552,13 +775,74 @@ var CacheStack = class {
|
|
|
552
775
|
getMetrics() {
|
|
553
776
|
return { ...this.metrics };
|
|
554
777
|
}
|
|
778
|
+
getStats() {
|
|
779
|
+
return {
|
|
780
|
+
metrics: this.getMetrics(),
|
|
781
|
+
layers: this.layers.map((layer) => ({
|
|
782
|
+
name: layer.name,
|
|
783
|
+
isLocal: Boolean(layer.isLocal),
|
|
784
|
+
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
785
|
+
})),
|
|
786
|
+
backgroundRefreshes: this.backgroundRefreshes.size
|
|
787
|
+
};
|
|
788
|
+
}
|
|
555
789
|
resetMetrics() {
|
|
556
790
|
Object.assign(this.metrics, EMPTY_METRICS());
|
|
557
791
|
}
|
|
558
|
-
async
|
|
792
|
+
async exportState() {
|
|
793
|
+
await this.startup;
|
|
794
|
+
const exported = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const layer of this.layers) {
|
|
796
|
+
if (!layer.keys) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const keys = await layer.keys();
|
|
800
|
+
for (const key of keys) {
|
|
801
|
+
if (exported.has(key)) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
805
|
+
if (stored === null) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
exported.set(key, {
|
|
809
|
+
key,
|
|
810
|
+
value: stored,
|
|
811
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return [...exported.values()];
|
|
816
|
+
}
|
|
817
|
+
async importState(entries) {
|
|
559
818
|
await this.startup;
|
|
560
|
-
await
|
|
561
|
-
|
|
819
|
+
await Promise.all(entries.map(async (entry) => {
|
|
820
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
821
|
+
await this.tagIndex.touch(entry.key);
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
async persistToFile(filePath) {
|
|
825
|
+
const snapshot = await this.exportState();
|
|
826
|
+
await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
827
|
+
}
|
|
828
|
+
async restoreFromFile(filePath) {
|
|
829
|
+
const raw = await import_node_fs.promises.readFile(filePath, "utf8");
|
|
830
|
+
const snapshot = JSON.parse(raw);
|
|
831
|
+
if (!this.isCacheSnapshotEntries(snapshot)) {
|
|
832
|
+
throw new Error("Invalid snapshot file: expected CacheSnapshotEntry[]");
|
|
833
|
+
}
|
|
834
|
+
await this.importState(snapshot);
|
|
835
|
+
}
|
|
836
|
+
async disconnect() {
|
|
837
|
+
if (!this.disconnectPromise) {
|
|
838
|
+
this.isDisconnecting = true;
|
|
839
|
+
this.disconnectPromise = (async () => {
|
|
840
|
+
await this.startup;
|
|
841
|
+
await this.unsubscribeInvalidation?.();
|
|
842
|
+
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
843
|
+
})();
|
|
844
|
+
}
|
|
845
|
+
await this.disconnectPromise;
|
|
562
846
|
}
|
|
563
847
|
async initialize() {
|
|
564
848
|
if (!this.options.invalidationBus) {
|
|
@@ -598,6 +882,7 @@ var CacheStack = class {
|
|
|
598
882
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
599
883
|
const deadline = Date.now() + timeoutMs;
|
|
600
884
|
this.metrics.singleFlightWaits += 1;
|
|
885
|
+
this.emit("stampede-dedupe", { key });
|
|
601
886
|
while (Date.now() < deadline) {
|
|
602
887
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
603
888
|
if (hit.found) {
|
|
@@ -609,8 +894,16 @@ var CacheStack = class {
|
|
|
609
894
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
610
895
|
}
|
|
611
896
|
async fetchAndPopulate(key, fetcher, options) {
|
|
897
|
+
this.assertCircuitClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
612
898
|
this.metrics.fetches += 1;
|
|
613
|
-
|
|
899
|
+
let fetched;
|
|
900
|
+
try {
|
|
901
|
+
fetched = await fetcher();
|
|
902
|
+
this.resetCircuitBreaker(key);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
614
907
|
if (fetched === null || fetched === void 0) {
|
|
615
908
|
if (!this.shouldNegativeCache(options)) {
|
|
616
909
|
return null;
|
|
@@ -629,8 +922,9 @@ var CacheStack = class {
|
|
|
629
922
|
await this.tagIndex.touch(key);
|
|
630
923
|
}
|
|
631
924
|
this.metrics.sets += 1;
|
|
632
|
-
this.logger.debug("set", { key, kind, tags: options?.tags });
|
|
633
|
-
|
|
925
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
926
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
927
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
634
928
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
635
929
|
}
|
|
636
930
|
}
|
|
@@ -640,6 +934,7 @@ var CacheStack = class {
|
|
|
640
934
|
const layer = this.layers[index];
|
|
641
935
|
const stored = await this.readLayerEntry(layer, key);
|
|
642
936
|
if (stored === null) {
|
|
937
|
+
this.incrementMetricMap(this.metrics.missesByLayer, layer.name);
|
|
643
938
|
continue;
|
|
644
939
|
}
|
|
645
940
|
const resolved = resolveStoredValue(stored);
|
|
@@ -653,20 +948,34 @@ var CacheStack = class {
|
|
|
653
948
|
}
|
|
654
949
|
await this.tagIndex.touch(key);
|
|
655
950
|
await this.backfill(key, stored, index - 1, options);
|
|
656
|
-
this.
|
|
657
|
-
|
|
951
|
+
this.incrementMetricMap(this.metrics.hitsByLayer, layer.name);
|
|
952
|
+
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
953
|
+
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
954
|
+
return { found: true, value: resolved.value, stored, state: resolved.state, layerIndex: index, layerName: layer.name };
|
|
658
955
|
}
|
|
659
956
|
if (!sawRetainableValue) {
|
|
660
957
|
await this.tagIndex.remove(key);
|
|
661
958
|
}
|
|
662
|
-
this.logger.debug("miss", { key, mode });
|
|
959
|
+
this.logger.debug?.("miss", { key, mode });
|
|
960
|
+
this.emit("miss", { key, mode });
|
|
663
961
|
return { found: false, value: null, stored: null, state: "miss" };
|
|
664
962
|
}
|
|
665
963
|
async readLayerEntry(layer, key) {
|
|
964
|
+
if (this.shouldSkipLayer(layer)) {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
666
967
|
if (layer.getEntry) {
|
|
667
|
-
|
|
968
|
+
try {
|
|
969
|
+
return await layer.getEntry(key);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
return await layer.get(key);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
668
978
|
}
|
|
669
|
-
return layer.get(key);
|
|
670
979
|
}
|
|
671
980
|
async backfill(key, stored, upToIndex, options) {
|
|
672
981
|
if (upToIndex < 0) {
|
|
@@ -674,16 +983,28 @@ var CacheStack = class {
|
|
|
674
983
|
}
|
|
675
984
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
676
985
|
const layer = this.layers[index];
|
|
986
|
+
if (this.shouldSkipLayer(layer)) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
677
989
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
678
|
-
|
|
990
|
+
try {
|
|
991
|
+
await layer.set(key, stored, ttl);
|
|
992
|
+
} catch (error) {
|
|
993
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
679
996
|
this.metrics.backfills += 1;
|
|
680
|
-
this.logger.debug("backfill", { key, layer: layer.name });
|
|
997
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
998
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
681
999
|
}
|
|
682
1000
|
}
|
|
683
1001
|
async writeAcrossLayers(key, kind, value, options) {
|
|
684
1002
|
const now = Date.now();
|
|
685
1003
|
const operations = this.layers.map((layer) => async () => {
|
|
686
|
-
|
|
1004
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
687
1008
|
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
688
1009
|
layer.name,
|
|
689
1010
|
options?.staleWhileRevalidate,
|
|
@@ -703,7 +1024,11 @@ var CacheStack = class {
|
|
|
703
1024
|
now
|
|
704
1025
|
});
|
|
705
1026
|
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
706
|
-
|
|
1027
|
+
try {
|
|
1028
|
+
await layer.set(key, payload, ttl);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1031
|
+
}
|
|
707
1032
|
});
|
|
708
1033
|
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
709
1034
|
}
|
|
@@ -718,7 +1043,7 @@ var CacheStack = class {
|
|
|
718
1043
|
return;
|
|
719
1044
|
}
|
|
720
1045
|
this.metrics.writeFailures += failures.length;
|
|
721
|
-
this.logger.debug("write-failure", {
|
|
1046
|
+
this.logger.debug?.("write-failure", {
|
|
722
1047
|
...context,
|
|
723
1048
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
724
1049
|
});
|
|
@@ -729,15 +1054,21 @@ var CacheStack = class {
|
|
|
729
1054
|
);
|
|
730
1055
|
}
|
|
731
1056
|
}
|
|
732
|
-
resolveFreshTtl(layerName, kind, options, fallbackTtl) {
|
|
1057
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
733
1058
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
734
1059
|
layerName,
|
|
735
1060
|
options?.negativeTtl,
|
|
736
1061
|
this.options.negativeTtl,
|
|
737
1062
|
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
738
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
|
+
);
|
|
739
1070
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
740
|
-
return this.applyJitter(
|
|
1071
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
741
1072
|
}
|
|
742
1073
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
743
1074
|
if (override !== void 0) {
|
|
@@ -765,7 +1096,7 @@ var CacheStack = class {
|
|
|
765
1096
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
766
1097
|
}
|
|
767
1098
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
768
|
-
if (this.backgroundRefreshes.has(key)) {
|
|
1099
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
769
1100
|
return;
|
|
770
1101
|
}
|
|
771
1102
|
const refresh = (async () => {
|
|
@@ -774,7 +1105,7 @@ var CacheStack = class {
|
|
|
774
1105
|
await this.fetchWithGuards(key, fetcher, options);
|
|
775
1106
|
} catch (error) {
|
|
776
1107
|
this.metrics.refreshErrors += 1;
|
|
777
|
-
this.logger.debug("refresh-error", { key, error: this.formatError(error) });
|
|
1108
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
778
1109
|
} finally {
|
|
779
1110
|
this.backgroundRefreshes.delete(key);
|
|
780
1111
|
}
|
|
@@ -792,21 +1123,15 @@ var CacheStack = class {
|
|
|
792
1123
|
if (keys.length === 0) {
|
|
793
1124
|
return;
|
|
794
1125
|
}
|
|
795
|
-
await
|
|
796
|
-
this.layers.map(async (layer) => {
|
|
797
|
-
if (layer.deleteMany) {
|
|
798
|
-
await layer.deleteMany(keys);
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
802
|
-
})
|
|
803
|
-
);
|
|
1126
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
804
1127
|
for (const key of keys) {
|
|
805
1128
|
await this.tagIndex.remove(key);
|
|
1129
|
+
this.accessProfiles.delete(key);
|
|
806
1130
|
}
|
|
807
1131
|
this.metrics.deletes += keys.length;
|
|
808
1132
|
this.metrics.invalidations += 1;
|
|
809
|
-
this.logger.debug("delete", { keys });
|
|
1133
|
+
this.logger.debug?.("delete", { keys });
|
|
1134
|
+
this.emit("delete", { keys });
|
|
810
1135
|
}
|
|
811
1136
|
async publishInvalidation(message) {
|
|
812
1137
|
if (!this.options.invalidationBus) {
|
|
@@ -825,21 +1150,15 @@ var CacheStack = class {
|
|
|
825
1150
|
if (message.scope === "clear") {
|
|
826
1151
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
827
1152
|
await this.tagIndex.clear();
|
|
1153
|
+
this.accessProfiles.clear();
|
|
828
1154
|
return;
|
|
829
1155
|
}
|
|
830
1156
|
const keys = message.keys ?? [];
|
|
831
|
-
await
|
|
832
|
-
localLayers.map(async (layer) => {
|
|
833
|
-
if (layer.deleteMany) {
|
|
834
|
-
await layer.deleteMany(keys);
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
838
|
-
})
|
|
839
|
-
);
|
|
1157
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
840
1158
|
if (message.operation !== "write") {
|
|
841
1159
|
for (const key of keys) {
|
|
842
1160
|
await this.tagIndex.remove(key);
|
|
1161
|
+
this.accessProfiles.delete(key);
|
|
843
1162
|
}
|
|
844
1163
|
}
|
|
845
1164
|
}
|
|
@@ -852,6 +1171,257 @@ var CacheStack = class {
|
|
|
852
1171
|
sleep(ms) {
|
|
853
1172
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
854
1173
|
}
|
|
1174
|
+
shouldBroadcastL1Invalidation() {
|
|
1175
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1176
|
+
}
|
|
1177
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
1178
|
+
await Promise.all(
|
|
1179
|
+
layers.map(async (layer) => {
|
|
1180
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (layer.deleteMany) {
|
|
1184
|
+
try {
|
|
1185
|
+
await layer.deleteMany(keys);
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1188
|
+
}
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
await Promise.all(keys.map(async (key) => {
|
|
1192
|
+
try {
|
|
1193
|
+
await layer.delete(key);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1196
|
+
}
|
|
1197
|
+
}));
|
|
1198
|
+
})
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
validateConfiguration() {
|
|
1202
|
+
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
1203
|
+
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
1204
|
+
}
|
|
1205
|
+
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
1206
|
+
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
1207
|
+
}
|
|
1208
|
+
this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
1209
|
+
this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
1210
|
+
this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
1211
|
+
this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
1212
|
+
this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
1213
|
+
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1214
|
+
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1215
|
+
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1216
|
+
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1217
|
+
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1218
|
+
}
|
|
1219
|
+
validateWriteOptions(options) {
|
|
1220
|
+
if (!options) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
1224
|
+
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
1225
|
+
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
1226
|
+
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1227
|
+
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1228
|
+
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1229
|
+
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1230
|
+
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1231
|
+
}
|
|
1232
|
+
validateLayerNumberOption(name, value) {
|
|
1233
|
+
if (value === void 0) {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (typeof value === "number") {
|
|
1237
|
+
this.validateNonNegativeNumber(name, value);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1241
|
+
if (layerValue === void 0) {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
validatePositiveNumber(name, value) {
|
|
1248
|
+
if (value === void 0) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1252
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
validateNonNegativeNumber(name, value) {
|
|
1256
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1257
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
validateCacheKey(key) {
|
|
1261
|
+
if (key.length === 0) {
|
|
1262
|
+
throw new Error("Cache key must not be empty.");
|
|
1263
|
+
}
|
|
1264
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1265
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1266
|
+
}
|
|
1267
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1268
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1269
|
+
}
|
|
1270
|
+
return key;
|
|
1271
|
+
}
|
|
1272
|
+
serializeOptions(options) {
|
|
1273
|
+
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1274
|
+
}
|
|
1275
|
+
validateAdaptiveTtlOptions(options) {
|
|
1276
|
+
if (!options || options === true) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1280
|
+
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1281
|
+
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1282
|
+
}
|
|
1283
|
+
validateCircuitBreakerOptions(options) {
|
|
1284
|
+
if (!options) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1288
|
+
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1289
|
+
}
|
|
1290
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1291
|
+
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
1292
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
1293
|
+
if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
|
|
1294
|
+
const refreshed = refreshStoredEnvelope(hit.stored);
|
|
1295
|
+
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1296
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1297
|
+
const layer = this.layers[index];
|
|
1298
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
try {
|
|
1302
|
+
await layer.set(key, refreshed, ttl);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
|
|
1309
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
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
|
+
shouldSkipLayer(layer) {
|
|
1340
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1341
|
+
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
1342
|
+
}
|
|
1343
|
+
async handleLayerFailure(layer, operation, error) {
|
|
1344
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1348
|
+
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1349
|
+
this.metrics.degradedOperations += 1;
|
|
1350
|
+
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1351
|
+
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
isGracefulDegradationEnabled() {
|
|
1355
|
+
return Boolean(this.options.gracefulDegradation);
|
|
1356
|
+
}
|
|
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
|
+
recordCircuitFailure(key, options, error) {
|
|
1372
|
+
if (!options) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
1376
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
1377
|
+
const state = this.circuitBreakers.get(key) ?? { failures: 0, openUntil: null };
|
|
1378
|
+
state.failures += 1;
|
|
1379
|
+
if (state.failures >= failureThreshold) {
|
|
1380
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
1381
|
+
this.metrics.circuitBreakerTrips += 1;
|
|
1382
|
+
}
|
|
1383
|
+
this.circuitBreakers.set(key, state);
|
|
1384
|
+
this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
|
|
1385
|
+
}
|
|
1386
|
+
resetCircuitBreaker(key) {
|
|
1387
|
+
this.circuitBreakers.delete(key);
|
|
1388
|
+
}
|
|
1389
|
+
isNegativeStoredValue(stored) {
|
|
1390
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1391
|
+
}
|
|
1392
|
+
emitError(operation, context) {
|
|
1393
|
+
this.logger.error?.(operation, context);
|
|
1394
|
+
if (this.listenerCount("error") > 0) {
|
|
1395
|
+
this.emit("error", { operation, ...context });
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
serializeKeyPart(value) {
|
|
1399
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1400
|
+
return String(value);
|
|
1401
|
+
}
|
|
1402
|
+
return JSON.stringify(this.normalizeForSerialization(value));
|
|
1403
|
+
}
|
|
1404
|
+
isCacheSnapshotEntries(value) {
|
|
1405
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1406
|
+
if (!entry || typeof entry !== "object") {
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
const candidate = entry;
|
|
1410
|
+
return typeof candidate.key === "string";
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
normalizeForSerialization(value) {
|
|
1414
|
+
if (Array.isArray(value)) {
|
|
1415
|
+
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
1416
|
+
}
|
|
1417
|
+
if (value && typeof value === "object") {
|
|
1418
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
1419
|
+
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
1420
|
+
return normalized;
|
|
1421
|
+
}, {});
|
|
1422
|
+
}
|
|
1423
|
+
return value;
|
|
1424
|
+
}
|
|
855
1425
|
};
|
|
856
1426
|
|
|
857
1427
|
// src/module.ts
|
|
@@ -878,5 +1448,6 @@ CacheStackModule = __decorateClass([
|
|
|
878
1448
|
0 && (module.exports = {
|
|
879
1449
|
CACHE_STACK,
|
|
880
1450
|
CacheStackModule,
|
|
1451
|
+
Cacheable,
|
|
881
1452
|
InjectCacheStack
|
|
882
1453
|
});
|