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/dist/index.cjs CHANGED
@@ -400,8 +400,9 @@ var CircuitBreakerManager = class {
400
400
 
401
401
  // src/internal/FetchRateLimiter.ts
402
402
  var FetchRateLimiter = class {
403
- queue = [];
404
403
  buckets = /* @__PURE__ */ new Map();
404
+ queuesByBucket = /* @__PURE__ */ new Map();
405
+ pendingBuckets = /* @__PURE__ */ new Set();
405
406
  fetcherBuckets = /* @__PURE__ */ new WeakMap();
406
407
  nextFetcherBucketId = 0;
407
408
  drainTimer;
@@ -414,13 +415,17 @@ var FetchRateLimiter = class {
414
415
  return task();
415
416
  }
416
417
  return new Promise((resolve2, reject) => {
417
- this.queue.push({
418
- bucketKey: this.resolveBucketKey(normalized, context),
418
+ const bucketKey = this.resolveBucketKey(normalized, context);
419
+ const queue = this.queuesByBucket.get(bucketKey) ?? [];
420
+ queue.push({
421
+ bucketKey,
419
422
  options: normalized,
420
423
  task,
421
424
  resolve: resolve2,
422
425
  reject
423
426
  });
427
+ this.queuesByBucket.set(bucketKey, queue);
428
+ this.pendingBuckets.add(bucketKey);
424
429
  this.drain();
425
430
  });
426
431
  }
@@ -463,22 +468,30 @@ var FetchRateLimiter = class {
463
468
  clearTimeout(this.drainTimer);
464
469
  this.drainTimer = void 0;
465
470
  }
466
- while (this.queue.length > 0) {
467
- let nextIndex = -1;
471
+ while (this.pendingBuckets.size > 0) {
472
+ let nextBucketKey;
468
473
  let nextWaitMs = Number.POSITIVE_INFINITY;
469
- for (let index = 0; index < this.queue.length; index += 1) {
470
- const next2 = this.queue[index];
474
+ for (const bucketKey of this.pendingBuckets) {
475
+ const queue2 = this.queuesByBucket.get(bucketKey);
476
+ if (!queue2 || queue2.length === 0) {
477
+ this.pendingBuckets.delete(bucketKey);
478
+ this.queuesByBucket.delete(bucketKey);
479
+ continue;
480
+ }
481
+ const next2 = queue2[0];
471
482
  if (!next2) {
483
+ this.pendingBuckets.delete(bucketKey);
484
+ this.queuesByBucket.delete(bucketKey);
472
485
  continue;
473
486
  }
474
- const waitMs = this.waitTime(next2.bucketKey, next2.options);
487
+ const waitMs = this.waitTime(bucketKey, next2.options);
475
488
  if (waitMs <= 0) {
476
- nextIndex = index;
489
+ nextBucketKey = bucketKey;
477
490
  break;
478
491
  }
479
492
  nextWaitMs = Math.min(nextWaitMs, waitMs);
480
493
  }
481
- if (nextIndex < 0) {
494
+ if (!nextBucketKey) {
482
495
  if (Number.isFinite(nextWaitMs)) {
483
496
  this.drainTimer = setTimeout(() => {
484
497
  this.drainTimer = void 0;
@@ -488,15 +501,32 @@ var FetchRateLimiter = class {
488
501
  }
489
502
  return;
490
503
  }
491
- const next = this.queue.splice(nextIndex, 1)[0];
504
+ const queue = this.queuesByBucket.get(nextBucketKey);
505
+ const next = queue?.shift();
492
506
  if (!next) {
493
- return;
507
+ this.pendingBuckets.delete(nextBucketKey);
508
+ this.queuesByBucket.delete(nextBucketKey);
509
+ continue;
510
+ }
511
+ if (!queue || queue.length === 0) {
512
+ this.pendingBuckets.delete(nextBucketKey);
513
+ this.queuesByBucket.delete(nextBucketKey);
494
514
  }
495
515
  const bucket = this.bucketState(next.bucketKey);
516
+ if (bucket.cleanupTimer) {
517
+ clearTimeout(bucket.cleanupTimer);
518
+ bucket.cleanupTimer = void 0;
519
+ }
496
520
  bucket.active += 1;
497
- bucket.startedAt.push(Date.now());
521
+ if (next.options.intervalMs && next.options.maxPerInterval) {
522
+ bucket.startedAt.push(Date.now());
523
+ }
498
524
  void next.task().then(next.resolve, next.reject).finally(() => {
499
525
  bucket.active -= 1;
526
+ if ((this.queuesByBucket.get(next.bucketKey)?.length ?? 0) > 0) {
527
+ this.pendingBuckets.add(next.bucketKey);
528
+ }
529
+ this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
500
530
  this.drain();
501
531
  });
502
532
  }
@@ -538,6 +568,31 @@ var FetchRateLimiter = class {
538
568
  this.buckets.set(bucketKey, bucket);
539
569
  return bucket;
540
570
  }
571
+ cleanupBucket(bucketKey, bucket, intervalMs) {
572
+ const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
573
+ if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
574
+ this.buckets.delete(bucketKey);
575
+ this.queuesByBucket.delete(bucketKey);
576
+ this.pendingBuckets.delete(bucketKey);
577
+ return;
578
+ }
579
+ if (!intervalMs || bucket.active > 0 || queued > 0) {
580
+ return;
581
+ }
582
+ if (bucket.cleanupTimer) {
583
+ clearTimeout(bucket.cleanupTimer);
584
+ }
585
+ bucket.cleanupTimer = setTimeout(() => {
586
+ bucket.cleanupTimer = void 0;
587
+ this.prune(bucket, Date.now(), intervalMs);
588
+ if (bucket.active === 0 && bucket.startedAt.length === 0 && (this.queuesByBucket.get(bucketKey)?.length ?? 0) === 0) {
589
+ this.buckets.delete(bucketKey);
590
+ this.queuesByBucket.delete(bucketKey);
591
+ this.pendingBuckets.delete(bucketKey);
592
+ }
593
+ }, intervalMs);
594
+ bucket.cleanupTimer.unref?.();
595
+ }
541
596
  };
542
597
 
543
598
  // src/internal/MetricsCollector.ts
@@ -616,7 +671,30 @@ var MetricsCollector = class {
616
671
 
617
672
  // src/internal/StoredValue.ts
618
673
  function isStoredValueEnvelope(value) {
619
- return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
674
+ if (typeof value !== "object" || value === null) {
675
+ return false;
676
+ }
677
+ const v = value;
678
+ if (v.__layercache !== 1) {
679
+ return false;
680
+ }
681
+ if (v.kind !== "value" && v.kind !== "empty") {
682
+ return false;
683
+ }
684
+ if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
685
+ return false;
686
+ }
687
+ if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
688
+ return false;
689
+ }
690
+ if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
691
+ return false;
692
+ }
693
+ const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
694
+ if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
695
+ return false;
696
+ }
697
+ return true;
620
698
  }
621
699
  function createStoredValueEnvelope(options) {
622
700
  const now = options.now ?? Date.now();
@@ -881,15 +959,17 @@ var TagIndex = class {
881
959
  keyToTags = /* @__PURE__ */ new Map();
882
960
  knownKeys = /* @__PURE__ */ new Set();
883
961
  maxKnownKeys;
962
+ nextNodeId = 1;
963
+ root = this.createTrieNode();
884
964
  constructor(options = {}) {
885
- this.maxKnownKeys = options.maxKnownKeys;
965
+ this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
886
966
  }
887
967
  async touch(key) {
888
- this.knownKeys.add(key);
968
+ this.insertKnownKey(key);
889
969
  this.pruneKnownKeysIfNeeded();
890
970
  }
891
971
  async track(key, tags) {
892
- this.knownKeys.add(key);
972
+ this.insertKnownKey(key);
893
973
  this.pruneKnownKeysIfNeeded();
894
974
  if (tags.length === 0) {
895
975
  return;
@@ -915,18 +995,104 @@ var TagIndex = class {
915
995
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
916
996
  }
917
997
  async keysForPrefix(prefix) {
918
- return [...this.knownKeys].filter((key) => key.startsWith(prefix));
998
+ const node = this.findNode(prefix);
999
+ if (!node) {
1000
+ return [];
1001
+ }
1002
+ const matches = [];
1003
+ this.collectFromNode(node, prefix, matches);
1004
+ return matches;
919
1005
  }
920
1006
  async tagsForKey(key) {
921
1007
  return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
922
1008
  }
923
1009
  async matchPattern(pattern) {
924
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
1010
+ const matches = /* @__PURE__ */ new Set();
1011
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
1012
+ return [...matches];
925
1013
  }
926
1014
  async clear() {
927
1015
  this.tagToKeys.clear();
928
1016
  this.keyToTags.clear();
929
1017
  this.knownKeys.clear();
1018
+ this.root.children.clear();
1019
+ this.root.terminal = false;
1020
+ this.nextNodeId = this.root.id + 1;
1021
+ }
1022
+ createTrieNode() {
1023
+ return {
1024
+ id: this.nextNodeId++,
1025
+ terminal: false,
1026
+ children: /* @__PURE__ */ new Map()
1027
+ };
1028
+ }
1029
+ insertKnownKey(key) {
1030
+ if (this.knownKeys.has(key)) {
1031
+ return;
1032
+ }
1033
+ this.knownKeys.add(key);
1034
+ let node = this.root;
1035
+ for (const character of key) {
1036
+ let child = node.children.get(character);
1037
+ if (!child) {
1038
+ child = this.createTrieNode();
1039
+ node.children.set(character, child);
1040
+ }
1041
+ node = child;
1042
+ }
1043
+ node.terminal = true;
1044
+ }
1045
+ findNode(prefix) {
1046
+ let node = this.root;
1047
+ for (const character of prefix) {
1048
+ node = node.children.get(character);
1049
+ if (!node) {
1050
+ return void 0;
1051
+ }
1052
+ }
1053
+ return node;
1054
+ }
1055
+ collectFromNode(node, prefix, matches) {
1056
+ if (node.terminal) {
1057
+ matches.push(prefix);
1058
+ }
1059
+ for (const [character, child] of node.children) {
1060
+ this.collectFromNode(child, `${prefix}${character}`, matches);
1061
+ }
1062
+ }
1063
+ collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
1064
+ const stateKey = `${node.id}:${patternIndex}`;
1065
+ if (visited.has(stateKey)) {
1066
+ return;
1067
+ }
1068
+ visited.add(stateKey);
1069
+ if (patternIndex === pattern.length) {
1070
+ if (node.terminal) {
1071
+ matches.add(prefix);
1072
+ }
1073
+ return;
1074
+ }
1075
+ const patternChar = pattern[patternIndex];
1076
+ if (patternChar === void 0) {
1077
+ return;
1078
+ }
1079
+ if (patternChar === "*") {
1080
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
1081
+ for (const [character, child2] of node.children) {
1082
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
1083
+ }
1084
+ return;
1085
+ }
1086
+ if (patternChar === "?") {
1087
+ for (const [character, child2] of node.children) {
1088
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
1089
+ }
1090
+ return;
1091
+ }
1092
+ const child = node.children.get(patternChar);
1093
+ if (child) {
1094
+ this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
1095
+ }
930
1096
  }
931
1097
  pruneKnownKeysIfNeeded() {
932
1098
  if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
@@ -943,7 +1109,7 @@ var TagIndex = class {
943
1109
  }
944
1110
  }
945
1111
  removeKey(key) {
946
- this.knownKeys.delete(key);
1112
+ this.removeKnownKey(key);
947
1113
  const tags = this.keyToTags.get(key);
948
1114
  if (!tags) {
949
1115
  return;
@@ -960,8 +1126,71 @@ var TagIndex = class {
960
1126
  }
961
1127
  this.keyToTags.delete(key);
962
1128
  }
1129
+ removeKnownKey(key) {
1130
+ if (!this.knownKeys.delete(key)) {
1131
+ return;
1132
+ }
1133
+ const path = [];
1134
+ let node = this.root;
1135
+ for (const character of key) {
1136
+ const child = node.children.get(character);
1137
+ if (!child) {
1138
+ return;
1139
+ }
1140
+ path.push([node, character]);
1141
+ node = child;
1142
+ }
1143
+ node.terminal = false;
1144
+ for (let index = path.length - 1; index >= 0; index -= 1) {
1145
+ const entry = path[index];
1146
+ if (!entry) {
1147
+ continue;
1148
+ }
1149
+ const [parent, character] = entry;
1150
+ const child = parent.children.get(character);
1151
+ if (!child || child.terminal || child.children.size > 0) {
1152
+ break;
1153
+ }
1154
+ parent.children.delete(character);
1155
+ }
1156
+ }
963
1157
  };
964
1158
 
1159
+ // src/serialization/JsonSerializer.ts
1160
+ var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1161
+ var JsonSerializer = class {
1162
+ serialize(value) {
1163
+ return JSON.stringify(value);
1164
+ }
1165
+ deserialize(payload) {
1166
+ const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1167
+ return sanitizeJsonValue(JSON.parse(normalized), 0);
1168
+ }
1169
+ };
1170
+ var MAX_SANITIZE_DEPTH = 200;
1171
+ function sanitizeJsonValue(value, depth) {
1172
+ if (depth > MAX_SANITIZE_DEPTH) {
1173
+ return value;
1174
+ }
1175
+ if (Array.isArray(value)) {
1176
+ return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
1177
+ }
1178
+ if (!isPlainObject(value)) {
1179
+ return value;
1180
+ }
1181
+ const sanitized = {};
1182
+ for (const [key, entry] of Object.entries(value)) {
1183
+ if (DANGEROUS_JSON_KEYS.has(key)) {
1184
+ continue;
1185
+ }
1186
+ sanitized[key] = sanitizeJsonValue(entry, depth + 1);
1187
+ }
1188
+ return sanitized;
1189
+ }
1190
+ function isPlainObject(value) {
1191
+ return Object.prototype.toString.call(value) === "[object Object]";
1192
+ }
1193
+
965
1194
  // src/stampede/StampedeGuard.ts
966
1195
  var import_async_mutex2 = require("async-mutex");
967
1196
  var StampedeGuard = class {
@@ -972,7 +1201,8 @@ var StampedeGuard = class {
972
1201
  return await entry.mutex.runExclusive(task);
973
1202
  } finally {
974
1203
  entry.references -= 1;
975
- if (entry.references === 0 && !entry.mutex.isLocked()) {
1204
+ const current = this.mutexes.get(key);
1205
+ if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
976
1206
  this.mutexes.delete(key);
977
1207
  }
978
1208
  }
@@ -1002,8 +1232,10 @@ var CacheMissError = class extends Error {
1002
1232
  var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
1003
1233
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1004
1234
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1235
+ var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1005
1236
  var MAX_CACHE_KEY_LENGTH = 1024;
1006
1237
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1238
+ var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1007
1239
  var DebugLogger = class {
1008
1240
  enabled;
1009
1241
  constructor(enabled) {
@@ -1050,6 +1282,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
1050
1282
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
1051
1283
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
1052
1284
  this.tagIndex = options.tagIndex ?? new TagIndex();
1285
+ if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
1286
+ this.logger.warn?.(
1287
+ "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."
1288
+ );
1289
+ }
1290
+ if (!options.tagIndex && layers.some((layer) => layer.isLocal === false && !layer.keys)) {
1291
+ this.logger.warn?.(
1292
+ "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."
1293
+ );
1294
+ }
1295
+ if (options.invalidationBus && options.broadcastL1Invalidation === void 0 && options.publishSetInvalidation === void 0) {
1296
+ this.logger.warn?.(
1297
+ "broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
1298
+ );
1299
+ }
1053
1300
  this.initializeWriteBehind(options.writeBehind);
1054
1301
  this.startup = this.initialize();
1055
1302
  }
@@ -1063,6 +1310,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1063
1310
  logger;
1064
1311
  tagIndex;
1065
1312
  fetchRateLimiter = new FetchRateLimiter();
1313
+ snapshotSerializer = new JsonSerializer();
1066
1314
  backgroundRefreshes = /* @__PURE__ */ new Map();
1067
1315
  layerDegradedUntil = /* @__PURE__ */ new Map();
1068
1316
  ttlResolver;
@@ -1071,6 +1319,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1071
1319
  writeBehindQueue = [];
1072
1320
  writeBehindTimer;
1073
1321
  writeBehindFlushPromise;
1322
+ generationCleanupPromise;
1074
1323
  isDisconnecting = false;
1075
1324
  disconnectPromise;
1076
1325
  /**
@@ -1083,6 +1332,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1083
1332
  const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1084
1333
  this.validateWriteOptions(options);
1085
1334
  await this.awaitStartup("get");
1335
+ return this.getPrepared(normalizedKey, fetcher, options);
1336
+ }
1337
+ async getPrepared(normalizedKey, fetcher, options) {
1086
1338
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
1087
1339
  if (hit.found) {
1088
1340
  this.ttlResolver.recordAccess(normalizedKey);
@@ -1160,6 +1412,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1160
1412
  return true;
1161
1413
  }
1162
1414
  } catch {
1415
+ await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
1163
1416
  }
1164
1417
  } else {
1165
1418
  try {
@@ -1167,7 +1420,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
1167
1420
  if (value !== null) {
1168
1421
  return true;
1169
1422
  }
1170
- } catch {
1423
+ } catch (error) {
1424
+ await this.reportRecoverableLayerFailure(layer, "has", error);
1171
1425
  }
1172
1426
  }
1173
1427
  }
@@ -1259,13 +1513,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
1259
1513
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1260
1514
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
1261
1515
  if (!canFastPath) {
1516
+ await this.awaitStartup("mget");
1262
1517
  const pendingReads = /* @__PURE__ */ new Map();
1263
1518
  return Promise.all(
1264
1519
  normalizedEntries.map((entry) => {
1265
1520
  const optionsSignature = this.serializeOptions(entry.options);
1266
1521
  const existing = pendingReads.get(entry.key);
1267
1522
  if (!existing) {
1268
- const promise = this.get(entry.key, entry.fetch, entry.options);
1523
+ const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
1269
1524
  pendingReads.set(entry.key, {
1270
1525
  promise,
1271
1526
  fetch: entry.fetch,
@@ -1404,14 +1659,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
1404
1659
  }
1405
1660
  async invalidateByPattern(pattern) {
1406
1661
  await this.awaitStartup("invalidateByPattern");
1407
- const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1662
+ const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1408
1663
  await this.deleteKeys(keys);
1409
1664
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1410
1665
  }
1411
1666
  async invalidateByPrefix(prefix) {
1412
1667
  await this.awaitStartup("invalidateByPrefix");
1413
1668
  const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1414
- const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
1669
+ const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
1415
1670
  await this.deleteKeys(keys);
1416
1671
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1417
1672
  }
@@ -1461,9 +1716,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
1461
1716
  })
1462
1717
  );
1463
1718
  }
1719
+ /**
1720
+ * Rotates the active generation prefix used for all future cache keys.
1721
+ * Previous-generation keys remain in the underlying layers until they expire,
1722
+ * unless `generationCleanup` is enabled to prune them in the background.
1723
+ */
1464
1724
  bumpGeneration(nextGeneration) {
1465
1725
  const current = this.currentGeneration ?? 0;
1726
+ const previousGeneration = this.currentGeneration;
1466
1727
  this.currentGeneration = nextGeneration ?? current + 1;
1728
+ if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
1729
+ this.scheduleGenerationCleanup(previousGeneration);
1730
+ }
1467
1731
  return this.currentGeneration;
1468
1732
  }
1469
1733
  /**
@@ -1547,27 +1811,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1547
1811
  this.assertActive("persistToFile");
1548
1812
  const snapshot = await this.exportState();
1549
1813
  const { promises: fs2 } = await import("fs");
1550
- await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1814
+ await fs2.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
1551
1815
  }
1552
1816
  async restoreFromFile(filePath) {
1553
1817
  this.assertActive("restoreFromFile");
1554
1818
  const { promises: fs2 } = await import("fs");
1555
- const raw = await fs2.readFile(filePath, "utf8");
1819
+ const raw = await fs2.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
1556
1820
  let parsed;
1557
1821
  try {
1558
- parsed = JSON.parse(raw, (_key, value) => {
1559
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1560
- return Object.assign(/* @__PURE__ */ Object.create(null), value);
1561
- }
1562
- return value;
1563
- });
1822
+ parsed = JSON.parse(raw);
1564
1823
  } catch (cause) {
1565
1824
  throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
1566
1825
  }
1567
1826
  if (!this.isCacheSnapshotEntries(parsed)) {
1568
1827
  throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
1569
1828
  }
1570
- await this.importState(parsed);
1829
+ await this.importState(
1830
+ parsed.map((entry) => ({
1831
+ key: entry.key,
1832
+ value: this.sanitizeSnapshotValue(entry.value),
1833
+ ttl: entry.ttl
1834
+ }))
1835
+ );
1571
1836
  }
1572
1837
  async disconnect() {
1573
1838
  if (!this.disconnectPromise) {
@@ -1576,6 +1841,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1576
1841
  await this.startup;
1577
1842
  await this.unsubscribeInvalidation?.();
1578
1843
  await this.flushWriteBehindQueue();
1844
+ await this.generationCleanupPromise;
1579
1845
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1580
1846
  if (this.writeBehindTimer) {
1581
1847
  clearInterval(this.writeBehindTimer);
@@ -1659,8 +1925,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
1659
1925
  await this.storeEntry(key, "empty", null, options);
1660
1926
  return null;
1661
1927
  }
1662
- if (options?.shouldCache && !options.shouldCache(fetched)) {
1663
- return fetched;
1928
+ if (options?.shouldCache) {
1929
+ try {
1930
+ if (!options.shouldCache(fetched)) {
1931
+ return fetched;
1932
+ }
1933
+ } catch (error) {
1934
+ this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
1935
+ }
1664
1936
  }
1665
1937
  await this.storeEntry(key, "value", fetched, options);
1666
1938
  return fetched;
@@ -1887,7 +2159,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1887
2159
  const refresh = (async () => {
1888
2160
  this.metricsCollector.increment("refreshes");
1889
2161
  try {
1890
- await this.fetchWithGuards(key, fetcher, options);
2162
+ await this.runBackgroundRefresh(key, fetcher, options);
1891
2163
  } catch (error) {
1892
2164
  this.metricsCollector.increment("refreshErrors");
1893
2165
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
@@ -1897,6 +2169,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
1897
2169
  })();
1898
2170
  this.backgroundRefreshes.set(key, refresh);
1899
2171
  }
2172
+ async runBackgroundRefresh(key, fetcher, options) {
2173
+ const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
2174
+ await this.fetchWithGuards(
2175
+ key,
2176
+ () => this.withTimeout(fetcher(), timeoutMs, () => {
2177
+ return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
2178
+ }),
2179
+ options
2180
+ );
2181
+ }
1900
2182
  resolveSingleFlightOptions() {
1901
2183
  return {
1902
2184
  leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
@@ -1964,8 +2246,120 @@ var CacheStack = class extends import_node_events.EventEmitter {
1964
2246
  sleep(ms) {
1965
2247
  return new Promise((resolve2) => setTimeout(resolve2, ms));
1966
2248
  }
2249
+ async withTimeout(promise, timeoutMs, onTimeout) {
2250
+ if (timeoutMs <= 0) {
2251
+ return promise;
2252
+ }
2253
+ let timer;
2254
+ const observedPromise = promise.then(
2255
+ (value) => ({ kind: "value", value }),
2256
+ (error) => ({ kind: "error", error })
2257
+ );
2258
+ try {
2259
+ const result = await Promise.race([
2260
+ observedPromise,
2261
+ new Promise((_, reject) => {
2262
+ timer = setTimeout(() => reject(onTimeout()), timeoutMs);
2263
+ timer.unref?.();
2264
+ })
2265
+ ]);
2266
+ if (result && typeof result === "object" && "kind" in result) {
2267
+ if (result.kind === "error") {
2268
+ throw result.error;
2269
+ }
2270
+ return result.value;
2271
+ }
2272
+ return result;
2273
+ } finally {
2274
+ if (timer) {
2275
+ clearTimeout(timer);
2276
+ }
2277
+ }
2278
+ }
1967
2279
  shouldBroadcastL1Invalidation() {
1968
- return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
2280
+ return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
2281
+ }
2282
+ async collectKeysWithPrefix(prefix) {
2283
+ const matches = new Set(
2284
+ this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
2285
+ );
2286
+ await Promise.all(
2287
+ this.layers.map(async (layer) => {
2288
+ if (!layer.keys || this.shouldSkipLayer(layer)) {
2289
+ return;
2290
+ }
2291
+ try {
2292
+ const keys = await layer.keys();
2293
+ for (const key of keys) {
2294
+ if (key.startsWith(prefix)) {
2295
+ matches.add(key);
2296
+ }
2297
+ }
2298
+ } catch (error) {
2299
+ await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
2300
+ }
2301
+ })
2302
+ );
2303
+ return [...matches];
2304
+ }
2305
+ async collectKeysMatchingPattern(pattern) {
2306
+ const matches = new Set(await this.tagIndex.matchPattern(pattern));
2307
+ await Promise.all(
2308
+ this.layers.map(async (layer) => {
2309
+ if (!layer.keys || this.shouldSkipLayer(layer)) {
2310
+ return;
2311
+ }
2312
+ try {
2313
+ const keys = await layer.keys();
2314
+ for (const key of keys) {
2315
+ if (PatternMatcher.matches(pattern, key)) {
2316
+ matches.add(key);
2317
+ }
2318
+ }
2319
+ } catch (error) {
2320
+ await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
2321
+ }
2322
+ })
2323
+ );
2324
+ return [...matches];
2325
+ }
2326
+ shouldCleanupGenerations() {
2327
+ return Boolean(this.options.generationCleanup);
2328
+ }
2329
+ generationCleanupBatchSize() {
2330
+ const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
2331
+ return configured ?? 500;
2332
+ }
2333
+ scheduleGenerationCleanup(generation) {
2334
+ const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
2335
+ this.logger.warn?.("generation-cleanup-error", {
2336
+ generation,
2337
+ error: this.formatError(error)
2338
+ });
2339
+ });
2340
+ this.generationCleanupPromise = task.finally(() => {
2341
+ if (this.generationCleanupPromise === task) {
2342
+ this.generationCleanupPromise = void 0;
2343
+ }
2344
+ });
2345
+ }
2346
+ async cleanupGeneration(generation) {
2347
+ const prefix = `v${generation}:`;
2348
+ const keys = await this.collectKeysWithPrefix(prefix);
2349
+ if (keys.length === 0) {
2350
+ return;
2351
+ }
2352
+ const batchSize = this.generationCleanupBatchSize();
2353
+ for (let index = 0; index < keys.length; index += batchSize) {
2354
+ const batch = keys.slice(index, index + batchSize);
2355
+ await this.deleteKeys(batch);
2356
+ await this.publishInvalidation({
2357
+ scope: "keys",
2358
+ keys: batch,
2359
+ sourceId: this.instanceId,
2360
+ operation: "invalidate"
2361
+ });
2362
+ }
1969
2363
  }
1970
2364
  initializeWriteBehind(options) {
1971
2365
  if (this.options.writeStrategy !== "write-behind") {
@@ -2003,7 +2397,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
2003
2397
  const batchSize = this.options.writeBehind?.batchSize ?? 100;
2004
2398
  const batch = this.writeBehindQueue.splice(0, batchSize);
2005
2399
  this.writeBehindFlushPromise = (async () => {
2006
- await Promise.allSettled(batch.map((operation) => operation()));
2400
+ const results = await Promise.allSettled(batch.map((operation) => operation()));
2401
+ const failures = results.filter((result) => result.status === "rejected");
2402
+ if (failures.length > 0) {
2403
+ this.metricsCollector.increment("writeFailures", failures.length);
2404
+ this.logger.error?.("write-behind-flush-failure", {
2405
+ failed: failures.length,
2406
+ total: batch.length,
2407
+ errors: failures.map((failure) => this.formatError(failure.reason))
2408
+ });
2409
+ this.emitError("write-behind", { failed: failures.length, total: batch.length });
2410
+ }
2007
2411
  })();
2008
2412
  await this.writeBehindFlushPromise;
2009
2413
  this.writeBehindFlushPromise = void 0;
@@ -2108,9 +2512,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2108
2512
  this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
2109
2513
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2110
2514
  this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2515
+ this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
2111
2516
  this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
2112
2517
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
2113
2518
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2519
+ if (typeof this.options.generationCleanup === "object") {
2520
+ this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
2521
+ }
2114
2522
  if (this.options.generation !== void 0) {
2115
2523
  this.validateNonNegativeNumber("generation", this.options.generation);
2116
2524
  }
@@ -2182,6 +2590,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2182
2590
  if (/[\u0000-\u001F\u007F]/.test(key)) {
2183
2591
  throw new Error("Cache key contains unsupported control characters.");
2184
2592
  }
2593
+ if (/[\uD800-\uDFFF]/.test(key)) {
2594
+ throw new Error("Cache key contains unsupported surrogate code points.");
2595
+ }
2185
2596
  return key;
2186
2597
  }
2187
2598
  validateTtlPolicy(name, policy) {
@@ -2259,6 +2670,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
2259
2670
  this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
2260
2671
  return null;
2261
2672
  }
2673
+ async reportRecoverableLayerFailure(layer, operation, error) {
2674
+ if (this.isGracefulDegradationEnabled()) {
2675
+ await this.handleLayerFailure(layer, operation, error);
2676
+ return;
2677
+ }
2678
+ this.logger.warn?.("layer-operation-failed", { layer: layer.name, operation, error: this.formatError(error) });
2679
+ this.emitError(operation, { layer: layer.name, degraded: false, error: this.formatError(error) });
2680
+ }
2262
2681
  isGracefulDegradationEnabled() {
2263
2682
  return Boolean(this.options.gracefulDegradation);
2264
2683
  }
@@ -2282,10 +2701,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
2282
2701
  }
2283
2702
  }
2284
2703
  serializeKeyPart(value) {
2285
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2286
- return String(value);
2704
+ if (typeof value === "string") {
2705
+ return `s:${value}`;
2287
2706
  }
2288
- return JSON.stringify(this.normalizeForSerialization(value));
2707
+ if (typeof value === "number") {
2708
+ return `n:${value}`;
2709
+ }
2710
+ if (typeof value === "boolean") {
2711
+ return `b:${value}`;
2712
+ }
2713
+ return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
2289
2714
  }
2290
2715
  isCacheSnapshotEntries(value) {
2291
2716
  return Array.isArray(value) && value.every((entry) => {
@@ -2293,15 +2718,39 @@ var CacheStack = class extends import_node_events.EventEmitter {
2293
2718
  return false;
2294
2719
  }
2295
2720
  const candidate = entry;
2296
- return typeof candidate.key === "string";
2721
+ return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
2297
2722
  });
2298
2723
  }
2724
+ sanitizeSnapshotValue(value) {
2725
+ return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
2726
+ }
2727
+ async validateSnapshotFilePath(filePath) {
2728
+ if (filePath.length === 0) {
2729
+ throw new Error("filePath must not be empty.");
2730
+ }
2731
+ if (filePath.includes("\0")) {
2732
+ throw new Error("filePath must not contain null bytes.");
2733
+ }
2734
+ const path = await import("path");
2735
+ const resolved = path.resolve(filePath);
2736
+ const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
2737
+ if (baseDir !== false) {
2738
+ const relative = path.relative(baseDir, resolved);
2739
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
2740
+ throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
2741
+ }
2742
+ }
2743
+ return resolved;
2744
+ }
2299
2745
  normalizeForSerialization(value) {
2300
2746
  if (Array.isArray(value)) {
2301
2747
  return value.map((entry) => this.normalizeForSerialization(entry));
2302
2748
  }
2303
2749
  if (value && typeof value === "object") {
2304
2750
  return Object.keys(value).sort().reduce((normalized, key) => {
2751
+ if (DANGEROUS_OBJECT_KEYS.has(key)) {
2752
+ return normalized;
2753
+ }
2305
2754
  normalized[key] = this.normalizeForSerialization(value[key]);
2306
2755
  return normalized;
2307
2756
  }, {});
@@ -2562,7 +3011,7 @@ function createCachedMethodDecorator(options) {
2562
3011
  function createFastifyLayercachePlugin(cache, options = {}) {
2563
3012
  return async (fastify) => {
2564
3013
  fastify.decorate("cache", cache);
2565
- if (options.exposeStatsRoute !== false && fastify.get) {
3014
+ if (options.exposeStatsRoute === true && fastify.get) {
2566
3015
  fastify.get(options.statsPath ?? "/cache/stats", async () => cache.getStats());
2567
3016
  }
2568
3017
  };
@@ -2578,7 +3027,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
2578
3027
  next();
2579
3028
  return;
2580
3029
  }
2581
- const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
3030
+ const rawUrl = req.originalUrl ?? req.url ?? "/";
3031
+ const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
2582
3032
  const cached = await cache.get(key, void 0, options);
2583
3033
  if (cached !== null) {
2584
3034
  res.setHeader?.("content-type", "application/json; charset=utf-8");
@@ -2594,7 +3044,12 @@ function createExpressCacheMiddleware(cache, options = {}) {
2594
3044
  if (originalJson) {
2595
3045
  res.json = (body) => {
2596
3046
  res.setHeader?.("x-cache", "MISS");
2597
- void cache.set(key, body, options);
3047
+ cache.set(key, body, options).catch((err) => {
3048
+ cache.emit("error", {
3049
+ operation: "set",
3050
+ error: err instanceof Error ? err.message : String(err)
3051
+ });
3052
+ });
2598
3053
  return originalJson(body);
2599
3054
  };
2600
3055
  }
@@ -2604,6 +3059,15 @@ function createExpressCacheMiddleware(cache, options = {}) {
2604
3059
  }
2605
3060
  };
2606
3061
  }
3062
+ function normalizeUrl(url) {
3063
+ try {
3064
+ const parsed = new URL(url, "http://localhost");
3065
+ parsed.searchParams.sort();
3066
+ return decodeURIComponent(parsed.pathname) + parsed.search;
3067
+ } catch {
3068
+ return url;
3069
+ }
3070
+ }
2607
3071
 
2608
3072
  // src/integrations/graphql.ts
2609
3073
  function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
@@ -2623,7 +3087,8 @@ function createHonoCacheMiddleware(cache, options = {}) {
2623
3087
  await next();
2624
3088
  return;
2625
3089
  }
2626
- const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${context.req.path ?? context.req.url ?? "/"}`;
3090
+ const rawPath = context.req.path ?? context.req.url ?? "/";
3091
+ const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl2(rawPath)}`;
2627
3092
  const cached = await cache.get(key, void 0, options);
2628
3093
  if (cached !== null) {
2629
3094
  context.header?.("x-cache", "HIT");
@@ -2634,12 +3099,26 @@ function createHonoCacheMiddleware(cache, options = {}) {
2634
3099
  const originalJson = context.json.bind(context);
2635
3100
  context.json = (body, status) => {
2636
3101
  context.header?.("x-cache", "MISS");
2637
- void cache.set(key, body, options);
3102
+ cache.set(key, body, options).catch((err) => {
3103
+ cache.emit("error", {
3104
+ operation: "set",
3105
+ error: err instanceof Error ? err.message : String(err)
3106
+ });
3107
+ });
2638
3108
  return originalJson(body, status);
2639
3109
  };
2640
3110
  await next();
2641
3111
  };
2642
3112
  }
3113
+ function normalizeUrl2(url) {
3114
+ try {
3115
+ const parsed = new URL(url, "http://localhost");
3116
+ parsed.searchParams.sort();
3117
+ return decodeURIComponent(parsed.pathname) + parsed.search;
3118
+ } catch {
3119
+ return url;
3120
+ }
3121
+ }
2643
3122
 
2644
3123
  // src/integrations/opentelemetry.ts
2645
3124
  function createOpenTelemetryPlugin(cache, tracer) {
@@ -2774,16 +3253,10 @@ var MemoryLayer = class {
2774
3253
  return entry.value;
2775
3254
  }
2776
3255
  async getMany(keys) {
2777
- const values = [];
2778
- for (const key of keys) {
2779
- values.push(await this.getEntry(key));
2780
- }
2781
- return values;
3256
+ return Promise.all(keys.map((key) => this.getEntry(key)));
2782
3257
  }
2783
3258
  async setMany(entries) {
2784
- for (const entry of entries) {
2785
- await this.set(entry.key, entry.value, entry.ttl);
2786
- }
3259
+ await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
2787
3260
  }
2788
3261
  async set(key, value, ttl = this.defaultTtl) {
2789
3262
  this.entries.delete(key);
@@ -2919,39 +3392,6 @@ var MemoryLayer = class {
2919
3392
  // src/layers/RedisLayer.ts
2920
3393
  var import_node_util = require("util");
2921
3394
  var import_node_zlib = require("zlib");
2922
-
2923
- // src/serialization/JsonSerializer.ts
2924
- var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2925
- var JsonSerializer = class {
2926
- serialize(value) {
2927
- return JSON.stringify(value);
2928
- }
2929
- deserialize(payload) {
2930
- const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2931
- return sanitizeJsonValue(JSON.parse(normalized));
2932
- }
2933
- };
2934
- function sanitizeJsonValue(value) {
2935
- if (Array.isArray(value)) {
2936
- return value.map((entry) => sanitizeJsonValue(entry));
2937
- }
2938
- if (!isPlainObject(value)) {
2939
- return value;
2940
- }
2941
- const sanitized = {};
2942
- for (const [key, entry] of Object.entries(value)) {
2943
- if (DANGEROUS_JSON_KEYS.has(key)) {
2944
- continue;
2945
- }
2946
- sanitized[key] = sanitizeJsonValue(entry);
2947
- }
2948
- return sanitized;
2949
- }
2950
- function isPlainObject(value) {
2951
- return Object.prototype.toString.call(value) === "[object Object]";
2952
- }
2953
-
2954
- // src/layers/RedisLayer.ts
2955
3395
  var BATCH_DELETE_SIZE = 500;
2956
3396
  var gzipAsync = (0, import_node_util.promisify)(import_node_zlib.gzip);
2957
3397
  var gunzipAsync = (0, import_node_util.promisify)(import_node_zlib.gunzip);
@@ -2968,6 +3408,7 @@ var RedisLayer = class {
2968
3408
  scanCount;
2969
3409
  compression;
2970
3410
  compressionThreshold;
3411
+ decompressionMaxBytes;
2971
3412
  disconnectOnDispose;
2972
3413
  constructor(options) {
2973
3414
  this.client = options.client;
@@ -2979,6 +3420,7 @@ var RedisLayer = class {
2979
3420
  this.scanCount = options.scanCount ?? 100;
2980
3421
  this.compression = options.compression;
2981
3422
  this.compressionThreshold = options.compressionThreshold ?? 1024;
3423
+ this.decompressionMaxBytes = options.decompressionMaxBytes ?? 64 * 1024 * 1024;
2982
3424
  this.disconnectOnDispose = options.disconnectOnDispose ?? false;
2983
3425
  }
2984
3426
  async get(key) {
@@ -3178,16 +3620,29 @@ var RedisLayer = class {
3178
3620
  }
3179
3621
  /**
3180
3622
  * Decompresses the payload asynchronously if a compression header is present.
3623
+ * Enforces a maximum decompressed size to prevent decompression bomb attacks.
3181
3624
  */
3182
3625
  async decodePayload(payload) {
3183
3626
  if (!Buffer.isBuffer(payload)) {
3184
3627
  return payload;
3185
3628
  }
3186
3629
  if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
3187
- return gunzipAsync(payload.subarray(10));
3630
+ const decompressed = await gunzipAsync(payload.subarray(10));
3631
+ if (decompressed.byteLength > this.decompressionMaxBytes) {
3632
+ throw new Error(
3633
+ `Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
3634
+ );
3635
+ }
3636
+ return decompressed;
3188
3637
  }
3189
3638
  if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
3190
- return brotliDecompressAsync(payload.subarray(12));
3639
+ const decompressed = await brotliDecompressAsync(payload.subarray(12));
3640
+ if (decompressed.byteLength > this.decompressionMaxBytes) {
3641
+ throw new Error(
3642
+ `Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
3643
+ );
3644
+ }
3645
+ return decompressed;
3191
3646
  }
3192
3647
  return payload;
3193
3648
  }
@@ -3247,8 +3702,13 @@ var DiskLayer = class {
3247
3702
  const payload = this.serializer.serialize(entry);
3248
3703
  const targetPath = this.keyToPath(key);
3249
3704
  const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3250
- await import_node_fs.promises.writeFile(tempPath, payload);
3251
- await import_node_fs.promises.rename(tempPath, targetPath);
3705
+ try {
3706
+ await import_node_fs.promises.writeFile(tempPath, payload);
3707
+ await import_node_fs.promises.rename(tempPath, targetPath);
3708
+ } catch (error) {
3709
+ await this.safeDelete(tempPath);
3710
+ throw error;
3711
+ }
3252
3712
  if (this.maxFiles !== void 0) {
3253
3713
  await this.enforceMaxFiles();
3254
3714
  }
@@ -3258,9 +3718,7 @@ var DiskLayer = class {
3258
3718
  return Promise.all(keys.map((key) => this.getEntry(key)));
3259
3719
  }
3260
3720
  async setMany(entries) {
3261
- for (const entry of entries) {
3262
- await this.set(entry.key, entry.value, entry.ttl);
3263
- }
3721
+ await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
3264
3722
  }
3265
3723
  async has(key) {
3266
3724
  const value = await this.getEntry(key);
@@ -3464,6 +3922,7 @@ var MemcachedLayer = class {
3464
3922
  return unwrapStoredValue(await this.getEntry(key));
3465
3923
  }
3466
3924
  async getEntry(key) {
3925
+ this.validateKey(key);
3467
3926
  const result = await this.client.get(this.withPrefix(key));
3468
3927
  if (!result || result.value === null) {
3469
3928
  return null;
@@ -3478,16 +3937,19 @@ var MemcachedLayer = class {
3478
3937
  return Promise.all(keys.map((key) => this.getEntry(key)));
3479
3938
  }
3480
3939
  async set(key, value, ttl = this.defaultTtl) {
3940
+ this.validateKey(key);
3481
3941
  const payload = this.serializer.serialize(value);
3482
3942
  await this.client.set(this.withPrefix(key), payload, {
3483
3943
  expires: ttl && ttl > 0 ? ttl : void 0
3484
3944
  });
3485
3945
  }
3486
3946
  async has(key) {
3947
+ this.validateKey(key);
3487
3948
  const result = await this.client.get(this.withPrefix(key));
3488
3949
  return result !== null && result.value !== null;
3489
3950
  }
3490
3951
  async delete(key) {
3952
+ this.validateKey(key);
3491
3953
  await this.client.delete(this.withPrefix(key));
3492
3954
  }
3493
3955
  async deleteMany(keys) {
@@ -3501,19 +3963,50 @@ var MemcachedLayer = class {
3501
3963
  withPrefix(key) {
3502
3964
  return `${this.keyPrefix}${key}`;
3503
3965
  }
3966
+ validateKey(key) {
3967
+ const fullKey = this.withPrefix(key);
3968
+ if (Buffer.byteLength(fullKey, "utf8") > 250) {
3969
+ throw new Error(`MemcachedLayer: key exceeds 250-byte Memcached limit: "${fullKey.slice(0, 60)}..."`);
3970
+ }
3971
+ if (/[\s\x00-\x1f\x7f]/.test(fullKey)) {
3972
+ throw new Error(
3973
+ "MemcachedLayer: key contains invalid characters (whitespace or control characters are not allowed)."
3974
+ );
3975
+ }
3976
+ }
3504
3977
  };
3505
3978
 
3506
3979
  // src/serialization/MsgpackSerializer.ts
3507
3980
  var import_msgpack = require("@msgpack/msgpack");
3981
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3508
3982
  var MsgpackSerializer = class {
3509
3983
  serialize(value) {
3510
3984
  return Buffer.from((0, import_msgpack.encode)(value));
3511
3985
  }
3512
3986
  deserialize(payload) {
3513
3987
  const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
3514
- return (0, import_msgpack.decode)(normalized);
3988
+ return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized));
3515
3989
  }
3516
3990
  };
3991
+ function sanitizeMsgpackValue(value) {
3992
+ if (Array.isArray(value)) {
3993
+ return value.map((entry) => sanitizeMsgpackValue(entry));
3994
+ }
3995
+ if (!isPlainObject2(value)) {
3996
+ return value;
3997
+ }
3998
+ const sanitized = {};
3999
+ for (const [key, entry] of Object.entries(value)) {
4000
+ if (DANGEROUS_KEYS.has(key)) {
4001
+ continue;
4002
+ }
4003
+ sanitized[key] = sanitizeMsgpackValue(entry);
4004
+ }
4005
+ return sanitized;
4006
+ }
4007
+ function isPlainObject2(value) {
4008
+ return Object.prototype.toString.call(value) === "[object Object]";
4009
+ }
3517
4010
 
3518
4011
  // src/singleflight/RedisSingleFlightCoordinator.ts
3519
4012
  var import_node_crypto2 = require("crypto");
@@ -3642,7 +4135,7 @@ function createPrometheusMetricsExporter(stacks) {
3642
4135
  };
3643
4136
  }
3644
4137
  function sanitizeLabel(value) {
3645
- return value.replace(/["\\\n]/g, "_");
4138
+ return value.replace(/["\\\n\r]/g, "_");
3646
4139
  }
3647
4140
  // Annotate the CommonJS export names for ESM import in node:
3648
4141
  0 && (module.exports = {