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
|
@@ -598,8 +598,9 @@ var CircuitBreakerManager = class {
|
|
|
598
598
|
|
|
599
599
|
// ../../src/internal/FetchRateLimiter.ts
|
|
600
600
|
var FetchRateLimiter = class {
|
|
601
|
-
queue = [];
|
|
602
601
|
buckets = /* @__PURE__ */ new Map();
|
|
602
|
+
queuesByBucket = /* @__PURE__ */ new Map();
|
|
603
|
+
pendingBuckets = /* @__PURE__ */ new Set();
|
|
603
604
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
604
605
|
nextFetcherBucketId = 0;
|
|
605
606
|
drainTimer;
|
|
@@ -612,13 +613,17 @@ var FetchRateLimiter = class {
|
|
|
612
613
|
return task();
|
|
613
614
|
}
|
|
614
615
|
return new Promise((resolve, reject) => {
|
|
615
|
-
this.
|
|
616
|
-
|
|
616
|
+
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
617
|
+
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
618
|
+
queue.push({
|
|
619
|
+
bucketKey,
|
|
617
620
|
options: normalized,
|
|
618
621
|
task,
|
|
619
622
|
resolve,
|
|
620
623
|
reject
|
|
621
624
|
});
|
|
625
|
+
this.queuesByBucket.set(bucketKey, queue);
|
|
626
|
+
this.pendingBuckets.add(bucketKey);
|
|
622
627
|
this.drain();
|
|
623
628
|
});
|
|
624
629
|
}
|
|
@@ -661,22 +666,30 @@ var FetchRateLimiter = class {
|
|
|
661
666
|
clearTimeout(this.drainTimer);
|
|
662
667
|
this.drainTimer = void 0;
|
|
663
668
|
}
|
|
664
|
-
while (this.
|
|
665
|
-
let
|
|
669
|
+
while (this.pendingBuckets.size > 0) {
|
|
670
|
+
let nextBucketKey;
|
|
666
671
|
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
667
|
-
for (
|
|
668
|
-
const
|
|
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];
|
|
669
680
|
if (!next2) {
|
|
681
|
+
this.pendingBuckets.delete(bucketKey);
|
|
682
|
+
this.queuesByBucket.delete(bucketKey);
|
|
670
683
|
continue;
|
|
671
684
|
}
|
|
672
|
-
const waitMs = this.waitTime(
|
|
685
|
+
const waitMs = this.waitTime(bucketKey, next2.options);
|
|
673
686
|
if (waitMs <= 0) {
|
|
674
|
-
|
|
687
|
+
nextBucketKey = bucketKey;
|
|
675
688
|
break;
|
|
676
689
|
}
|
|
677
690
|
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
678
691
|
}
|
|
679
|
-
if (
|
|
692
|
+
if (!nextBucketKey) {
|
|
680
693
|
if (Number.isFinite(nextWaitMs)) {
|
|
681
694
|
this.drainTimer = setTimeout(() => {
|
|
682
695
|
this.drainTimer = void 0;
|
|
@@ -686,15 +699,32 @@ var FetchRateLimiter = class {
|
|
|
686
699
|
}
|
|
687
700
|
return;
|
|
688
701
|
}
|
|
689
|
-
const
|
|
702
|
+
const queue = this.queuesByBucket.get(nextBucketKey);
|
|
703
|
+
const next = queue?.shift();
|
|
690
704
|
if (!next) {
|
|
691
|
-
|
|
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);
|
|
692
712
|
}
|
|
693
713
|
const bucket = this.bucketState(next.bucketKey);
|
|
714
|
+
if (bucket.cleanupTimer) {
|
|
715
|
+
clearTimeout(bucket.cleanupTimer);
|
|
716
|
+
bucket.cleanupTimer = void 0;
|
|
717
|
+
}
|
|
694
718
|
bucket.active += 1;
|
|
695
|
-
|
|
719
|
+
if (next.options.intervalMs && next.options.maxPerInterval) {
|
|
720
|
+
bucket.startedAt.push(Date.now());
|
|
721
|
+
}
|
|
696
722
|
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
697
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);
|
|
698
728
|
this.drain();
|
|
699
729
|
});
|
|
700
730
|
}
|
|
@@ -736,6 +766,31 @@ var FetchRateLimiter = class {
|
|
|
736
766
|
this.buckets.set(bucketKey, bucket);
|
|
737
767
|
return bucket;
|
|
738
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?.();
|
|
793
|
+
}
|
|
739
794
|
};
|
|
740
795
|
|
|
741
796
|
// ../../src/internal/MetricsCollector.ts
|
|
@@ -814,7 +869,30 @@ var MetricsCollector = class {
|
|
|
814
869
|
|
|
815
870
|
// ../../src/internal/StoredValue.ts
|
|
816
871
|
function isStoredValueEnvelope(value) {
|
|
817
|
-
|
|
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;
|
|
818
896
|
}
|
|
819
897
|
function createStoredValueEnvelope(options) {
|
|
820
898
|
const now = options.now ?? Date.now();
|
|
@@ -1079,15 +1157,17 @@ var TagIndex = class {
|
|
|
1079
1157
|
keyToTags = /* @__PURE__ */ new Map();
|
|
1080
1158
|
knownKeys = /* @__PURE__ */ new Set();
|
|
1081
1159
|
maxKnownKeys;
|
|
1160
|
+
nextNodeId = 1;
|
|
1161
|
+
root = this.createTrieNode();
|
|
1082
1162
|
constructor(options = {}) {
|
|
1083
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
1163
|
+
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
1084
1164
|
}
|
|
1085
1165
|
async touch(key) {
|
|
1086
|
-
this.
|
|
1166
|
+
this.insertKnownKey(key);
|
|
1087
1167
|
this.pruneKnownKeysIfNeeded();
|
|
1088
1168
|
}
|
|
1089
1169
|
async track(key, tags) {
|
|
1090
|
-
this.
|
|
1170
|
+
this.insertKnownKey(key);
|
|
1091
1171
|
this.pruneKnownKeysIfNeeded();
|
|
1092
1172
|
if (tags.length === 0) {
|
|
1093
1173
|
return;
|
|
@@ -1113,18 +1193,104 @@ var TagIndex = class {
|
|
|
1113
1193
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1114
1194
|
}
|
|
1115
1195
|
async keysForPrefix(prefix) {
|
|
1116
|
-
|
|
1196
|
+
const node = this.findNode(prefix);
|
|
1197
|
+
if (!node) {
|
|
1198
|
+
return [];
|
|
1199
|
+
}
|
|
1200
|
+
const matches = [];
|
|
1201
|
+
this.collectFromNode(node, prefix, matches);
|
|
1202
|
+
return matches;
|
|
1117
1203
|
}
|
|
1118
1204
|
async tagsForKey(key) {
|
|
1119
1205
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1120
1206
|
}
|
|
1121
1207
|
async matchPattern(pattern) {
|
|
1122
|
-
|
|
1208
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1209
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1210
|
+
return [...matches];
|
|
1123
1211
|
}
|
|
1124
1212
|
async clear() {
|
|
1125
1213
|
this.tagToKeys.clear();
|
|
1126
1214
|
this.keyToTags.clear();
|
|
1127
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
|
+
}
|
|
1128
1294
|
}
|
|
1129
1295
|
pruneKnownKeysIfNeeded() {
|
|
1130
1296
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
@@ -1141,7 +1307,7 @@ var TagIndex = class {
|
|
|
1141
1307
|
}
|
|
1142
1308
|
}
|
|
1143
1309
|
removeKey(key) {
|
|
1144
|
-
this.
|
|
1310
|
+
this.removeKnownKey(key);
|
|
1145
1311
|
const tags = this.keyToTags.get(key);
|
|
1146
1312
|
if (!tags) {
|
|
1147
1313
|
return;
|
|
@@ -1158,8 +1324,71 @@ var TagIndex = class {
|
|
|
1158
1324
|
}
|
|
1159
1325
|
this.keyToTags.delete(key);
|
|
1160
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
|
+
}
|
|
1161
1355
|
};
|
|
1162
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
|
+
}
|
|
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
|
+
}
|
|
1391
|
+
|
|
1163
1392
|
// ../../src/stampede/StampedeGuard.ts
|
|
1164
1393
|
var StampedeGuard = class {
|
|
1165
1394
|
mutexes = /* @__PURE__ */ new Map();
|
|
@@ -1169,7 +1398,8 @@ var StampedeGuard = class {
|
|
|
1169
1398
|
return await entry.mutex.runExclusive(task);
|
|
1170
1399
|
} finally {
|
|
1171
1400
|
entry.references -= 1;
|
|
1172
|
-
|
|
1401
|
+
const current = this.mutexes.get(key);
|
|
1402
|
+
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
1173
1403
|
this.mutexes.delete(key);
|
|
1174
1404
|
}
|
|
1175
1405
|
}
|
|
@@ -1199,8 +1429,10 @@ var CacheMissError = class extends Error {
|
|
|
1199
1429
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1200
1430
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1201
1431
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1432
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1202
1433
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1203
1434
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1435
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1204
1436
|
var DebugLogger = class {
|
|
1205
1437
|
enabled;
|
|
1206
1438
|
constructor(enabled) {
|
|
@@ -1247,6 +1479,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1247
1479
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1248
1480
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1249
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
|
+
}
|
|
1250
1497
|
this.initializeWriteBehind(options.writeBehind);
|
|
1251
1498
|
this.startup = this.initialize();
|
|
1252
1499
|
}
|
|
@@ -1260,6 +1507,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1260
1507
|
logger;
|
|
1261
1508
|
tagIndex;
|
|
1262
1509
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1510
|
+
snapshotSerializer = new JsonSerializer();
|
|
1263
1511
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1264
1512
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1265
1513
|
ttlResolver;
|
|
@@ -1268,6 +1516,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1268
1516
|
writeBehindQueue = [];
|
|
1269
1517
|
writeBehindTimer;
|
|
1270
1518
|
writeBehindFlushPromise;
|
|
1519
|
+
generationCleanupPromise;
|
|
1271
1520
|
isDisconnecting = false;
|
|
1272
1521
|
disconnectPromise;
|
|
1273
1522
|
/**
|
|
@@ -1280,6 +1529,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1280
1529
|
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1281
1530
|
this.validateWriteOptions(options);
|
|
1282
1531
|
await this.awaitStartup("get");
|
|
1532
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1533
|
+
}
|
|
1534
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
1283
1535
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1284
1536
|
if (hit.found) {
|
|
1285
1537
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -1357,6 +1609,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1357
1609
|
return true;
|
|
1358
1610
|
}
|
|
1359
1611
|
} catch {
|
|
1612
|
+
await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
|
|
1360
1613
|
}
|
|
1361
1614
|
} else {
|
|
1362
1615
|
try {
|
|
@@ -1364,7 +1617,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1364
1617
|
if (value !== null) {
|
|
1365
1618
|
return true;
|
|
1366
1619
|
}
|
|
1367
|
-
} catch {
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
await this.reportRecoverableLayerFailure(layer, "has", error);
|
|
1368
1622
|
}
|
|
1369
1623
|
}
|
|
1370
1624
|
}
|
|
@@ -1456,13 +1710,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1456
1710
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1457
1711
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
1458
1712
|
if (!canFastPath) {
|
|
1713
|
+
await this.awaitStartup("mget");
|
|
1459
1714
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1460
1715
|
return Promise.all(
|
|
1461
1716
|
normalizedEntries.map((entry) => {
|
|
1462
1717
|
const optionsSignature = this.serializeOptions(entry.options);
|
|
1463
1718
|
const existing = pendingReads.get(entry.key);
|
|
1464
1719
|
if (!existing) {
|
|
1465
|
-
const promise = this.
|
|
1720
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
1466
1721
|
pendingReads.set(entry.key, {
|
|
1467
1722
|
promise,
|
|
1468
1723
|
fetch: entry.fetch,
|
|
@@ -1601,14 +1856,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1601
1856
|
}
|
|
1602
1857
|
async invalidateByPattern(pattern) {
|
|
1603
1858
|
await this.awaitStartup("invalidateByPattern");
|
|
1604
|
-
const keys = await this.
|
|
1859
|
+
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1605
1860
|
await this.deleteKeys(keys);
|
|
1606
1861
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1607
1862
|
}
|
|
1608
1863
|
async invalidateByPrefix(prefix) {
|
|
1609
1864
|
await this.awaitStartup("invalidateByPrefix");
|
|
1610
1865
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1611
|
-
const keys =
|
|
1866
|
+
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1612
1867
|
await this.deleteKeys(keys);
|
|
1613
1868
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1614
1869
|
}
|
|
@@ -1658,9 +1913,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1658
1913
|
})
|
|
1659
1914
|
);
|
|
1660
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
|
+
*/
|
|
1661
1921
|
bumpGeneration(nextGeneration) {
|
|
1662
1922
|
const current = this.currentGeneration ?? 0;
|
|
1923
|
+
const previousGeneration = this.currentGeneration;
|
|
1663
1924
|
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1925
|
+
if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
|
|
1926
|
+
this.scheduleGenerationCleanup(previousGeneration);
|
|
1927
|
+
}
|
|
1664
1928
|
return this.currentGeneration;
|
|
1665
1929
|
}
|
|
1666
1930
|
/**
|
|
@@ -1744,27 +2008,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1744
2008
|
this.assertActive("persistToFile");
|
|
1745
2009
|
const snapshot = await this.exportState();
|
|
1746
2010
|
const { promises: fs } = await import("fs");
|
|
1747
|
-
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
2011
|
+
await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
|
|
1748
2012
|
}
|
|
1749
2013
|
async restoreFromFile(filePath) {
|
|
1750
2014
|
this.assertActive("restoreFromFile");
|
|
1751
2015
|
const { promises: fs } = await import("fs");
|
|
1752
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
2016
|
+
const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
|
|
1753
2017
|
let parsed;
|
|
1754
2018
|
try {
|
|
1755
|
-
parsed = JSON.parse(raw
|
|
1756
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1757
|
-
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1758
|
-
}
|
|
1759
|
-
return value;
|
|
1760
|
-
});
|
|
2019
|
+
parsed = JSON.parse(raw);
|
|
1761
2020
|
} catch (cause) {
|
|
1762
2021
|
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1763
2022
|
}
|
|
1764
2023
|
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1765
2024
|
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1766
2025
|
}
|
|
1767
|
-
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
|
+
);
|
|
1768
2033
|
}
|
|
1769
2034
|
async disconnect() {
|
|
1770
2035
|
if (!this.disconnectPromise) {
|
|
@@ -1773,6 +2038,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1773
2038
|
await this.startup;
|
|
1774
2039
|
await this.unsubscribeInvalidation?.();
|
|
1775
2040
|
await this.flushWriteBehindQueue();
|
|
2041
|
+
await this.generationCleanupPromise;
|
|
1776
2042
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1777
2043
|
if (this.writeBehindTimer) {
|
|
1778
2044
|
clearInterval(this.writeBehindTimer);
|
|
@@ -1856,8 +2122,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1856
2122
|
await this.storeEntry(key, "empty", null, options);
|
|
1857
2123
|
return null;
|
|
1858
2124
|
}
|
|
1859
|
-
if (options?.shouldCache
|
|
1860
|
-
|
|
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
|
+
}
|
|
1861
2133
|
}
|
|
1862
2134
|
await this.storeEntry(key, "value", fetched, options);
|
|
1863
2135
|
return fetched;
|
|
@@ -2084,7 +2356,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2084
2356
|
const refresh = (async () => {
|
|
2085
2357
|
this.metricsCollector.increment("refreshes");
|
|
2086
2358
|
try {
|
|
2087
|
-
await this.
|
|
2359
|
+
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2088
2360
|
} catch (error) {
|
|
2089
2361
|
this.metricsCollector.increment("refreshErrors");
|
|
2090
2362
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2094,6 +2366,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2094
2366
|
})();
|
|
2095
2367
|
this.backgroundRefreshes.set(key, refresh);
|
|
2096
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
|
+
}
|
|
2097
2379
|
resolveSingleFlightOptions() {
|
|
2098
2380
|
return {
|
|
2099
2381
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
@@ -2161,8 +2443,120 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2161
2443
|
sleep(ms) {
|
|
2162
2444
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2163
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
|
+
}
|
|
2164
2476
|
shouldBroadcastL1Invalidation() {
|
|
2165
|
-
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
|
+
}
|
|
2166
2560
|
}
|
|
2167
2561
|
initializeWriteBehind(options) {
|
|
2168
2562
|
if (this.options.writeStrategy !== "write-behind") {
|
|
@@ -2200,7 +2594,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2200
2594
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2201
2595
|
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2202
2596
|
this.writeBehindFlushPromise = (async () => {
|
|
2203
|
-
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
|
+
}
|
|
2204
2608
|
})();
|
|
2205
2609
|
await this.writeBehindFlushPromise;
|
|
2206
2610
|
this.writeBehindFlushPromise = void 0;
|
|
@@ -2305,9 +2709,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2305
2709
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
2306
2710
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2307
2711
|
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2712
|
+
this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
2308
2713
|
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
2309
2714
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
2310
2715
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2716
|
+
if (typeof this.options.generationCleanup === "object") {
|
|
2717
|
+
this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2718
|
+
}
|
|
2311
2719
|
if (this.options.generation !== void 0) {
|
|
2312
2720
|
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2313
2721
|
}
|
|
@@ -2379,6 +2787,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2379
2787
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2380
2788
|
throw new Error("Cache key contains unsupported control characters.");
|
|
2381
2789
|
}
|
|
2790
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2791
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2792
|
+
}
|
|
2382
2793
|
return key;
|
|
2383
2794
|
}
|
|
2384
2795
|
validateTtlPolicy(name, policy) {
|
|
@@ -2456,6 +2867,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2456
2867
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
2457
2868
|
return null;
|
|
2458
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
|
+
}
|
|
2459
2878
|
isGracefulDegradationEnabled() {
|
|
2460
2879
|
return Boolean(this.options.gracefulDegradation);
|
|
2461
2880
|
}
|
|
@@ -2479,10 +2898,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2479
2898
|
}
|
|
2480
2899
|
}
|
|
2481
2900
|
serializeKeyPart(value) {
|
|
2482
|
-
if (typeof value === "string"
|
|
2483
|
-
return
|
|
2901
|
+
if (typeof value === "string") {
|
|
2902
|
+
return `s:${value}`;
|
|
2903
|
+
}
|
|
2904
|
+
if (typeof value === "number") {
|
|
2905
|
+
return `n:${value}`;
|
|
2484
2906
|
}
|
|
2485
|
-
|
|
2907
|
+
if (typeof value === "boolean") {
|
|
2908
|
+
return `b:${value}`;
|
|
2909
|
+
}
|
|
2910
|
+
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2486
2911
|
}
|
|
2487
2912
|
isCacheSnapshotEntries(value) {
|
|
2488
2913
|
return Array.isArray(value) && value.every((entry) => {
|
|
@@ -2490,15 +2915,39 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2490
2915
|
return false;
|
|
2491
2916
|
}
|
|
2492
2917
|
const candidate = entry;
|
|
2493
|
-
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);
|
|
2494
2919
|
});
|
|
2495
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
|
+
}
|
|
2496
2942
|
normalizeForSerialization(value) {
|
|
2497
2943
|
if (Array.isArray(value)) {
|
|
2498
2944
|
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
2499
2945
|
}
|
|
2500
2946
|
if (value && typeof value === "object") {
|
|
2501
2947
|
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
2948
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
2949
|
+
return normalized;
|
|
2950
|
+
}
|
|
2502
2951
|
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
2503
2952
|
return normalized;
|
|
2504
2953
|
}, {});
|