layercache 1.2.2 → 1.2.4
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 +119 -89
- package/dist/{chunk-ZMDB5KOK.js → chunk-7V7XAB74.js} +24 -1
- package/dist/{chunk-46UH7LNM.js → chunk-KOYGHLVP.js} +142 -18
- package/dist/{chunk-IXCMHVHP.js → chunk-QHWG7QS5.js} +1 -1
- package/dist/cli.cjs +37 -3
- package/dist/cli.js +15 -4
- package/dist/{edge-DLpdQN0W.d.ts → edge-Dw97n89L.d.cts} +33 -1
- package/dist/{edge-DLpdQN0W.d.cts → edge-Dw97n89L.d.ts} +33 -1
- package/dist/edge.cjs +165 -17
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +657 -146
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +447 -84
- package/package.json +4 -4
- package/packages/nestjs/dist/index.cjs +558 -91
- package/packages/nestjs/dist/index.d.cts +24 -0
- package/packages/nestjs/dist/index.d.ts +24 -0
- package/packages/nestjs/dist/index.js +558 -91
|
@@ -468,6 +468,107 @@ function addMap(base, delta) {
|
|
|
468
468
|
return result;
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
472
|
+
var PatternMatcher = class _PatternMatcher {
|
|
473
|
+
/**
|
|
474
|
+
* Tests whether a glob-style pattern matches a value.
|
|
475
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
476
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
477
|
+
* quadratic memory usage on long patterns/keys.
|
|
478
|
+
*/
|
|
479
|
+
static matches(pattern, value) {
|
|
480
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
484
|
+
*/
|
|
485
|
+
static matchLinear(pattern, value) {
|
|
486
|
+
let patternIndex = 0;
|
|
487
|
+
let valueIndex = 0;
|
|
488
|
+
let starIndex = -1;
|
|
489
|
+
let backtrackValueIndex = 0;
|
|
490
|
+
while (valueIndex < value.length) {
|
|
491
|
+
const patternChar = pattern[patternIndex];
|
|
492
|
+
const valueChar = value[valueIndex];
|
|
493
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
494
|
+
starIndex = patternIndex;
|
|
495
|
+
patternIndex += 1;
|
|
496
|
+
backtrackValueIndex = valueIndex;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
500
|
+
patternIndex += 1;
|
|
501
|
+
valueIndex += 1;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (starIndex !== -1) {
|
|
505
|
+
patternIndex = starIndex + 1;
|
|
506
|
+
backtrackValueIndex += 1;
|
|
507
|
+
valueIndex = backtrackValueIndex;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
513
|
+
patternIndex += 1;
|
|
514
|
+
}
|
|
515
|
+
return patternIndex === pattern.length;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// ../../src/internal/CacheKeyDiscovery.ts
|
|
520
|
+
var CacheKeyDiscovery = class {
|
|
521
|
+
constructor(options) {
|
|
522
|
+
this.options = options;
|
|
523
|
+
}
|
|
524
|
+
options;
|
|
525
|
+
async collectKeysWithPrefix(prefix) {
|
|
526
|
+
const { tagIndex } = this.options;
|
|
527
|
+
const matches = new Set(
|
|
528
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
529
|
+
);
|
|
530
|
+
await Promise.all(
|
|
531
|
+
this.options.layers.map(async (layer) => {
|
|
532
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const keys = await layer.keys();
|
|
537
|
+
for (const key of keys) {
|
|
538
|
+
if (key.startsWith(prefix)) {
|
|
539
|
+
matches.add(key);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
);
|
|
547
|
+
return [...matches];
|
|
548
|
+
}
|
|
549
|
+
async collectKeysMatchingPattern(pattern) {
|
|
550
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
551
|
+
await Promise.all(
|
|
552
|
+
this.options.layers.map(async (layer) => {
|
|
553
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const keys = await layer.keys();
|
|
558
|
+
for (const key of keys) {
|
|
559
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
560
|
+
matches.add(key);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
);
|
|
568
|
+
return [...matches];
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
471
572
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
472
573
|
var CircuitBreakerManager = class {
|
|
473
574
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -562,8 +663,9 @@ var CircuitBreakerManager = class {
|
|
|
562
663
|
|
|
563
664
|
// ../../src/internal/FetchRateLimiter.ts
|
|
564
665
|
var FetchRateLimiter = class {
|
|
565
|
-
queue = [];
|
|
566
666
|
buckets = /* @__PURE__ */ new Map();
|
|
667
|
+
queuesByBucket = /* @__PURE__ */ new Map();
|
|
668
|
+
pendingBuckets = /* @__PURE__ */ new Set();
|
|
567
669
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
568
670
|
nextFetcherBucketId = 0;
|
|
569
671
|
drainTimer;
|
|
@@ -576,13 +678,17 @@ var FetchRateLimiter = class {
|
|
|
576
678
|
return task();
|
|
577
679
|
}
|
|
578
680
|
return new Promise((resolve, reject) => {
|
|
579
|
-
this.
|
|
580
|
-
|
|
681
|
+
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
682
|
+
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
683
|
+
queue.push({
|
|
684
|
+
bucketKey,
|
|
581
685
|
options: normalized,
|
|
582
686
|
task,
|
|
583
687
|
resolve,
|
|
584
688
|
reject
|
|
585
689
|
});
|
|
690
|
+
this.queuesByBucket.set(bucketKey, queue);
|
|
691
|
+
this.pendingBuckets.add(bucketKey);
|
|
586
692
|
this.drain();
|
|
587
693
|
});
|
|
588
694
|
}
|
|
@@ -625,22 +731,30 @@ var FetchRateLimiter = class {
|
|
|
625
731
|
clearTimeout(this.drainTimer);
|
|
626
732
|
this.drainTimer = void 0;
|
|
627
733
|
}
|
|
628
|
-
while (this.
|
|
629
|
-
let
|
|
734
|
+
while (this.pendingBuckets.size > 0) {
|
|
735
|
+
let nextBucketKey;
|
|
630
736
|
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
631
|
-
for (
|
|
632
|
-
const
|
|
737
|
+
for (const bucketKey of this.pendingBuckets) {
|
|
738
|
+
const queue2 = this.queuesByBucket.get(bucketKey);
|
|
739
|
+
if (!queue2 || queue2.length === 0) {
|
|
740
|
+
this.pendingBuckets.delete(bucketKey);
|
|
741
|
+
this.queuesByBucket.delete(bucketKey);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const next2 = queue2[0];
|
|
633
745
|
if (!next2) {
|
|
746
|
+
this.pendingBuckets.delete(bucketKey);
|
|
747
|
+
this.queuesByBucket.delete(bucketKey);
|
|
634
748
|
continue;
|
|
635
749
|
}
|
|
636
|
-
const waitMs = this.waitTime(
|
|
750
|
+
const waitMs = this.waitTime(bucketKey, next2.options);
|
|
637
751
|
if (waitMs <= 0) {
|
|
638
|
-
|
|
752
|
+
nextBucketKey = bucketKey;
|
|
639
753
|
break;
|
|
640
754
|
}
|
|
641
755
|
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
642
756
|
}
|
|
643
|
-
if (
|
|
757
|
+
if (!nextBucketKey) {
|
|
644
758
|
if (Number.isFinite(nextWaitMs)) {
|
|
645
759
|
this.drainTimer = setTimeout(() => {
|
|
646
760
|
this.drainTimer = void 0;
|
|
@@ -650,15 +764,32 @@ var FetchRateLimiter = class {
|
|
|
650
764
|
}
|
|
651
765
|
return;
|
|
652
766
|
}
|
|
653
|
-
const
|
|
767
|
+
const queue = this.queuesByBucket.get(nextBucketKey);
|
|
768
|
+
const next = queue?.shift();
|
|
654
769
|
if (!next) {
|
|
655
|
-
|
|
770
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
771
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (!queue || queue.length === 0) {
|
|
775
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
776
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
656
777
|
}
|
|
657
778
|
const bucket = this.bucketState(next.bucketKey);
|
|
779
|
+
if (bucket.cleanupTimer) {
|
|
780
|
+
clearTimeout(bucket.cleanupTimer);
|
|
781
|
+
bucket.cleanupTimer = void 0;
|
|
782
|
+
}
|
|
658
783
|
bucket.active += 1;
|
|
659
|
-
|
|
784
|
+
if (next.options.intervalMs && next.options.maxPerInterval) {
|
|
785
|
+
bucket.startedAt.push(Date.now());
|
|
786
|
+
}
|
|
660
787
|
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
661
788
|
bucket.active -= 1;
|
|
789
|
+
if ((this.queuesByBucket.get(next.bucketKey)?.length ?? 0) > 0) {
|
|
790
|
+
this.pendingBuckets.add(next.bucketKey);
|
|
791
|
+
}
|
|
792
|
+
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
662
793
|
this.drain();
|
|
663
794
|
});
|
|
664
795
|
}
|
|
@@ -700,6 +831,31 @@ var FetchRateLimiter = class {
|
|
|
700
831
|
this.buckets.set(bucketKey, bucket);
|
|
701
832
|
return bucket;
|
|
702
833
|
}
|
|
834
|
+
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
835
|
+
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
836
|
+
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
837
|
+
this.buckets.delete(bucketKey);
|
|
838
|
+
this.queuesByBucket.delete(bucketKey);
|
|
839
|
+
this.pendingBuckets.delete(bucketKey);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (!intervalMs || bucket.active > 0 || queued > 0) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (bucket.cleanupTimer) {
|
|
846
|
+
clearTimeout(bucket.cleanupTimer);
|
|
847
|
+
}
|
|
848
|
+
bucket.cleanupTimer = setTimeout(() => {
|
|
849
|
+
bucket.cleanupTimer = void 0;
|
|
850
|
+
this.prune(bucket, Date.now(), intervalMs);
|
|
851
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && (this.queuesByBucket.get(bucketKey)?.length ?? 0) === 0) {
|
|
852
|
+
this.buckets.delete(bucketKey);
|
|
853
|
+
this.queuesByBucket.delete(bucketKey);
|
|
854
|
+
this.pendingBuckets.delete(bucketKey);
|
|
855
|
+
}
|
|
856
|
+
}, intervalMs);
|
|
857
|
+
bucket.cleanupTimer.unref?.();
|
|
858
|
+
}
|
|
703
859
|
};
|
|
704
860
|
|
|
705
861
|
// ../../src/internal/MetricsCollector.ts
|
|
@@ -778,7 +934,30 @@ var MetricsCollector = class {
|
|
|
778
934
|
|
|
779
935
|
// ../../src/internal/StoredValue.ts
|
|
780
936
|
function isStoredValueEnvelope(value) {
|
|
781
|
-
|
|
937
|
+
if (typeof value !== "object" || value === null) {
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
const v = value;
|
|
941
|
+
if (v.__layercache !== 1) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
957
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
return true;
|
|
782
961
|
}
|
|
783
962
|
function createStoredValueEnvelope(options) {
|
|
784
963
|
const now = options.now ?? Date.now();
|
|
@@ -989,69 +1168,23 @@ var TtlResolver = class {
|
|
|
989
1168
|
}
|
|
990
1169
|
};
|
|
991
1170
|
|
|
992
|
-
// ../../src/invalidation/PatternMatcher.ts
|
|
993
|
-
var PatternMatcher = class _PatternMatcher {
|
|
994
|
-
/**
|
|
995
|
-
* Tests whether a glob-style pattern matches a value.
|
|
996
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
997
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
998
|
-
* quadratic memory usage on long patterns/keys.
|
|
999
|
-
*/
|
|
1000
|
-
static matches(pattern, value) {
|
|
1001
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
1002
|
-
}
|
|
1003
|
-
/**
|
|
1004
|
-
* Linear-time glob matching with O(1) extra memory.
|
|
1005
|
-
*/
|
|
1006
|
-
static matchLinear(pattern, value) {
|
|
1007
|
-
let patternIndex = 0;
|
|
1008
|
-
let valueIndex = 0;
|
|
1009
|
-
let starIndex = -1;
|
|
1010
|
-
let backtrackValueIndex = 0;
|
|
1011
|
-
while (valueIndex < value.length) {
|
|
1012
|
-
const patternChar = pattern[patternIndex];
|
|
1013
|
-
const valueChar = value[valueIndex];
|
|
1014
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
1015
|
-
starIndex = patternIndex;
|
|
1016
|
-
patternIndex += 1;
|
|
1017
|
-
backtrackValueIndex = valueIndex;
|
|
1018
|
-
continue;
|
|
1019
|
-
}
|
|
1020
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
1021
|
-
patternIndex += 1;
|
|
1022
|
-
valueIndex += 1;
|
|
1023
|
-
continue;
|
|
1024
|
-
}
|
|
1025
|
-
if (starIndex !== -1) {
|
|
1026
|
-
patternIndex = starIndex + 1;
|
|
1027
|
-
backtrackValueIndex += 1;
|
|
1028
|
-
valueIndex = backtrackValueIndex;
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1031
|
-
return false;
|
|
1032
|
-
}
|
|
1033
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
1034
|
-
patternIndex += 1;
|
|
1035
|
-
}
|
|
1036
|
-
return patternIndex === pattern.length;
|
|
1037
|
-
}
|
|
1038
|
-
};
|
|
1039
|
-
|
|
1040
1171
|
// ../../src/invalidation/TagIndex.ts
|
|
1041
1172
|
var TagIndex = class {
|
|
1042
1173
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
1043
1174
|
keyToTags = /* @__PURE__ */ new Map();
|
|
1044
1175
|
knownKeys = /* @__PURE__ */ new Set();
|
|
1045
1176
|
maxKnownKeys;
|
|
1177
|
+
nextNodeId = 1;
|
|
1178
|
+
root = this.createTrieNode();
|
|
1046
1179
|
constructor(options = {}) {
|
|
1047
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
1180
|
+
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
1048
1181
|
}
|
|
1049
1182
|
async touch(key) {
|
|
1050
|
-
this.
|
|
1183
|
+
this.insertKnownKey(key);
|
|
1051
1184
|
this.pruneKnownKeysIfNeeded();
|
|
1052
1185
|
}
|
|
1053
1186
|
async track(key, tags) {
|
|
1054
|
-
this.
|
|
1187
|
+
this.insertKnownKey(key);
|
|
1055
1188
|
this.pruneKnownKeysIfNeeded();
|
|
1056
1189
|
if (tags.length === 0) {
|
|
1057
1190
|
return;
|
|
@@ -1077,18 +1210,104 @@ var TagIndex = class {
|
|
|
1077
1210
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1078
1211
|
}
|
|
1079
1212
|
async keysForPrefix(prefix) {
|
|
1080
|
-
|
|
1213
|
+
const node = this.findNode(prefix);
|
|
1214
|
+
if (!node) {
|
|
1215
|
+
return [];
|
|
1216
|
+
}
|
|
1217
|
+
const matches = [];
|
|
1218
|
+
this.collectFromNode(node, prefix, matches);
|
|
1219
|
+
return matches;
|
|
1081
1220
|
}
|
|
1082
1221
|
async tagsForKey(key) {
|
|
1083
1222
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1084
1223
|
}
|
|
1085
1224
|
async matchPattern(pattern) {
|
|
1086
|
-
|
|
1225
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1226
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1227
|
+
return [...matches];
|
|
1087
1228
|
}
|
|
1088
1229
|
async clear() {
|
|
1089
1230
|
this.tagToKeys.clear();
|
|
1090
1231
|
this.keyToTags.clear();
|
|
1091
1232
|
this.knownKeys.clear();
|
|
1233
|
+
this.root.children.clear();
|
|
1234
|
+
this.root.terminal = false;
|
|
1235
|
+
this.nextNodeId = this.root.id + 1;
|
|
1236
|
+
}
|
|
1237
|
+
createTrieNode() {
|
|
1238
|
+
return {
|
|
1239
|
+
id: this.nextNodeId++,
|
|
1240
|
+
terminal: false,
|
|
1241
|
+
children: /* @__PURE__ */ new Map()
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
insertKnownKey(key) {
|
|
1245
|
+
if (this.knownKeys.has(key)) {
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
this.knownKeys.add(key);
|
|
1249
|
+
let node = this.root;
|
|
1250
|
+
for (const character of key) {
|
|
1251
|
+
let child = node.children.get(character);
|
|
1252
|
+
if (!child) {
|
|
1253
|
+
child = this.createTrieNode();
|
|
1254
|
+
node.children.set(character, child);
|
|
1255
|
+
}
|
|
1256
|
+
node = child;
|
|
1257
|
+
}
|
|
1258
|
+
node.terminal = true;
|
|
1259
|
+
}
|
|
1260
|
+
findNode(prefix) {
|
|
1261
|
+
let node = this.root;
|
|
1262
|
+
for (const character of prefix) {
|
|
1263
|
+
node = node.children.get(character);
|
|
1264
|
+
if (!node) {
|
|
1265
|
+
return void 0;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return node;
|
|
1269
|
+
}
|
|
1270
|
+
collectFromNode(node, prefix, matches) {
|
|
1271
|
+
if (node.terminal) {
|
|
1272
|
+
matches.push(prefix);
|
|
1273
|
+
}
|
|
1274
|
+
for (const [character, child] of node.children) {
|
|
1275
|
+
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
1279
|
+
const stateKey = `${node.id}:${patternIndex}`;
|
|
1280
|
+
if (visited.has(stateKey)) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
visited.add(stateKey);
|
|
1284
|
+
if (patternIndex === pattern.length) {
|
|
1285
|
+
if (node.terminal) {
|
|
1286
|
+
matches.add(prefix);
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const patternChar = pattern[patternIndex];
|
|
1291
|
+
if (patternChar === void 0) {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (patternChar === "*") {
|
|
1295
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1296
|
+
for (const [character, child2] of node.children) {
|
|
1297
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1298
|
+
}
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (patternChar === "?") {
|
|
1302
|
+
for (const [character, child2] of node.children) {
|
|
1303
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
|
|
1304
|
+
}
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const child = node.children.get(patternChar);
|
|
1308
|
+
if (child) {
|
|
1309
|
+
this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
|
|
1310
|
+
}
|
|
1092
1311
|
}
|
|
1093
1312
|
pruneKnownKeysIfNeeded() {
|
|
1094
1313
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
@@ -1105,7 +1324,7 @@ var TagIndex = class {
|
|
|
1105
1324
|
}
|
|
1106
1325
|
}
|
|
1107
1326
|
removeKey(key) {
|
|
1108
|
-
this.
|
|
1327
|
+
this.removeKnownKey(key);
|
|
1109
1328
|
const tags = this.keyToTags.get(key);
|
|
1110
1329
|
if (!tags) {
|
|
1111
1330
|
return;
|
|
@@ -1122,7 +1341,70 @@ var TagIndex = class {
|
|
|
1122
1341
|
}
|
|
1123
1342
|
this.keyToTags.delete(key);
|
|
1124
1343
|
}
|
|
1344
|
+
removeKnownKey(key) {
|
|
1345
|
+
if (!this.knownKeys.delete(key)) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const path = [];
|
|
1349
|
+
let node = this.root;
|
|
1350
|
+
for (const character of key) {
|
|
1351
|
+
const child = node.children.get(character);
|
|
1352
|
+
if (!child) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
path.push([node, character]);
|
|
1356
|
+
node = child;
|
|
1357
|
+
}
|
|
1358
|
+
node.terminal = false;
|
|
1359
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
1360
|
+
const entry = path[index];
|
|
1361
|
+
if (!entry) {
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
const [parent, character] = entry;
|
|
1365
|
+
const child = parent.children.get(character);
|
|
1366
|
+
if (!child || child.terminal || child.children.size > 0) {
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
parent.children.delete(character);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
// ../../src/serialization/JsonSerializer.ts
|
|
1375
|
+
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1376
|
+
var JsonSerializer = class {
|
|
1377
|
+
serialize(value) {
|
|
1378
|
+
return JSON.stringify(value);
|
|
1379
|
+
}
|
|
1380
|
+
deserialize(payload) {
|
|
1381
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1382
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1383
|
+
}
|
|
1125
1384
|
};
|
|
1385
|
+
var MAX_SANITIZE_DEPTH = 200;
|
|
1386
|
+
function sanitizeJsonValue(value, depth) {
|
|
1387
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1388
|
+
return value;
|
|
1389
|
+
}
|
|
1390
|
+
if (Array.isArray(value)) {
|
|
1391
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1392
|
+
}
|
|
1393
|
+
if (!isPlainObject(value)) {
|
|
1394
|
+
return value;
|
|
1395
|
+
}
|
|
1396
|
+
const sanitized = {};
|
|
1397
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1398
|
+
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1402
|
+
}
|
|
1403
|
+
return sanitized;
|
|
1404
|
+
}
|
|
1405
|
+
function isPlainObject(value) {
|
|
1406
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1407
|
+
}
|
|
1126
1408
|
|
|
1127
1409
|
// ../../src/stampede/StampedeGuard.ts
|
|
1128
1410
|
var StampedeGuard = class {
|
|
@@ -1133,7 +1415,8 @@ var StampedeGuard = class {
|
|
|
1133
1415
|
return await entry.mutex.runExclusive(task);
|
|
1134
1416
|
} finally {
|
|
1135
1417
|
entry.references -= 1;
|
|
1136
|
-
|
|
1418
|
+
const current = this.mutexes.get(key);
|
|
1419
|
+
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
1137
1420
|
this.mutexes.delete(key);
|
|
1138
1421
|
}
|
|
1139
1422
|
}
|
|
@@ -1163,8 +1446,10 @@ var CacheMissError = class extends Error {
|
|
|
1163
1446
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1164
1447
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1165
1448
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1449
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1166
1450
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1167
1451
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1452
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1168
1453
|
var DebugLogger = class {
|
|
1169
1454
|
enabled;
|
|
1170
1455
|
constructor(enabled) {
|
|
@@ -1211,6 +1496,29 @@ var CacheStack = class extends EventEmitter {
|
|
|
1211
1496
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1212
1497
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1213
1498
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1499
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
1500
|
+
layers: this.layers,
|
|
1501
|
+
tagIndex: this.tagIndex,
|
|
1502
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1503
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1504
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1508
|
+
this.logger.warn?.(
|
|
1509
|
+
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false && !layer.keys)) {
|
|
1513
|
+
this.logger.warn?.(
|
|
1514
|
+
"Using the default in-memory TagIndex with a shared cache layer that does not implement keys() can leave invalidateByPattern() and invalidateByPrefix() incomplete after restarts. Use RedisTagIndex or implement keys() on the shared layer."
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
if (options.invalidationBus && options.broadcastL1Invalidation === void 0 && options.publishSetInvalidation === void 0) {
|
|
1518
|
+
this.logger.warn?.(
|
|
1519
|
+
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1214
1522
|
this.initializeWriteBehind(options.writeBehind);
|
|
1215
1523
|
this.startup = this.initialize();
|
|
1216
1524
|
}
|
|
@@ -1223,7 +1531,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1223
1531
|
unsubscribeInvalidation;
|
|
1224
1532
|
logger;
|
|
1225
1533
|
tagIndex;
|
|
1534
|
+
keyDiscovery;
|
|
1226
1535
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1536
|
+
snapshotSerializer = new JsonSerializer();
|
|
1227
1537
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1228
1538
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1229
1539
|
ttlResolver;
|
|
@@ -1232,6 +1542,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1232
1542
|
writeBehindQueue = [];
|
|
1233
1543
|
writeBehindTimer;
|
|
1234
1544
|
writeBehindFlushPromise;
|
|
1545
|
+
generationCleanupPromise;
|
|
1235
1546
|
isDisconnecting = false;
|
|
1236
1547
|
disconnectPromise;
|
|
1237
1548
|
/**
|
|
@@ -1244,6 +1555,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1244
1555
|
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1245
1556
|
this.validateWriteOptions(options);
|
|
1246
1557
|
await this.awaitStartup("get");
|
|
1558
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1559
|
+
}
|
|
1560
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
1247
1561
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1248
1562
|
if (hit.found) {
|
|
1249
1563
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -1321,6 +1635,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1321
1635
|
return true;
|
|
1322
1636
|
}
|
|
1323
1637
|
} catch {
|
|
1638
|
+
await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
|
|
1324
1639
|
}
|
|
1325
1640
|
} else {
|
|
1326
1641
|
try {
|
|
@@ -1328,7 +1643,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1328
1643
|
if (value !== null) {
|
|
1329
1644
|
return true;
|
|
1330
1645
|
}
|
|
1331
|
-
} catch {
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
await this.reportRecoverableLayerFailure(layer, "has", error);
|
|
1332
1648
|
}
|
|
1333
1649
|
}
|
|
1334
1650
|
}
|
|
@@ -1420,13 +1736,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1420
1736
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1421
1737
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
1422
1738
|
if (!canFastPath) {
|
|
1739
|
+
await this.awaitStartup("mget");
|
|
1423
1740
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1424
1741
|
return Promise.all(
|
|
1425
1742
|
normalizedEntries.map((entry) => {
|
|
1426
1743
|
const optionsSignature = this.serializeOptions(entry.options);
|
|
1427
1744
|
const existing = pendingReads.get(entry.key);
|
|
1428
1745
|
if (!existing) {
|
|
1429
|
-
const promise = this.
|
|
1746
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
1430
1747
|
pendingReads.set(entry.key, {
|
|
1431
1748
|
promise,
|
|
1432
1749
|
fetch: entry.fetch,
|
|
@@ -1565,14 +1882,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1565
1882
|
}
|
|
1566
1883
|
async invalidateByPattern(pattern) {
|
|
1567
1884
|
await this.awaitStartup("invalidateByPattern");
|
|
1568
|
-
const keys = await this.
|
|
1885
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1569
1886
|
await this.deleteKeys(keys);
|
|
1570
1887
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1571
1888
|
}
|
|
1572
1889
|
async invalidateByPrefix(prefix) {
|
|
1573
1890
|
await this.awaitStartup("invalidateByPrefix");
|
|
1574
1891
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1575
|
-
const keys =
|
|
1892
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1576
1893
|
await this.deleteKeys(keys);
|
|
1577
1894
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1578
1895
|
}
|
|
@@ -1622,9 +1939,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
1622
1939
|
})
|
|
1623
1940
|
);
|
|
1624
1941
|
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Rotates the active generation prefix used for all future cache keys.
|
|
1944
|
+
* Previous-generation keys remain in the underlying layers until they expire,
|
|
1945
|
+
* unless `generationCleanup` is enabled to prune them in the background.
|
|
1946
|
+
*/
|
|
1625
1947
|
bumpGeneration(nextGeneration) {
|
|
1626
1948
|
const current = this.currentGeneration ?? 0;
|
|
1949
|
+
const previousGeneration = this.currentGeneration;
|
|
1627
1950
|
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1951
|
+
if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
|
|
1952
|
+
this.scheduleGenerationCleanup(previousGeneration);
|
|
1953
|
+
}
|
|
1628
1954
|
return this.currentGeneration;
|
|
1629
1955
|
}
|
|
1630
1956
|
/**
|
|
@@ -1708,27 +2034,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1708
2034
|
this.assertActive("persistToFile");
|
|
1709
2035
|
const snapshot = await this.exportState();
|
|
1710
2036
|
const { promises: fs } = await import("fs");
|
|
1711
|
-
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
2037
|
+
await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
|
|
1712
2038
|
}
|
|
1713
2039
|
async restoreFromFile(filePath) {
|
|
1714
2040
|
this.assertActive("restoreFromFile");
|
|
1715
2041
|
const { promises: fs } = await import("fs");
|
|
1716
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
2042
|
+
const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
|
|
1717
2043
|
let parsed;
|
|
1718
2044
|
try {
|
|
1719
|
-
parsed = JSON.parse(raw
|
|
1720
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1721
|
-
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1722
|
-
}
|
|
1723
|
-
return value;
|
|
1724
|
-
});
|
|
2045
|
+
parsed = JSON.parse(raw);
|
|
1725
2046
|
} catch (cause) {
|
|
1726
2047
|
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1727
2048
|
}
|
|
1728
2049
|
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1729
2050
|
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1730
2051
|
}
|
|
1731
|
-
await this.importState(
|
|
2052
|
+
await this.importState(
|
|
2053
|
+
parsed.map((entry) => ({
|
|
2054
|
+
key: entry.key,
|
|
2055
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
2056
|
+
ttl: entry.ttl
|
|
2057
|
+
}))
|
|
2058
|
+
);
|
|
1732
2059
|
}
|
|
1733
2060
|
async disconnect() {
|
|
1734
2061
|
if (!this.disconnectPromise) {
|
|
@@ -1737,6 +2064,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1737
2064
|
await this.startup;
|
|
1738
2065
|
await this.unsubscribeInvalidation?.();
|
|
1739
2066
|
await this.flushWriteBehindQueue();
|
|
2067
|
+
await this.generationCleanupPromise;
|
|
1740
2068
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1741
2069
|
if (this.writeBehindTimer) {
|
|
1742
2070
|
clearInterval(this.writeBehindTimer);
|
|
@@ -1820,8 +2148,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1820
2148
|
await this.storeEntry(key, "empty", null, options);
|
|
1821
2149
|
return null;
|
|
1822
2150
|
}
|
|
1823
|
-
if (options?.shouldCache
|
|
1824
|
-
|
|
2151
|
+
if (options?.shouldCache) {
|
|
2152
|
+
try {
|
|
2153
|
+
if (!options.shouldCache(fetched)) {
|
|
2154
|
+
return fetched;
|
|
2155
|
+
}
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2158
|
+
}
|
|
1825
2159
|
}
|
|
1826
2160
|
await this.storeEntry(key, "value", fetched, options);
|
|
1827
2161
|
return fetched;
|
|
@@ -2048,7 +2382,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2048
2382
|
const refresh = (async () => {
|
|
2049
2383
|
this.metricsCollector.increment("refreshes");
|
|
2050
2384
|
try {
|
|
2051
|
-
await this.
|
|
2385
|
+
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2052
2386
|
} catch (error) {
|
|
2053
2387
|
this.metricsCollector.increment("refreshErrors");
|
|
2054
2388
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2058,6 +2392,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2058
2392
|
})();
|
|
2059
2393
|
this.backgroundRefreshes.set(key, refresh);
|
|
2060
2394
|
}
|
|
2395
|
+
async runBackgroundRefresh(key, fetcher, options) {
|
|
2396
|
+
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2397
|
+
await this.fetchWithGuards(
|
|
2398
|
+
key,
|
|
2399
|
+
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2400
|
+
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2401
|
+
}),
|
|
2402
|
+
options
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2061
2405
|
resolveSingleFlightOptions() {
|
|
2062
2406
|
return {
|
|
2063
2407
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
@@ -2125,8 +2469,76 @@ var CacheStack = class extends EventEmitter {
|
|
|
2125
2469
|
sleep(ms) {
|
|
2126
2470
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2127
2471
|
}
|
|
2472
|
+
async withTimeout(promise, timeoutMs, onTimeout) {
|
|
2473
|
+
if (timeoutMs <= 0) {
|
|
2474
|
+
return promise;
|
|
2475
|
+
}
|
|
2476
|
+
let timer;
|
|
2477
|
+
const observedPromise = promise.then(
|
|
2478
|
+
(value) => ({ kind: "value", value }),
|
|
2479
|
+
(error) => ({ kind: "error", error })
|
|
2480
|
+
);
|
|
2481
|
+
try {
|
|
2482
|
+
const result = await Promise.race([
|
|
2483
|
+
observedPromise,
|
|
2484
|
+
new Promise((_, reject) => {
|
|
2485
|
+
timer = setTimeout(() => reject(onTimeout()), timeoutMs);
|
|
2486
|
+
timer.unref?.();
|
|
2487
|
+
})
|
|
2488
|
+
]);
|
|
2489
|
+
if (result && typeof result === "object" && "kind" in result) {
|
|
2490
|
+
if (result.kind === "error") {
|
|
2491
|
+
throw result.error;
|
|
2492
|
+
}
|
|
2493
|
+
return result.value;
|
|
2494
|
+
}
|
|
2495
|
+
return result;
|
|
2496
|
+
} finally {
|
|
2497
|
+
if (timer) {
|
|
2498
|
+
clearTimeout(timer);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2128
2502
|
shouldBroadcastL1Invalidation() {
|
|
2129
|
-
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ??
|
|
2503
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2504
|
+
}
|
|
2505
|
+
shouldCleanupGenerations() {
|
|
2506
|
+
return Boolean(this.options.generationCleanup);
|
|
2507
|
+
}
|
|
2508
|
+
generationCleanupBatchSize() {
|
|
2509
|
+
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
2510
|
+
return configured ?? 500;
|
|
2511
|
+
}
|
|
2512
|
+
scheduleGenerationCleanup(generation) {
|
|
2513
|
+
const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
|
|
2514
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
2515
|
+
generation,
|
|
2516
|
+
error: this.formatError(error)
|
|
2517
|
+
});
|
|
2518
|
+
});
|
|
2519
|
+
this.generationCleanupPromise = task.finally(() => {
|
|
2520
|
+
if (this.generationCleanupPromise === task) {
|
|
2521
|
+
this.generationCleanupPromise = void 0;
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
async cleanupGeneration(generation) {
|
|
2526
|
+
const prefix = `v${generation}:`;
|
|
2527
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2528
|
+
if (keys.length === 0) {
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
const batchSize = this.generationCleanupBatchSize();
|
|
2532
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2533
|
+
const batch = keys.slice(index, index + batchSize);
|
|
2534
|
+
await this.deleteKeys(batch);
|
|
2535
|
+
await this.publishInvalidation({
|
|
2536
|
+
scope: "keys",
|
|
2537
|
+
keys: batch,
|
|
2538
|
+
sourceId: this.instanceId,
|
|
2539
|
+
operation: "invalidate"
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2130
2542
|
}
|
|
2131
2543
|
initializeWriteBehind(options) {
|
|
2132
2544
|
if (this.options.writeStrategy !== "write-behind") {
|
|
@@ -2164,7 +2576,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
2164
2576
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2165
2577
|
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2166
2578
|
this.writeBehindFlushPromise = (async () => {
|
|
2167
|
-
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2579
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
2580
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2581
|
+
if (failures.length > 0) {
|
|
2582
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2583
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
2584
|
+
failed: failures.length,
|
|
2585
|
+
total: batch.length,
|
|
2586
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2587
|
+
});
|
|
2588
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2589
|
+
}
|
|
2168
2590
|
})();
|
|
2169
2591
|
await this.writeBehindFlushPromise;
|
|
2170
2592
|
this.writeBehindFlushPromise = void 0;
|
|
@@ -2269,9 +2691,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
2269
2691
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
2270
2692
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2271
2693
|
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2694
|
+
this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
2272
2695
|
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
2273
2696
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
2274
2697
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2698
|
+
if (typeof this.options.generationCleanup === "object") {
|
|
2699
|
+
this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2700
|
+
}
|
|
2275
2701
|
if (this.options.generation !== void 0) {
|
|
2276
2702
|
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2277
2703
|
}
|
|
@@ -2343,6 +2769,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2343
2769
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2344
2770
|
throw new Error("Cache key contains unsupported control characters.");
|
|
2345
2771
|
}
|
|
2772
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2773
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2774
|
+
}
|
|
2346
2775
|
return key;
|
|
2347
2776
|
}
|
|
2348
2777
|
validateTtlPolicy(name, policy) {
|
|
@@ -2420,6 +2849,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2420
2849
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
2421
2850
|
return null;
|
|
2422
2851
|
}
|
|
2852
|
+
async reportRecoverableLayerFailure(layer, operation, error) {
|
|
2853
|
+
if (this.isGracefulDegradationEnabled()) {
|
|
2854
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
this.logger.warn?.("layer-operation-failed", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2858
|
+
this.emitError(operation, { layer: layer.name, degraded: false, error: this.formatError(error) });
|
|
2859
|
+
}
|
|
2423
2860
|
isGracefulDegradationEnabled() {
|
|
2424
2861
|
return Boolean(this.options.gracefulDegradation);
|
|
2425
2862
|
}
|
|
@@ -2443,10 +2880,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2443
2880
|
}
|
|
2444
2881
|
}
|
|
2445
2882
|
serializeKeyPart(value) {
|
|
2446
|
-
if (typeof value === "string"
|
|
2447
|
-
return
|
|
2883
|
+
if (typeof value === "string") {
|
|
2884
|
+
return `s:${value}`;
|
|
2885
|
+
}
|
|
2886
|
+
if (typeof value === "number") {
|
|
2887
|
+
return `n:${value}`;
|
|
2448
2888
|
}
|
|
2449
|
-
|
|
2889
|
+
if (typeof value === "boolean") {
|
|
2890
|
+
return `b:${value}`;
|
|
2891
|
+
}
|
|
2892
|
+
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2450
2893
|
}
|
|
2451
2894
|
isCacheSnapshotEntries(value) {
|
|
2452
2895
|
return Array.isArray(value) && value.every((entry) => {
|
|
@@ -2454,15 +2897,39 @@ var CacheStack = class extends EventEmitter {
|
|
|
2454
2897
|
return false;
|
|
2455
2898
|
}
|
|
2456
2899
|
const candidate = entry;
|
|
2457
|
-
return typeof candidate.key === "string";
|
|
2900
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
2458
2901
|
});
|
|
2459
2902
|
}
|
|
2903
|
+
sanitizeSnapshotValue(value) {
|
|
2904
|
+
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2905
|
+
}
|
|
2906
|
+
async validateSnapshotFilePath(filePath) {
|
|
2907
|
+
if (filePath.length === 0) {
|
|
2908
|
+
throw new Error("filePath must not be empty.");
|
|
2909
|
+
}
|
|
2910
|
+
if (filePath.includes("\0")) {
|
|
2911
|
+
throw new Error("filePath must not contain null bytes.");
|
|
2912
|
+
}
|
|
2913
|
+
const path = await import("path");
|
|
2914
|
+
const resolved = path.resolve(filePath);
|
|
2915
|
+
const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
|
|
2916
|
+
if (baseDir !== false) {
|
|
2917
|
+
const relative = path.relative(baseDir, resolved);
|
|
2918
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2919
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
return resolved;
|
|
2923
|
+
}
|
|
2460
2924
|
normalizeForSerialization(value) {
|
|
2461
2925
|
if (Array.isArray(value)) {
|
|
2462
2926
|
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
2463
2927
|
}
|
|
2464
2928
|
if (value && typeof value === "object") {
|
|
2465
2929
|
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
2930
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
2931
|
+
return normalized;
|
|
2932
|
+
}
|
|
2466
2933
|
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
2467
2934
|
return normalized;
|
|
2468
2935
|
}, {});
|