layercache 1.3.0 → 1.3.1

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.
@@ -741,15 +741,14 @@ function createInstanceId() {
741
741
  if (globalThis.crypto?.randomUUID) {
742
742
  return globalThis.crypto.randomUUID();
743
743
  }
744
- const bytes = new Uint8Array(16);
745
744
  if (globalThis.crypto?.getRandomValues) {
745
+ const bytes = new Uint8Array(16);
746
746
  globalThis.crypto.getRandomValues(bytes);
747
- } else {
748
- for (let i = 0; i < bytes.length; i += 1) {
749
- bytes[i] = Math.floor(Math.random() * 256);
750
- }
747
+ return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
751
748
  }
752
- return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
749
+ throw new Error(
750
+ "layercache requires a cryptographic random source. Neither crypto.randomUUID nor crypto.getRandomValues is available in this runtime."
751
+ );
753
752
  }
754
753
 
755
754
  // ../../src/internal/CacheStackGeneration.ts
@@ -1728,7 +1727,8 @@ var CircuitBreakerManager = class {
1728
1727
  }
1729
1728
  const remainingMs = state.openUntil - now;
1730
1729
  const remainingSecs = Math.ceil(remainingMs / 1e3);
1731
- throw new Error(`Circuit breaker is open for key "${key}" (resets in ${remainingSecs}s).`);
1730
+ const displayKey = key.length > 64 ? `${key.slice(0, 64)}...` : key;
1731
+ throw new Error(`Circuit breaker is open for key "${displayKey}" (resets in ${remainingSecs}s).`);
1732
1732
  }
1733
1733
  recordFailure(key, options) {
1734
1734
  if (!options) {
@@ -2493,7 +2493,14 @@ var JsonSerializer = class {
2493
2493
  }
2494
2494
  deserialize(payload) {
2495
2495
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2496
- return sanitizeStructuredData(JSON.parse(normalized), {
2496
+ let parsed;
2497
+ try {
2498
+ parsed = JSON.parse(normalized);
2499
+ } catch (error) {
2500
+ const message = error instanceof Error ? error.message : String(error);
2501
+ throw new Error(`JsonSerializer: failed to parse JSON payload: ${message}`);
2502
+ }
2503
+ return sanitizeStructuredData(parsed, {
2497
2504
  label: "JSON payload",
2498
2505
  maxDepth: 200,
2499
2506
  maxNodes: 1e4
@@ -2504,6 +2511,12 @@ var JsonSerializer = class {
2504
2511
  // ../../src/stampede/StampedeGuard.ts
2505
2512
  var StampedeGuard = class {
2506
2513
  inFlight = /* @__PURE__ */ new Map();
2514
+ maxInFlight;
2515
+ entryTimeoutMs;
2516
+ constructor(options = {}) {
2517
+ this.maxInFlight = options.maxInFlight ?? 1e4;
2518
+ this.entryTimeoutMs = options.entryTimeoutMs;
2519
+ }
2507
2520
  async execute(key, task) {
2508
2521
  const existing = this.inFlight.get(key);
2509
2522
  if (existing) {
@@ -2514,8 +2527,15 @@ var StampedeGuard = class {
2514
2527
  this.releaseEntry(key, existing);
2515
2528
  }
2516
2529
  }
2530
+ if (this.inFlight.size >= this.maxInFlight) {
2531
+ throw new Error(
2532
+ `StampedeGuard: in-flight limit of ${this.maxInFlight} exceeded. Rejecting new key to prevent memory exhaustion.`
2533
+ );
2534
+ }
2535
+ const taskPromise = Promise.resolve().then(task);
2536
+ const guardedPromise = this.entryTimeoutMs ? this.withTimeout(key, taskPromise, this.entryTimeoutMs) : taskPromise;
2517
2537
  const entry = {
2518
- promise: Promise.resolve().then(task),
2538
+ promise: guardedPromise,
2519
2539
  references: 1
2520
2540
  };
2521
2541
  this.inFlight.set(key, entry);
@@ -2525,6 +2545,27 @@ var StampedeGuard = class {
2525
2545
  this.releaseEntry(key, entry);
2526
2546
  }
2527
2547
  }
2548
+ withTimeout(key, promise, timeoutMs) {
2549
+ return new Promise((resolve, reject) => {
2550
+ const timer = setTimeout(() => {
2551
+ reject(
2552
+ new Error(
2553
+ `StampedeGuard: task for key "${key.slice(0, 64)}${key.length > 64 ? "..." : ""}" timed out after ${timeoutMs}ms.`
2554
+ )
2555
+ );
2556
+ }, timeoutMs);
2557
+ promise.then(
2558
+ (value) => {
2559
+ clearTimeout(timer);
2560
+ resolve(value);
2561
+ },
2562
+ (error) => {
2563
+ clearTimeout(timer);
2564
+ reject(error);
2565
+ }
2566
+ );
2567
+ });
2568
+ }
2528
2569
  releaseEntry(key, entry) {
2529
2570
  entry.references -= 1;
2530
2571
  const current = this.inFlight.get(key);
@@ -2590,6 +2631,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
2590
2631
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
2591
2632
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
2592
2633
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
2634
+ this.stampedeGuard = new StampedeGuard({
2635
+ maxInFlight: options.stampedeMaxInFlight,
2636
+ entryTimeoutMs: options.stampedeEntryTimeoutMs
2637
+ });
2593
2638
  this.currentGeneration = options.generation;
2594
2639
  if (options.publishSetInvalidation !== void 0) {
2595
2640
  console.warn(
@@ -2668,7 +2713,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2668
2713
  }
2669
2714
  layers;
2670
2715
  options;
2671
- stampedeGuard = new StampedeGuard();
2716
+ stampedeGuard;
2672
2717
  metricsCollector = new MetricsCollector();
2673
2718
  instanceId = createInstanceId();
2674
2719
  startup;
@@ -2906,7 +2951,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
2906
2951
  return promise;
2907
2952
  }
2908
2953
  if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2909
- throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2954
+ const displayKey = entry.key.length > 64 ? `${entry.key.slice(0, 64)}...` : entry.key;
2955
+ throw new Error(`mget received conflicting entries for key "${displayKey}".`);
2910
2956
  }
2911
2957
  return existing.promise;
2912
2958
  })
@@ -169,6 +169,8 @@ interface CacheStackOptions {
169
169
  logger?: CacheLogger | boolean;
170
170
  metrics?: boolean;
171
171
  stampedePrevention?: boolean;
172
+ stampedeMaxInFlight?: number;
173
+ stampedeEntryTimeoutMs?: number;
172
174
  invalidationBus?: InvalidationBus;
173
175
  tagIndex?: CacheTagIndex;
174
176
  generation?: number;
@@ -169,6 +169,8 @@ interface CacheStackOptions {
169
169
  logger?: CacheLogger | boolean;
170
170
  metrics?: boolean;
171
171
  stampedePrevention?: boolean;
172
+ stampedeMaxInFlight?: number;
173
+ stampedeEntryTimeoutMs?: number;
172
174
  invalidationBus?: InvalidationBus;
173
175
  tagIndex?: CacheTagIndex;
174
176
  generation?: number;
@@ -705,15 +705,14 @@ function createInstanceId() {
705
705
  if (globalThis.crypto?.randomUUID) {
706
706
  return globalThis.crypto.randomUUID();
707
707
  }
708
- const bytes = new Uint8Array(16);
709
708
  if (globalThis.crypto?.getRandomValues) {
709
+ const bytes = new Uint8Array(16);
710
710
  globalThis.crypto.getRandomValues(bytes);
711
- } else {
712
- for (let i = 0; i < bytes.length; i += 1) {
713
- bytes[i] = Math.floor(Math.random() * 256);
714
- }
711
+ return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
715
712
  }
716
- return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
713
+ throw new Error(
714
+ "layercache requires a cryptographic random source. Neither crypto.randomUUID nor crypto.getRandomValues is available in this runtime."
715
+ );
717
716
  }
718
717
 
719
718
  // ../../src/internal/CacheStackGeneration.ts
@@ -1692,7 +1691,8 @@ var CircuitBreakerManager = class {
1692
1691
  }
1693
1692
  const remainingMs = state.openUntil - now;
1694
1693
  const remainingSecs = Math.ceil(remainingMs / 1e3);
1695
- throw new Error(`Circuit breaker is open for key "${key}" (resets in ${remainingSecs}s).`);
1694
+ const displayKey = key.length > 64 ? `${key.slice(0, 64)}...` : key;
1695
+ throw new Error(`Circuit breaker is open for key "${displayKey}" (resets in ${remainingSecs}s).`);
1696
1696
  }
1697
1697
  recordFailure(key, options) {
1698
1698
  if (!options) {
@@ -2457,7 +2457,14 @@ var JsonSerializer = class {
2457
2457
  }
2458
2458
  deserialize(payload) {
2459
2459
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2460
- return sanitizeStructuredData(JSON.parse(normalized), {
2460
+ let parsed;
2461
+ try {
2462
+ parsed = JSON.parse(normalized);
2463
+ } catch (error) {
2464
+ const message = error instanceof Error ? error.message : String(error);
2465
+ throw new Error(`JsonSerializer: failed to parse JSON payload: ${message}`);
2466
+ }
2467
+ return sanitizeStructuredData(parsed, {
2461
2468
  label: "JSON payload",
2462
2469
  maxDepth: 200,
2463
2470
  maxNodes: 1e4
@@ -2468,6 +2475,12 @@ var JsonSerializer = class {
2468
2475
  // ../../src/stampede/StampedeGuard.ts
2469
2476
  var StampedeGuard = class {
2470
2477
  inFlight = /* @__PURE__ */ new Map();
2478
+ maxInFlight;
2479
+ entryTimeoutMs;
2480
+ constructor(options = {}) {
2481
+ this.maxInFlight = options.maxInFlight ?? 1e4;
2482
+ this.entryTimeoutMs = options.entryTimeoutMs;
2483
+ }
2471
2484
  async execute(key, task) {
2472
2485
  const existing = this.inFlight.get(key);
2473
2486
  if (existing) {
@@ -2478,8 +2491,15 @@ var StampedeGuard = class {
2478
2491
  this.releaseEntry(key, existing);
2479
2492
  }
2480
2493
  }
2494
+ if (this.inFlight.size >= this.maxInFlight) {
2495
+ throw new Error(
2496
+ `StampedeGuard: in-flight limit of ${this.maxInFlight} exceeded. Rejecting new key to prevent memory exhaustion.`
2497
+ );
2498
+ }
2499
+ const taskPromise = Promise.resolve().then(task);
2500
+ const guardedPromise = this.entryTimeoutMs ? this.withTimeout(key, taskPromise, this.entryTimeoutMs) : taskPromise;
2481
2501
  const entry = {
2482
- promise: Promise.resolve().then(task),
2502
+ promise: guardedPromise,
2483
2503
  references: 1
2484
2504
  };
2485
2505
  this.inFlight.set(key, entry);
@@ -2489,6 +2509,27 @@ var StampedeGuard = class {
2489
2509
  this.releaseEntry(key, entry);
2490
2510
  }
2491
2511
  }
2512
+ withTimeout(key, promise, timeoutMs) {
2513
+ return new Promise((resolve, reject) => {
2514
+ const timer = setTimeout(() => {
2515
+ reject(
2516
+ new Error(
2517
+ `StampedeGuard: task for key "${key.slice(0, 64)}${key.length > 64 ? "..." : ""}" timed out after ${timeoutMs}ms.`
2518
+ )
2519
+ );
2520
+ }, timeoutMs);
2521
+ promise.then(
2522
+ (value) => {
2523
+ clearTimeout(timer);
2524
+ resolve(value);
2525
+ },
2526
+ (error) => {
2527
+ clearTimeout(timer);
2528
+ reject(error);
2529
+ }
2530
+ );
2531
+ });
2532
+ }
2492
2533
  releaseEntry(key, entry) {
2493
2534
  entry.references -= 1;
2494
2535
  const current = this.inFlight.get(key);
@@ -2554,6 +2595,10 @@ var CacheStack = class extends EventEmitter {
2554
2595
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
2555
2596
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
2556
2597
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
2598
+ this.stampedeGuard = new StampedeGuard({
2599
+ maxInFlight: options.stampedeMaxInFlight,
2600
+ entryTimeoutMs: options.stampedeEntryTimeoutMs
2601
+ });
2557
2602
  this.currentGeneration = options.generation;
2558
2603
  if (options.publishSetInvalidation !== void 0) {
2559
2604
  console.warn(
@@ -2632,7 +2677,7 @@ var CacheStack = class extends EventEmitter {
2632
2677
  }
2633
2678
  layers;
2634
2679
  options;
2635
- stampedeGuard = new StampedeGuard();
2680
+ stampedeGuard;
2636
2681
  metricsCollector = new MetricsCollector();
2637
2682
  instanceId = createInstanceId();
2638
2683
  startup;
@@ -2870,7 +2915,8 @@ var CacheStack = class extends EventEmitter {
2870
2915
  return promise;
2871
2916
  }
2872
2917
  if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2873
- throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2918
+ const displayKey = entry.key.length > 64 ? `${entry.key.slice(0, 64)}...` : entry.key;
2919
+ throw new Error(`mget received conflicting entries for key "${displayKey}".`);
2874
2920
  }
2875
2921
  return existing.promise;
2876
2922
  })