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.
- package/README.md +2 -13
- package/dist/cli.cjs +23 -1
- package/dist/cli.js +23 -1
- package/dist/{edge-BXWTKlI1.d.cts → edge-CUHTP9Bc.d.cts} +2 -0
- package/dist/{edge-BXWTKlI1.d.ts → edge-CUHTP9Bc.d.ts} +2 -0
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +267 -31
- package/dist/index.d.cts +48 -3
- package/dist/index.d.ts +48 -3
- package/dist/index.js +265 -29
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +57 -11
- package/packages/nestjs/dist/index.d.cts +2 -0
- package/packages/nestjs/dist/index.d.ts +2 -0
- package/packages/nestjs/dist/index.js +57 -11
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
})
|