layercache 1.2.3 → 1.2.5

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.
@@ -338,6 +338,7 @@ var CacheNamespace = class _CacheNamespace {
338
338
  * ```
339
339
  */
340
340
  namespace(childPrefix) {
341
+ validateNamespaceKey(childPrefix);
341
342
  return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
342
343
  }
343
344
  qualify(key) {
@@ -467,6 +468,118 @@ function addMap(base, delta) {
467
468
  }
468
469
  return result;
469
470
  }
471
+ function validateNamespaceKey(key) {
472
+ if (key.length === 0) {
473
+ throw new Error("Namespace prefix must not be empty.");
474
+ }
475
+ if (key.length > 256) {
476
+ throw new Error("Namespace prefix must be at most 256 characters.");
477
+ }
478
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
479
+ throw new Error("Namespace prefix contains unsupported control characters.");
480
+ }
481
+ }
482
+
483
+ // ../../src/invalidation/PatternMatcher.ts
484
+ var PatternMatcher = class _PatternMatcher {
485
+ /**
486
+ * Tests whether a glob-style pattern matches a value.
487
+ * Supports `*` (any sequence of characters) and `?` (any single character).
488
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
489
+ * quadratic memory usage on long patterns/keys.
490
+ */
491
+ static matches(pattern, value) {
492
+ return _PatternMatcher.matchLinear(pattern, value);
493
+ }
494
+ /**
495
+ * Linear-time glob matching with O(1) extra memory.
496
+ */
497
+ static matchLinear(pattern, value) {
498
+ let patternIndex = 0;
499
+ let valueIndex = 0;
500
+ let starIndex = -1;
501
+ let backtrackValueIndex = 0;
502
+ while (valueIndex < value.length) {
503
+ const patternChar = pattern[patternIndex];
504
+ const valueChar = value[valueIndex];
505
+ if (patternChar === "*" && patternIndex < pattern.length) {
506
+ starIndex = patternIndex;
507
+ patternIndex += 1;
508
+ backtrackValueIndex = valueIndex;
509
+ continue;
510
+ }
511
+ if (patternChar === "?" || patternChar === valueChar) {
512
+ patternIndex += 1;
513
+ valueIndex += 1;
514
+ continue;
515
+ }
516
+ if (starIndex !== -1) {
517
+ patternIndex = starIndex + 1;
518
+ backtrackValueIndex += 1;
519
+ valueIndex = backtrackValueIndex;
520
+ continue;
521
+ }
522
+ return false;
523
+ }
524
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
525
+ patternIndex += 1;
526
+ }
527
+ return patternIndex === pattern.length;
528
+ }
529
+ };
530
+
531
+ // ../../src/internal/CacheKeyDiscovery.ts
532
+ var CacheKeyDiscovery = class {
533
+ constructor(options) {
534
+ this.options = options;
535
+ }
536
+ options;
537
+ async collectKeysWithPrefix(prefix) {
538
+ const { tagIndex } = this.options;
539
+ const matches = new Set(
540
+ tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
541
+ );
542
+ await Promise.all(
543
+ this.options.layers.map(async (layer) => {
544
+ if (!layer.keys || this.options.shouldSkipLayer(layer)) {
545
+ return;
546
+ }
547
+ try {
548
+ const keys = await layer.keys();
549
+ for (const key of keys) {
550
+ if (key.startsWith(prefix)) {
551
+ matches.add(key);
552
+ }
553
+ }
554
+ } catch (error) {
555
+ await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
556
+ }
557
+ })
558
+ );
559
+ return [...matches];
560
+ }
561
+ async collectKeysMatchingPattern(pattern) {
562
+ const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
563
+ await Promise.all(
564
+ this.options.layers.map(async (layer) => {
565
+ if (!layer.keys || this.options.shouldSkipLayer(layer)) {
566
+ return;
567
+ }
568
+ try {
569
+ const keys = await layer.keys();
570
+ for (const key of keys) {
571
+ if (PatternMatcher.matches(pattern, key)) {
572
+ matches.add(key);
573
+ }
574
+ }
575
+ } catch (error) {
576
+ await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
577
+ }
578
+ })
579
+ );
580
+ return [...matches];
581
+ }
582
+ };
470
583
 
471
584
  // ../../src/internal/CircuitBreakerManager.ts
472
585
  var CircuitBreakerManager = class {
@@ -486,9 +599,7 @@ var CircuitBreakerManager = class {
486
599
  }
487
600
  const now = Date.now();
488
601
  if (state.openUntil <= now) {
489
- state.openUntil = null;
490
- state.failures = 0;
491
- this.breakers.set(key, state);
602
+ this.breakers.delete(key);
492
603
  return;
493
604
  }
494
605
  const remainingMs = state.openUntil - now;
@@ -499,15 +610,15 @@ var CircuitBreakerManager = class {
499
610
  if (!options) {
500
611
  return;
501
612
  }
613
+ this.pruneIfNeeded();
502
614
  const failureThreshold = options.failureThreshold ?? 3;
503
615
  const cooldownMs = options.cooldownMs ?? 3e4;
504
- const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
616
+ const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
505
617
  state.failures += 1;
506
618
  if (state.failures >= failureThreshold) {
507
619
  state.openUntil = Date.now() + cooldownMs;
508
620
  }
509
621
  this.breakers.set(key, state);
510
- this.pruneIfNeeded();
511
622
  }
512
623
  recordSuccess(key) {
513
624
  this.breakers.delete(key);
@@ -518,8 +629,7 @@ var CircuitBreakerManager = class {
518
629
  return false;
519
630
  }
520
631
  if (state.openUntil <= Date.now()) {
521
- state.openUntil = null;
522
- state.failures = 0;
632
+ this.breakers.delete(key);
523
633
  return false;
524
634
  }
525
635
  return true;
@@ -543,15 +653,20 @@ var CircuitBreakerManager = class {
543
653
  if (this.breakers.size <= this.maxEntries) {
544
654
  return;
545
655
  }
656
+ const now = Date.now();
546
657
  for (const [key, state] of this.breakers.entries()) {
547
658
  if (this.breakers.size <= this.maxEntries) {
548
- break;
659
+ return;
549
660
  }
550
- if (!state.openUntil || state.openUntil <= Date.now()) {
661
+ if (!state.openUntil || state.openUntil <= now) {
551
662
  this.breakers.delete(key);
552
663
  }
553
664
  }
554
- for (const key of this.breakers.keys()) {
665
+ if (this.breakers.size <= this.maxEntries) {
666
+ return;
667
+ }
668
+ const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
669
+ for (const [key] of sorted) {
555
670
  if (this.breakers.size <= this.maxEntries) {
556
671
  break;
557
672
  }
@@ -561,6 +676,7 @@ var CircuitBreakerManager = class {
561
676
  };
562
677
 
563
678
  // ../../src/internal/FetchRateLimiter.ts
679
+ var MAX_BUCKETS = 1e4;
564
680
  var FetchRateLimiter = class {
565
681
  buckets = /* @__PURE__ */ new Map();
566
682
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -726,10 +842,25 @@ var FetchRateLimiter = class {
726
842
  if (existing) {
727
843
  return existing;
728
844
  }
845
+ if (this.buckets.size >= MAX_BUCKETS) {
846
+ this.evictIdleBuckets();
847
+ }
729
848
  const bucket = { active: 0, startedAt: [] };
730
849
  this.buckets.set(bucketKey, bucket);
731
850
  return bucket;
732
851
  }
852
+ evictIdleBuckets() {
853
+ for (const [key, bucket] of this.buckets.entries()) {
854
+ if (this.buckets.size <= MAX_BUCKETS * 0.9) {
855
+ break;
856
+ }
857
+ if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
858
+ this.buckets.delete(key);
859
+ this.queuesByBucket.delete(key);
860
+ this.pendingBuckets.delete(key);
861
+ }
862
+ }
863
+ }
733
864
  cleanupBucket(bucketKey, bucket, intervalMs) {
734
865
  const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
735
866
  if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
@@ -1056,66 +1187,18 @@ var TtlResolver = class {
1056
1187
  return;
1057
1188
  }
1058
1189
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
1059
- let removed = 0;
1060
- for (const key of this.accessProfiles.keys()) {
1061
- if (removed >= toRemove) {
1062
- break;
1190
+ const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
1191
+ for (let i = 0; i < toRemove && i < sorted.length; i++) {
1192
+ const entry = sorted[i];
1193
+ if (entry) {
1194
+ this.accessProfiles.delete(entry[0]);
1063
1195
  }
1064
- this.accessProfiles.delete(key);
1065
- removed += 1;
1066
- }
1067
- }
1068
- };
1069
-
1070
- // ../../src/invalidation/PatternMatcher.ts
1071
- var PatternMatcher = class _PatternMatcher {
1072
- /**
1073
- * Tests whether a glob-style pattern matches a value.
1074
- * Supports `*` (any sequence of characters) and `?` (any single character).
1075
- * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
1076
- * quadratic memory usage on long patterns/keys.
1077
- */
1078
- static matches(pattern, value) {
1079
- return _PatternMatcher.matchLinear(pattern, value);
1080
- }
1081
- /**
1082
- * Linear-time glob matching with O(1) extra memory.
1083
- */
1084
- static matchLinear(pattern, value) {
1085
- let patternIndex = 0;
1086
- let valueIndex = 0;
1087
- let starIndex = -1;
1088
- let backtrackValueIndex = 0;
1089
- while (valueIndex < value.length) {
1090
- const patternChar = pattern[patternIndex];
1091
- const valueChar = value[valueIndex];
1092
- if (patternChar === "*" && patternIndex < pattern.length) {
1093
- starIndex = patternIndex;
1094
- patternIndex += 1;
1095
- backtrackValueIndex = valueIndex;
1096
- continue;
1097
- }
1098
- if (patternChar === "?" || patternChar === valueChar) {
1099
- patternIndex += 1;
1100
- valueIndex += 1;
1101
- continue;
1102
- }
1103
- if (starIndex !== -1) {
1104
- patternIndex = starIndex + 1;
1105
- backtrackValueIndex += 1;
1106
- valueIndex = backtrackValueIndex;
1107
- continue;
1108
- }
1109
- return false;
1110
- }
1111
- while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
1112
- patternIndex += 1;
1113
1196
  }
1114
- return patternIndex === pattern.length;
1115
1197
  }
1116
1198
  };
1117
1199
 
1118
1200
  // ../../src/invalidation/TagIndex.ts
1201
+ var MAX_PATTERN_RECURSION_DEPTH = 500;
1119
1202
  var TagIndex = class {
1120
1203
  tagToKeys = /* @__PURE__ */ new Map();
1121
1204
  keyToTags = /* @__PURE__ */ new Map();
@@ -1170,7 +1253,7 @@ var TagIndex = class {
1170
1253
  }
1171
1254
  async matchPattern(pattern) {
1172
1255
  const matches = /* @__PURE__ */ new Set();
1173
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
1256
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
1174
1257
  return [...matches];
1175
1258
  }
1176
1259
  async clear() {
@@ -1222,7 +1305,10 @@ var TagIndex = class {
1222
1305
  this.collectFromNode(child, `${prefix}${character}`, matches);
1223
1306
  }
1224
1307
  }
1225
- collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
1308
+ collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
1309
+ if (depth > MAX_PATTERN_RECURSION_DEPTH) {
1310
+ return;
1311
+ }
1226
1312
  const stateKey = `${node.id}:${patternIndex}`;
1227
1313
  if (visited.has(stateKey)) {
1228
1314
  return;
@@ -1239,21 +1325,37 @@ var TagIndex = class {
1239
1325
  return;
1240
1326
  }
1241
1327
  if (patternChar === "*") {
1242
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
1328
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
1243
1329
  for (const [character, child2] of node.children) {
1244
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
1330
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
1245
1331
  }
1246
1332
  return;
1247
1333
  }
1248
1334
  if (patternChar === "?") {
1249
1335
  for (const [character, child2] of node.children) {
1250
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
1336
+ this.collectPatternMatches(
1337
+ child2,
1338
+ `${prefix}${character}`,
1339
+ pattern,
1340
+ patternIndex + 1,
1341
+ matches,
1342
+ visited,
1343
+ depth + 1
1344
+ );
1251
1345
  }
1252
1346
  return;
1253
1347
  }
1254
1348
  const child = node.children.get(patternChar);
1255
1349
  if (child) {
1256
- this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
1350
+ this.collectPatternMatches(
1351
+ child,
1352
+ `${prefix}${patternChar}`,
1353
+ pattern,
1354
+ patternIndex + 1,
1355
+ matches,
1356
+ visited,
1357
+ depth + 1
1358
+ );
1257
1359
  }
1258
1360
  }
1259
1361
  pruneKnownKeysIfNeeded() {
@@ -1395,6 +1497,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1395
1497
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1396
1498
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1397
1499
  var MAX_CACHE_KEY_LENGTH = 1024;
1500
+ var MAX_PATTERN_LENGTH = 1024;
1398
1501
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1399
1502
  var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1400
1503
  var DebugLogger = class {
@@ -1443,6 +1546,14 @@ var CacheStack = class extends EventEmitter {
1443
1546
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
1444
1547
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
1445
1548
  this.tagIndex = options.tagIndex ?? new TagIndex();
1549
+ this.keyDiscovery = new CacheKeyDiscovery({
1550
+ layers: this.layers,
1551
+ tagIndex: this.tagIndex,
1552
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
1553
+ handleLayerFailure: async (layer, operation, error) => {
1554
+ await this.handleLayerFailure(layer, operation, error);
1555
+ }
1556
+ });
1446
1557
  if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
1447
1558
  this.logger.warn?.(
1448
1559
  "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."
@@ -1470,6 +1581,7 @@ var CacheStack = class extends EventEmitter {
1470
1581
  unsubscribeInvalidation;
1471
1582
  logger;
1472
1583
  tagIndex;
1584
+ keyDiscovery;
1473
1585
  fetchRateLimiter = new FetchRateLimiter();
1474
1586
  snapshotSerializer = new JsonSerializer();
1475
1587
  backgroundRefreshes = /* @__PURE__ */ new Map();
@@ -1819,15 +1931,16 @@ var CacheStack = class extends EventEmitter {
1819
1931
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1820
1932
  }
1821
1933
  async invalidateByPattern(pattern) {
1934
+ this.validatePattern(pattern);
1822
1935
  await this.awaitStartup("invalidateByPattern");
1823
- const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1936
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1824
1937
  await this.deleteKeys(keys);
1825
1938
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1826
1939
  }
1827
1940
  async invalidateByPrefix(prefix) {
1828
1941
  await this.awaitStartup("invalidateByPrefix");
1829
1942
  const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1830
- const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
1943
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
1831
1944
  await this.deleteKeys(keys);
1832
1945
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1833
1946
  }
@@ -2440,50 +2553,6 @@ var CacheStack = class extends EventEmitter {
2440
2553
  shouldBroadcastL1Invalidation() {
2441
2554
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
2442
2555
  }
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
2556
  shouldCleanupGenerations() {
2488
2557
  return Boolean(this.options.generationCleanup);
2489
2558
  }
@@ -2506,7 +2575,7 @@ var CacheStack = class extends EventEmitter {
2506
2575
  }
2507
2576
  async cleanupGeneration(generation) {
2508
2577
  const prefix = `v${generation}:`;
2509
- const keys = await this.collectKeysWithPrefix(prefix);
2578
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
2510
2579
  if (keys.length === 0) {
2511
2580
  return;
2512
2581
  }
@@ -2756,6 +2825,17 @@ var CacheStack = class extends EventEmitter {
2756
2825
  }
2757
2826
  return key;
2758
2827
  }
2828
+ validatePattern(pattern) {
2829
+ if (pattern.length === 0) {
2830
+ throw new Error("Pattern must not be empty.");
2831
+ }
2832
+ if (pattern.length > MAX_PATTERN_LENGTH) {
2833
+ throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
2834
+ }
2835
+ if (/[\u0000-\u001F\u007F]/.test(pattern)) {
2836
+ throw new Error("Pattern contains unsupported control characters.");
2837
+ }
2838
+ }
2759
2839
  validateTtlPolicy(name, policy) {
2760
2840
  if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2761
2841
  return;
@@ -2920,7 +3000,18 @@ var CacheStack = class extends EventEmitter {
2920
3000
  }
2921
3001
  };
2922
3002
  function createInstanceId() {
2923
- return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
3003
+ if (globalThis.crypto?.randomUUID) {
3004
+ return globalThis.crypto.randomUUID();
3005
+ }
3006
+ const bytes = new Uint8Array(16);
3007
+ if (globalThis.crypto?.getRandomValues) {
3008
+ globalThis.crypto.getRandomValues(bytes);
3009
+ } else {
3010
+ for (let i = 0; i < bytes.length; i++) {
3011
+ bytes[i] = Math.floor(Math.random() * 256);
3012
+ }
3013
+ }
3014
+ return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
2924
3015
  }
2925
3016
 
2926
3017
  // src/module.ts