layercache 1.2.1 → 1.2.3
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 +66 -11
- package/dist/{chunk-ZMDB5KOK.js → chunk-7V7XAB74.js} +24 -1
- package/dist/{chunk-46UH7LNM.js → chunk-KOYGHLVP.js} +142 -18
- package/dist/{chunk-GF47Y3XR.js → chunk-QHWG7QS5.js} +56 -25
- package/dist/cli.cjs +92 -27
- package/dist/cli.js +15 -4
- package/dist/{edge-C1sBhTfv.d.ts → edge-B_rUqDy6.d.cts} +39 -1
- package/dist/{edge-C1sBhTfv.d.cts → edge-B_rUqDy6.d.ts} +39 -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 +798 -127
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +582 -90
- package/package.json +5 -5
- package/packages/nestjs/dist/index.cjs +582 -61
- package/packages/nestjs/dist/index.d.cts +30 -0
- package/packages/nestjs/dist/index.d.ts +30 -0
- package/packages/nestjs/dist/index.js +582 -61
|
@@ -562,11 +562,13 @@ var CircuitBreakerManager = class {
|
|
|
562
562
|
|
|
563
563
|
// ../../src/internal/FetchRateLimiter.ts
|
|
564
564
|
var FetchRateLimiter = class {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
565
|
+
buckets = /* @__PURE__ */ new Map();
|
|
566
|
+
queuesByBucket = /* @__PURE__ */ new Map();
|
|
567
|
+
pendingBuckets = /* @__PURE__ */ new Set();
|
|
568
|
+
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
569
|
+
nextFetcherBucketId = 0;
|
|
568
570
|
drainTimer;
|
|
569
|
-
async schedule(options, task) {
|
|
571
|
+
async schedule(options, context, task) {
|
|
570
572
|
if (!options) {
|
|
571
573
|
return task();
|
|
572
574
|
}
|
|
@@ -575,7 +577,17 @@ var FetchRateLimiter = class {
|
|
|
575
577
|
return task();
|
|
576
578
|
}
|
|
577
579
|
return new Promise((resolve, reject) => {
|
|
578
|
-
this.
|
|
580
|
+
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
581
|
+
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
582
|
+
queue.push({
|
|
583
|
+
bucketKey,
|
|
584
|
+
options: normalized,
|
|
585
|
+
task,
|
|
586
|
+
resolve,
|
|
587
|
+
reject
|
|
588
|
+
});
|
|
589
|
+
this.queuesByBucket.set(bucketKey, queue);
|
|
590
|
+
this.pendingBuckets.add(bucketKey);
|
|
579
591
|
this.drain();
|
|
580
592
|
});
|
|
581
593
|
}
|
|
@@ -589,63 +601,159 @@ var FetchRateLimiter = class {
|
|
|
589
601
|
return {
|
|
590
602
|
maxConcurrent,
|
|
591
603
|
intervalMs,
|
|
592
|
-
maxPerInterval
|
|
604
|
+
maxPerInterval,
|
|
605
|
+
scope: options.scope ?? "global",
|
|
606
|
+
bucketKey: options.bucketKey
|
|
593
607
|
};
|
|
594
608
|
}
|
|
609
|
+
resolveBucketKey(options, context) {
|
|
610
|
+
if (options.bucketKey) {
|
|
611
|
+
return `custom:${options.bucketKey}`;
|
|
612
|
+
}
|
|
613
|
+
if (options.scope === "key") {
|
|
614
|
+
return `key:${context.key}`;
|
|
615
|
+
}
|
|
616
|
+
if (options.scope === "fetcher") {
|
|
617
|
+
const existing = this.fetcherBuckets.get(context.fetcher);
|
|
618
|
+
if (existing) {
|
|
619
|
+
return existing;
|
|
620
|
+
}
|
|
621
|
+
const bucket = `fetcher:${this.nextFetcherBucketId}`;
|
|
622
|
+
this.nextFetcherBucketId += 1;
|
|
623
|
+
this.fetcherBuckets.set(context.fetcher, bucket);
|
|
624
|
+
return bucket;
|
|
625
|
+
}
|
|
626
|
+
return "global";
|
|
627
|
+
}
|
|
595
628
|
drain() {
|
|
596
629
|
if (this.drainTimer) {
|
|
597
630
|
clearTimeout(this.drainTimer);
|
|
598
631
|
this.drainTimer = void 0;
|
|
599
632
|
}
|
|
600
|
-
while (this.
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
633
|
+
while (this.pendingBuckets.size > 0) {
|
|
634
|
+
let nextBucketKey;
|
|
635
|
+
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
636
|
+
for (const bucketKey of this.pendingBuckets) {
|
|
637
|
+
const queue2 = this.queuesByBucket.get(bucketKey);
|
|
638
|
+
if (!queue2 || queue2.length === 0) {
|
|
639
|
+
this.pendingBuckets.delete(bucketKey);
|
|
640
|
+
this.queuesByBucket.delete(bucketKey);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const next2 = queue2[0];
|
|
644
|
+
if (!next2) {
|
|
645
|
+
this.pendingBuckets.delete(bucketKey);
|
|
646
|
+
this.queuesByBucket.delete(bucketKey);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const waitMs = this.waitTime(bucketKey, next2.options);
|
|
650
|
+
if (waitMs <= 0) {
|
|
651
|
+
nextBucketKey = bucketKey;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
604
655
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
656
|
+
if (!nextBucketKey) {
|
|
657
|
+
if (Number.isFinite(nextWaitMs)) {
|
|
658
|
+
this.drainTimer = setTimeout(() => {
|
|
659
|
+
this.drainTimer = void 0;
|
|
660
|
+
this.drain();
|
|
661
|
+
}, nextWaitMs);
|
|
662
|
+
this.drainTimer.unref?.();
|
|
663
|
+
}
|
|
612
664
|
return;
|
|
613
665
|
}
|
|
614
|
-
this.
|
|
615
|
-
|
|
616
|
-
|
|
666
|
+
const queue = this.queuesByBucket.get(nextBucketKey);
|
|
667
|
+
const next = queue?.shift();
|
|
668
|
+
if (!next) {
|
|
669
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
670
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (!queue || queue.length === 0) {
|
|
674
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
675
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
676
|
+
}
|
|
677
|
+
const bucket = this.bucketState(next.bucketKey);
|
|
678
|
+
if (bucket.cleanupTimer) {
|
|
679
|
+
clearTimeout(bucket.cleanupTimer);
|
|
680
|
+
bucket.cleanupTimer = void 0;
|
|
681
|
+
}
|
|
682
|
+
bucket.active += 1;
|
|
683
|
+
if (next.options.intervalMs && next.options.maxPerInterval) {
|
|
684
|
+
bucket.startedAt.push(Date.now());
|
|
685
|
+
}
|
|
617
686
|
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
618
|
-
|
|
687
|
+
bucket.active -= 1;
|
|
688
|
+
if ((this.queuesByBucket.get(next.bucketKey)?.length ?? 0) > 0) {
|
|
689
|
+
this.pendingBuckets.add(next.bucketKey);
|
|
690
|
+
}
|
|
691
|
+
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
619
692
|
this.drain();
|
|
620
693
|
});
|
|
621
694
|
}
|
|
622
695
|
}
|
|
623
|
-
waitTime(options) {
|
|
696
|
+
waitTime(bucketKey, options) {
|
|
697
|
+
const bucket = this.bucketState(bucketKey);
|
|
624
698
|
const now = Date.now();
|
|
625
|
-
if (options.maxConcurrent &&
|
|
699
|
+
if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
|
|
626
700
|
return 1;
|
|
627
701
|
}
|
|
628
702
|
if (!options.intervalMs || !options.maxPerInterval) {
|
|
629
703
|
return 0;
|
|
630
704
|
}
|
|
631
|
-
this.prune(now, options.intervalMs);
|
|
632
|
-
if (
|
|
705
|
+
this.prune(bucket, now, options.intervalMs);
|
|
706
|
+
if (bucket.startedAt.length < options.maxPerInterval) {
|
|
633
707
|
return 0;
|
|
634
708
|
}
|
|
635
|
-
const oldest =
|
|
709
|
+
const oldest = bucket.startedAt[0];
|
|
636
710
|
if (!oldest) {
|
|
637
711
|
return 0;
|
|
638
712
|
}
|
|
639
713
|
return Math.max(1, options.intervalMs - (now - oldest));
|
|
640
714
|
}
|
|
641
|
-
prune(now, intervalMs) {
|
|
642
|
-
while (
|
|
643
|
-
const startedAt =
|
|
715
|
+
prune(bucket, now, intervalMs) {
|
|
716
|
+
while (bucket.startedAt.length > 0) {
|
|
717
|
+
const startedAt = bucket.startedAt[0];
|
|
644
718
|
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
645
719
|
break;
|
|
646
720
|
}
|
|
647
|
-
|
|
721
|
+
bucket.startedAt.shift();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
bucketState(bucketKey) {
|
|
725
|
+
const existing = this.buckets.get(bucketKey);
|
|
726
|
+
if (existing) {
|
|
727
|
+
return existing;
|
|
648
728
|
}
|
|
729
|
+
const bucket = { active: 0, startedAt: [] };
|
|
730
|
+
this.buckets.set(bucketKey, bucket);
|
|
731
|
+
return bucket;
|
|
732
|
+
}
|
|
733
|
+
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
734
|
+
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
735
|
+
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
736
|
+
this.buckets.delete(bucketKey);
|
|
737
|
+
this.queuesByBucket.delete(bucketKey);
|
|
738
|
+
this.pendingBuckets.delete(bucketKey);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (!intervalMs || bucket.active > 0 || queued > 0) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (bucket.cleanupTimer) {
|
|
745
|
+
clearTimeout(bucket.cleanupTimer);
|
|
746
|
+
}
|
|
747
|
+
bucket.cleanupTimer = setTimeout(() => {
|
|
748
|
+
bucket.cleanupTimer = void 0;
|
|
749
|
+
this.prune(bucket, Date.now(), intervalMs);
|
|
750
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && (this.queuesByBucket.get(bucketKey)?.length ?? 0) === 0) {
|
|
751
|
+
this.buckets.delete(bucketKey);
|
|
752
|
+
this.queuesByBucket.delete(bucketKey);
|
|
753
|
+
this.pendingBuckets.delete(bucketKey);
|
|
754
|
+
}
|
|
755
|
+
}, intervalMs);
|
|
756
|
+
bucket.cleanupTimer.unref?.();
|
|
649
757
|
}
|
|
650
758
|
};
|
|
651
759
|
|
|
@@ -725,7 +833,30 @@ var MetricsCollector = class {
|
|
|
725
833
|
|
|
726
834
|
// ../../src/internal/StoredValue.ts
|
|
727
835
|
function isStoredValueEnvelope(value) {
|
|
728
|
-
|
|
836
|
+
if (typeof value !== "object" || value === null) {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
const v = value;
|
|
840
|
+
if (v.__layercache !== 1) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
856
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
return true;
|
|
729
860
|
}
|
|
730
861
|
function createStoredValueEnvelope(options) {
|
|
731
862
|
const now = options.now ?? Date.now();
|
|
@@ -990,15 +1121,17 @@ var TagIndex = class {
|
|
|
990
1121
|
keyToTags = /* @__PURE__ */ new Map();
|
|
991
1122
|
knownKeys = /* @__PURE__ */ new Set();
|
|
992
1123
|
maxKnownKeys;
|
|
1124
|
+
nextNodeId = 1;
|
|
1125
|
+
root = this.createTrieNode();
|
|
993
1126
|
constructor(options = {}) {
|
|
994
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
1127
|
+
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
995
1128
|
}
|
|
996
1129
|
async touch(key) {
|
|
997
|
-
this.
|
|
1130
|
+
this.insertKnownKey(key);
|
|
998
1131
|
this.pruneKnownKeysIfNeeded();
|
|
999
1132
|
}
|
|
1000
1133
|
async track(key, tags) {
|
|
1001
|
-
this.
|
|
1134
|
+
this.insertKnownKey(key);
|
|
1002
1135
|
this.pruneKnownKeysIfNeeded();
|
|
1003
1136
|
if (tags.length === 0) {
|
|
1004
1137
|
return;
|
|
@@ -1024,18 +1157,104 @@ var TagIndex = class {
|
|
|
1024
1157
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1025
1158
|
}
|
|
1026
1159
|
async keysForPrefix(prefix) {
|
|
1027
|
-
|
|
1160
|
+
const node = this.findNode(prefix);
|
|
1161
|
+
if (!node) {
|
|
1162
|
+
return [];
|
|
1163
|
+
}
|
|
1164
|
+
const matches = [];
|
|
1165
|
+
this.collectFromNode(node, prefix, matches);
|
|
1166
|
+
return matches;
|
|
1028
1167
|
}
|
|
1029
1168
|
async tagsForKey(key) {
|
|
1030
1169
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1031
1170
|
}
|
|
1032
1171
|
async matchPattern(pattern) {
|
|
1033
|
-
|
|
1172
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1173
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1174
|
+
return [...matches];
|
|
1034
1175
|
}
|
|
1035
1176
|
async clear() {
|
|
1036
1177
|
this.tagToKeys.clear();
|
|
1037
1178
|
this.keyToTags.clear();
|
|
1038
1179
|
this.knownKeys.clear();
|
|
1180
|
+
this.root.children.clear();
|
|
1181
|
+
this.root.terminal = false;
|
|
1182
|
+
this.nextNodeId = this.root.id + 1;
|
|
1183
|
+
}
|
|
1184
|
+
createTrieNode() {
|
|
1185
|
+
return {
|
|
1186
|
+
id: this.nextNodeId++,
|
|
1187
|
+
terminal: false,
|
|
1188
|
+
children: /* @__PURE__ */ new Map()
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
insertKnownKey(key) {
|
|
1192
|
+
if (this.knownKeys.has(key)) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
this.knownKeys.add(key);
|
|
1196
|
+
let node = this.root;
|
|
1197
|
+
for (const character of key) {
|
|
1198
|
+
let child = node.children.get(character);
|
|
1199
|
+
if (!child) {
|
|
1200
|
+
child = this.createTrieNode();
|
|
1201
|
+
node.children.set(character, child);
|
|
1202
|
+
}
|
|
1203
|
+
node = child;
|
|
1204
|
+
}
|
|
1205
|
+
node.terminal = true;
|
|
1206
|
+
}
|
|
1207
|
+
findNode(prefix) {
|
|
1208
|
+
let node = this.root;
|
|
1209
|
+
for (const character of prefix) {
|
|
1210
|
+
node = node.children.get(character);
|
|
1211
|
+
if (!node) {
|
|
1212
|
+
return void 0;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return node;
|
|
1216
|
+
}
|
|
1217
|
+
collectFromNode(node, prefix, matches) {
|
|
1218
|
+
if (node.terminal) {
|
|
1219
|
+
matches.push(prefix);
|
|
1220
|
+
}
|
|
1221
|
+
for (const [character, child] of node.children) {
|
|
1222
|
+
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
1226
|
+
const stateKey = `${node.id}:${patternIndex}`;
|
|
1227
|
+
if (visited.has(stateKey)) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
visited.add(stateKey);
|
|
1231
|
+
if (patternIndex === pattern.length) {
|
|
1232
|
+
if (node.terminal) {
|
|
1233
|
+
matches.add(prefix);
|
|
1234
|
+
}
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const patternChar = pattern[patternIndex];
|
|
1238
|
+
if (patternChar === void 0) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (patternChar === "*") {
|
|
1242
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1243
|
+
for (const [character, child2] of node.children) {
|
|
1244
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1245
|
+
}
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (patternChar === "?") {
|
|
1249
|
+
for (const [character, child2] of node.children) {
|
|
1250
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const child = node.children.get(patternChar);
|
|
1255
|
+
if (child) {
|
|
1256
|
+
this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
|
|
1257
|
+
}
|
|
1039
1258
|
}
|
|
1040
1259
|
pruneKnownKeysIfNeeded() {
|
|
1041
1260
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
@@ -1052,7 +1271,7 @@ var TagIndex = class {
|
|
|
1052
1271
|
}
|
|
1053
1272
|
}
|
|
1054
1273
|
removeKey(key) {
|
|
1055
|
-
this.
|
|
1274
|
+
this.removeKnownKey(key);
|
|
1056
1275
|
const tags = this.keyToTags.get(key);
|
|
1057
1276
|
if (!tags) {
|
|
1058
1277
|
return;
|
|
@@ -1069,7 +1288,70 @@ var TagIndex = class {
|
|
|
1069
1288
|
}
|
|
1070
1289
|
this.keyToTags.delete(key);
|
|
1071
1290
|
}
|
|
1291
|
+
removeKnownKey(key) {
|
|
1292
|
+
if (!this.knownKeys.delete(key)) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const path = [];
|
|
1296
|
+
let node = this.root;
|
|
1297
|
+
for (const character of key) {
|
|
1298
|
+
const child = node.children.get(character);
|
|
1299
|
+
if (!child) {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
path.push([node, character]);
|
|
1303
|
+
node = child;
|
|
1304
|
+
}
|
|
1305
|
+
node.terminal = false;
|
|
1306
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
1307
|
+
const entry = path[index];
|
|
1308
|
+
if (!entry) {
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
const [parent, character] = entry;
|
|
1312
|
+
const child = parent.children.get(character);
|
|
1313
|
+
if (!child || child.terminal || child.children.size > 0) {
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
parent.children.delete(character);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
// ../../src/serialization/JsonSerializer.ts
|
|
1322
|
+
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1323
|
+
var JsonSerializer = class {
|
|
1324
|
+
serialize(value) {
|
|
1325
|
+
return JSON.stringify(value);
|
|
1326
|
+
}
|
|
1327
|
+
deserialize(payload) {
|
|
1328
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1329
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1330
|
+
}
|
|
1072
1331
|
};
|
|
1332
|
+
var MAX_SANITIZE_DEPTH = 200;
|
|
1333
|
+
function sanitizeJsonValue(value, depth) {
|
|
1334
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1335
|
+
return value;
|
|
1336
|
+
}
|
|
1337
|
+
if (Array.isArray(value)) {
|
|
1338
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1339
|
+
}
|
|
1340
|
+
if (!isPlainObject(value)) {
|
|
1341
|
+
return value;
|
|
1342
|
+
}
|
|
1343
|
+
const sanitized = {};
|
|
1344
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1345
|
+
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1349
|
+
}
|
|
1350
|
+
return sanitized;
|
|
1351
|
+
}
|
|
1352
|
+
function isPlainObject(value) {
|
|
1353
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1354
|
+
}
|
|
1073
1355
|
|
|
1074
1356
|
// ../../src/stampede/StampedeGuard.ts
|
|
1075
1357
|
var StampedeGuard = class {
|
|
@@ -1080,7 +1362,8 @@ var StampedeGuard = class {
|
|
|
1080
1362
|
return await entry.mutex.runExclusive(task);
|
|
1081
1363
|
} finally {
|
|
1082
1364
|
entry.references -= 1;
|
|
1083
|
-
|
|
1365
|
+
const current = this.mutexes.get(key);
|
|
1366
|
+
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
1084
1367
|
this.mutexes.delete(key);
|
|
1085
1368
|
}
|
|
1086
1369
|
}
|
|
@@ -1110,8 +1393,10 @@ var CacheMissError = class extends Error {
|
|
|
1110
1393
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1111
1394
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1112
1395
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1396
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1113
1397
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1114
1398
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1399
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1115
1400
|
var DebugLogger = class {
|
|
1116
1401
|
enabled;
|
|
1117
1402
|
constructor(enabled) {
|
|
@@ -1158,6 +1443,21 @@ var CacheStack = class extends EventEmitter {
|
|
|
1158
1443
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1159
1444
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1160
1445
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1446
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1447
|
+
this.logger.warn?.(
|
|
1448
|
+
"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."
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false && !layer.keys)) {
|
|
1452
|
+
this.logger.warn?.(
|
|
1453
|
+
"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."
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (options.invalidationBus && options.broadcastL1Invalidation === void 0 && options.publishSetInvalidation === void 0) {
|
|
1457
|
+
this.logger.warn?.(
|
|
1458
|
+
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1161
1461
|
this.initializeWriteBehind(options.writeBehind);
|
|
1162
1462
|
this.startup = this.initialize();
|
|
1163
1463
|
}
|
|
@@ -1171,6 +1471,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1171
1471
|
logger;
|
|
1172
1472
|
tagIndex;
|
|
1173
1473
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1474
|
+
snapshotSerializer = new JsonSerializer();
|
|
1174
1475
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1175
1476
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1176
1477
|
ttlResolver;
|
|
@@ -1179,6 +1480,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1179
1480
|
writeBehindQueue = [];
|
|
1180
1481
|
writeBehindTimer;
|
|
1181
1482
|
writeBehindFlushPromise;
|
|
1483
|
+
generationCleanupPromise;
|
|
1182
1484
|
isDisconnecting = false;
|
|
1183
1485
|
disconnectPromise;
|
|
1184
1486
|
/**
|
|
@@ -1191,6 +1493,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1191
1493
|
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1192
1494
|
this.validateWriteOptions(options);
|
|
1193
1495
|
await this.awaitStartup("get");
|
|
1496
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1497
|
+
}
|
|
1498
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
1194
1499
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1195
1500
|
if (hit.found) {
|
|
1196
1501
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -1268,6 +1573,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1268
1573
|
return true;
|
|
1269
1574
|
}
|
|
1270
1575
|
} catch {
|
|
1576
|
+
await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
|
|
1271
1577
|
}
|
|
1272
1578
|
} else {
|
|
1273
1579
|
try {
|
|
@@ -1275,7 +1581,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1275
1581
|
if (value !== null) {
|
|
1276
1582
|
return true;
|
|
1277
1583
|
}
|
|
1278
|
-
} catch {
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
await this.reportRecoverableLayerFailure(layer, "has", error);
|
|
1279
1586
|
}
|
|
1280
1587
|
}
|
|
1281
1588
|
}
|
|
@@ -1367,13 +1674,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1367
1674
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1368
1675
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
1369
1676
|
if (!canFastPath) {
|
|
1677
|
+
await this.awaitStartup("mget");
|
|
1370
1678
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1371
1679
|
return Promise.all(
|
|
1372
1680
|
normalizedEntries.map((entry) => {
|
|
1373
1681
|
const optionsSignature = this.serializeOptions(entry.options);
|
|
1374
1682
|
const existing = pendingReads.get(entry.key);
|
|
1375
1683
|
if (!existing) {
|
|
1376
|
-
const promise = this.
|
|
1684
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
1377
1685
|
pendingReads.set(entry.key, {
|
|
1378
1686
|
promise,
|
|
1379
1687
|
fetch: entry.fetch,
|
|
@@ -1512,14 +1820,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1512
1820
|
}
|
|
1513
1821
|
async invalidateByPattern(pattern) {
|
|
1514
1822
|
await this.awaitStartup("invalidateByPattern");
|
|
1515
|
-
const keys = await this.
|
|
1823
|
+
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1516
1824
|
await this.deleteKeys(keys);
|
|
1517
1825
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1518
1826
|
}
|
|
1519
1827
|
async invalidateByPrefix(prefix) {
|
|
1520
1828
|
await this.awaitStartup("invalidateByPrefix");
|
|
1521
1829
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1522
|
-
const keys =
|
|
1830
|
+
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1523
1831
|
await this.deleteKeys(keys);
|
|
1524
1832
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1525
1833
|
}
|
|
@@ -1569,9 +1877,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
1569
1877
|
})
|
|
1570
1878
|
);
|
|
1571
1879
|
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Rotates the active generation prefix used for all future cache keys.
|
|
1882
|
+
* Previous-generation keys remain in the underlying layers until they expire,
|
|
1883
|
+
* unless `generationCleanup` is enabled to prune them in the background.
|
|
1884
|
+
*/
|
|
1572
1885
|
bumpGeneration(nextGeneration) {
|
|
1573
1886
|
const current = this.currentGeneration ?? 0;
|
|
1887
|
+
const previousGeneration = this.currentGeneration;
|
|
1574
1888
|
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1889
|
+
if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
|
|
1890
|
+
this.scheduleGenerationCleanup(previousGeneration);
|
|
1891
|
+
}
|
|
1575
1892
|
return this.currentGeneration;
|
|
1576
1893
|
}
|
|
1577
1894
|
/**
|
|
@@ -1655,27 +1972,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1655
1972
|
this.assertActive("persistToFile");
|
|
1656
1973
|
const snapshot = await this.exportState();
|
|
1657
1974
|
const { promises: fs } = await import("fs");
|
|
1658
|
-
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1975
|
+
await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
|
|
1659
1976
|
}
|
|
1660
1977
|
async restoreFromFile(filePath) {
|
|
1661
1978
|
this.assertActive("restoreFromFile");
|
|
1662
1979
|
const { promises: fs } = await import("fs");
|
|
1663
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
1980
|
+
const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
|
|
1664
1981
|
let parsed;
|
|
1665
1982
|
try {
|
|
1666
|
-
parsed = JSON.parse(raw
|
|
1667
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1668
|
-
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1669
|
-
}
|
|
1670
|
-
return value;
|
|
1671
|
-
});
|
|
1983
|
+
parsed = JSON.parse(raw);
|
|
1672
1984
|
} catch (cause) {
|
|
1673
1985
|
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1674
1986
|
}
|
|
1675
1987
|
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1676
1988
|
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1677
1989
|
}
|
|
1678
|
-
await this.importState(
|
|
1990
|
+
await this.importState(
|
|
1991
|
+
parsed.map((entry) => ({
|
|
1992
|
+
key: entry.key,
|
|
1993
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1994
|
+
ttl: entry.ttl
|
|
1995
|
+
}))
|
|
1996
|
+
);
|
|
1679
1997
|
}
|
|
1680
1998
|
async disconnect() {
|
|
1681
1999
|
if (!this.disconnectPromise) {
|
|
@@ -1684,6 +2002,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1684
2002
|
await this.startup;
|
|
1685
2003
|
await this.unsubscribeInvalidation?.();
|
|
1686
2004
|
await this.flushWriteBehindQueue();
|
|
2005
|
+
await this.generationCleanupPromise;
|
|
1687
2006
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1688
2007
|
if (this.writeBehindTimer) {
|
|
1689
2008
|
clearInterval(this.writeBehindTimer);
|
|
@@ -1751,6 +2070,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1751
2070
|
try {
|
|
1752
2071
|
fetched = await this.fetchRateLimiter.schedule(
|
|
1753
2072
|
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
2073
|
+
{ key, fetcher },
|
|
1754
2074
|
fetcher
|
|
1755
2075
|
);
|
|
1756
2076
|
this.circuitBreakerManager.recordSuccess(key);
|
|
@@ -1766,8 +2086,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1766
2086
|
await this.storeEntry(key, "empty", null, options);
|
|
1767
2087
|
return null;
|
|
1768
2088
|
}
|
|
1769
|
-
if (options?.shouldCache
|
|
1770
|
-
|
|
2089
|
+
if (options?.shouldCache) {
|
|
2090
|
+
try {
|
|
2091
|
+
if (!options.shouldCache(fetched)) {
|
|
2092
|
+
return fetched;
|
|
2093
|
+
}
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2096
|
+
}
|
|
1771
2097
|
}
|
|
1772
2098
|
await this.storeEntry(key, "value", fetched, options);
|
|
1773
2099
|
return fetched;
|
|
@@ -1994,7 +2320,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1994
2320
|
const refresh = (async () => {
|
|
1995
2321
|
this.metricsCollector.increment("refreshes");
|
|
1996
2322
|
try {
|
|
1997
|
-
await this.
|
|
2323
|
+
await this.runBackgroundRefresh(key, fetcher, options);
|
|
1998
2324
|
} catch (error) {
|
|
1999
2325
|
this.metricsCollector.increment("refreshErrors");
|
|
2000
2326
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2004,11 +2330,22 @@ var CacheStack = class extends EventEmitter {
|
|
|
2004
2330
|
})();
|
|
2005
2331
|
this.backgroundRefreshes.set(key, refresh);
|
|
2006
2332
|
}
|
|
2333
|
+
async runBackgroundRefresh(key, fetcher, options) {
|
|
2334
|
+
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2335
|
+
await this.fetchWithGuards(
|
|
2336
|
+
key,
|
|
2337
|
+
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2338
|
+
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2339
|
+
}),
|
|
2340
|
+
options
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2007
2343
|
resolveSingleFlightOptions() {
|
|
2008
2344
|
return {
|
|
2009
2345
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
2010
2346
|
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
2011
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
|
|
2347
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
2348
|
+
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
2012
2349
|
};
|
|
2013
2350
|
}
|
|
2014
2351
|
async deleteKeys(keys) {
|
|
@@ -2070,8 +2407,120 @@ var CacheStack = class extends EventEmitter {
|
|
|
2070
2407
|
sleep(ms) {
|
|
2071
2408
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2072
2409
|
}
|
|
2410
|
+
async withTimeout(promise, timeoutMs, onTimeout) {
|
|
2411
|
+
if (timeoutMs <= 0) {
|
|
2412
|
+
return promise;
|
|
2413
|
+
}
|
|
2414
|
+
let timer;
|
|
2415
|
+
const observedPromise = promise.then(
|
|
2416
|
+
(value) => ({ kind: "value", value }),
|
|
2417
|
+
(error) => ({ kind: "error", error })
|
|
2418
|
+
);
|
|
2419
|
+
try {
|
|
2420
|
+
const result = await Promise.race([
|
|
2421
|
+
observedPromise,
|
|
2422
|
+
new Promise((_, reject) => {
|
|
2423
|
+
timer = setTimeout(() => reject(onTimeout()), timeoutMs);
|
|
2424
|
+
timer.unref?.();
|
|
2425
|
+
})
|
|
2426
|
+
]);
|
|
2427
|
+
if (result && typeof result === "object" && "kind" in result) {
|
|
2428
|
+
if (result.kind === "error") {
|
|
2429
|
+
throw result.error;
|
|
2430
|
+
}
|
|
2431
|
+
return result.value;
|
|
2432
|
+
}
|
|
2433
|
+
return result;
|
|
2434
|
+
} finally {
|
|
2435
|
+
if (timer) {
|
|
2436
|
+
clearTimeout(timer);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2073
2440
|
shouldBroadcastL1Invalidation() {
|
|
2074
|
-
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ??
|
|
2441
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2442
|
+
}
|
|
2443
|
+
async collectKeysWithPrefix(prefix) {
|
|
2444
|
+
const matches = new Set(
|
|
2445
|
+
this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
|
|
2446
|
+
);
|
|
2447
|
+
await Promise.all(
|
|
2448
|
+
this.layers.map(async (layer) => {
|
|
2449
|
+
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
try {
|
|
2453
|
+
const keys = await layer.keys();
|
|
2454
|
+
for (const key of keys) {
|
|
2455
|
+
if (key.startsWith(prefix)) {
|
|
2456
|
+
matches.add(key);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
} catch (error) {
|
|
2460
|
+
await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
2461
|
+
}
|
|
2462
|
+
})
|
|
2463
|
+
);
|
|
2464
|
+
return [...matches];
|
|
2465
|
+
}
|
|
2466
|
+
async collectKeysMatchingPattern(pattern) {
|
|
2467
|
+
const matches = new Set(await this.tagIndex.matchPattern(pattern));
|
|
2468
|
+
await Promise.all(
|
|
2469
|
+
this.layers.map(async (layer) => {
|
|
2470
|
+
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
try {
|
|
2474
|
+
const keys = await layer.keys();
|
|
2475
|
+
for (const key of keys) {
|
|
2476
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
2477
|
+
matches.add(key);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
} catch (error) {
|
|
2481
|
+
await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
2482
|
+
}
|
|
2483
|
+
})
|
|
2484
|
+
);
|
|
2485
|
+
return [...matches];
|
|
2486
|
+
}
|
|
2487
|
+
shouldCleanupGenerations() {
|
|
2488
|
+
return Boolean(this.options.generationCleanup);
|
|
2489
|
+
}
|
|
2490
|
+
generationCleanupBatchSize() {
|
|
2491
|
+
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
2492
|
+
return configured ?? 500;
|
|
2493
|
+
}
|
|
2494
|
+
scheduleGenerationCleanup(generation) {
|
|
2495
|
+
const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
|
|
2496
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
2497
|
+
generation,
|
|
2498
|
+
error: this.formatError(error)
|
|
2499
|
+
});
|
|
2500
|
+
});
|
|
2501
|
+
this.generationCleanupPromise = task.finally(() => {
|
|
2502
|
+
if (this.generationCleanupPromise === task) {
|
|
2503
|
+
this.generationCleanupPromise = void 0;
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
async cleanupGeneration(generation) {
|
|
2508
|
+
const prefix = `v${generation}:`;
|
|
2509
|
+
const keys = await this.collectKeysWithPrefix(prefix);
|
|
2510
|
+
if (keys.length === 0) {
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
const batchSize = this.generationCleanupBatchSize();
|
|
2514
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2515
|
+
const batch = keys.slice(index, index + batchSize);
|
|
2516
|
+
await this.deleteKeys(batch);
|
|
2517
|
+
await this.publishInvalidation({
|
|
2518
|
+
scope: "keys",
|
|
2519
|
+
keys: batch,
|
|
2520
|
+
sourceId: this.instanceId,
|
|
2521
|
+
operation: "invalidate"
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2075
2524
|
}
|
|
2076
2525
|
initializeWriteBehind(options) {
|
|
2077
2526
|
if (this.options.writeStrategy !== "write-behind") {
|
|
@@ -2109,7 +2558,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
2109
2558
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2110
2559
|
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2111
2560
|
this.writeBehindFlushPromise = (async () => {
|
|
2112
|
-
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2561
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
2562
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2563
|
+
if (failures.length > 0) {
|
|
2564
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2565
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
2566
|
+
failed: failures.length,
|
|
2567
|
+
total: batch.length,
|
|
2568
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2569
|
+
});
|
|
2570
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2571
|
+
}
|
|
2113
2572
|
})();
|
|
2114
2573
|
await this.writeBehindFlushPromise;
|
|
2115
2574
|
this.writeBehindFlushPromise = void 0;
|
|
@@ -2213,8 +2672,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2213
2672
|
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
2214
2673
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
2215
2674
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2675
|
+
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2676
|
+
this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
2677
|
+
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
2216
2678
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
2217
2679
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2680
|
+
if (typeof this.options.generationCleanup === "object") {
|
|
2681
|
+
this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2682
|
+
}
|
|
2218
2683
|
if (this.options.generation !== void 0) {
|
|
2219
2684
|
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2220
2685
|
}
|
|
@@ -2232,6 +2697,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2232
2697
|
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
2233
2698
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
2234
2699
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
2700
|
+
this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
2235
2701
|
}
|
|
2236
2702
|
validateLayerNumberOption(name, value) {
|
|
2237
2703
|
if (value === void 0) {
|
|
@@ -2256,6 +2722,20 @@ var CacheStack = class extends EventEmitter {
|
|
|
2256
2722
|
throw new Error(`${name} must be a positive finite number.`);
|
|
2257
2723
|
}
|
|
2258
2724
|
}
|
|
2725
|
+
validateRateLimitOptions(name, options) {
|
|
2726
|
+
if (!options) {
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2730
|
+
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2731
|
+
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2732
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2733
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2734
|
+
}
|
|
2735
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2736
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2259
2739
|
validateNonNegativeNumber(name, value) {
|
|
2260
2740
|
if (!Number.isFinite(value) || value < 0) {
|
|
2261
2741
|
throw new Error(`${name} must be a non-negative finite number.`);
|
|
@@ -2271,6 +2751,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2271
2751
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2272
2752
|
throw new Error("Cache key contains unsupported control characters.");
|
|
2273
2753
|
}
|
|
2754
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2755
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2756
|
+
}
|
|
2274
2757
|
return key;
|
|
2275
2758
|
}
|
|
2276
2759
|
validateTtlPolicy(name, policy) {
|
|
@@ -2348,6 +2831,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2348
2831
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
2349
2832
|
return null;
|
|
2350
2833
|
}
|
|
2834
|
+
async reportRecoverableLayerFailure(layer, operation, error) {
|
|
2835
|
+
if (this.isGracefulDegradationEnabled()) {
|
|
2836
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
this.logger.warn?.("layer-operation-failed", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2840
|
+
this.emitError(operation, { layer: layer.name, degraded: false, error: this.formatError(error) });
|
|
2841
|
+
}
|
|
2351
2842
|
isGracefulDegradationEnabled() {
|
|
2352
2843
|
return Boolean(this.options.gracefulDegradation);
|
|
2353
2844
|
}
|
|
@@ -2371,10 +2862,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2371
2862
|
}
|
|
2372
2863
|
}
|
|
2373
2864
|
serializeKeyPart(value) {
|
|
2374
|
-
if (typeof value === "string"
|
|
2375
|
-
return
|
|
2865
|
+
if (typeof value === "string") {
|
|
2866
|
+
return `s:${value}`;
|
|
2867
|
+
}
|
|
2868
|
+
if (typeof value === "number") {
|
|
2869
|
+
return `n:${value}`;
|
|
2870
|
+
}
|
|
2871
|
+
if (typeof value === "boolean") {
|
|
2872
|
+
return `b:${value}`;
|
|
2376
2873
|
}
|
|
2377
|
-
return JSON.stringify(this.normalizeForSerialization(value))
|
|
2874
|
+
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2378
2875
|
}
|
|
2379
2876
|
isCacheSnapshotEntries(value) {
|
|
2380
2877
|
return Array.isArray(value) && value.every((entry) => {
|
|
@@ -2382,15 +2879,39 @@ var CacheStack = class extends EventEmitter {
|
|
|
2382
2879
|
return false;
|
|
2383
2880
|
}
|
|
2384
2881
|
const candidate = entry;
|
|
2385
|
-
return typeof candidate.key === "string";
|
|
2882
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
2386
2883
|
});
|
|
2387
2884
|
}
|
|
2885
|
+
sanitizeSnapshotValue(value) {
|
|
2886
|
+
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2887
|
+
}
|
|
2888
|
+
async validateSnapshotFilePath(filePath) {
|
|
2889
|
+
if (filePath.length === 0) {
|
|
2890
|
+
throw new Error("filePath must not be empty.");
|
|
2891
|
+
}
|
|
2892
|
+
if (filePath.includes("\0")) {
|
|
2893
|
+
throw new Error("filePath must not contain null bytes.");
|
|
2894
|
+
}
|
|
2895
|
+
const path = await import("path");
|
|
2896
|
+
const resolved = path.resolve(filePath);
|
|
2897
|
+
const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
|
|
2898
|
+
if (baseDir !== false) {
|
|
2899
|
+
const relative = path.relative(baseDir, resolved);
|
|
2900
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2901
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return resolved;
|
|
2905
|
+
}
|
|
2388
2906
|
normalizeForSerialization(value) {
|
|
2389
2907
|
if (Array.isArray(value)) {
|
|
2390
2908
|
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
2391
2909
|
}
|
|
2392
2910
|
if (value && typeof value === "object") {
|
|
2393
2911
|
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
2912
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
2913
|
+
return normalized;
|
|
2914
|
+
}
|
|
2394
2915
|
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
2395
2916
|
return normalized;
|
|
2396
2917
|
}, {});
|