layercache 1.2.2 → 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 +27 -8
- 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-B_rUqDy6.d.cts} +34 -1
- package/dist/{edge-DLpdQN0W.d.cts → edge-B_rUqDy6.d.ts} +34 -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 +591 -98
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +429 -84
- package/package.json +4 -4
- package/packages/nestjs/dist/index.cjs +492 -43
- package/packages/nestjs/dist/index.d.cts +25 -0
- package/packages/nestjs/dist/index.d.ts +25 -0
- package/packages/nestjs/dist/index.js +492 -43
|
@@ -562,8 +562,9 @@ var CircuitBreakerManager = class {
|
|
|
562
562
|
|
|
563
563
|
// ../../src/internal/FetchRateLimiter.ts
|
|
564
564
|
var FetchRateLimiter = class {
|
|
565
|
-
queue = [];
|
|
566
565
|
buckets = /* @__PURE__ */ new Map();
|
|
566
|
+
queuesByBucket = /* @__PURE__ */ new Map();
|
|
567
|
+
pendingBuckets = /* @__PURE__ */ new Set();
|
|
567
568
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
568
569
|
nextFetcherBucketId = 0;
|
|
569
570
|
drainTimer;
|
|
@@ -576,13 +577,17 @@ var FetchRateLimiter = class {
|
|
|
576
577
|
return task();
|
|
577
578
|
}
|
|
578
579
|
return new Promise((resolve, reject) => {
|
|
579
|
-
this.
|
|
580
|
-
|
|
580
|
+
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
581
|
+
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
582
|
+
queue.push({
|
|
583
|
+
bucketKey,
|
|
581
584
|
options: normalized,
|
|
582
585
|
task,
|
|
583
586
|
resolve,
|
|
584
587
|
reject
|
|
585
588
|
});
|
|
589
|
+
this.queuesByBucket.set(bucketKey, queue);
|
|
590
|
+
this.pendingBuckets.add(bucketKey);
|
|
586
591
|
this.drain();
|
|
587
592
|
});
|
|
588
593
|
}
|
|
@@ -625,22 +630,30 @@ var FetchRateLimiter = class {
|
|
|
625
630
|
clearTimeout(this.drainTimer);
|
|
626
631
|
this.drainTimer = void 0;
|
|
627
632
|
}
|
|
628
|
-
while (this.
|
|
629
|
-
let
|
|
633
|
+
while (this.pendingBuckets.size > 0) {
|
|
634
|
+
let nextBucketKey;
|
|
630
635
|
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
631
|
-
for (
|
|
632
|
-
const
|
|
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];
|
|
633
644
|
if (!next2) {
|
|
645
|
+
this.pendingBuckets.delete(bucketKey);
|
|
646
|
+
this.queuesByBucket.delete(bucketKey);
|
|
634
647
|
continue;
|
|
635
648
|
}
|
|
636
|
-
const waitMs = this.waitTime(
|
|
649
|
+
const waitMs = this.waitTime(bucketKey, next2.options);
|
|
637
650
|
if (waitMs <= 0) {
|
|
638
|
-
|
|
651
|
+
nextBucketKey = bucketKey;
|
|
639
652
|
break;
|
|
640
653
|
}
|
|
641
654
|
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
642
655
|
}
|
|
643
|
-
if (
|
|
656
|
+
if (!nextBucketKey) {
|
|
644
657
|
if (Number.isFinite(nextWaitMs)) {
|
|
645
658
|
this.drainTimer = setTimeout(() => {
|
|
646
659
|
this.drainTimer = void 0;
|
|
@@ -650,15 +663,32 @@ var FetchRateLimiter = class {
|
|
|
650
663
|
}
|
|
651
664
|
return;
|
|
652
665
|
}
|
|
653
|
-
const
|
|
666
|
+
const queue = this.queuesByBucket.get(nextBucketKey);
|
|
667
|
+
const next = queue?.shift();
|
|
654
668
|
if (!next) {
|
|
655
|
-
|
|
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);
|
|
656
676
|
}
|
|
657
677
|
const bucket = this.bucketState(next.bucketKey);
|
|
678
|
+
if (bucket.cleanupTimer) {
|
|
679
|
+
clearTimeout(bucket.cleanupTimer);
|
|
680
|
+
bucket.cleanupTimer = void 0;
|
|
681
|
+
}
|
|
658
682
|
bucket.active += 1;
|
|
659
|
-
|
|
683
|
+
if (next.options.intervalMs && next.options.maxPerInterval) {
|
|
684
|
+
bucket.startedAt.push(Date.now());
|
|
685
|
+
}
|
|
660
686
|
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
661
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);
|
|
662
692
|
this.drain();
|
|
663
693
|
});
|
|
664
694
|
}
|
|
@@ -700,6 +730,31 @@ var FetchRateLimiter = class {
|
|
|
700
730
|
this.buckets.set(bucketKey, bucket);
|
|
701
731
|
return bucket;
|
|
702
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?.();
|
|
757
|
+
}
|
|
703
758
|
};
|
|
704
759
|
|
|
705
760
|
// ../../src/internal/MetricsCollector.ts
|
|
@@ -778,7 +833,30 @@ var MetricsCollector = class {
|
|
|
778
833
|
|
|
779
834
|
// ../../src/internal/StoredValue.ts
|
|
780
835
|
function isStoredValueEnvelope(value) {
|
|
781
|
-
|
|
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;
|
|
782
860
|
}
|
|
783
861
|
function createStoredValueEnvelope(options) {
|
|
784
862
|
const now = options.now ?? Date.now();
|
|
@@ -1043,15 +1121,17 @@ var TagIndex = class {
|
|
|
1043
1121
|
keyToTags = /* @__PURE__ */ new Map();
|
|
1044
1122
|
knownKeys = /* @__PURE__ */ new Set();
|
|
1045
1123
|
maxKnownKeys;
|
|
1124
|
+
nextNodeId = 1;
|
|
1125
|
+
root = this.createTrieNode();
|
|
1046
1126
|
constructor(options = {}) {
|
|
1047
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
1127
|
+
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
1048
1128
|
}
|
|
1049
1129
|
async touch(key) {
|
|
1050
|
-
this.
|
|
1130
|
+
this.insertKnownKey(key);
|
|
1051
1131
|
this.pruneKnownKeysIfNeeded();
|
|
1052
1132
|
}
|
|
1053
1133
|
async track(key, tags) {
|
|
1054
|
-
this.
|
|
1134
|
+
this.insertKnownKey(key);
|
|
1055
1135
|
this.pruneKnownKeysIfNeeded();
|
|
1056
1136
|
if (tags.length === 0) {
|
|
1057
1137
|
return;
|
|
@@ -1077,18 +1157,104 @@ var TagIndex = class {
|
|
|
1077
1157
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1078
1158
|
}
|
|
1079
1159
|
async keysForPrefix(prefix) {
|
|
1080
|
-
|
|
1160
|
+
const node = this.findNode(prefix);
|
|
1161
|
+
if (!node) {
|
|
1162
|
+
return [];
|
|
1163
|
+
}
|
|
1164
|
+
const matches = [];
|
|
1165
|
+
this.collectFromNode(node, prefix, matches);
|
|
1166
|
+
return matches;
|
|
1081
1167
|
}
|
|
1082
1168
|
async tagsForKey(key) {
|
|
1083
1169
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1084
1170
|
}
|
|
1085
1171
|
async matchPattern(pattern) {
|
|
1086
|
-
|
|
1172
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1173
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1174
|
+
return [...matches];
|
|
1087
1175
|
}
|
|
1088
1176
|
async clear() {
|
|
1089
1177
|
this.tagToKeys.clear();
|
|
1090
1178
|
this.keyToTags.clear();
|
|
1091
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
|
+
}
|
|
1092
1258
|
}
|
|
1093
1259
|
pruneKnownKeysIfNeeded() {
|
|
1094
1260
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
@@ -1105,7 +1271,7 @@ var TagIndex = class {
|
|
|
1105
1271
|
}
|
|
1106
1272
|
}
|
|
1107
1273
|
removeKey(key) {
|
|
1108
|
-
this.
|
|
1274
|
+
this.removeKnownKey(key);
|
|
1109
1275
|
const tags = this.keyToTags.get(key);
|
|
1110
1276
|
if (!tags) {
|
|
1111
1277
|
return;
|
|
@@ -1122,8 +1288,71 @@ var TagIndex = class {
|
|
|
1122
1288
|
}
|
|
1123
1289
|
this.keyToTags.delete(key);
|
|
1124
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
|
+
}
|
|
1125
1319
|
};
|
|
1126
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
|
+
}
|
|
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
|
+
}
|
|
1355
|
+
|
|
1127
1356
|
// ../../src/stampede/StampedeGuard.ts
|
|
1128
1357
|
var StampedeGuard = class {
|
|
1129
1358
|
mutexes = /* @__PURE__ */ new Map();
|
|
@@ -1133,7 +1362,8 @@ var StampedeGuard = class {
|
|
|
1133
1362
|
return await entry.mutex.runExclusive(task);
|
|
1134
1363
|
} finally {
|
|
1135
1364
|
entry.references -= 1;
|
|
1136
|
-
|
|
1365
|
+
const current = this.mutexes.get(key);
|
|
1366
|
+
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
1137
1367
|
this.mutexes.delete(key);
|
|
1138
1368
|
}
|
|
1139
1369
|
}
|
|
@@ -1163,8 +1393,10 @@ var CacheMissError = class extends Error {
|
|
|
1163
1393
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1164
1394
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1165
1395
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1396
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1166
1397
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1167
1398
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1399
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1168
1400
|
var DebugLogger = class {
|
|
1169
1401
|
enabled;
|
|
1170
1402
|
constructor(enabled) {
|
|
@@ -1211,6 +1443,21 @@ var CacheStack = class extends EventEmitter {
|
|
|
1211
1443
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1212
1444
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1213
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
|
+
}
|
|
1214
1461
|
this.initializeWriteBehind(options.writeBehind);
|
|
1215
1462
|
this.startup = this.initialize();
|
|
1216
1463
|
}
|
|
@@ -1224,6 +1471,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1224
1471
|
logger;
|
|
1225
1472
|
tagIndex;
|
|
1226
1473
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1474
|
+
snapshotSerializer = new JsonSerializer();
|
|
1227
1475
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1228
1476
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1229
1477
|
ttlResolver;
|
|
@@ -1232,6 +1480,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1232
1480
|
writeBehindQueue = [];
|
|
1233
1481
|
writeBehindTimer;
|
|
1234
1482
|
writeBehindFlushPromise;
|
|
1483
|
+
generationCleanupPromise;
|
|
1235
1484
|
isDisconnecting = false;
|
|
1236
1485
|
disconnectPromise;
|
|
1237
1486
|
/**
|
|
@@ -1244,6 +1493,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1244
1493
|
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1245
1494
|
this.validateWriteOptions(options);
|
|
1246
1495
|
await this.awaitStartup("get");
|
|
1496
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1497
|
+
}
|
|
1498
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
1247
1499
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1248
1500
|
if (hit.found) {
|
|
1249
1501
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -1321,6 +1573,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1321
1573
|
return true;
|
|
1322
1574
|
}
|
|
1323
1575
|
} catch {
|
|
1576
|
+
await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
|
|
1324
1577
|
}
|
|
1325
1578
|
} else {
|
|
1326
1579
|
try {
|
|
@@ -1328,7 +1581,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1328
1581
|
if (value !== null) {
|
|
1329
1582
|
return true;
|
|
1330
1583
|
}
|
|
1331
|
-
} catch {
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
await this.reportRecoverableLayerFailure(layer, "has", error);
|
|
1332
1586
|
}
|
|
1333
1587
|
}
|
|
1334
1588
|
}
|
|
@@ -1420,13 +1674,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1420
1674
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1421
1675
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
1422
1676
|
if (!canFastPath) {
|
|
1677
|
+
await this.awaitStartup("mget");
|
|
1423
1678
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1424
1679
|
return Promise.all(
|
|
1425
1680
|
normalizedEntries.map((entry) => {
|
|
1426
1681
|
const optionsSignature = this.serializeOptions(entry.options);
|
|
1427
1682
|
const existing = pendingReads.get(entry.key);
|
|
1428
1683
|
if (!existing) {
|
|
1429
|
-
const promise = this.
|
|
1684
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
1430
1685
|
pendingReads.set(entry.key, {
|
|
1431
1686
|
promise,
|
|
1432
1687
|
fetch: entry.fetch,
|
|
@@ -1565,14 +1820,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1565
1820
|
}
|
|
1566
1821
|
async invalidateByPattern(pattern) {
|
|
1567
1822
|
await this.awaitStartup("invalidateByPattern");
|
|
1568
|
-
const keys = await this.
|
|
1823
|
+
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1569
1824
|
await this.deleteKeys(keys);
|
|
1570
1825
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1571
1826
|
}
|
|
1572
1827
|
async invalidateByPrefix(prefix) {
|
|
1573
1828
|
await this.awaitStartup("invalidateByPrefix");
|
|
1574
1829
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1575
|
-
const keys =
|
|
1830
|
+
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1576
1831
|
await this.deleteKeys(keys);
|
|
1577
1832
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1578
1833
|
}
|
|
@@ -1622,9 +1877,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
1622
1877
|
})
|
|
1623
1878
|
);
|
|
1624
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
|
+
*/
|
|
1625
1885
|
bumpGeneration(nextGeneration) {
|
|
1626
1886
|
const current = this.currentGeneration ?? 0;
|
|
1887
|
+
const previousGeneration = this.currentGeneration;
|
|
1627
1888
|
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1889
|
+
if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
|
|
1890
|
+
this.scheduleGenerationCleanup(previousGeneration);
|
|
1891
|
+
}
|
|
1628
1892
|
return this.currentGeneration;
|
|
1629
1893
|
}
|
|
1630
1894
|
/**
|
|
@@ -1708,27 +1972,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1708
1972
|
this.assertActive("persistToFile");
|
|
1709
1973
|
const snapshot = await this.exportState();
|
|
1710
1974
|
const { promises: fs } = await import("fs");
|
|
1711
|
-
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1975
|
+
await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
|
|
1712
1976
|
}
|
|
1713
1977
|
async restoreFromFile(filePath) {
|
|
1714
1978
|
this.assertActive("restoreFromFile");
|
|
1715
1979
|
const { promises: fs } = await import("fs");
|
|
1716
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
1980
|
+
const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
|
|
1717
1981
|
let parsed;
|
|
1718
1982
|
try {
|
|
1719
|
-
parsed = JSON.parse(raw
|
|
1720
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1721
|
-
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1722
|
-
}
|
|
1723
|
-
return value;
|
|
1724
|
-
});
|
|
1983
|
+
parsed = JSON.parse(raw);
|
|
1725
1984
|
} catch (cause) {
|
|
1726
1985
|
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1727
1986
|
}
|
|
1728
1987
|
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1729
1988
|
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1730
1989
|
}
|
|
1731
|
-
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
|
+
);
|
|
1732
1997
|
}
|
|
1733
1998
|
async disconnect() {
|
|
1734
1999
|
if (!this.disconnectPromise) {
|
|
@@ -1737,6 +2002,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1737
2002
|
await this.startup;
|
|
1738
2003
|
await this.unsubscribeInvalidation?.();
|
|
1739
2004
|
await this.flushWriteBehindQueue();
|
|
2005
|
+
await this.generationCleanupPromise;
|
|
1740
2006
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1741
2007
|
if (this.writeBehindTimer) {
|
|
1742
2008
|
clearInterval(this.writeBehindTimer);
|
|
@@ -1820,8 +2086,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1820
2086
|
await this.storeEntry(key, "empty", null, options);
|
|
1821
2087
|
return null;
|
|
1822
2088
|
}
|
|
1823
|
-
if (options?.shouldCache
|
|
1824
|
-
|
|
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
|
+
}
|
|
1825
2097
|
}
|
|
1826
2098
|
await this.storeEntry(key, "value", fetched, options);
|
|
1827
2099
|
return fetched;
|
|
@@ -2048,7 +2320,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2048
2320
|
const refresh = (async () => {
|
|
2049
2321
|
this.metricsCollector.increment("refreshes");
|
|
2050
2322
|
try {
|
|
2051
|
-
await this.
|
|
2323
|
+
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2052
2324
|
} catch (error) {
|
|
2053
2325
|
this.metricsCollector.increment("refreshErrors");
|
|
2054
2326
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2058,6 +2330,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2058
2330
|
})();
|
|
2059
2331
|
this.backgroundRefreshes.set(key, refresh);
|
|
2060
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
|
+
}
|
|
2061
2343
|
resolveSingleFlightOptions() {
|
|
2062
2344
|
return {
|
|
2063
2345
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
@@ -2125,8 +2407,120 @@ var CacheStack = class extends EventEmitter {
|
|
|
2125
2407
|
sleep(ms) {
|
|
2126
2408
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2127
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
|
+
}
|
|
2128
2440
|
shouldBroadcastL1Invalidation() {
|
|
2129
|
-
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
|
+
}
|
|
2130
2524
|
}
|
|
2131
2525
|
initializeWriteBehind(options) {
|
|
2132
2526
|
if (this.options.writeStrategy !== "write-behind") {
|
|
@@ -2164,7 +2558,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
2164
2558
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2165
2559
|
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2166
2560
|
this.writeBehindFlushPromise = (async () => {
|
|
2167
|
-
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
|
+
}
|
|
2168
2572
|
})();
|
|
2169
2573
|
await this.writeBehindFlushPromise;
|
|
2170
2574
|
this.writeBehindFlushPromise = void 0;
|
|
@@ -2269,9 +2673,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
2269
2673
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
2270
2674
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2271
2675
|
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2676
|
+
this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
2272
2677
|
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
2273
2678
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
2274
2679
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2680
|
+
if (typeof this.options.generationCleanup === "object") {
|
|
2681
|
+
this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2682
|
+
}
|
|
2275
2683
|
if (this.options.generation !== void 0) {
|
|
2276
2684
|
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2277
2685
|
}
|
|
@@ -2343,6 +2751,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2343
2751
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2344
2752
|
throw new Error("Cache key contains unsupported control characters.");
|
|
2345
2753
|
}
|
|
2754
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2755
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2756
|
+
}
|
|
2346
2757
|
return key;
|
|
2347
2758
|
}
|
|
2348
2759
|
validateTtlPolicy(name, policy) {
|
|
@@ -2420,6 +2831,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2420
2831
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
2421
2832
|
return null;
|
|
2422
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
|
+
}
|
|
2423
2842
|
isGracefulDegradationEnabled() {
|
|
2424
2843
|
return Boolean(this.options.gracefulDegradation);
|
|
2425
2844
|
}
|
|
@@ -2443,10 +2862,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2443
2862
|
}
|
|
2444
2863
|
}
|
|
2445
2864
|
serializeKeyPart(value) {
|
|
2446
|
-
if (typeof value === "string"
|
|
2447
|
-
return
|
|
2865
|
+
if (typeof value === "string") {
|
|
2866
|
+
return `s:${value}`;
|
|
2867
|
+
}
|
|
2868
|
+
if (typeof value === "number") {
|
|
2869
|
+
return `n:${value}`;
|
|
2448
2870
|
}
|
|
2449
|
-
|
|
2871
|
+
if (typeof value === "boolean") {
|
|
2872
|
+
return `b:${value}`;
|
|
2873
|
+
}
|
|
2874
|
+
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2450
2875
|
}
|
|
2451
2876
|
isCacheSnapshotEntries(value) {
|
|
2452
2877
|
return Array.isArray(value) && value.every((entry) => {
|
|
@@ -2454,15 +2879,39 @@ var CacheStack = class extends EventEmitter {
|
|
|
2454
2879
|
return false;
|
|
2455
2880
|
}
|
|
2456
2881
|
const candidate = entry;
|
|
2457
|
-
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);
|
|
2458
2883
|
});
|
|
2459
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
|
+
}
|
|
2460
2906
|
normalizeForSerialization(value) {
|
|
2461
2907
|
if (Array.isArray(value)) {
|
|
2462
2908
|
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
2463
2909
|
}
|
|
2464
2910
|
if (value && typeof value === "object") {
|
|
2465
2911
|
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
2912
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
2913
|
+
return normalized;
|
|
2914
|
+
}
|
|
2466
2915
|
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
2467
2916
|
return normalized;
|
|
2468
2917
|
}, {});
|