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
|
@@ -598,11 +598,13 @@ var CircuitBreakerManager = class {
|
|
|
598
598
|
|
|
599
599
|
// ../../src/internal/FetchRateLimiter.ts
|
|
600
600
|
var FetchRateLimiter = class {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
buckets = /* @__PURE__ */ new Map();
|
|
602
|
+
queuesByBucket = /* @__PURE__ */ new Map();
|
|
603
|
+
pendingBuckets = /* @__PURE__ */ new Set();
|
|
604
|
+
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
605
|
+
nextFetcherBucketId = 0;
|
|
604
606
|
drainTimer;
|
|
605
|
-
async schedule(options, task) {
|
|
607
|
+
async schedule(options, context, task) {
|
|
606
608
|
if (!options) {
|
|
607
609
|
return task();
|
|
608
610
|
}
|
|
@@ -611,7 +613,17 @@ var FetchRateLimiter = class {
|
|
|
611
613
|
return task();
|
|
612
614
|
}
|
|
613
615
|
return new Promise((resolve, reject) => {
|
|
614
|
-
this.
|
|
616
|
+
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
617
|
+
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
618
|
+
queue.push({
|
|
619
|
+
bucketKey,
|
|
620
|
+
options: normalized,
|
|
621
|
+
task,
|
|
622
|
+
resolve,
|
|
623
|
+
reject
|
|
624
|
+
});
|
|
625
|
+
this.queuesByBucket.set(bucketKey, queue);
|
|
626
|
+
this.pendingBuckets.add(bucketKey);
|
|
615
627
|
this.drain();
|
|
616
628
|
});
|
|
617
629
|
}
|
|
@@ -625,63 +637,159 @@ var FetchRateLimiter = class {
|
|
|
625
637
|
return {
|
|
626
638
|
maxConcurrent,
|
|
627
639
|
intervalMs,
|
|
628
|
-
maxPerInterval
|
|
640
|
+
maxPerInterval,
|
|
641
|
+
scope: options.scope ?? "global",
|
|
642
|
+
bucketKey: options.bucketKey
|
|
629
643
|
};
|
|
630
644
|
}
|
|
645
|
+
resolveBucketKey(options, context) {
|
|
646
|
+
if (options.bucketKey) {
|
|
647
|
+
return `custom:${options.bucketKey}`;
|
|
648
|
+
}
|
|
649
|
+
if (options.scope === "key") {
|
|
650
|
+
return `key:${context.key}`;
|
|
651
|
+
}
|
|
652
|
+
if (options.scope === "fetcher") {
|
|
653
|
+
const existing = this.fetcherBuckets.get(context.fetcher);
|
|
654
|
+
if (existing) {
|
|
655
|
+
return existing;
|
|
656
|
+
}
|
|
657
|
+
const bucket = `fetcher:${this.nextFetcherBucketId}`;
|
|
658
|
+
this.nextFetcherBucketId += 1;
|
|
659
|
+
this.fetcherBuckets.set(context.fetcher, bucket);
|
|
660
|
+
return bucket;
|
|
661
|
+
}
|
|
662
|
+
return "global";
|
|
663
|
+
}
|
|
631
664
|
drain() {
|
|
632
665
|
if (this.drainTimer) {
|
|
633
666
|
clearTimeout(this.drainTimer);
|
|
634
667
|
this.drainTimer = void 0;
|
|
635
668
|
}
|
|
636
|
-
while (this.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
669
|
+
while (this.pendingBuckets.size > 0) {
|
|
670
|
+
let nextBucketKey;
|
|
671
|
+
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
672
|
+
for (const bucketKey of this.pendingBuckets) {
|
|
673
|
+
const queue2 = this.queuesByBucket.get(bucketKey);
|
|
674
|
+
if (!queue2 || queue2.length === 0) {
|
|
675
|
+
this.pendingBuckets.delete(bucketKey);
|
|
676
|
+
this.queuesByBucket.delete(bucketKey);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const next2 = queue2[0];
|
|
680
|
+
if (!next2) {
|
|
681
|
+
this.pendingBuckets.delete(bucketKey);
|
|
682
|
+
this.queuesByBucket.delete(bucketKey);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const waitMs = this.waitTime(bucketKey, next2.options);
|
|
686
|
+
if (waitMs <= 0) {
|
|
687
|
+
nextBucketKey = bucketKey;
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
640
691
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
692
|
+
if (!nextBucketKey) {
|
|
693
|
+
if (Number.isFinite(nextWaitMs)) {
|
|
694
|
+
this.drainTimer = setTimeout(() => {
|
|
695
|
+
this.drainTimer = void 0;
|
|
696
|
+
this.drain();
|
|
697
|
+
}, nextWaitMs);
|
|
698
|
+
this.drainTimer.unref?.();
|
|
699
|
+
}
|
|
648
700
|
return;
|
|
649
701
|
}
|
|
650
|
-
this.
|
|
651
|
-
|
|
652
|
-
|
|
702
|
+
const queue = this.queuesByBucket.get(nextBucketKey);
|
|
703
|
+
const next = queue?.shift();
|
|
704
|
+
if (!next) {
|
|
705
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
706
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (!queue || queue.length === 0) {
|
|
710
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
711
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
712
|
+
}
|
|
713
|
+
const bucket = this.bucketState(next.bucketKey);
|
|
714
|
+
if (bucket.cleanupTimer) {
|
|
715
|
+
clearTimeout(bucket.cleanupTimer);
|
|
716
|
+
bucket.cleanupTimer = void 0;
|
|
717
|
+
}
|
|
718
|
+
bucket.active += 1;
|
|
719
|
+
if (next.options.intervalMs && next.options.maxPerInterval) {
|
|
720
|
+
bucket.startedAt.push(Date.now());
|
|
721
|
+
}
|
|
653
722
|
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
654
|
-
|
|
723
|
+
bucket.active -= 1;
|
|
724
|
+
if ((this.queuesByBucket.get(next.bucketKey)?.length ?? 0) > 0) {
|
|
725
|
+
this.pendingBuckets.add(next.bucketKey);
|
|
726
|
+
}
|
|
727
|
+
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
655
728
|
this.drain();
|
|
656
729
|
});
|
|
657
730
|
}
|
|
658
731
|
}
|
|
659
|
-
waitTime(options) {
|
|
732
|
+
waitTime(bucketKey, options) {
|
|
733
|
+
const bucket = this.bucketState(bucketKey);
|
|
660
734
|
const now = Date.now();
|
|
661
|
-
if (options.maxConcurrent &&
|
|
735
|
+
if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
|
|
662
736
|
return 1;
|
|
663
737
|
}
|
|
664
738
|
if (!options.intervalMs || !options.maxPerInterval) {
|
|
665
739
|
return 0;
|
|
666
740
|
}
|
|
667
|
-
this.prune(now, options.intervalMs);
|
|
668
|
-
if (
|
|
741
|
+
this.prune(bucket, now, options.intervalMs);
|
|
742
|
+
if (bucket.startedAt.length < options.maxPerInterval) {
|
|
669
743
|
return 0;
|
|
670
744
|
}
|
|
671
|
-
const oldest =
|
|
745
|
+
const oldest = bucket.startedAt[0];
|
|
672
746
|
if (!oldest) {
|
|
673
747
|
return 0;
|
|
674
748
|
}
|
|
675
749
|
return Math.max(1, options.intervalMs - (now - oldest));
|
|
676
750
|
}
|
|
677
|
-
prune(now, intervalMs) {
|
|
678
|
-
while (
|
|
679
|
-
const startedAt =
|
|
751
|
+
prune(bucket, now, intervalMs) {
|
|
752
|
+
while (bucket.startedAt.length > 0) {
|
|
753
|
+
const startedAt = bucket.startedAt[0];
|
|
680
754
|
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
681
755
|
break;
|
|
682
756
|
}
|
|
683
|
-
|
|
757
|
+
bucket.startedAt.shift();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
bucketState(bucketKey) {
|
|
761
|
+
const existing = this.buckets.get(bucketKey);
|
|
762
|
+
if (existing) {
|
|
763
|
+
return existing;
|
|
684
764
|
}
|
|
765
|
+
const bucket = { active: 0, startedAt: [] };
|
|
766
|
+
this.buckets.set(bucketKey, bucket);
|
|
767
|
+
return bucket;
|
|
768
|
+
}
|
|
769
|
+
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
770
|
+
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
771
|
+
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
772
|
+
this.buckets.delete(bucketKey);
|
|
773
|
+
this.queuesByBucket.delete(bucketKey);
|
|
774
|
+
this.pendingBuckets.delete(bucketKey);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (!intervalMs || bucket.active > 0 || queued > 0) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (bucket.cleanupTimer) {
|
|
781
|
+
clearTimeout(bucket.cleanupTimer);
|
|
782
|
+
}
|
|
783
|
+
bucket.cleanupTimer = setTimeout(() => {
|
|
784
|
+
bucket.cleanupTimer = void 0;
|
|
785
|
+
this.prune(bucket, Date.now(), intervalMs);
|
|
786
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && (this.queuesByBucket.get(bucketKey)?.length ?? 0) === 0) {
|
|
787
|
+
this.buckets.delete(bucketKey);
|
|
788
|
+
this.queuesByBucket.delete(bucketKey);
|
|
789
|
+
this.pendingBuckets.delete(bucketKey);
|
|
790
|
+
}
|
|
791
|
+
}, intervalMs);
|
|
792
|
+
bucket.cleanupTimer.unref?.();
|
|
685
793
|
}
|
|
686
794
|
};
|
|
687
795
|
|
|
@@ -761,7 +869,30 @@ var MetricsCollector = class {
|
|
|
761
869
|
|
|
762
870
|
// ../../src/internal/StoredValue.ts
|
|
763
871
|
function isStoredValueEnvelope(value) {
|
|
764
|
-
|
|
872
|
+
if (typeof value !== "object" || value === null) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
const v = value;
|
|
876
|
+
if (v.__layercache !== 1) {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
892
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
return true;
|
|
765
896
|
}
|
|
766
897
|
function createStoredValueEnvelope(options) {
|
|
767
898
|
const now = options.now ?? Date.now();
|
|
@@ -1026,15 +1157,17 @@ var TagIndex = class {
|
|
|
1026
1157
|
keyToTags = /* @__PURE__ */ new Map();
|
|
1027
1158
|
knownKeys = /* @__PURE__ */ new Set();
|
|
1028
1159
|
maxKnownKeys;
|
|
1160
|
+
nextNodeId = 1;
|
|
1161
|
+
root = this.createTrieNode();
|
|
1029
1162
|
constructor(options = {}) {
|
|
1030
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
1163
|
+
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
1031
1164
|
}
|
|
1032
1165
|
async touch(key) {
|
|
1033
|
-
this.
|
|
1166
|
+
this.insertKnownKey(key);
|
|
1034
1167
|
this.pruneKnownKeysIfNeeded();
|
|
1035
1168
|
}
|
|
1036
1169
|
async track(key, tags) {
|
|
1037
|
-
this.
|
|
1170
|
+
this.insertKnownKey(key);
|
|
1038
1171
|
this.pruneKnownKeysIfNeeded();
|
|
1039
1172
|
if (tags.length === 0) {
|
|
1040
1173
|
return;
|
|
@@ -1060,18 +1193,104 @@ var TagIndex = class {
|
|
|
1060
1193
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1061
1194
|
}
|
|
1062
1195
|
async keysForPrefix(prefix) {
|
|
1063
|
-
|
|
1196
|
+
const node = this.findNode(prefix);
|
|
1197
|
+
if (!node) {
|
|
1198
|
+
return [];
|
|
1199
|
+
}
|
|
1200
|
+
const matches = [];
|
|
1201
|
+
this.collectFromNode(node, prefix, matches);
|
|
1202
|
+
return matches;
|
|
1064
1203
|
}
|
|
1065
1204
|
async tagsForKey(key) {
|
|
1066
1205
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1067
1206
|
}
|
|
1068
1207
|
async matchPattern(pattern) {
|
|
1069
|
-
|
|
1208
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1209
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1210
|
+
return [...matches];
|
|
1070
1211
|
}
|
|
1071
1212
|
async clear() {
|
|
1072
1213
|
this.tagToKeys.clear();
|
|
1073
1214
|
this.keyToTags.clear();
|
|
1074
1215
|
this.knownKeys.clear();
|
|
1216
|
+
this.root.children.clear();
|
|
1217
|
+
this.root.terminal = false;
|
|
1218
|
+
this.nextNodeId = this.root.id + 1;
|
|
1219
|
+
}
|
|
1220
|
+
createTrieNode() {
|
|
1221
|
+
return {
|
|
1222
|
+
id: this.nextNodeId++,
|
|
1223
|
+
terminal: false,
|
|
1224
|
+
children: /* @__PURE__ */ new Map()
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
insertKnownKey(key) {
|
|
1228
|
+
if (this.knownKeys.has(key)) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
this.knownKeys.add(key);
|
|
1232
|
+
let node = this.root;
|
|
1233
|
+
for (const character of key) {
|
|
1234
|
+
let child = node.children.get(character);
|
|
1235
|
+
if (!child) {
|
|
1236
|
+
child = this.createTrieNode();
|
|
1237
|
+
node.children.set(character, child);
|
|
1238
|
+
}
|
|
1239
|
+
node = child;
|
|
1240
|
+
}
|
|
1241
|
+
node.terminal = true;
|
|
1242
|
+
}
|
|
1243
|
+
findNode(prefix) {
|
|
1244
|
+
let node = this.root;
|
|
1245
|
+
for (const character of prefix) {
|
|
1246
|
+
node = node.children.get(character);
|
|
1247
|
+
if (!node) {
|
|
1248
|
+
return void 0;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return node;
|
|
1252
|
+
}
|
|
1253
|
+
collectFromNode(node, prefix, matches) {
|
|
1254
|
+
if (node.terminal) {
|
|
1255
|
+
matches.push(prefix);
|
|
1256
|
+
}
|
|
1257
|
+
for (const [character, child] of node.children) {
|
|
1258
|
+
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
1262
|
+
const stateKey = `${node.id}:${patternIndex}`;
|
|
1263
|
+
if (visited.has(stateKey)) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
visited.add(stateKey);
|
|
1267
|
+
if (patternIndex === pattern.length) {
|
|
1268
|
+
if (node.terminal) {
|
|
1269
|
+
matches.add(prefix);
|
|
1270
|
+
}
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
const patternChar = pattern[patternIndex];
|
|
1274
|
+
if (patternChar === void 0) {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (patternChar === "*") {
|
|
1278
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1279
|
+
for (const [character, child2] of node.children) {
|
|
1280
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (patternChar === "?") {
|
|
1285
|
+
for (const [character, child2] of node.children) {
|
|
1286
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const child = node.children.get(patternChar);
|
|
1291
|
+
if (child) {
|
|
1292
|
+
this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
|
|
1293
|
+
}
|
|
1075
1294
|
}
|
|
1076
1295
|
pruneKnownKeysIfNeeded() {
|
|
1077
1296
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
@@ -1088,7 +1307,7 @@ var TagIndex = class {
|
|
|
1088
1307
|
}
|
|
1089
1308
|
}
|
|
1090
1309
|
removeKey(key) {
|
|
1091
|
-
this.
|
|
1310
|
+
this.removeKnownKey(key);
|
|
1092
1311
|
const tags = this.keyToTags.get(key);
|
|
1093
1312
|
if (!tags) {
|
|
1094
1313
|
return;
|
|
@@ -1105,7 +1324,70 @@ var TagIndex = class {
|
|
|
1105
1324
|
}
|
|
1106
1325
|
this.keyToTags.delete(key);
|
|
1107
1326
|
}
|
|
1327
|
+
removeKnownKey(key) {
|
|
1328
|
+
if (!this.knownKeys.delete(key)) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const path = [];
|
|
1332
|
+
let node = this.root;
|
|
1333
|
+
for (const character of key) {
|
|
1334
|
+
const child = node.children.get(character);
|
|
1335
|
+
if (!child) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
path.push([node, character]);
|
|
1339
|
+
node = child;
|
|
1340
|
+
}
|
|
1341
|
+
node.terminal = false;
|
|
1342
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
1343
|
+
const entry = path[index];
|
|
1344
|
+
if (!entry) {
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const [parent, character] = entry;
|
|
1348
|
+
const child = parent.children.get(character);
|
|
1349
|
+
if (!child || child.terminal || child.children.size > 0) {
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
parent.children.delete(character);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
// ../../src/serialization/JsonSerializer.ts
|
|
1358
|
+
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1359
|
+
var JsonSerializer = class {
|
|
1360
|
+
serialize(value) {
|
|
1361
|
+
return JSON.stringify(value);
|
|
1362
|
+
}
|
|
1363
|
+
deserialize(payload) {
|
|
1364
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1365
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1366
|
+
}
|
|
1108
1367
|
};
|
|
1368
|
+
var MAX_SANITIZE_DEPTH = 200;
|
|
1369
|
+
function sanitizeJsonValue(value, depth) {
|
|
1370
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1371
|
+
return value;
|
|
1372
|
+
}
|
|
1373
|
+
if (Array.isArray(value)) {
|
|
1374
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1375
|
+
}
|
|
1376
|
+
if (!isPlainObject(value)) {
|
|
1377
|
+
return value;
|
|
1378
|
+
}
|
|
1379
|
+
const sanitized = {};
|
|
1380
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1381
|
+
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1385
|
+
}
|
|
1386
|
+
return sanitized;
|
|
1387
|
+
}
|
|
1388
|
+
function isPlainObject(value) {
|
|
1389
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1390
|
+
}
|
|
1109
1391
|
|
|
1110
1392
|
// ../../src/stampede/StampedeGuard.ts
|
|
1111
1393
|
var StampedeGuard = class {
|
|
@@ -1116,7 +1398,8 @@ var StampedeGuard = class {
|
|
|
1116
1398
|
return await entry.mutex.runExclusive(task);
|
|
1117
1399
|
} finally {
|
|
1118
1400
|
entry.references -= 1;
|
|
1119
|
-
|
|
1401
|
+
const current = this.mutexes.get(key);
|
|
1402
|
+
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
1120
1403
|
this.mutexes.delete(key);
|
|
1121
1404
|
}
|
|
1122
1405
|
}
|
|
@@ -1146,8 +1429,10 @@ var CacheMissError = class extends Error {
|
|
|
1146
1429
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1147
1430
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1148
1431
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1432
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1149
1433
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1150
1434
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1435
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1151
1436
|
var DebugLogger = class {
|
|
1152
1437
|
enabled;
|
|
1153
1438
|
constructor(enabled) {
|
|
@@ -1194,6 +1479,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1194
1479
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1195
1480
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1196
1481
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1482
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1483
|
+
this.logger.warn?.(
|
|
1484
|
+
"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."
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false && !layer.keys)) {
|
|
1488
|
+
this.logger.warn?.(
|
|
1489
|
+
"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."
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
if (options.invalidationBus && options.broadcastL1Invalidation === void 0 && options.publishSetInvalidation === void 0) {
|
|
1493
|
+
this.logger.warn?.(
|
|
1494
|
+
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1197
1497
|
this.initializeWriteBehind(options.writeBehind);
|
|
1198
1498
|
this.startup = this.initialize();
|
|
1199
1499
|
}
|
|
@@ -1207,6 +1507,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1207
1507
|
logger;
|
|
1208
1508
|
tagIndex;
|
|
1209
1509
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1510
|
+
snapshotSerializer = new JsonSerializer();
|
|
1210
1511
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1211
1512
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1212
1513
|
ttlResolver;
|
|
@@ -1215,6 +1516,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1215
1516
|
writeBehindQueue = [];
|
|
1216
1517
|
writeBehindTimer;
|
|
1217
1518
|
writeBehindFlushPromise;
|
|
1519
|
+
generationCleanupPromise;
|
|
1218
1520
|
isDisconnecting = false;
|
|
1219
1521
|
disconnectPromise;
|
|
1220
1522
|
/**
|
|
@@ -1227,6 +1529,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1227
1529
|
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1228
1530
|
this.validateWriteOptions(options);
|
|
1229
1531
|
await this.awaitStartup("get");
|
|
1532
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1533
|
+
}
|
|
1534
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
1230
1535
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1231
1536
|
if (hit.found) {
|
|
1232
1537
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -1304,6 +1609,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1304
1609
|
return true;
|
|
1305
1610
|
}
|
|
1306
1611
|
} catch {
|
|
1612
|
+
await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
|
|
1307
1613
|
}
|
|
1308
1614
|
} else {
|
|
1309
1615
|
try {
|
|
@@ -1311,7 +1617,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1311
1617
|
if (value !== null) {
|
|
1312
1618
|
return true;
|
|
1313
1619
|
}
|
|
1314
|
-
} catch {
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
await this.reportRecoverableLayerFailure(layer, "has", error);
|
|
1315
1622
|
}
|
|
1316
1623
|
}
|
|
1317
1624
|
}
|
|
@@ -1403,13 +1710,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1403
1710
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1404
1711
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
1405
1712
|
if (!canFastPath) {
|
|
1713
|
+
await this.awaitStartup("mget");
|
|
1406
1714
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1407
1715
|
return Promise.all(
|
|
1408
1716
|
normalizedEntries.map((entry) => {
|
|
1409
1717
|
const optionsSignature = this.serializeOptions(entry.options);
|
|
1410
1718
|
const existing = pendingReads.get(entry.key);
|
|
1411
1719
|
if (!existing) {
|
|
1412
|
-
const promise = this.
|
|
1720
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
1413
1721
|
pendingReads.set(entry.key, {
|
|
1414
1722
|
promise,
|
|
1415
1723
|
fetch: entry.fetch,
|
|
@@ -1548,14 +1856,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1548
1856
|
}
|
|
1549
1857
|
async invalidateByPattern(pattern) {
|
|
1550
1858
|
await this.awaitStartup("invalidateByPattern");
|
|
1551
|
-
const keys = await this.
|
|
1859
|
+
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1552
1860
|
await this.deleteKeys(keys);
|
|
1553
1861
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1554
1862
|
}
|
|
1555
1863
|
async invalidateByPrefix(prefix) {
|
|
1556
1864
|
await this.awaitStartup("invalidateByPrefix");
|
|
1557
1865
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1558
|
-
const keys =
|
|
1866
|
+
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1559
1867
|
await this.deleteKeys(keys);
|
|
1560
1868
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1561
1869
|
}
|
|
@@ -1605,9 +1913,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1605
1913
|
})
|
|
1606
1914
|
);
|
|
1607
1915
|
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Rotates the active generation prefix used for all future cache keys.
|
|
1918
|
+
* Previous-generation keys remain in the underlying layers until they expire,
|
|
1919
|
+
* unless `generationCleanup` is enabled to prune them in the background.
|
|
1920
|
+
*/
|
|
1608
1921
|
bumpGeneration(nextGeneration) {
|
|
1609
1922
|
const current = this.currentGeneration ?? 0;
|
|
1923
|
+
const previousGeneration = this.currentGeneration;
|
|
1610
1924
|
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1925
|
+
if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
|
|
1926
|
+
this.scheduleGenerationCleanup(previousGeneration);
|
|
1927
|
+
}
|
|
1611
1928
|
return this.currentGeneration;
|
|
1612
1929
|
}
|
|
1613
1930
|
/**
|
|
@@ -1691,27 +2008,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1691
2008
|
this.assertActive("persistToFile");
|
|
1692
2009
|
const snapshot = await this.exportState();
|
|
1693
2010
|
const { promises: fs } = await import("fs");
|
|
1694
|
-
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
2011
|
+
await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
|
|
1695
2012
|
}
|
|
1696
2013
|
async restoreFromFile(filePath) {
|
|
1697
2014
|
this.assertActive("restoreFromFile");
|
|
1698
2015
|
const { promises: fs } = await import("fs");
|
|
1699
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
2016
|
+
const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
|
|
1700
2017
|
let parsed;
|
|
1701
2018
|
try {
|
|
1702
|
-
parsed = JSON.parse(raw
|
|
1703
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1704
|
-
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1705
|
-
}
|
|
1706
|
-
return value;
|
|
1707
|
-
});
|
|
2019
|
+
parsed = JSON.parse(raw);
|
|
1708
2020
|
} catch (cause) {
|
|
1709
2021
|
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1710
2022
|
}
|
|
1711
2023
|
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1712
2024
|
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1713
2025
|
}
|
|
1714
|
-
await this.importState(
|
|
2026
|
+
await this.importState(
|
|
2027
|
+
parsed.map((entry) => ({
|
|
2028
|
+
key: entry.key,
|
|
2029
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
2030
|
+
ttl: entry.ttl
|
|
2031
|
+
}))
|
|
2032
|
+
);
|
|
1715
2033
|
}
|
|
1716
2034
|
async disconnect() {
|
|
1717
2035
|
if (!this.disconnectPromise) {
|
|
@@ -1720,6 +2038,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1720
2038
|
await this.startup;
|
|
1721
2039
|
await this.unsubscribeInvalidation?.();
|
|
1722
2040
|
await this.flushWriteBehindQueue();
|
|
2041
|
+
await this.generationCleanupPromise;
|
|
1723
2042
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1724
2043
|
if (this.writeBehindTimer) {
|
|
1725
2044
|
clearInterval(this.writeBehindTimer);
|
|
@@ -1787,6 +2106,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1787
2106
|
try {
|
|
1788
2107
|
fetched = await this.fetchRateLimiter.schedule(
|
|
1789
2108
|
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
2109
|
+
{ key, fetcher },
|
|
1790
2110
|
fetcher
|
|
1791
2111
|
);
|
|
1792
2112
|
this.circuitBreakerManager.recordSuccess(key);
|
|
@@ -1802,8 +2122,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1802
2122
|
await this.storeEntry(key, "empty", null, options);
|
|
1803
2123
|
return null;
|
|
1804
2124
|
}
|
|
1805
|
-
if (options?.shouldCache
|
|
1806
|
-
|
|
2125
|
+
if (options?.shouldCache) {
|
|
2126
|
+
try {
|
|
2127
|
+
if (!options.shouldCache(fetched)) {
|
|
2128
|
+
return fetched;
|
|
2129
|
+
}
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2132
|
+
}
|
|
1807
2133
|
}
|
|
1808
2134
|
await this.storeEntry(key, "value", fetched, options);
|
|
1809
2135
|
return fetched;
|
|
@@ -2030,7 +2356,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2030
2356
|
const refresh = (async () => {
|
|
2031
2357
|
this.metricsCollector.increment("refreshes");
|
|
2032
2358
|
try {
|
|
2033
|
-
await this.
|
|
2359
|
+
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2034
2360
|
} catch (error) {
|
|
2035
2361
|
this.metricsCollector.increment("refreshErrors");
|
|
2036
2362
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2040,11 +2366,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2040
2366
|
})();
|
|
2041
2367
|
this.backgroundRefreshes.set(key, refresh);
|
|
2042
2368
|
}
|
|
2369
|
+
async runBackgroundRefresh(key, fetcher, options) {
|
|
2370
|
+
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2371
|
+
await this.fetchWithGuards(
|
|
2372
|
+
key,
|
|
2373
|
+
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2374
|
+
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2375
|
+
}),
|
|
2376
|
+
options
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2043
2379
|
resolveSingleFlightOptions() {
|
|
2044
2380
|
return {
|
|
2045
2381
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
2046
2382
|
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
2047
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
|
|
2383
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
2384
|
+
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
2048
2385
|
};
|
|
2049
2386
|
}
|
|
2050
2387
|
async deleteKeys(keys) {
|
|
@@ -2106,8 +2443,120 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2106
2443
|
sleep(ms) {
|
|
2107
2444
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2108
2445
|
}
|
|
2446
|
+
async withTimeout(promise, timeoutMs, onTimeout) {
|
|
2447
|
+
if (timeoutMs <= 0) {
|
|
2448
|
+
return promise;
|
|
2449
|
+
}
|
|
2450
|
+
let timer;
|
|
2451
|
+
const observedPromise = promise.then(
|
|
2452
|
+
(value) => ({ kind: "value", value }),
|
|
2453
|
+
(error) => ({ kind: "error", error })
|
|
2454
|
+
);
|
|
2455
|
+
try {
|
|
2456
|
+
const result = await Promise.race([
|
|
2457
|
+
observedPromise,
|
|
2458
|
+
new Promise((_, reject) => {
|
|
2459
|
+
timer = setTimeout(() => reject(onTimeout()), timeoutMs);
|
|
2460
|
+
timer.unref?.();
|
|
2461
|
+
})
|
|
2462
|
+
]);
|
|
2463
|
+
if (result && typeof result === "object" && "kind" in result) {
|
|
2464
|
+
if (result.kind === "error") {
|
|
2465
|
+
throw result.error;
|
|
2466
|
+
}
|
|
2467
|
+
return result.value;
|
|
2468
|
+
}
|
|
2469
|
+
return result;
|
|
2470
|
+
} finally {
|
|
2471
|
+
if (timer) {
|
|
2472
|
+
clearTimeout(timer);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2109
2476
|
shouldBroadcastL1Invalidation() {
|
|
2110
|
-
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ??
|
|
2477
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2478
|
+
}
|
|
2479
|
+
async collectKeysWithPrefix(prefix) {
|
|
2480
|
+
const matches = new Set(
|
|
2481
|
+
this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
|
|
2482
|
+
);
|
|
2483
|
+
await Promise.all(
|
|
2484
|
+
this.layers.map(async (layer) => {
|
|
2485
|
+
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
try {
|
|
2489
|
+
const keys = await layer.keys();
|
|
2490
|
+
for (const key of keys) {
|
|
2491
|
+
if (key.startsWith(prefix)) {
|
|
2492
|
+
matches.add(key);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
2497
|
+
}
|
|
2498
|
+
})
|
|
2499
|
+
);
|
|
2500
|
+
return [...matches];
|
|
2501
|
+
}
|
|
2502
|
+
async collectKeysMatchingPattern(pattern) {
|
|
2503
|
+
const matches = new Set(await this.tagIndex.matchPattern(pattern));
|
|
2504
|
+
await Promise.all(
|
|
2505
|
+
this.layers.map(async (layer) => {
|
|
2506
|
+
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
try {
|
|
2510
|
+
const keys = await layer.keys();
|
|
2511
|
+
for (const key of keys) {
|
|
2512
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
2513
|
+
matches.add(key);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
2518
|
+
}
|
|
2519
|
+
})
|
|
2520
|
+
);
|
|
2521
|
+
return [...matches];
|
|
2522
|
+
}
|
|
2523
|
+
shouldCleanupGenerations() {
|
|
2524
|
+
return Boolean(this.options.generationCleanup);
|
|
2525
|
+
}
|
|
2526
|
+
generationCleanupBatchSize() {
|
|
2527
|
+
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
2528
|
+
return configured ?? 500;
|
|
2529
|
+
}
|
|
2530
|
+
scheduleGenerationCleanup(generation) {
|
|
2531
|
+
const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
|
|
2532
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
2533
|
+
generation,
|
|
2534
|
+
error: this.formatError(error)
|
|
2535
|
+
});
|
|
2536
|
+
});
|
|
2537
|
+
this.generationCleanupPromise = task.finally(() => {
|
|
2538
|
+
if (this.generationCleanupPromise === task) {
|
|
2539
|
+
this.generationCleanupPromise = void 0;
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
async cleanupGeneration(generation) {
|
|
2544
|
+
const prefix = `v${generation}:`;
|
|
2545
|
+
const keys = await this.collectKeysWithPrefix(prefix);
|
|
2546
|
+
if (keys.length === 0) {
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
const batchSize = this.generationCleanupBatchSize();
|
|
2550
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2551
|
+
const batch = keys.slice(index, index + batchSize);
|
|
2552
|
+
await this.deleteKeys(batch);
|
|
2553
|
+
await this.publishInvalidation({
|
|
2554
|
+
scope: "keys",
|
|
2555
|
+
keys: batch,
|
|
2556
|
+
sourceId: this.instanceId,
|
|
2557
|
+
operation: "invalidate"
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2111
2560
|
}
|
|
2112
2561
|
initializeWriteBehind(options) {
|
|
2113
2562
|
if (this.options.writeStrategy !== "write-behind") {
|
|
@@ -2145,7 +2594,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2145
2594
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2146
2595
|
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2147
2596
|
this.writeBehindFlushPromise = (async () => {
|
|
2148
|
-
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2597
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
2598
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2599
|
+
if (failures.length > 0) {
|
|
2600
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2601
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
2602
|
+
failed: failures.length,
|
|
2603
|
+
total: batch.length,
|
|
2604
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2605
|
+
});
|
|
2606
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2607
|
+
}
|
|
2149
2608
|
})();
|
|
2150
2609
|
await this.writeBehindFlushPromise;
|
|
2151
2610
|
this.writeBehindFlushPromise = void 0;
|
|
@@ -2249,8 +2708,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2249
2708
|
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
2250
2709
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
2251
2710
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2711
|
+
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2712
|
+
this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
2713
|
+
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
2252
2714
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
2253
2715
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2716
|
+
if (typeof this.options.generationCleanup === "object") {
|
|
2717
|
+
this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2718
|
+
}
|
|
2254
2719
|
if (this.options.generation !== void 0) {
|
|
2255
2720
|
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2256
2721
|
}
|
|
@@ -2268,6 +2733,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2268
2733
|
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
2269
2734
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
2270
2735
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
2736
|
+
this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
2271
2737
|
}
|
|
2272
2738
|
validateLayerNumberOption(name, value) {
|
|
2273
2739
|
if (value === void 0) {
|
|
@@ -2292,6 +2758,20 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2292
2758
|
throw new Error(`${name} must be a positive finite number.`);
|
|
2293
2759
|
}
|
|
2294
2760
|
}
|
|
2761
|
+
validateRateLimitOptions(name, options) {
|
|
2762
|
+
if (!options) {
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2766
|
+
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2767
|
+
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2768
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2769
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2770
|
+
}
|
|
2771
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2772
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2295
2775
|
validateNonNegativeNumber(name, value) {
|
|
2296
2776
|
if (!Number.isFinite(value) || value < 0) {
|
|
2297
2777
|
throw new Error(`${name} must be a non-negative finite number.`);
|
|
@@ -2307,6 +2787,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2307
2787
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2308
2788
|
throw new Error("Cache key contains unsupported control characters.");
|
|
2309
2789
|
}
|
|
2790
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2791
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2792
|
+
}
|
|
2310
2793
|
return key;
|
|
2311
2794
|
}
|
|
2312
2795
|
validateTtlPolicy(name, policy) {
|
|
@@ -2384,6 +2867,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2384
2867
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
2385
2868
|
return null;
|
|
2386
2869
|
}
|
|
2870
|
+
async reportRecoverableLayerFailure(layer, operation, error) {
|
|
2871
|
+
if (this.isGracefulDegradationEnabled()) {
|
|
2872
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
this.logger.warn?.("layer-operation-failed", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2876
|
+
this.emitError(operation, { layer: layer.name, degraded: false, error: this.formatError(error) });
|
|
2877
|
+
}
|
|
2387
2878
|
isGracefulDegradationEnabled() {
|
|
2388
2879
|
return Boolean(this.options.gracefulDegradation);
|
|
2389
2880
|
}
|
|
@@ -2407,10 +2898,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2407
2898
|
}
|
|
2408
2899
|
}
|
|
2409
2900
|
serializeKeyPart(value) {
|
|
2410
|
-
if (typeof value === "string"
|
|
2411
|
-
return
|
|
2901
|
+
if (typeof value === "string") {
|
|
2902
|
+
return `s:${value}`;
|
|
2903
|
+
}
|
|
2904
|
+
if (typeof value === "number") {
|
|
2905
|
+
return `n:${value}`;
|
|
2906
|
+
}
|
|
2907
|
+
if (typeof value === "boolean") {
|
|
2908
|
+
return `b:${value}`;
|
|
2412
2909
|
}
|
|
2413
|
-
return JSON.stringify(this.normalizeForSerialization(value))
|
|
2910
|
+
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2414
2911
|
}
|
|
2415
2912
|
isCacheSnapshotEntries(value) {
|
|
2416
2913
|
return Array.isArray(value) && value.every((entry) => {
|
|
@@ -2418,15 +2915,39 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2418
2915
|
return false;
|
|
2419
2916
|
}
|
|
2420
2917
|
const candidate = entry;
|
|
2421
|
-
return typeof candidate.key === "string";
|
|
2918
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
2422
2919
|
});
|
|
2423
2920
|
}
|
|
2921
|
+
sanitizeSnapshotValue(value) {
|
|
2922
|
+
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2923
|
+
}
|
|
2924
|
+
async validateSnapshotFilePath(filePath) {
|
|
2925
|
+
if (filePath.length === 0) {
|
|
2926
|
+
throw new Error("filePath must not be empty.");
|
|
2927
|
+
}
|
|
2928
|
+
if (filePath.includes("\0")) {
|
|
2929
|
+
throw new Error("filePath must not contain null bytes.");
|
|
2930
|
+
}
|
|
2931
|
+
const path = await import("path");
|
|
2932
|
+
const resolved = path.resolve(filePath);
|
|
2933
|
+
const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
|
|
2934
|
+
if (baseDir !== false) {
|
|
2935
|
+
const relative = path.relative(baseDir, resolved);
|
|
2936
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2937
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return resolved;
|
|
2941
|
+
}
|
|
2424
2942
|
normalizeForSerialization(value) {
|
|
2425
2943
|
if (Array.isArray(value)) {
|
|
2426
2944
|
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
2427
2945
|
}
|
|
2428
2946
|
if (value && typeof value === "object") {
|
|
2429
2947
|
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
2948
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
2949
|
+
return normalized;
|
|
2950
|
+
}
|
|
2430
2951
|
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
2431
2952
|
return normalized;
|
|
2432
2953
|
}, {});
|