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
|
@@ -504,6 +504,107 @@ function addMap(base, delta) {
|
|
|
504
504
|
return result;
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
508
|
+
var PatternMatcher = class _PatternMatcher {
|
|
509
|
+
/**
|
|
510
|
+
* Tests whether a glob-style pattern matches a value.
|
|
511
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
512
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
513
|
+
* quadratic memory usage on long patterns/keys.
|
|
514
|
+
*/
|
|
515
|
+
static matches(pattern, value) {
|
|
516
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
520
|
+
*/
|
|
521
|
+
static matchLinear(pattern, value) {
|
|
522
|
+
let patternIndex = 0;
|
|
523
|
+
let valueIndex = 0;
|
|
524
|
+
let starIndex = -1;
|
|
525
|
+
let backtrackValueIndex = 0;
|
|
526
|
+
while (valueIndex < value.length) {
|
|
527
|
+
const patternChar = pattern[patternIndex];
|
|
528
|
+
const valueChar = value[valueIndex];
|
|
529
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
530
|
+
starIndex = patternIndex;
|
|
531
|
+
patternIndex += 1;
|
|
532
|
+
backtrackValueIndex = valueIndex;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
536
|
+
patternIndex += 1;
|
|
537
|
+
valueIndex += 1;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (starIndex !== -1) {
|
|
541
|
+
patternIndex = starIndex + 1;
|
|
542
|
+
backtrackValueIndex += 1;
|
|
543
|
+
valueIndex = backtrackValueIndex;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
549
|
+
patternIndex += 1;
|
|
550
|
+
}
|
|
551
|
+
return patternIndex === pattern.length;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// ../../src/internal/CacheKeyDiscovery.ts
|
|
556
|
+
var CacheKeyDiscovery = class {
|
|
557
|
+
constructor(options) {
|
|
558
|
+
this.options = options;
|
|
559
|
+
}
|
|
560
|
+
options;
|
|
561
|
+
async collectKeysWithPrefix(prefix) {
|
|
562
|
+
const { tagIndex } = this.options;
|
|
563
|
+
const matches = new Set(
|
|
564
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
565
|
+
);
|
|
566
|
+
await Promise.all(
|
|
567
|
+
this.options.layers.map(async (layer) => {
|
|
568
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const keys = await layer.keys();
|
|
573
|
+
for (const key of keys) {
|
|
574
|
+
if (key.startsWith(prefix)) {
|
|
575
|
+
matches.add(key);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
);
|
|
583
|
+
return [...matches];
|
|
584
|
+
}
|
|
585
|
+
async collectKeysMatchingPattern(pattern) {
|
|
586
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
587
|
+
await Promise.all(
|
|
588
|
+
this.options.layers.map(async (layer) => {
|
|
589
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
const keys = await layer.keys();
|
|
594
|
+
for (const key of keys) {
|
|
595
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
596
|
+
matches.add(key);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} catch (error) {
|
|
600
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
return [...matches];
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
507
608
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
508
609
|
var CircuitBreakerManager = class {
|
|
509
610
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -598,8 +699,9 @@ var CircuitBreakerManager = class {
|
|
|
598
699
|
|
|
599
700
|
// ../../src/internal/FetchRateLimiter.ts
|
|
600
701
|
var FetchRateLimiter = class {
|
|
601
|
-
queue = [];
|
|
602
702
|
buckets = /* @__PURE__ */ new Map();
|
|
703
|
+
queuesByBucket = /* @__PURE__ */ new Map();
|
|
704
|
+
pendingBuckets = /* @__PURE__ */ new Set();
|
|
603
705
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
604
706
|
nextFetcherBucketId = 0;
|
|
605
707
|
drainTimer;
|
|
@@ -612,13 +714,17 @@ var FetchRateLimiter = class {
|
|
|
612
714
|
return task();
|
|
613
715
|
}
|
|
614
716
|
return new Promise((resolve, reject) => {
|
|
615
|
-
this.
|
|
616
|
-
|
|
717
|
+
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
718
|
+
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
719
|
+
queue.push({
|
|
720
|
+
bucketKey,
|
|
617
721
|
options: normalized,
|
|
618
722
|
task,
|
|
619
723
|
resolve,
|
|
620
724
|
reject
|
|
621
725
|
});
|
|
726
|
+
this.queuesByBucket.set(bucketKey, queue);
|
|
727
|
+
this.pendingBuckets.add(bucketKey);
|
|
622
728
|
this.drain();
|
|
623
729
|
});
|
|
624
730
|
}
|
|
@@ -661,22 +767,30 @@ var FetchRateLimiter = class {
|
|
|
661
767
|
clearTimeout(this.drainTimer);
|
|
662
768
|
this.drainTimer = void 0;
|
|
663
769
|
}
|
|
664
|
-
while (this.
|
|
665
|
-
let
|
|
770
|
+
while (this.pendingBuckets.size > 0) {
|
|
771
|
+
let nextBucketKey;
|
|
666
772
|
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
667
|
-
for (
|
|
668
|
-
const
|
|
773
|
+
for (const bucketKey of this.pendingBuckets) {
|
|
774
|
+
const queue2 = this.queuesByBucket.get(bucketKey);
|
|
775
|
+
if (!queue2 || queue2.length === 0) {
|
|
776
|
+
this.pendingBuckets.delete(bucketKey);
|
|
777
|
+
this.queuesByBucket.delete(bucketKey);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const next2 = queue2[0];
|
|
669
781
|
if (!next2) {
|
|
782
|
+
this.pendingBuckets.delete(bucketKey);
|
|
783
|
+
this.queuesByBucket.delete(bucketKey);
|
|
670
784
|
continue;
|
|
671
785
|
}
|
|
672
|
-
const waitMs = this.waitTime(
|
|
786
|
+
const waitMs = this.waitTime(bucketKey, next2.options);
|
|
673
787
|
if (waitMs <= 0) {
|
|
674
|
-
|
|
788
|
+
nextBucketKey = bucketKey;
|
|
675
789
|
break;
|
|
676
790
|
}
|
|
677
791
|
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
678
792
|
}
|
|
679
|
-
if (
|
|
793
|
+
if (!nextBucketKey) {
|
|
680
794
|
if (Number.isFinite(nextWaitMs)) {
|
|
681
795
|
this.drainTimer = setTimeout(() => {
|
|
682
796
|
this.drainTimer = void 0;
|
|
@@ -686,15 +800,32 @@ var FetchRateLimiter = class {
|
|
|
686
800
|
}
|
|
687
801
|
return;
|
|
688
802
|
}
|
|
689
|
-
const
|
|
803
|
+
const queue = this.queuesByBucket.get(nextBucketKey);
|
|
804
|
+
const next = queue?.shift();
|
|
690
805
|
if (!next) {
|
|
691
|
-
|
|
806
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
807
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (!queue || queue.length === 0) {
|
|
811
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
812
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
692
813
|
}
|
|
693
814
|
const bucket = this.bucketState(next.bucketKey);
|
|
815
|
+
if (bucket.cleanupTimer) {
|
|
816
|
+
clearTimeout(bucket.cleanupTimer);
|
|
817
|
+
bucket.cleanupTimer = void 0;
|
|
818
|
+
}
|
|
694
819
|
bucket.active += 1;
|
|
695
|
-
|
|
820
|
+
if (next.options.intervalMs && next.options.maxPerInterval) {
|
|
821
|
+
bucket.startedAt.push(Date.now());
|
|
822
|
+
}
|
|
696
823
|
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
697
824
|
bucket.active -= 1;
|
|
825
|
+
if ((this.queuesByBucket.get(next.bucketKey)?.length ?? 0) > 0) {
|
|
826
|
+
this.pendingBuckets.add(next.bucketKey);
|
|
827
|
+
}
|
|
828
|
+
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
698
829
|
this.drain();
|
|
699
830
|
});
|
|
700
831
|
}
|
|
@@ -736,6 +867,31 @@ var FetchRateLimiter = class {
|
|
|
736
867
|
this.buckets.set(bucketKey, bucket);
|
|
737
868
|
return bucket;
|
|
738
869
|
}
|
|
870
|
+
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
871
|
+
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
872
|
+
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
873
|
+
this.buckets.delete(bucketKey);
|
|
874
|
+
this.queuesByBucket.delete(bucketKey);
|
|
875
|
+
this.pendingBuckets.delete(bucketKey);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (!intervalMs || bucket.active > 0 || queued > 0) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (bucket.cleanupTimer) {
|
|
882
|
+
clearTimeout(bucket.cleanupTimer);
|
|
883
|
+
}
|
|
884
|
+
bucket.cleanupTimer = setTimeout(() => {
|
|
885
|
+
bucket.cleanupTimer = void 0;
|
|
886
|
+
this.prune(bucket, Date.now(), intervalMs);
|
|
887
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && (this.queuesByBucket.get(bucketKey)?.length ?? 0) === 0) {
|
|
888
|
+
this.buckets.delete(bucketKey);
|
|
889
|
+
this.queuesByBucket.delete(bucketKey);
|
|
890
|
+
this.pendingBuckets.delete(bucketKey);
|
|
891
|
+
}
|
|
892
|
+
}, intervalMs);
|
|
893
|
+
bucket.cleanupTimer.unref?.();
|
|
894
|
+
}
|
|
739
895
|
};
|
|
740
896
|
|
|
741
897
|
// ../../src/internal/MetricsCollector.ts
|
|
@@ -814,7 +970,30 @@ var MetricsCollector = class {
|
|
|
814
970
|
|
|
815
971
|
// ../../src/internal/StoredValue.ts
|
|
816
972
|
function isStoredValueEnvelope(value) {
|
|
817
|
-
|
|
973
|
+
if (typeof value !== "object" || value === null) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
const v = value;
|
|
977
|
+
if (v.__layercache !== 1) {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
993
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
return true;
|
|
818
997
|
}
|
|
819
998
|
function createStoredValueEnvelope(options) {
|
|
820
999
|
const now = options.now ?? Date.now();
|
|
@@ -1025,69 +1204,23 @@ var TtlResolver = class {
|
|
|
1025
1204
|
}
|
|
1026
1205
|
};
|
|
1027
1206
|
|
|
1028
|
-
// ../../src/invalidation/PatternMatcher.ts
|
|
1029
|
-
var PatternMatcher = class _PatternMatcher {
|
|
1030
|
-
/**
|
|
1031
|
-
* Tests whether a glob-style pattern matches a value.
|
|
1032
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
1033
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
1034
|
-
* quadratic memory usage on long patterns/keys.
|
|
1035
|
-
*/
|
|
1036
|
-
static matches(pattern, value) {
|
|
1037
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Linear-time glob matching with O(1) extra memory.
|
|
1041
|
-
*/
|
|
1042
|
-
static matchLinear(pattern, value) {
|
|
1043
|
-
let patternIndex = 0;
|
|
1044
|
-
let valueIndex = 0;
|
|
1045
|
-
let starIndex = -1;
|
|
1046
|
-
let backtrackValueIndex = 0;
|
|
1047
|
-
while (valueIndex < value.length) {
|
|
1048
|
-
const patternChar = pattern[patternIndex];
|
|
1049
|
-
const valueChar = value[valueIndex];
|
|
1050
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
1051
|
-
starIndex = patternIndex;
|
|
1052
|
-
patternIndex += 1;
|
|
1053
|
-
backtrackValueIndex = valueIndex;
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
1057
|
-
patternIndex += 1;
|
|
1058
|
-
valueIndex += 1;
|
|
1059
|
-
continue;
|
|
1060
|
-
}
|
|
1061
|
-
if (starIndex !== -1) {
|
|
1062
|
-
patternIndex = starIndex + 1;
|
|
1063
|
-
backtrackValueIndex += 1;
|
|
1064
|
-
valueIndex = backtrackValueIndex;
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
1070
|
-
patternIndex += 1;
|
|
1071
|
-
}
|
|
1072
|
-
return patternIndex === pattern.length;
|
|
1073
|
-
}
|
|
1074
|
-
};
|
|
1075
|
-
|
|
1076
1207
|
// ../../src/invalidation/TagIndex.ts
|
|
1077
1208
|
var TagIndex = class {
|
|
1078
1209
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
1079
1210
|
keyToTags = /* @__PURE__ */ new Map();
|
|
1080
1211
|
knownKeys = /* @__PURE__ */ new Set();
|
|
1081
1212
|
maxKnownKeys;
|
|
1213
|
+
nextNodeId = 1;
|
|
1214
|
+
root = this.createTrieNode();
|
|
1082
1215
|
constructor(options = {}) {
|
|
1083
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
1216
|
+
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
1084
1217
|
}
|
|
1085
1218
|
async touch(key) {
|
|
1086
|
-
this.
|
|
1219
|
+
this.insertKnownKey(key);
|
|
1087
1220
|
this.pruneKnownKeysIfNeeded();
|
|
1088
1221
|
}
|
|
1089
1222
|
async track(key, tags) {
|
|
1090
|
-
this.
|
|
1223
|
+
this.insertKnownKey(key);
|
|
1091
1224
|
this.pruneKnownKeysIfNeeded();
|
|
1092
1225
|
if (tags.length === 0) {
|
|
1093
1226
|
return;
|
|
@@ -1113,18 +1246,104 @@ var TagIndex = class {
|
|
|
1113
1246
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1114
1247
|
}
|
|
1115
1248
|
async keysForPrefix(prefix) {
|
|
1116
|
-
|
|
1249
|
+
const node = this.findNode(prefix);
|
|
1250
|
+
if (!node) {
|
|
1251
|
+
return [];
|
|
1252
|
+
}
|
|
1253
|
+
const matches = [];
|
|
1254
|
+
this.collectFromNode(node, prefix, matches);
|
|
1255
|
+
return matches;
|
|
1117
1256
|
}
|
|
1118
1257
|
async tagsForKey(key) {
|
|
1119
1258
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1120
1259
|
}
|
|
1121
1260
|
async matchPattern(pattern) {
|
|
1122
|
-
|
|
1261
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1262
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1263
|
+
return [...matches];
|
|
1123
1264
|
}
|
|
1124
1265
|
async clear() {
|
|
1125
1266
|
this.tagToKeys.clear();
|
|
1126
1267
|
this.keyToTags.clear();
|
|
1127
1268
|
this.knownKeys.clear();
|
|
1269
|
+
this.root.children.clear();
|
|
1270
|
+
this.root.terminal = false;
|
|
1271
|
+
this.nextNodeId = this.root.id + 1;
|
|
1272
|
+
}
|
|
1273
|
+
createTrieNode() {
|
|
1274
|
+
return {
|
|
1275
|
+
id: this.nextNodeId++,
|
|
1276
|
+
terminal: false,
|
|
1277
|
+
children: /* @__PURE__ */ new Map()
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
insertKnownKey(key) {
|
|
1281
|
+
if (this.knownKeys.has(key)) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
this.knownKeys.add(key);
|
|
1285
|
+
let node = this.root;
|
|
1286
|
+
for (const character of key) {
|
|
1287
|
+
let child = node.children.get(character);
|
|
1288
|
+
if (!child) {
|
|
1289
|
+
child = this.createTrieNode();
|
|
1290
|
+
node.children.set(character, child);
|
|
1291
|
+
}
|
|
1292
|
+
node = child;
|
|
1293
|
+
}
|
|
1294
|
+
node.terminal = true;
|
|
1295
|
+
}
|
|
1296
|
+
findNode(prefix) {
|
|
1297
|
+
let node = this.root;
|
|
1298
|
+
for (const character of prefix) {
|
|
1299
|
+
node = node.children.get(character);
|
|
1300
|
+
if (!node) {
|
|
1301
|
+
return void 0;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return node;
|
|
1305
|
+
}
|
|
1306
|
+
collectFromNode(node, prefix, matches) {
|
|
1307
|
+
if (node.terminal) {
|
|
1308
|
+
matches.push(prefix);
|
|
1309
|
+
}
|
|
1310
|
+
for (const [character, child] of node.children) {
|
|
1311
|
+
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
1315
|
+
const stateKey = `${node.id}:${patternIndex}`;
|
|
1316
|
+
if (visited.has(stateKey)) {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
visited.add(stateKey);
|
|
1320
|
+
if (patternIndex === pattern.length) {
|
|
1321
|
+
if (node.terminal) {
|
|
1322
|
+
matches.add(prefix);
|
|
1323
|
+
}
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const patternChar = pattern[patternIndex];
|
|
1327
|
+
if (patternChar === void 0) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (patternChar === "*") {
|
|
1331
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1332
|
+
for (const [character, child2] of node.children) {
|
|
1333
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1334
|
+
}
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (patternChar === "?") {
|
|
1338
|
+
for (const [character, child2] of node.children) {
|
|
1339
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
|
|
1340
|
+
}
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const child = node.children.get(patternChar);
|
|
1344
|
+
if (child) {
|
|
1345
|
+
this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
|
|
1346
|
+
}
|
|
1128
1347
|
}
|
|
1129
1348
|
pruneKnownKeysIfNeeded() {
|
|
1130
1349
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
@@ -1141,7 +1360,7 @@ var TagIndex = class {
|
|
|
1141
1360
|
}
|
|
1142
1361
|
}
|
|
1143
1362
|
removeKey(key) {
|
|
1144
|
-
this.
|
|
1363
|
+
this.removeKnownKey(key);
|
|
1145
1364
|
const tags = this.keyToTags.get(key);
|
|
1146
1365
|
if (!tags) {
|
|
1147
1366
|
return;
|
|
@@ -1158,7 +1377,70 @@ var TagIndex = class {
|
|
|
1158
1377
|
}
|
|
1159
1378
|
this.keyToTags.delete(key);
|
|
1160
1379
|
}
|
|
1380
|
+
removeKnownKey(key) {
|
|
1381
|
+
if (!this.knownKeys.delete(key)) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
const path = [];
|
|
1385
|
+
let node = this.root;
|
|
1386
|
+
for (const character of key) {
|
|
1387
|
+
const child = node.children.get(character);
|
|
1388
|
+
if (!child) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
path.push([node, character]);
|
|
1392
|
+
node = child;
|
|
1393
|
+
}
|
|
1394
|
+
node.terminal = false;
|
|
1395
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
1396
|
+
const entry = path[index];
|
|
1397
|
+
if (!entry) {
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
const [parent, character] = entry;
|
|
1401
|
+
const child = parent.children.get(character);
|
|
1402
|
+
if (!child || child.terminal || child.children.size > 0) {
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
parent.children.delete(character);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
// ../../src/serialization/JsonSerializer.ts
|
|
1411
|
+
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1412
|
+
var JsonSerializer = class {
|
|
1413
|
+
serialize(value) {
|
|
1414
|
+
return JSON.stringify(value);
|
|
1415
|
+
}
|
|
1416
|
+
deserialize(payload) {
|
|
1417
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1418
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1419
|
+
}
|
|
1161
1420
|
};
|
|
1421
|
+
var MAX_SANITIZE_DEPTH = 200;
|
|
1422
|
+
function sanitizeJsonValue(value, depth) {
|
|
1423
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1424
|
+
return value;
|
|
1425
|
+
}
|
|
1426
|
+
if (Array.isArray(value)) {
|
|
1427
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1428
|
+
}
|
|
1429
|
+
if (!isPlainObject(value)) {
|
|
1430
|
+
return value;
|
|
1431
|
+
}
|
|
1432
|
+
const sanitized = {};
|
|
1433
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1434
|
+
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1438
|
+
}
|
|
1439
|
+
return sanitized;
|
|
1440
|
+
}
|
|
1441
|
+
function isPlainObject(value) {
|
|
1442
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1443
|
+
}
|
|
1162
1444
|
|
|
1163
1445
|
// ../../src/stampede/StampedeGuard.ts
|
|
1164
1446
|
var StampedeGuard = class {
|
|
@@ -1169,7 +1451,8 @@ var StampedeGuard = class {
|
|
|
1169
1451
|
return await entry.mutex.runExclusive(task);
|
|
1170
1452
|
} finally {
|
|
1171
1453
|
entry.references -= 1;
|
|
1172
|
-
|
|
1454
|
+
const current = this.mutexes.get(key);
|
|
1455
|
+
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
1173
1456
|
this.mutexes.delete(key);
|
|
1174
1457
|
}
|
|
1175
1458
|
}
|
|
@@ -1199,8 +1482,10 @@ var CacheMissError = class extends Error {
|
|
|
1199
1482
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1200
1483
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1201
1484
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1485
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1202
1486
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1203
1487
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1488
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1204
1489
|
var DebugLogger = class {
|
|
1205
1490
|
enabled;
|
|
1206
1491
|
constructor(enabled) {
|
|
@@ -1247,6 +1532,29 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1247
1532
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1248
1533
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1249
1534
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1535
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
1536
|
+
layers: this.layers,
|
|
1537
|
+
tagIndex: this.tagIndex,
|
|
1538
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1539
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1540
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1544
|
+
this.logger.warn?.(
|
|
1545
|
+
"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."
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false && !layer.keys)) {
|
|
1549
|
+
this.logger.warn?.(
|
|
1550
|
+
"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."
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
if (options.invalidationBus && options.broadcastL1Invalidation === void 0 && options.publishSetInvalidation === void 0) {
|
|
1554
|
+
this.logger.warn?.(
|
|
1555
|
+
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1250
1558
|
this.initializeWriteBehind(options.writeBehind);
|
|
1251
1559
|
this.startup = this.initialize();
|
|
1252
1560
|
}
|
|
@@ -1259,7 +1567,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1259
1567
|
unsubscribeInvalidation;
|
|
1260
1568
|
logger;
|
|
1261
1569
|
tagIndex;
|
|
1570
|
+
keyDiscovery;
|
|
1262
1571
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1572
|
+
snapshotSerializer = new JsonSerializer();
|
|
1263
1573
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1264
1574
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1265
1575
|
ttlResolver;
|
|
@@ -1268,6 +1578,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1268
1578
|
writeBehindQueue = [];
|
|
1269
1579
|
writeBehindTimer;
|
|
1270
1580
|
writeBehindFlushPromise;
|
|
1581
|
+
generationCleanupPromise;
|
|
1271
1582
|
isDisconnecting = false;
|
|
1272
1583
|
disconnectPromise;
|
|
1273
1584
|
/**
|
|
@@ -1280,6 +1591,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1280
1591
|
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1281
1592
|
this.validateWriteOptions(options);
|
|
1282
1593
|
await this.awaitStartup("get");
|
|
1594
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1595
|
+
}
|
|
1596
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
1283
1597
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1284
1598
|
if (hit.found) {
|
|
1285
1599
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -1357,6 +1671,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1357
1671
|
return true;
|
|
1358
1672
|
}
|
|
1359
1673
|
} catch {
|
|
1674
|
+
await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
|
|
1360
1675
|
}
|
|
1361
1676
|
} else {
|
|
1362
1677
|
try {
|
|
@@ -1364,7 +1679,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1364
1679
|
if (value !== null) {
|
|
1365
1680
|
return true;
|
|
1366
1681
|
}
|
|
1367
|
-
} catch {
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
await this.reportRecoverableLayerFailure(layer, "has", error);
|
|
1368
1684
|
}
|
|
1369
1685
|
}
|
|
1370
1686
|
}
|
|
@@ -1456,13 +1772,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1456
1772
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1457
1773
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
1458
1774
|
if (!canFastPath) {
|
|
1775
|
+
await this.awaitStartup("mget");
|
|
1459
1776
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1460
1777
|
return Promise.all(
|
|
1461
1778
|
normalizedEntries.map((entry) => {
|
|
1462
1779
|
const optionsSignature = this.serializeOptions(entry.options);
|
|
1463
1780
|
const existing = pendingReads.get(entry.key);
|
|
1464
1781
|
if (!existing) {
|
|
1465
|
-
const promise = this.
|
|
1782
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
1466
1783
|
pendingReads.set(entry.key, {
|
|
1467
1784
|
promise,
|
|
1468
1785
|
fetch: entry.fetch,
|
|
@@ -1601,14 +1918,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1601
1918
|
}
|
|
1602
1919
|
async invalidateByPattern(pattern) {
|
|
1603
1920
|
await this.awaitStartup("invalidateByPattern");
|
|
1604
|
-
const keys = await this.
|
|
1921
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1605
1922
|
await this.deleteKeys(keys);
|
|
1606
1923
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1607
1924
|
}
|
|
1608
1925
|
async invalidateByPrefix(prefix) {
|
|
1609
1926
|
await this.awaitStartup("invalidateByPrefix");
|
|
1610
1927
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1611
|
-
const keys =
|
|
1928
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1612
1929
|
await this.deleteKeys(keys);
|
|
1613
1930
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1614
1931
|
}
|
|
@@ -1658,9 +1975,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1658
1975
|
})
|
|
1659
1976
|
);
|
|
1660
1977
|
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Rotates the active generation prefix used for all future cache keys.
|
|
1980
|
+
* Previous-generation keys remain in the underlying layers until they expire,
|
|
1981
|
+
* unless `generationCleanup` is enabled to prune them in the background.
|
|
1982
|
+
*/
|
|
1661
1983
|
bumpGeneration(nextGeneration) {
|
|
1662
1984
|
const current = this.currentGeneration ?? 0;
|
|
1985
|
+
const previousGeneration = this.currentGeneration;
|
|
1663
1986
|
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1987
|
+
if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
|
|
1988
|
+
this.scheduleGenerationCleanup(previousGeneration);
|
|
1989
|
+
}
|
|
1664
1990
|
return this.currentGeneration;
|
|
1665
1991
|
}
|
|
1666
1992
|
/**
|
|
@@ -1744,27 +2070,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1744
2070
|
this.assertActive("persistToFile");
|
|
1745
2071
|
const snapshot = await this.exportState();
|
|
1746
2072
|
const { promises: fs } = await import("fs");
|
|
1747
|
-
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
2073
|
+
await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
|
|
1748
2074
|
}
|
|
1749
2075
|
async restoreFromFile(filePath) {
|
|
1750
2076
|
this.assertActive("restoreFromFile");
|
|
1751
2077
|
const { promises: fs } = await import("fs");
|
|
1752
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
2078
|
+
const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
|
|
1753
2079
|
let parsed;
|
|
1754
2080
|
try {
|
|
1755
|
-
parsed = JSON.parse(raw
|
|
1756
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1757
|
-
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1758
|
-
}
|
|
1759
|
-
return value;
|
|
1760
|
-
});
|
|
2081
|
+
parsed = JSON.parse(raw);
|
|
1761
2082
|
} catch (cause) {
|
|
1762
2083
|
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1763
2084
|
}
|
|
1764
2085
|
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1765
2086
|
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1766
2087
|
}
|
|
1767
|
-
await this.importState(
|
|
2088
|
+
await this.importState(
|
|
2089
|
+
parsed.map((entry) => ({
|
|
2090
|
+
key: entry.key,
|
|
2091
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
2092
|
+
ttl: entry.ttl
|
|
2093
|
+
}))
|
|
2094
|
+
);
|
|
1768
2095
|
}
|
|
1769
2096
|
async disconnect() {
|
|
1770
2097
|
if (!this.disconnectPromise) {
|
|
@@ -1773,6 +2100,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1773
2100
|
await this.startup;
|
|
1774
2101
|
await this.unsubscribeInvalidation?.();
|
|
1775
2102
|
await this.flushWriteBehindQueue();
|
|
2103
|
+
await this.generationCleanupPromise;
|
|
1776
2104
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1777
2105
|
if (this.writeBehindTimer) {
|
|
1778
2106
|
clearInterval(this.writeBehindTimer);
|
|
@@ -1856,8 +2184,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1856
2184
|
await this.storeEntry(key, "empty", null, options);
|
|
1857
2185
|
return null;
|
|
1858
2186
|
}
|
|
1859
|
-
if (options?.shouldCache
|
|
1860
|
-
|
|
2187
|
+
if (options?.shouldCache) {
|
|
2188
|
+
try {
|
|
2189
|
+
if (!options.shouldCache(fetched)) {
|
|
2190
|
+
return fetched;
|
|
2191
|
+
}
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2194
|
+
}
|
|
1861
2195
|
}
|
|
1862
2196
|
await this.storeEntry(key, "value", fetched, options);
|
|
1863
2197
|
return fetched;
|
|
@@ -2084,7 +2418,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2084
2418
|
const refresh = (async () => {
|
|
2085
2419
|
this.metricsCollector.increment("refreshes");
|
|
2086
2420
|
try {
|
|
2087
|
-
await this.
|
|
2421
|
+
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2088
2422
|
} catch (error) {
|
|
2089
2423
|
this.metricsCollector.increment("refreshErrors");
|
|
2090
2424
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2094,6 +2428,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2094
2428
|
})();
|
|
2095
2429
|
this.backgroundRefreshes.set(key, refresh);
|
|
2096
2430
|
}
|
|
2431
|
+
async runBackgroundRefresh(key, fetcher, options) {
|
|
2432
|
+
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2433
|
+
await this.fetchWithGuards(
|
|
2434
|
+
key,
|
|
2435
|
+
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2436
|
+
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2437
|
+
}),
|
|
2438
|
+
options
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2097
2441
|
resolveSingleFlightOptions() {
|
|
2098
2442
|
return {
|
|
2099
2443
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
@@ -2161,8 +2505,76 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2161
2505
|
sleep(ms) {
|
|
2162
2506
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2163
2507
|
}
|
|
2508
|
+
async withTimeout(promise, timeoutMs, onTimeout) {
|
|
2509
|
+
if (timeoutMs <= 0) {
|
|
2510
|
+
return promise;
|
|
2511
|
+
}
|
|
2512
|
+
let timer;
|
|
2513
|
+
const observedPromise = promise.then(
|
|
2514
|
+
(value) => ({ kind: "value", value }),
|
|
2515
|
+
(error) => ({ kind: "error", error })
|
|
2516
|
+
);
|
|
2517
|
+
try {
|
|
2518
|
+
const result = await Promise.race([
|
|
2519
|
+
observedPromise,
|
|
2520
|
+
new Promise((_, reject) => {
|
|
2521
|
+
timer = setTimeout(() => reject(onTimeout()), timeoutMs);
|
|
2522
|
+
timer.unref?.();
|
|
2523
|
+
})
|
|
2524
|
+
]);
|
|
2525
|
+
if (result && typeof result === "object" && "kind" in result) {
|
|
2526
|
+
if (result.kind === "error") {
|
|
2527
|
+
throw result.error;
|
|
2528
|
+
}
|
|
2529
|
+
return result.value;
|
|
2530
|
+
}
|
|
2531
|
+
return result;
|
|
2532
|
+
} finally {
|
|
2533
|
+
if (timer) {
|
|
2534
|
+
clearTimeout(timer);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2164
2538
|
shouldBroadcastL1Invalidation() {
|
|
2165
|
-
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ??
|
|
2539
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2540
|
+
}
|
|
2541
|
+
shouldCleanupGenerations() {
|
|
2542
|
+
return Boolean(this.options.generationCleanup);
|
|
2543
|
+
}
|
|
2544
|
+
generationCleanupBatchSize() {
|
|
2545
|
+
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
2546
|
+
return configured ?? 500;
|
|
2547
|
+
}
|
|
2548
|
+
scheduleGenerationCleanup(generation) {
|
|
2549
|
+
const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
|
|
2550
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
2551
|
+
generation,
|
|
2552
|
+
error: this.formatError(error)
|
|
2553
|
+
});
|
|
2554
|
+
});
|
|
2555
|
+
this.generationCleanupPromise = task.finally(() => {
|
|
2556
|
+
if (this.generationCleanupPromise === task) {
|
|
2557
|
+
this.generationCleanupPromise = void 0;
|
|
2558
|
+
}
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
async cleanupGeneration(generation) {
|
|
2562
|
+
const prefix = `v${generation}:`;
|
|
2563
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2564
|
+
if (keys.length === 0) {
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
const batchSize = this.generationCleanupBatchSize();
|
|
2568
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2569
|
+
const batch = keys.slice(index, index + batchSize);
|
|
2570
|
+
await this.deleteKeys(batch);
|
|
2571
|
+
await this.publishInvalidation({
|
|
2572
|
+
scope: "keys",
|
|
2573
|
+
keys: batch,
|
|
2574
|
+
sourceId: this.instanceId,
|
|
2575
|
+
operation: "invalidate"
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2166
2578
|
}
|
|
2167
2579
|
initializeWriteBehind(options) {
|
|
2168
2580
|
if (this.options.writeStrategy !== "write-behind") {
|
|
@@ -2200,7 +2612,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2200
2612
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2201
2613
|
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2202
2614
|
this.writeBehindFlushPromise = (async () => {
|
|
2203
|
-
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2615
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
2616
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2617
|
+
if (failures.length > 0) {
|
|
2618
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2619
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
2620
|
+
failed: failures.length,
|
|
2621
|
+
total: batch.length,
|
|
2622
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2623
|
+
});
|
|
2624
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2625
|
+
}
|
|
2204
2626
|
})();
|
|
2205
2627
|
await this.writeBehindFlushPromise;
|
|
2206
2628
|
this.writeBehindFlushPromise = void 0;
|
|
@@ -2305,9 +2727,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2305
2727
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
2306
2728
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2307
2729
|
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2730
|
+
this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
2308
2731
|
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
2309
2732
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
2310
2733
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2734
|
+
if (typeof this.options.generationCleanup === "object") {
|
|
2735
|
+
this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2736
|
+
}
|
|
2311
2737
|
if (this.options.generation !== void 0) {
|
|
2312
2738
|
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2313
2739
|
}
|
|
@@ -2379,6 +2805,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2379
2805
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2380
2806
|
throw new Error("Cache key contains unsupported control characters.");
|
|
2381
2807
|
}
|
|
2808
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2809
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2810
|
+
}
|
|
2382
2811
|
return key;
|
|
2383
2812
|
}
|
|
2384
2813
|
validateTtlPolicy(name, policy) {
|
|
@@ -2456,6 +2885,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2456
2885
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
2457
2886
|
return null;
|
|
2458
2887
|
}
|
|
2888
|
+
async reportRecoverableLayerFailure(layer, operation, error) {
|
|
2889
|
+
if (this.isGracefulDegradationEnabled()) {
|
|
2890
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
this.logger.warn?.("layer-operation-failed", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2894
|
+
this.emitError(operation, { layer: layer.name, degraded: false, error: this.formatError(error) });
|
|
2895
|
+
}
|
|
2459
2896
|
isGracefulDegradationEnabled() {
|
|
2460
2897
|
return Boolean(this.options.gracefulDegradation);
|
|
2461
2898
|
}
|
|
@@ -2479,10 +2916,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2479
2916
|
}
|
|
2480
2917
|
}
|
|
2481
2918
|
serializeKeyPart(value) {
|
|
2482
|
-
if (typeof value === "string"
|
|
2483
|
-
return
|
|
2919
|
+
if (typeof value === "string") {
|
|
2920
|
+
return `s:${value}`;
|
|
2921
|
+
}
|
|
2922
|
+
if (typeof value === "number") {
|
|
2923
|
+
return `n:${value}`;
|
|
2484
2924
|
}
|
|
2485
|
-
|
|
2925
|
+
if (typeof value === "boolean") {
|
|
2926
|
+
return `b:${value}`;
|
|
2927
|
+
}
|
|
2928
|
+
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2486
2929
|
}
|
|
2487
2930
|
isCacheSnapshotEntries(value) {
|
|
2488
2931
|
return Array.isArray(value) && value.every((entry) => {
|
|
@@ -2490,15 +2933,39 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2490
2933
|
return false;
|
|
2491
2934
|
}
|
|
2492
2935
|
const candidate = entry;
|
|
2493
|
-
return typeof candidate.key === "string";
|
|
2936
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
2494
2937
|
});
|
|
2495
2938
|
}
|
|
2939
|
+
sanitizeSnapshotValue(value) {
|
|
2940
|
+
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2941
|
+
}
|
|
2942
|
+
async validateSnapshotFilePath(filePath) {
|
|
2943
|
+
if (filePath.length === 0) {
|
|
2944
|
+
throw new Error("filePath must not be empty.");
|
|
2945
|
+
}
|
|
2946
|
+
if (filePath.includes("\0")) {
|
|
2947
|
+
throw new Error("filePath must not contain null bytes.");
|
|
2948
|
+
}
|
|
2949
|
+
const path = await import("path");
|
|
2950
|
+
const resolved = path.resolve(filePath);
|
|
2951
|
+
const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
|
|
2952
|
+
if (baseDir !== false) {
|
|
2953
|
+
const relative = path.relative(baseDir, resolved);
|
|
2954
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2955
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
return resolved;
|
|
2959
|
+
}
|
|
2496
2960
|
normalizeForSerialization(value) {
|
|
2497
2961
|
if (Array.isArray(value)) {
|
|
2498
2962
|
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
2499
2963
|
}
|
|
2500
2964
|
if (value && typeof value === "object") {
|
|
2501
2965
|
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
2966
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
2967
|
+
return normalized;
|
|
2968
|
+
}
|
|
2502
2969
|
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
2503
2970
|
return normalized;
|
|
2504
2971
|
}, {});
|