layercache 1.2.9 → 1.3.0
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 +10 -3
- package/benchmarks/direct.ts +221 -0
- package/benchmarks/edge-utils.ts +28 -0
- package/benchmarks/edge.ts +491 -0
- package/benchmarks/http.ts +99 -0
- package/benchmarks/memory-pressure.ts +144 -0
- package/benchmarks/multi-process-fanout.ts +231 -0
- package/benchmarks/multi-process-worker.ts +151 -0
- package/benchmarks/paths.ts +25 -0
- package/benchmarks/queue-amplification-utils.ts +48 -0
- package/benchmarks/queue-amplification.ts +230 -0
- package/benchmarks/redis-latency-proxy.ts +100 -0
- package/benchmarks/redis.ts +107 -0
- package/benchmarks/scenario-utils.ts +38 -0
- package/benchmarks/server.ts +157 -0
- package/benchmarks/slow-redis-latency.ts +309 -0
- package/benchmarks/slow-redis-utils.ts +29 -0
- package/benchmarks/slow-redis.ts +47 -0
- package/benchmarks/stats.ts +46 -0
- package/benchmarks/workload.ts +77 -0
- package/dist/index.cjs +158 -51
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +158 -51
- package/package.json +12 -2
- package/packages/nestjs/dist/index.cjs +47 -27
- package/packages/nestjs/dist/index.js +47 -27
package/dist/index.cjs
CHANGED
|
@@ -2306,29 +2306,35 @@ var JsonSerializer = class {
|
|
|
2306
2306
|
};
|
|
2307
2307
|
|
|
2308
2308
|
// src/stampede/StampedeGuard.ts
|
|
2309
|
-
var import_async_mutex2 = require("async-mutex");
|
|
2310
2309
|
var StampedeGuard = class {
|
|
2311
|
-
|
|
2310
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
2312
2311
|
async execute(key, task) {
|
|
2313
|
-
const
|
|
2312
|
+
const existing = this.inFlight.get(key);
|
|
2313
|
+
if (existing) {
|
|
2314
|
+
existing.references += 1;
|
|
2315
|
+
try {
|
|
2316
|
+
return await existing.promise;
|
|
2317
|
+
} finally {
|
|
2318
|
+
this.releaseEntry(key, existing);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const entry = {
|
|
2322
|
+
promise: Promise.resolve().then(task),
|
|
2323
|
+
references: 1
|
|
2324
|
+
};
|
|
2325
|
+
this.inFlight.set(key, entry);
|
|
2314
2326
|
try {
|
|
2315
|
-
return await entry.
|
|
2327
|
+
return await entry.promise;
|
|
2316
2328
|
} finally {
|
|
2317
|
-
|
|
2318
|
-
const current = this.mutexes.get(key);
|
|
2319
|
-
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
2320
|
-
this.mutexes.delete(key);
|
|
2321
|
-
}
|
|
2329
|
+
this.releaseEntry(key, entry);
|
|
2322
2330
|
}
|
|
2323
2331
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
this.
|
|
2332
|
+
releaseEntry(key, entry) {
|
|
2333
|
+
entry.references -= 1;
|
|
2334
|
+
const current = this.inFlight.get(key);
|
|
2335
|
+
if (current === entry && entry.references === 0) {
|
|
2336
|
+
this.inFlight.delete(key);
|
|
2329
2337
|
}
|
|
2330
|
-
entry.references += 1;
|
|
2331
|
-
return entry;
|
|
2332
2338
|
}
|
|
2333
2339
|
};
|
|
2334
2340
|
|
|
@@ -2543,7 +2549,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2543
2549
|
if (!fetcher) {
|
|
2544
2550
|
return null;
|
|
2545
2551
|
}
|
|
2546
|
-
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
2552
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
|
|
2547
2553
|
}
|
|
2548
2554
|
/**
|
|
2549
2555
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
@@ -3034,12 +3040,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3034
3040
|
await this.handleInvalidationMessage(message);
|
|
3035
3041
|
});
|
|
3036
3042
|
}
|
|
3037
|
-
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
3043
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
|
|
3038
3044
|
const fetchTask = async () => {
|
|
3039
|
-
const
|
|
3040
|
-
if (
|
|
3041
|
-
this.
|
|
3042
|
-
|
|
3045
|
+
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
3046
|
+
if (shouldRecheckFreshLayers) {
|
|
3047
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
3048
|
+
if (secondHit.found) {
|
|
3049
|
+
this.metricsCollector.increment("hits");
|
|
3050
|
+
return secondHit.value;
|
|
3051
|
+
}
|
|
3043
3052
|
}
|
|
3044
3053
|
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
3045
3054
|
};
|
|
@@ -3047,12 +3056,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3047
3056
|
if (!this.options.singleFlightCoordinator) {
|
|
3048
3057
|
return fetchTask();
|
|
3049
3058
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3059
|
+
try {
|
|
3060
|
+
return await this.options.singleFlightCoordinator.execute(
|
|
3061
|
+
key,
|
|
3062
|
+
this.resolveSingleFlightOptions(),
|
|
3063
|
+
fetchTask,
|
|
3064
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
3065
|
+
);
|
|
3066
|
+
} catch (error) {
|
|
3067
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
3068
|
+
throw error;
|
|
3069
|
+
}
|
|
3070
|
+
this.metricsCollector.increment("degradedOperations");
|
|
3071
|
+
this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
|
|
3072
|
+
this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
|
|
3073
|
+
return fetchTask();
|
|
3074
|
+
}
|
|
3056
3075
|
};
|
|
3057
3076
|
if (this.options.stampedePrevention === false) {
|
|
3058
3077
|
return singleFlightTask();
|
|
@@ -4393,6 +4412,7 @@ var RedisLayer = class {
|
|
|
4393
4412
|
compression;
|
|
4394
4413
|
compressionThreshold;
|
|
4395
4414
|
decompressionMaxBytes;
|
|
4415
|
+
commandTimeoutMs;
|
|
4396
4416
|
disconnectOnDispose;
|
|
4397
4417
|
constructor(options) {
|
|
4398
4418
|
this.client = options.client;
|
|
@@ -4405,6 +4425,7 @@ var RedisLayer = class {
|
|
|
4405
4425
|
this.compression = options.compression;
|
|
4406
4426
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
4407
4427
|
this.decompressionMaxBytes = options.decompressionMaxBytes ?? 64 * 1024 * 1024;
|
|
4428
|
+
this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
|
|
4408
4429
|
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
4409
4430
|
}
|
|
4410
4431
|
async get(key) {
|
|
@@ -4412,7 +4433,7 @@ var RedisLayer = class {
|
|
|
4412
4433
|
return unwrapStoredValue(payload);
|
|
4413
4434
|
}
|
|
4414
4435
|
async getEntry(key) {
|
|
4415
|
-
const payload = await this.client.getBuffer(this.withPrefix(key));
|
|
4436
|
+
const payload = await this.runCommand(`get("${key}")`, () => this.client.getBuffer(this.withPrefix(key)));
|
|
4416
4437
|
if (payload === null) {
|
|
4417
4438
|
return null;
|
|
4418
4439
|
}
|
|
@@ -4426,7 +4447,7 @@ var RedisLayer = class {
|
|
|
4426
4447
|
for (const key of keys) {
|
|
4427
4448
|
pipeline.getBuffer(this.withPrefix(key));
|
|
4428
4449
|
}
|
|
4429
|
-
const results = await pipeline.exec();
|
|
4450
|
+
const results = await this.runCommand(`mget(${keys.length})`, () => pipeline.exec());
|
|
4430
4451
|
if (results === null) {
|
|
4431
4452
|
return keys.map(() => null);
|
|
4432
4453
|
}
|
|
@@ -4455,33 +4476,36 @@ var RedisLayer = class {
|
|
|
4455
4476
|
pipeline.set(normalizedKey, payload);
|
|
4456
4477
|
}
|
|
4457
4478
|
}
|
|
4458
|
-
await pipeline.exec();
|
|
4479
|
+
await this.runCommand(`mset(${entries.length})`, () => pipeline.exec());
|
|
4459
4480
|
}
|
|
4460
4481
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4461
4482
|
const serialized = this.primarySerializer().serialize(value);
|
|
4462
4483
|
const payload = await this.encodePayload(serialized);
|
|
4463
4484
|
const normalizedKey = this.withPrefix(key);
|
|
4464
4485
|
if (ttl && ttl > 0) {
|
|
4465
|
-
await this.client.set(normalizedKey, payload, "EX", ttl);
|
|
4486
|
+
await this.runCommand(`set("${key}")`, () => this.client.set(normalizedKey, payload, "EX", ttl));
|
|
4466
4487
|
return;
|
|
4467
4488
|
}
|
|
4468
|
-
await this.client.set(normalizedKey, payload);
|
|
4489
|
+
await this.runCommand(`set("${key}")`, () => this.client.set(normalizedKey, payload));
|
|
4469
4490
|
}
|
|
4470
4491
|
async delete(key) {
|
|
4471
|
-
await this.client.del(this.withPrefix(key));
|
|
4492
|
+
await this.runCommand(`delete("${key}")`, () => this.client.del(this.withPrefix(key)));
|
|
4472
4493
|
}
|
|
4473
4494
|
async deleteMany(keys) {
|
|
4474
4495
|
if (keys.length === 0) {
|
|
4475
4496
|
return;
|
|
4476
4497
|
}
|
|
4477
|
-
await this.
|
|
4498
|
+
await this.runCommand(
|
|
4499
|
+
`deleteMany(${keys.length})`,
|
|
4500
|
+
() => this.client.del(...keys.map((key) => this.withPrefix(key)))
|
|
4501
|
+
);
|
|
4478
4502
|
}
|
|
4479
4503
|
async has(key) {
|
|
4480
|
-
const exists = await this.client.exists(this.withPrefix(key));
|
|
4504
|
+
const exists = await this.runCommand(`has("${key}")`, () => this.client.exists(this.withPrefix(key)));
|
|
4481
4505
|
return exists > 0;
|
|
4482
4506
|
}
|
|
4483
4507
|
async ttl(key) {
|
|
4484
|
-
const remaining = await this.client.ttl(this.withPrefix(key));
|
|
4508
|
+
const remaining = await this.runCommand(`ttl("${key}")`, () => this.client.ttl(this.withPrefix(key)));
|
|
4485
4509
|
if (remaining < 0) {
|
|
4486
4510
|
return null;
|
|
4487
4511
|
}
|
|
@@ -4489,13 +4513,16 @@ var RedisLayer = class {
|
|
|
4489
4513
|
}
|
|
4490
4514
|
async size() {
|
|
4491
4515
|
if (!this.prefix) {
|
|
4492
|
-
return this.client.dbsize();
|
|
4516
|
+
return this.runCommand("dbsize()", () => this.client.dbsize());
|
|
4493
4517
|
}
|
|
4494
4518
|
const pattern = `${this.prefix}*`;
|
|
4495
4519
|
let cursor = "0";
|
|
4496
4520
|
let count = 0;
|
|
4497
4521
|
do {
|
|
4498
|
-
const [nextCursor, keys] = await this.
|
|
4522
|
+
const [nextCursor, keys] = await this.runCommand(
|
|
4523
|
+
`scan("${pattern}")`,
|
|
4524
|
+
() => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
|
|
4525
|
+
);
|
|
4499
4526
|
cursor = nextCursor;
|
|
4500
4527
|
count += keys.length;
|
|
4501
4528
|
} while (cursor !== "0");
|
|
@@ -4503,7 +4530,7 @@ var RedisLayer = class {
|
|
|
4503
4530
|
}
|
|
4504
4531
|
async ping() {
|
|
4505
4532
|
try {
|
|
4506
|
-
return await this.client.ping() === "PONG";
|
|
4533
|
+
return await this.runCommand("ping()", () => this.client.ping()) === "PONG";
|
|
4507
4534
|
} catch {
|
|
4508
4535
|
return false;
|
|
4509
4536
|
}
|
|
@@ -4526,14 +4553,17 @@ var RedisLayer = class {
|
|
|
4526
4553
|
const pattern = `${this.prefix}*`;
|
|
4527
4554
|
let cursor = "0";
|
|
4528
4555
|
do {
|
|
4529
|
-
const [nextCursor, keys] = await this.
|
|
4556
|
+
const [nextCursor, keys] = await this.runCommand(
|
|
4557
|
+
`scan("${pattern}")`,
|
|
4558
|
+
() => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
|
|
4559
|
+
);
|
|
4530
4560
|
cursor = nextCursor;
|
|
4531
4561
|
if (keys.length === 0) {
|
|
4532
4562
|
continue;
|
|
4533
4563
|
}
|
|
4534
4564
|
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
4535
4565
|
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
4536
|
-
await this.client.del(...batch);
|
|
4566
|
+
await this.runCommand(`clear-del(${batch.length})`, () => this.client.del(...batch));
|
|
4537
4567
|
}
|
|
4538
4568
|
} while (cursor !== "0");
|
|
4539
4569
|
}
|
|
@@ -4549,7 +4579,10 @@ var RedisLayer = class {
|
|
|
4549
4579
|
const pattern = `${this.prefix}*`;
|
|
4550
4580
|
let cursor = "0";
|
|
4551
4581
|
do {
|
|
4552
|
-
const [nextCursor, keys] = await this.
|
|
4582
|
+
const [nextCursor, keys] = await this.runCommand(
|
|
4583
|
+
`scan("${pattern}")`,
|
|
4584
|
+
() => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
|
|
4585
|
+
);
|
|
4553
4586
|
cursor = nextCursor;
|
|
4554
4587
|
for (const key of keys) {
|
|
4555
4588
|
await visitor(this.prefix ? key.slice(this.prefix.length) : key);
|
|
@@ -4560,7 +4593,10 @@ var RedisLayer = class {
|
|
|
4560
4593
|
const matches = [];
|
|
4561
4594
|
let cursor = "0";
|
|
4562
4595
|
do {
|
|
4563
|
-
const [nextCursor, keys] = await this.
|
|
4596
|
+
const [nextCursor, keys] = await this.runCommand(
|
|
4597
|
+
`scan("${pattern}")`,
|
|
4598
|
+
() => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
|
|
4599
|
+
);
|
|
4564
4600
|
cursor = nextCursor;
|
|
4565
4601
|
matches.push(...keys);
|
|
4566
4602
|
} while (cursor !== "0");
|
|
@@ -4592,7 +4628,7 @@ var RedisLayer = class {
|
|
|
4592
4628
|
}
|
|
4593
4629
|
async deleteCorruptedKey(key) {
|
|
4594
4630
|
try {
|
|
4595
|
-
await this.client.del(this.withPrefix(key));
|
|
4631
|
+
await this.runCommand(`deleteCorrupted("${key}")`, () => this.client.del(this.withPrefix(key)));
|
|
4596
4632
|
} catch (deleteError) {
|
|
4597
4633
|
console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
|
|
4598
4634
|
}
|
|
@@ -4600,12 +4636,15 @@ var RedisLayer = class {
|
|
|
4600
4636
|
async rewriteWithPrimarySerializer(key, value) {
|
|
4601
4637
|
const serialized = this.primarySerializer().serialize(value);
|
|
4602
4638
|
const payload = await this.encodePayload(serialized);
|
|
4603
|
-
const ttl = await this.client.ttl(this.withPrefix(key));
|
|
4639
|
+
const ttl = await this.runCommand(`rewrite-ttl("${key}")`, () => this.client.ttl(this.withPrefix(key)));
|
|
4604
4640
|
if (ttl > 0) {
|
|
4605
|
-
await this.
|
|
4641
|
+
await this.runCommand(
|
|
4642
|
+
`rewrite-set("${key}")`,
|
|
4643
|
+
() => this.client.set(this.withPrefix(key), payload, "EX", ttl)
|
|
4644
|
+
);
|
|
4606
4645
|
return;
|
|
4607
4646
|
}
|
|
4608
|
-
await this.client.set(this.withPrefix(key), payload);
|
|
4647
|
+
await this.runCommand(`rewrite-set("${key}")`, () => this.client.set(this.withPrefix(key), payload));
|
|
4609
4648
|
}
|
|
4610
4649
|
primarySerializer() {
|
|
4611
4650
|
const serializer = this.serializers[0];
|
|
@@ -4701,6 +4740,35 @@ var RedisLayer = class {
|
|
|
4701
4740
|
source.pipe(decompressor);
|
|
4702
4741
|
});
|
|
4703
4742
|
}
|
|
4743
|
+
normalizeCommandTimeoutMs(value) {
|
|
4744
|
+
if (value === void 0) {
|
|
4745
|
+
return void 0;
|
|
4746
|
+
}
|
|
4747
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
4748
|
+
throw new Error("RedisLayer.commandTimeoutMs must be a positive number.");
|
|
4749
|
+
}
|
|
4750
|
+
return value;
|
|
4751
|
+
}
|
|
4752
|
+
async runCommand(operation, command) {
|
|
4753
|
+
const promise = command();
|
|
4754
|
+
if (!this.commandTimeoutMs) {
|
|
4755
|
+
return promise;
|
|
4756
|
+
}
|
|
4757
|
+
let timer;
|
|
4758
|
+
return Promise.race([
|
|
4759
|
+
promise,
|
|
4760
|
+
new Promise((_, reject) => {
|
|
4761
|
+
timer = setTimeout(() => {
|
|
4762
|
+
reject(new Error(`RedisLayer command ${operation} timed out after ${this.commandTimeoutMs}ms.`));
|
|
4763
|
+
}, this.commandTimeoutMs);
|
|
4764
|
+
timer.unref?.();
|
|
4765
|
+
})
|
|
4766
|
+
]).finally(() => {
|
|
4767
|
+
if (timer) {
|
|
4768
|
+
clearTimeout(timer);
|
|
4769
|
+
}
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4704
4772
|
};
|
|
4705
4773
|
|
|
4706
4774
|
// src/layers/DiskLayer.ts
|
|
@@ -5151,14 +5219,19 @@ return 0
|
|
|
5151
5219
|
var RedisSingleFlightCoordinator = class {
|
|
5152
5220
|
client;
|
|
5153
5221
|
prefix;
|
|
5222
|
+
commandTimeoutMs;
|
|
5154
5223
|
constructor(options) {
|
|
5155
5224
|
this.client = options.client;
|
|
5156
5225
|
this.prefix = options.prefix ?? "layercache:singleflight";
|
|
5226
|
+
this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
|
|
5157
5227
|
}
|
|
5158
5228
|
async execute(key, options, worker, waiter) {
|
|
5159
5229
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
5160
5230
|
const token = (0, import_node_crypto3.randomUUID)();
|
|
5161
|
-
const acquired = await this.
|
|
5231
|
+
const acquired = await this.runCommand(
|
|
5232
|
+
`acquire("${key}")`,
|
|
5233
|
+
() => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")
|
|
5234
|
+
);
|
|
5162
5235
|
if (acquired === "OK") {
|
|
5163
5236
|
const renewTimer = this.startLeaseRenewal(lockKey, token, options);
|
|
5164
5237
|
try {
|
|
@@ -5167,7 +5240,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
5167
5240
|
if (renewTimer) {
|
|
5168
5241
|
clearInterval(renewTimer);
|
|
5169
5242
|
}
|
|
5170
|
-
await this.client.eval(RELEASE_SCRIPT, 1, lockKey, token);
|
|
5243
|
+
await this.runCommand(`release("${key}")`, () => this.client.eval(RELEASE_SCRIPT, 1, lockKey, token));
|
|
5171
5244
|
}
|
|
5172
5245
|
}
|
|
5173
5246
|
return waiter();
|
|
@@ -5178,11 +5251,45 @@ var RedisSingleFlightCoordinator = class {
|
|
|
5178
5251
|
return void 0;
|
|
5179
5252
|
}
|
|
5180
5253
|
const timer = setInterval(() => {
|
|
5181
|
-
void this.
|
|
5254
|
+
void this.runCommand(
|
|
5255
|
+
`renew("${lockKey}")`,
|
|
5256
|
+
() => this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs))
|
|
5257
|
+
).catch(() => void 0);
|
|
5182
5258
|
}, renewIntervalMs);
|
|
5183
5259
|
timer.unref?.();
|
|
5184
5260
|
return timer;
|
|
5185
5261
|
}
|
|
5262
|
+
normalizeCommandTimeoutMs(value) {
|
|
5263
|
+
if (value === void 0) {
|
|
5264
|
+
return void 0;
|
|
5265
|
+
}
|
|
5266
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
5267
|
+
throw new Error("RedisSingleFlightCoordinator.commandTimeoutMs must be a positive number.");
|
|
5268
|
+
}
|
|
5269
|
+
return value;
|
|
5270
|
+
}
|
|
5271
|
+
async runCommand(operation, command) {
|
|
5272
|
+
const promise = command();
|
|
5273
|
+
if (!this.commandTimeoutMs) {
|
|
5274
|
+
return promise;
|
|
5275
|
+
}
|
|
5276
|
+
let timer;
|
|
5277
|
+
return Promise.race([
|
|
5278
|
+
promise,
|
|
5279
|
+
new Promise((_, reject) => {
|
|
5280
|
+
timer = setTimeout(() => {
|
|
5281
|
+
reject(
|
|
5282
|
+
new Error(`RedisSingleFlightCoordinator command ${operation} timed out after ${this.commandTimeoutMs}ms.`)
|
|
5283
|
+
);
|
|
5284
|
+
}, this.commandTimeoutMs);
|
|
5285
|
+
timer.unref?.();
|
|
5286
|
+
})
|
|
5287
|
+
]).finally(() => {
|
|
5288
|
+
if (timer) {
|
|
5289
|
+
clearTimeout(timer);
|
|
5290
|
+
}
|
|
5291
|
+
});
|
|
5292
|
+
}
|
|
5186
5293
|
};
|
|
5187
5294
|
|
|
5188
5295
|
// src/metrics/PrometheusExporter.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -203,6 +203,11 @@ interface RedisLayerOptions {
|
|
|
203
203
|
* Prevents decompression bomb attacks. Defaults to 64 MiB.
|
|
204
204
|
*/
|
|
205
205
|
decompressionMaxBytes?: number;
|
|
206
|
+
/**
|
|
207
|
+
* Per-command timeout in milliseconds for Redis round-trips.
|
|
208
|
+
* Slow commands reject so CacheStack can treat the layer as degraded.
|
|
209
|
+
*/
|
|
210
|
+
commandTimeoutMs?: number;
|
|
206
211
|
disconnectOnDispose?: boolean;
|
|
207
212
|
}
|
|
208
213
|
declare class RedisLayer implements CacheLayer {
|
|
@@ -217,6 +222,7 @@ declare class RedisLayer implements CacheLayer {
|
|
|
217
222
|
private readonly compression?;
|
|
218
223
|
private readonly compressionThreshold;
|
|
219
224
|
private readonly decompressionMaxBytes;
|
|
225
|
+
private readonly commandTimeoutMs;
|
|
220
226
|
private readonly disconnectOnDispose;
|
|
221
227
|
constructor(options: RedisLayerOptions);
|
|
222
228
|
get<T>(key: string): Promise<T | null>;
|
|
@@ -256,6 +262,8 @@ declare class RedisLayer implements CacheLayer {
|
|
|
256
262
|
*/
|
|
257
263
|
private decodePayload;
|
|
258
264
|
private decompressWithLimit;
|
|
265
|
+
private normalizeCommandTimeoutMs;
|
|
266
|
+
private runCommand;
|
|
259
267
|
}
|
|
260
268
|
|
|
261
269
|
interface DiskLayerOptions {
|
|
@@ -408,19 +416,23 @@ declare class MsgpackSerializer implements CacheSerializer {
|
|
|
408
416
|
interface RedisSingleFlightCoordinatorOptions {
|
|
409
417
|
client: Redis;
|
|
410
418
|
prefix?: string;
|
|
419
|
+
commandTimeoutMs?: number;
|
|
411
420
|
}
|
|
412
421
|
declare class RedisSingleFlightCoordinator implements CacheSingleFlightCoordinator {
|
|
413
422
|
private readonly client;
|
|
414
423
|
private readonly prefix;
|
|
424
|
+
private readonly commandTimeoutMs;
|
|
415
425
|
constructor(options: RedisSingleFlightCoordinatorOptions);
|
|
416
426
|
execute<T>(key: string, options: CacheSingleFlightExecutionOptions, worker: () => Promise<T>, waiter: () => Promise<T>): Promise<T>;
|
|
417
427
|
private startLeaseRenewal;
|
|
428
|
+
private normalizeCommandTimeoutMs;
|
|
429
|
+
private runCommand;
|
|
418
430
|
}
|
|
419
431
|
|
|
420
432
|
declare class StampedeGuard {
|
|
421
|
-
private readonly
|
|
433
|
+
private readonly inFlight;
|
|
422
434
|
execute<T>(key: string, task: () => Promise<T>): Promise<T>;
|
|
423
|
-
private
|
|
435
|
+
private releaseEntry;
|
|
424
436
|
}
|
|
425
437
|
|
|
426
438
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -203,6 +203,11 @@ interface RedisLayerOptions {
|
|
|
203
203
|
* Prevents decompression bomb attacks. Defaults to 64 MiB.
|
|
204
204
|
*/
|
|
205
205
|
decompressionMaxBytes?: number;
|
|
206
|
+
/**
|
|
207
|
+
* Per-command timeout in milliseconds for Redis round-trips.
|
|
208
|
+
* Slow commands reject so CacheStack can treat the layer as degraded.
|
|
209
|
+
*/
|
|
210
|
+
commandTimeoutMs?: number;
|
|
206
211
|
disconnectOnDispose?: boolean;
|
|
207
212
|
}
|
|
208
213
|
declare class RedisLayer implements CacheLayer {
|
|
@@ -217,6 +222,7 @@ declare class RedisLayer implements CacheLayer {
|
|
|
217
222
|
private readonly compression?;
|
|
218
223
|
private readonly compressionThreshold;
|
|
219
224
|
private readonly decompressionMaxBytes;
|
|
225
|
+
private readonly commandTimeoutMs;
|
|
220
226
|
private readonly disconnectOnDispose;
|
|
221
227
|
constructor(options: RedisLayerOptions);
|
|
222
228
|
get<T>(key: string): Promise<T | null>;
|
|
@@ -256,6 +262,8 @@ declare class RedisLayer implements CacheLayer {
|
|
|
256
262
|
*/
|
|
257
263
|
private decodePayload;
|
|
258
264
|
private decompressWithLimit;
|
|
265
|
+
private normalizeCommandTimeoutMs;
|
|
266
|
+
private runCommand;
|
|
259
267
|
}
|
|
260
268
|
|
|
261
269
|
interface DiskLayerOptions {
|
|
@@ -408,19 +416,23 @@ declare class MsgpackSerializer implements CacheSerializer {
|
|
|
408
416
|
interface RedisSingleFlightCoordinatorOptions {
|
|
409
417
|
client: Redis;
|
|
410
418
|
prefix?: string;
|
|
419
|
+
commandTimeoutMs?: number;
|
|
411
420
|
}
|
|
412
421
|
declare class RedisSingleFlightCoordinator implements CacheSingleFlightCoordinator {
|
|
413
422
|
private readonly client;
|
|
414
423
|
private readonly prefix;
|
|
424
|
+
private readonly commandTimeoutMs;
|
|
415
425
|
constructor(options: RedisSingleFlightCoordinatorOptions);
|
|
416
426
|
execute<T>(key: string, options: CacheSingleFlightExecutionOptions, worker: () => Promise<T>, waiter: () => Promise<T>): Promise<T>;
|
|
417
427
|
private startLeaseRenewal;
|
|
428
|
+
private normalizeCommandTimeoutMs;
|
|
429
|
+
private runCommand;
|
|
418
430
|
}
|
|
419
431
|
|
|
420
432
|
declare class StampedeGuard {
|
|
421
|
-
private readonly
|
|
433
|
+
private readonly inFlight;
|
|
422
434
|
execute<T>(key: string, task: () => Promise<T>): Promise<T>;
|
|
423
|
-
private
|
|
435
|
+
private releaseEntry;
|
|
424
436
|
}
|
|
425
437
|
|
|
426
438
|
/**
|