layercache 1.2.4 → 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,17 @@ 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
+ }
470
482
 
471
483
  // ../../src/invalidation/PatternMatcher.ts
472
484
  var PatternMatcher = class _PatternMatcher {
@@ -587,9 +599,7 @@ var CircuitBreakerManager = class {
587
599
  }
588
600
  const now = Date.now();
589
601
  if (state.openUntil <= now) {
590
- state.openUntil = null;
591
- state.failures = 0;
592
- this.breakers.set(key, state);
602
+ this.breakers.delete(key);
593
603
  return;
594
604
  }
595
605
  const remainingMs = state.openUntil - now;
@@ -600,15 +610,15 @@ var CircuitBreakerManager = class {
600
610
  if (!options) {
601
611
  return;
602
612
  }
613
+ this.pruneIfNeeded();
603
614
  const failureThreshold = options.failureThreshold ?? 3;
604
615
  const cooldownMs = options.cooldownMs ?? 3e4;
605
- const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
616
+ const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
606
617
  state.failures += 1;
607
618
  if (state.failures >= failureThreshold) {
608
619
  state.openUntil = Date.now() + cooldownMs;
609
620
  }
610
621
  this.breakers.set(key, state);
611
- this.pruneIfNeeded();
612
622
  }
613
623
  recordSuccess(key) {
614
624
  this.breakers.delete(key);
@@ -619,8 +629,7 @@ var CircuitBreakerManager = class {
619
629
  return false;
620
630
  }
621
631
  if (state.openUntil <= Date.now()) {
622
- state.openUntil = null;
623
- state.failures = 0;
632
+ this.breakers.delete(key);
624
633
  return false;
625
634
  }
626
635
  return true;
@@ -644,15 +653,20 @@ var CircuitBreakerManager = class {
644
653
  if (this.breakers.size <= this.maxEntries) {
645
654
  return;
646
655
  }
656
+ const now = Date.now();
647
657
  for (const [key, state] of this.breakers.entries()) {
648
658
  if (this.breakers.size <= this.maxEntries) {
649
- break;
659
+ return;
650
660
  }
651
- if (!state.openUntil || state.openUntil <= Date.now()) {
661
+ if (!state.openUntil || state.openUntil <= now) {
652
662
  this.breakers.delete(key);
653
663
  }
654
664
  }
655
- 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) {
656
670
  if (this.breakers.size <= this.maxEntries) {
657
671
  break;
658
672
  }
@@ -662,6 +676,7 @@ var CircuitBreakerManager = class {
662
676
  };
663
677
 
664
678
  // ../../src/internal/FetchRateLimiter.ts
679
+ var MAX_BUCKETS = 1e4;
665
680
  var FetchRateLimiter = class {
666
681
  buckets = /* @__PURE__ */ new Map();
667
682
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -827,10 +842,25 @@ var FetchRateLimiter = class {
827
842
  if (existing) {
828
843
  return existing;
829
844
  }
845
+ if (this.buckets.size >= MAX_BUCKETS) {
846
+ this.evictIdleBuckets();
847
+ }
830
848
  const bucket = { active: 0, startedAt: [] };
831
849
  this.buckets.set(bucketKey, bucket);
832
850
  return bucket;
833
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
+ }
834
864
  cleanupBucket(bucketKey, bucket, intervalMs) {
835
865
  const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
836
866
  if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
@@ -1157,18 +1187,18 @@ var TtlResolver = class {
1157
1187
  return;
1158
1188
  }
1159
1189
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
1160
- let removed = 0;
1161
- for (const key of this.accessProfiles.keys()) {
1162
- if (removed >= toRemove) {
1163
- 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]);
1164
1195
  }
1165
- this.accessProfiles.delete(key);
1166
- removed += 1;
1167
1196
  }
1168
1197
  }
1169
1198
  };
1170
1199
 
1171
1200
  // ../../src/invalidation/TagIndex.ts
1201
+ var MAX_PATTERN_RECURSION_DEPTH = 500;
1172
1202
  var TagIndex = class {
1173
1203
  tagToKeys = /* @__PURE__ */ new Map();
1174
1204
  keyToTags = /* @__PURE__ */ new Map();
@@ -1223,7 +1253,7 @@ var TagIndex = class {
1223
1253
  }
1224
1254
  async matchPattern(pattern) {
1225
1255
  const matches = /* @__PURE__ */ new Set();
1226
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
1256
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
1227
1257
  return [...matches];
1228
1258
  }
1229
1259
  async clear() {
@@ -1275,7 +1305,10 @@ var TagIndex = class {
1275
1305
  this.collectFromNode(child, `${prefix}${character}`, matches);
1276
1306
  }
1277
1307
  }
1278
- 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
+ }
1279
1312
  const stateKey = `${node.id}:${patternIndex}`;
1280
1313
  if (visited.has(stateKey)) {
1281
1314
  return;
@@ -1292,21 +1325,37 @@ var TagIndex = class {
1292
1325
  return;
1293
1326
  }
1294
1327
  if (patternChar === "*") {
1295
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
1328
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
1296
1329
  for (const [character, child2] of node.children) {
1297
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
1330
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
1298
1331
  }
1299
1332
  return;
1300
1333
  }
1301
1334
  if (patternChar === "?") {
1302
1335
  for (const [character, child2] of node.children) {
1303
- 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
+ );
1304
1345
  }
1305
1346
  return;
1306
1347
  }
1307
1348
  const child = node.children.get(patternChar);
1308
1349
  if (child) {
1309
- 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
+ );
1310
1359
  }
1311
1360
  }
1312
1361
  pruneKnownKeysIfNeeded() {
@@ -1448,6 +1497,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1448
1497
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1449
1498
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1450
1499
  var MAX_CACHE_KEY_LENGTH = 1024;
1500
+ var MAX_PATTERN_LENGTH = 1024;
1451
1501
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1452
1502
  var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1453
1503
  var DebugLogger = class {
@@ -1881,6 +1931,7 @@ var CacheStack = class extends EventEmitter {
1881
1931
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1882
1932
  }
1883
1933
  async invalidateByPattern(pattern) {
1934
+ this.validatePattern(pattern);
1884
1935
  await this.awaitStartup("invalidateByPattern");
1885
1936
  const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1886
1937
  await this.deleteKeys(keys);
@@ -2774,6 +2825,17 @@ var CacheStack = class extends EventEmitter {
2774
2825
  }
2775
2826
  return key;
2776
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
+ }
2777
2839
  validateTtlPolicy(name, policy) {
2778
2840
  if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2779
2841
  return;
@@ -2938,7 +3000,18 @@ var CacheStack = class extends EventEmitter {
2938
3000
  }
2939
3001
  };
2940
3002
  function createInstanceId() {
2941
- 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("")}`;
2942
3015
  }
2943
3016
 
2944
3017
  // src/module.ts