layercache 1.2.8 → 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/dist/index.cjs CHANGED
@@ -528,7 +528,7 @@ function normalizeForSerialization(value) {
528
528
  }
529
529
  function serializeKeyPart(value) {
530
530
  if (typeof value === "string") {
531
- return `s:${value}`;
531
+ return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
532
532
  }
533
533
  if (typeof value === "number") {
534
534
  return `n:${value}`;
@@ -917,6 +917,7 @@ var CacheStackLayerWriter = class {
917
917
  }
918
918
  const results = await Promise.allSettled(operations.map((operation) => operation()));
919
919
  const failures = results.filter((result) => result.status === "rejected");
920
+ const degraded = results.filter((result) => result.status === "fulfilled");
920
921
  if (failures.length === 0) {
921
922
  return;
922
923
  }
@@ -1095,6 +1096,7 @@ function planFreshReadPolicies({
1095
1096
  }
1096
1097
 
1097
1098
  // src/internal/CacheStackSnapshotManager.ts
1099
+ var import_node_crypto = require("crypto");
1098
1100
  var import_node_fs = require("fs");
1099
1101
  var import_node_path = __toESM(require("path"), 1);
1100
1102
 
@@ -1194,6 +1196,42 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
1194
1196
  return Buffer.concat(chunks).toString("utf8");
1195
1197
  }
1196
1198
 
1199
+ // src/internal/StructuredDataSanitizer.ts
1200
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1201
+ function sanitizeStructuredData(value, options) {
1202
+ return sanitizeValue(value, 0, { count: 0 }, options);
1203
+ }
1204
+ function sanitizeValue(value, depth, state, options) {
1205
+ state.count += 1;
1206
+ if (state.count > options.maxNodes) {
1207
+ throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
1208
+ }
1209
+ if (depth > options.maxDepth) {
1210
+ throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
1211
+ }
1212
+ if (Array.isArray(value)) {
1213
+ const sanitized2 = [];
1214
+ for (const entry of value) {
1215
+ sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
1216
+ }
1217
+ return sanitized2;
1218
+ }
1219
+ if (!isPlainObject(value)) {
1220
+ return value;
1221
+ }
1222
+ const sanitized = options.createObject?.() ?? {};
1223
+ for (const [key, entry] of Object.entries(value)) {
1224
+ if (DANGEROUS_KEYS.has(key)) {
1225
+ continue;
1226
+ }
1227
+ sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
1228
+ }
1229
+ return sanitized;
1230
+ }
1231
+ function isPlainObject(value) {
1232
+ return Object.prototype.toString.call(value) === "[object Object]";
1233
+ }
1234
+
1197
1235
  // src/internal/CacheStackSnapshotManager.ts
1198
1236
  var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1199
1237
  var CacheStackSnapshotManager = class {
@@ -1218,7 +1256,16 @@ var CacheStackSnapshotManager = class {
1218
1256
  const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
1219
1257
  await Promise.all(
1220
1258
  batch.map(async (entry) => {
1221
- await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1259
+ await Promise.all(
1260
+ this.options.layers.map(async (layer) => {
1261
+ if (this.options.shouldSkipLayer(layer)) return;
1262
+ try {
1263
+ await layer.set(entry.key, entry.value, entry.ttl);
1264
+ } catch (error) {
1265
+ await this.options.handleLayerFailure(layer, "write", error);
1266
+ }
1267
+ })
1268
+ );
1222
1269
  await this.options.tagIndex.touch(entry.key);
1223
1270
  })
1224
1271
  );
@@ -1228,7 +1275,7 @@ var CacheStackSnapshotManager = class {
1228
1275
  const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
1229
1276
  const tempPath = import_node_path.default.join(
1230
1277
  import_node_path.default.dirname(targetPath),
1231
- `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
1278
+ `.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
1232
1279
  );
1233
1280
  let handle;
1234
1281
  try {
@@ -1328,7 +1375,13 @@ var CacheStackSnapshotManager = class {
1328
1375
  });
1329
1376
  }
1330
1377
  sanitizeSnapshotValue(value) {
1331
- return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
1378
+ const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
1379
+ return sanitizeStructuredData(roundTripped, {
1380
+ label: "Snapshot value",
1381
+ maxDepth: 64,
1382
+ maxNodes: 1e4,
1383
+ createObject: () => /* @__PURE__ */ Object.create(null)
1384
+ });
1332
1385
  }
1333
1386
  };
1334
1387
 
@@ -1708,7 +1761,13 @@ var FetchRateLimiter = class {
1708
1761
  this.pendingBuckets.add(next.bucketKey);
1709
1762
  }
1710
1763
  this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
1711
- this.drain();
1764
+ if (!this.drainTimer) {
1765
+ this.drainTimer = setTimeout(() => {
1766
+ this.drainTimer = void 0;
1767
+ this.drain();
1768
+ }, 0);
1769
+ this.drainTimer.unref?.();
1770
+ }
1712
1771
  });
1713
1772
  }
1714
1773
  }
@@ -1750,6 +1809,9 @@ var FetchRateLimiter = class {
1750
1809
  }
1751
1810
  if (this.buckets.size >= MAX_BUCKETS) {
1752
1811
  this.evictIdleBuckets();
1812
+ if (this.buckets.size >= MAX_BUCKETS) {
1813
+ throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
1814
+ }
1753
1815
  }
1754
1816
  const bucket = { active: 0, startedAt: [] };
1755
1817
  this.buckets.set(bucketKey, bucket);
@@ -2228,38 +2290,6 @@ var TagIndex = class {
2228
2290
  }
2229
2291
  };
2230
2292
 
2231
- // src/internal/StructuredDataSanitizer.ts
2232
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2233
- function sanitizeStructuredData(value, options) {
2234
- return sanitizeValue(value, 0, { count: 0 }, options);
2235
- }
2236
- function sanitizeValue(value, depth, state, options) {
2237
- state.count += 1;
2238
- if (state.count > options.maxNodes) {
2239
- throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
2240
- }
2241
- if (depth > options.maxDepth) {
2242
- throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
2243
- }
2244
- if (Array.isArray(value)) {
2245
- return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
2246
- }
2247
- if (!isPlainObject(value)) {
2248
- return value;
2249
- }
2250
- const sanitized = options.createObject?.() ?? {};
2251
- for (const [key, entry] of Object.entries(value)) {
2252
- if (DANGEROUS_KEYS.has(key)) {
2253
- continue;
2254
- }
2255
- sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
2256
- }
2257
- return sanitized;
2258
- }
2259
- function isPlainObject(value) {
2260
- return Object.prototype.toString.call(value) === "[object Object]";
2261
- }
2262
-
2263
2293
  // src/serialization/JsonSerializer.ts
2264
2294
  var JsonSerializer = class {
2265
2295
  serialize(value) {
@@ -2276,29 +2306,35 @@ var JsonSerializer = class {
2276
2306
  };
2277
2307
 
2278
2308
  // src/stampede/StampedeGuard.ts
2279
- var import_async_mutex2 = require("async-mutex");
2280
2309
  var StampedeGuard = class {
2281
- mutexes = /* @__PURE__ */ new Map();
2310
+ inFlight = /* @__PURE__ */ new Map();
2282
2311
  async execute(key, task) {
2283
- const entry = this.getMutexEntry(key);
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);
2284
2326
  try {
2285
- return await entry.mutex.runExclusive(task);
2327
+ return await entry.promise;
2286
2328
  } finally {
2287
- entry.references -= 1;
2288
- const current = this.mutexes.get(key);
2289
- if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
2290
- this.mutexes.delete(key);
2291
- }
2329
+ this.releaseEntry(key, entry);
2292
2330
  }
2293
2331
  }
2294
- getMutexEntry(key) {
2295
- let entry = this.mutexes.get(key);
2296
- if (!entry) {
2297
- entry = { mutex: new import_async_mutex2.Mutex(), references: 0 };
2298
- this.mutexes.set(key, entry);
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);
2299
2337
  }
2300
- entry.references += 1;
2301
- return entry;
2302
2338
  }
2303
2339
  };
2304
2340
 
@@ -2424,6 +2460,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
2424
2460
  tagIndex: this.tagIndex,
2425
2461
  snapshotSerializer: this.snapshotSerializer,
2426
2462
  readLayerEntry: this.readLayerEntry.bind(this),
2463
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2464
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2427
2465
  qualifyKey: this.qualifyKey.bind(this),
2428
2466
  stripQualifiedKey: this.stripQualifiedKey.bind(this),
2429
2467
  validateCacheKey,
@@ -2448,6 +2486,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2448
2486
  layerWriter;
2449
2487
  snapshots;
2450
2488
  backgroundRefreshes = /* @__PURE__ */ new Map();
2489
+ backgroundRefreshAbort = /* @__PURE__ */ new Map();
2451
2490
  layerDegradedUntil = /* @__PURE__ */ new Map();
2452
2491
  maintenance = new CacheStackMaintenance();
2453
2492
  ttlResolver;
@@ -2510,7 +2549,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2510
2549
  if (!fetcher) {
2511
2550
  return null;
2512
2551
  }
2513
- return this.fetchWithGuards(normalizedKey, fetcher, options);
2552
+ return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
2514
2553
  }
2515
2554
  /**
2516
2555
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
@@ -2692,7 +2731,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2692
2731
  }
2693
2732
  for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2694
2733
  const layer = this.layers[layerIndex];
2695
- if (!layer) continue;
2734
+ if (!layer || this.shouldSkipLayer(layer)) continue;
2696
2735
  const keys = [...pending];
2697
2736
  if (keys.length === 0) {
2698
2737
  break;
@@ -2709,6 +2748,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2709
2748
  await layer.delete(key);
2710
2749
  continue;
2711
2750
  }
2751
+ if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
2752
+ this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2753
+ }
2712
2754
  await this.tagIndex.touch(key);
2713
2755
  await this.backfill(key, stored, layerIndex - 1);
2714
2756
  resultsByKey.set(key, resolved.value);
@@ -2964,7 +3006,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
2964
3006
  await this.unsubscribeInvalidation?.();
2965
3007
  await this.flushWriteBehindQueue();
2966
3008
  await this.maintenance.waitForGenerationCleanup();
2967
- await Promise.allSettled([...this.backgroundRefreshes.values()]);
3009
+ for (const key of this.backgroundRefreshAbort.keys()) {
3010
+ this.backgroundRefreshAbort.set(key, true);
3011
+ }
3012
+ await Promise.allSettled(
3013
+ [...this.backgroundRefreshes.values()].map((promise) => {
3014
+ let timer;
3015
+ return Promise.race([
3016
+ promise,
3017
+ new Promise((resolve2) => {
3018
+ timer = setTimeout(resolve2, 5e3);
3019
+ timer.unref?.();
3020
+ })
3021
+ ]).finally(() => {
3022
+ if (timer) clearTimeout(timer);
3023
+ });
3024
+ })
3025
+ );
3026
+ this.backgroundRefreshes.clear();
3027
+ this.backgroundRefreshAbort.clear();
2968
3028
  this.maintenance.disposeWriteBehindTimer();
2969
3029
  this.fetchRateLimiter.dispose();
2970
3030
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
@@ -2980,12 +3040,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
2980
3040
  await this.handleInvalidationMessage(message);
2981
3041
  });
2982
3042
  }
2983
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3043
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
2984
3044
  const fetchTask = async () => {
2985
- const secondHit = await this.readFromLayers(key, options, "fresh-only");
2986
- if (secondHit.found) {
2987
- this.metricsCollector.increment("hits");
2988
- return secondHit.value;
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
+ }
2989
3052
  }
2990
3053
  return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2991
3054
  };
@@ -2993,12 +3056,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
2993
3056
  if (!this.options.singleFlightCoordinator) {
2994
3057
  return fetchTask();
2995
3058
  }
2996
- return this.options.singleFlightCoordinator.execute(
2997
- key,
2998
- this.resolveSingleFlightOptions(),
2999
- fetchTask,
3000
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
3001
- );
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
+ }
3002
3075
  };
3003
3076
  if (this.options.stampedePrevention === false) {
3004
3077
  return singleFlightTask();
@@ -3231,15 +3304,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
3231
3304
  }
3232
3305
  const clearEpoch = this.maintenance.currentClearEpoch();
3233
3306
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
3307
+ this.backgroundRefreshAbort.set(key, false);
3234
3308
  const refresh = (async () => {
3235
3309
  this.metricsCollector.increment("refreshes");
3236
3310
  try {
3311
+ if (this.backgroundRefreshAbort.get(key)) return;
3237
3312
  await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
3238
3313
  } catch (error) {
3314
+ if (this.backgroundRefreshAbort.get(key)) return;
3239
3315
  this.metricsCollector.increment("refreshErrors");
3240
3316
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
3241
3317
  } finally {
3242
3318
  this.backgroundRefreshes.delete(key);
3319
+ this.backgroundRefreshAbort.delete(key);
3243
3320
  }
3244
3321
  })();
3245
3322
  this.backgroundRefreshes.set(key, refresh);
@@ -3342,7 +3419,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3342
3419
  timer.unref?.();
3343
3420
  })
3344
3421
  ]);
3345
- if (result && typeof result === "object" && "kind" in result) {
3422
+ if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
3346
3423
  if (result.kind === "error") {
3347
3424
  throw result.error;
3348
3425
  }
@@ -3360,7 +3437,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3360
3437
  }
3361
3438
  async observeOperation(name, attributes, execute) {
3362
3439
  const id = this.nextOperationId;
3363
- this.nextOperationId += 1;
3440
+ this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
3364
3441
  this.emit("operation-start", { id, name, attributes });
3365
3442
  try {
3366
3443
  const result = await execute();
@@ -3596,6 +3673,7 @@ var RedisInvalidationBus = class {
3596
3673
  logger;
3597
3674
  handlers = /* @__PURE__ */ new Set();
3598
3675
  sharedListener;
3676
+ subscribePromise;
3599
3677
  constructor(options) {
3600
3678
  this.publisher = options.publisher;
3601
3679
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
@@ -3603,15 +3681,27 @@ var RedisInvalidationBus = class {
3603
3681
  this.logger = options.logger;
3604
3682
  }
3605
3683
  async subscribe(handler) {
3606
- if (this.handlers.size === 0) {
3607
- const listener = (_channel, payload) => {
3608
- void this.dispatchToHandlers(payload);
3609
- };
3610
- this.sharedListener = listener;
3611
- this.subscriber.on("message", listener);
3612
- await this.subscriber.subscribe(this.channel);
3684
+ const previousPromise = this.subscribePromise;
3685
+ let resolveThis;
3686
+ this.subscribePromise = new Promise((resolve2) => {
3687
+ resolveThis = resolve2;
3688
+ });
3689
+ if (previousPromise) {
3690
+ await previousPromise;
3691
+ }
3692
+ try {
3693
+ if (this.handlers.size === 0) {
3694
+ const listener = (_channel, payload) => {
3695
+ void this.dispatchToHandlers(payload);
3696
+ };
3697
+ this.sharedListener = listener;
3698
+ this.subscriber.on("message", listener);
3699
+ await this.subscriber.subscribe(this.channel);
3700
+ }
3701
+ this.handlers.add(handler);
3702
+ } finally {
3703
+ resolveThis();
3613
3704
  }
3614
- this.handlers.add(handler);
3615
3705
  return async () => {
3616
3706
  this.handlers.delete(handler);
3617
3707
  if (this.handlers.size === 0 && this.sharedListener) {
@@ -4037,10 +4127,21 @@ function normalizeUrl2(url) {
4037
4127
  }
4038
4128
 
4039
4129
  // src/integrations/opentelemetry.ts
4130
+ var MAX_SPANS = 1e4;
4040
4131
  function createOpenTelemetryPlugin(cache, tracer) {
4041
4132
  const spans = /* @__PURE__ */ new Map();
4042
4133
  const onStart = (event) => {
4043
- spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
4134
+ try {
4135
+ if (spans.size >= MAX_SPANS) {
4136
+ const oldest = spans.keys().next().value;
4137
+ if (oldest !== void 0) {
4138
+ spans.get(oldest)?.end();
4139
+ spans.delete(oldest);
4140
+ }
4141
+ }
4142
+ spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
4143
+ } catch {
4144
+ }
4044
4145
  };
4045
4146
  const onEnd = (event) => {
4046
4147
  const span = spans.get(event.id);
@@ -4048,12 +4149,15 @@ function createOpenTelemetryPlugin(cache, tracer) {
4048
4149
  return;
4049
4150
  }
4050
4151
  spans.delete(event.id);
4051
- span.setAttribute?.("layercache.success", event.success);
4052
- if (event.result) {
4053
- span.setAttribute?.("layercache.result", event.result);
4054
- }
4055
- if (event.error !== void 0) {
4056
- span.recordException?.(event.error);
4152
+ try {
4153
+ span.setAttribute?.("layercache.success", event.success);
4154
+ if (event.result) {
4155
+ span.setAttribute?.("layercache.result", event.result);
4156
+ }
4157
+ if (event.error !== void 0) {
4158
+ span.recordException?.(event.error);
4159
+ }
4160
+ } catch {
4057
4161
  }
4058
4162
  span.end();
4059
4163
  };
@@ -4308,6 +4412,7 @@ var RedisLayer = class {
4308
4412
  compression;
4309
4413
  compressionThreshold;
4310
4414
  decompressionMaxBytes;
4415
+ commandTimeoutMs;
4311
4416
  disconnectOnDispose;
4312
4417
  constructor(options) {
4313
4418
  this.client = options.client;
@@ -4320,6 +4425,7 @@ var RedisLayer = class {
4320
4425
  this.compression = options.compression;
4321
4426
  this.compressionThreshold = options.compressionThreshold ?? 1024;
4322
4427
  this.decompressionMaxBytes = options.decompressionMaxBytes ?? 64 * 1024 * 1024;
4428
+ this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
4323
4429
  this.disconnectOnDispose = options.disconnectOnDispose ?? false;
4324
4430
  }
4325
4431
  async get(key) {
@@ -4327,7 +4433,7 @@ var RedisLayer = class {
4327
4433
  return unwrapStoredValue(payload);
4328
4434
  }
4329
4435
  async getEntry(key) {
4330
- const payload = await this.client.getBuffer(this.withPrefix(key));
4436
+ const payload = await this.runCommand(`get("${key}")`, () => this.client.getBuffer(this.withPrefix(key)));
4331
4437
  if (payload === null) {
4332
4438
  return null;
4333
4439
  }
@@ -4341,7 +4447,7 @@ var RedisLayer = class {
4341
4447
  for (const key of keys) {
4342
4448
  pipeline.getBuffer(this.withPrefix(key));
4343
4449
  }
4344
- const results = await pipeline.exec();
4450
+ const results = await this.runCommand(`mget(${keys.length})`, () => pipeline.exec());
4345
4451
  if (results === null) {
4346
4452
  return keys.map(() => null);
4347
4453
  }
@@ -4370,33 +4476,36 @@ var RedisLayer = class {
4370
4476
  pipeline.set(normalizedKey, payload);
4371
4477
  }
4372
4478
  }
4373
- await pipeline.exec();
4479
+ await this.runCommand(`mset(${entries.length})`, () => pipeline.exec());
4374
4480
  }
4375
4481
  async set(key, value, ttl = this.defaultTtl) {
4376
4482
  const serialized = this.primarySerializer().serialize(value);
4377
4483
  const payload = await this.encodePayload(serialized);
4378
4484
  const normalizedKey = this.withPrefix(key);
4379
4485
  if (ttl && ttl > 0) {
4380
- await this.client.set(normalizedKey, payload, "EX", ttl);
4486
+ await this.runCommand(`set("${key}")`, () => this.client.set(normalizedKey, payload, "EX", ttl));
4381
4487
  return;
4382
4488
  }
4383
- await this.client.set(normalizedKey, payload);
4489
+ await this.runCommand(`set("${key}")`, () => this.client.set(normalizedKey, payload));
4384
4490
  }
4385
4491
  async delete(key) {
4386
- await this.client.del(this.withPrefix(key));
4492
+ await this.runCommand(`delete("${key}")`, () => this.client.del(this.withPrefix(key)));
4387
4493
  }
4388
4494
  async deleteMany(keys) {
4389
4495
  if (keys.length === 0) {
4390
4496
  return;
4391
4497
  }
4392
- await this.client.del(...keys.map((key) => this.withPrefix(key)));
4498
+ await this.runCommand(
4499
+ `deleteMany(${keys.length})`,
4500
+ () => this.client.del(...keys.map((key) => this.withPrefix(key)))
4501
+ );
4393
4502
  }
4394
4503
  async has(key) {
4395
- const exists = await this.client.exists(this.withPrefix(key));
4504
+ const exists = await this.runCommand(`has("${key}")`, () => this.client.exists(this.withPrefix(key)));
4396
4505
  return exists > 0;
4397
4506
  }
4398
4507
  async ttl(key) {
4399
- const remaining = await this.client.ttl(this.withPrefix(key));
4508
+ const remaining = await this.runCommand(`ttl("${key}")`, () => this.client.ttl(this.withPrefix(key)));
4400
4509
  if (remaining < 0) {
4401
4510
  return null;
4402
4511
  }
@@ -4404,13 +4513,16 @@ var RedisLayer = class {
4404
4513
  }
4405
4514
  async size() {
4406
4515
  if (!this.prefix) {
4407
- return this.client.dbsize();
4516
+ return this.runCommand("dbsize()", () => this.client.dbsize());
4408
4517
  }
4409
4518
  const pattern = `${this.prefix}*`;
4410
4519
  let cursor = "0";
4411
4520
  let count = 0;
4412
4521
  do {
4413
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
4522
+ const [nextCursor, keys] = await this.runCommand(
4523
+ `scan("${pattern}")`,
4524
+ () => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
4525
+ );
4414
4526
  cursor = nextCursor;
4415
4527
  count += keys.length;
4416
4528
  } while (cursor !== "0");
@@ -4418,7 +4530,7 @@ var RedisLayer = class {
4418
4530
  }
4419
4531
  async ping() {
4420
4532
  try {
4421
- return await this.client.ping() === "PONG";
4533
+ return await this.runCommand("ping()", () => this.client.ping()) === "PONG";
4422
4534
  } catch {
4423
4535
  return false;
4424
4536
  }
@@ -4441,14 +4553,17 @@ var RedisLayer = class {
4441
4553
  const pattern = `${this.prefix}*`;
4442
4554
  let cursor = "0";
4443
4555
  do {
4444
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
4556
+ const [nextCursor, keys] = await this.runCommand(
4557
+ `scan("${pattern}")`,
4558
+ () => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
4559
+ );
4445
4560
  cursor = nextCursor;
4446
4561
  if (keys.length === 0) {
4447
4562
  continue;
4448
4563
  }
4449
4564
  for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
4450
4565
  const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
4451
- await this.client.del(...batch);
4566
+ await this.runCommand(`clear-del(${batch.length})`, () => this.client.del(...batch));
4452
4567
  }
4453
4568
  } while (cursor !== "0");
4454
4569
  }
@@ -4464,7 +4579,10 @@ var RedisLayer = class {
4464
4579
  const pattern = `${this.prefix}*`;
4465
4580
  let cursor = "0";
4466
4581
  do {
4467
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
4582
+ const [nextCursor, keys] = await this.runCommand(
4583
+ `scan("${pattern}")`,
4584
+ () => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
4585
+ );
4468
4586
  cursor = nextCursor;
4469
4587
  for (const key of keys) {
4470
4588
  await visitor(this.prefix ? key.slice(this.prefix.length) : key);
@@ -4475,7 +4593,10 @@ var RedisLayer = class {
4475
4593
  const matches = [];
4476
4594
  let cursor = "0";
4477
4595
  do {
4478
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
4596
+ const [nextCursor, keys] = await this.runCommand(
4597
+ `scan("${pattern}")`,
4598
+ () => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
4599
+ );
4479
4600
  cursor = nextCursor;
4480
4601
  matches.push(...keys);
4481
4602
  } while (cursor !== "0");
@@ -4507,7 +4628,7 @@ var RedisLayer = class {
4507
4628
  }
4508
4629
  async deleteCorruptedKey(key) {
4509
4630
  try {
4510
- await this.client.del(this.withPrefix(key));
4631
+ await this.runCommand(`deleteCorrupted("${key}")`, () => this.client.del(this.withPrefix(key)));
4511
4632
  } catch (deleteError) {
4512
4633
  console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
4513
4634
  }
@@ -4515,12 +4636,15 @@ var RedisLayer = class {
4515
4636
  async rewriteWithPrimarySerializer(key, value) {
4516
4637
  const serialized = this.primarySerializer().serialize(value);
4517
4638
  const payload = await this.encodePayload(serialized);
4518
- const ttl = await this.client.ttl(this.withPrefix(key));
4639
+ const ttl = await this.runCommand(`rewrite-ttl("${key}")`, () => this.client.ttl(this.withPrefix(key)));
4519
4640
  if (ttl > 0) {
4520
- await this.client.set(this.withPrefix(key), payload, "EX", ttl);
4641
+ await this.runCommand(
4642
+ `rewrite-set("${key}")`,
4643
+ () => this.client.set(this.withPrefix(key), payload, "EX", ttl)
4644
+ );
4521
4645
  return;
4522
4646
  }
4523
- await this.client.set(this.withPrefix(key), payload);
4647
+ await this.runCommand(`rewrite-set("${key}")`, () => this.client.set(this.withPrefix(key), payload));
4524
4648
  }
4525
4649
  primarySerializer() {
4526
4650
  const serializer = this.serializers[0];
@@ -4616,10 +4740,39 @@ var RedisLayer = class {
4616
4740
  source.pipe(decompressor);
4617
4741
  });
4618
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
+ }
4619
4772
  };
4620
4773
 
4621
4774
  // src/layers/DiskLayer.ts
4622
- var import_node_crypto = require("crypto");
4775
+ var import_node_crypto2 = require("crypto");
4623
4776
  var import_node_fs2 = require("fs");
4624
4777
  var import_node_path2 = require("path");
4625
4778
  var FILE_SCAN_CONCURRENCY = 32;
@@ -4672,7 +4825,7 @@ var DiskLayer = class {
4672
4825
  };
4673
4826
  const payload = this.serializer.serialize(entry);
4674
4827
  const targetPath = this.keyToPath(key);
4675
- const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
4828
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
4676
4829
  try {
4677
4830
  await import_node_fs2.promises.writeFile(tempPath, payload);
4678
4831
  await import_node_fs2.promises.rename(tempPath, targetPath);
@@ -4772,7 +4925,7 @@ var DiskLayer = class {
4772
4925
  async dispose() {
4773
4926
  }
4774
4927
  keyToPath(key) {
4775
- const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
4928
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
4776
4929
  return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
4777
4930
  }
4778
4931
  resolveDirectory(directory) {
@@ -5050,7 +5203,7 @@ var MsgpackSerializer = class {
5050
5203
  };
5051
5204
 
5052
5205
  // src/singleflight/RedisSingleFlightCoordinator.ts
5053
- var import_node_crypto2 = require("crypto");
5206
+ var import_node_crypto3 = require("crypto");
5054
5207
  var RELEASE_SCRIPT = `
5055
5208
  if redis.call("get", KEYS[1]) == ARGV[1] then
5056
5209
  return redis.call("del", KEYS[1])
@@ -5066,14 +5219,19 @@ return 0
5066
5219
  var RedisSingleFlightCoordinator = class {
5067
5220
  client;
5068
5221
  prefix;
5222
+ commandTimeoutMs;
5069
5223
  constructor(options) {
5070
5224
  this.client = options.client;
5071
5225
  this.prefix = options.prefix ?? "layercache:singleflight";
5226
+ this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
5072
5227
  }
5073
5228
  async execute(key, options, worker, waiter) {
5074
5229
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
5075
- const token = (0, import_node_crypto2.randomUUID)();
5076
- const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
5230
+ const token = (0, import_node_crypto3.randomUUID)();
5231
+ const acquired = await this.runCommand(
5232
+ `acquire("${key}")`,
5233
+ () => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")
5234
+ );
5077
5235
  if (acquired === "OK") {
5078
5236
  const renewTimer = this.startLeaseRenewal(lockKey, token, options);
5079
5237
  try {
@@ -5082,7 +5240,7 @@ var RedisSingleFlightCoordinator = class {
5082
5240
  if (renewTimer) {
5083
5241
  clearInterval(renewTimer);
5084
5242
  }
5085
- await this.client.eval(RELEASE_SCRIPT, 1, lockKey, token);
5243
+ await this.runCommand(`release("${key}")`, () => this.client.eval(RELEASE_SCRIPT, 1, lockKey, token));
5086
5244
  }
5087
5245
  }
5088
5246
  return waiter();
@@ -5093,11 +5251,45 @@ var RedisSingleFlightCoordinator = class {
5093
5251
  return void 0;
5094
5252
  }
5095
5253
  const timer = setInterval(() => {
5096
- void this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs)).catch(() => void 0);
5254
+ void this.runCommand(
5255
+ `renew("${lockKey}")`,
5256
+ () => this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs))
5257
+ ).catch(() => void 0);
5097
5258
  }, renewIntervalMs);
5098
5259
  timer.unref?.();
5099
5260
  return timer;
5100
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
+ }
5101
5293
  };
5102
5294
 
5103
5295
  // src/metrics/PrometheusExporter.ts