layercache 1.2.8 → 1.2.9

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 CHANGED
@@ -15,8 +15,8 @@
15
15
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache_2.0-green" alt="license"></a>
16
16
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-first-3178C6?logo=typescript&logoColor=white" alt="TypeScript"></a>
17
17
  <img src="https://img.shields.io/badge/Node.js-%E2%89%A5_20-339933?logo=nodedotjs&logoColor=white" alt="Node.js >= 20">
18
- <img src="https://img.shields.io/badge/tests-397_passing-brightgreen" alt="tests">
19
- <a href="https://coveralls.io/github/flyingsquirrel0419/layercache?branch=main"><img src="https://coveralls.io/repos/github/flyingsquirrel0419/layercache/badge.svg?branch=main&t=20260409" alt="Coveralls"></a>
18
+ <img src="https://img.shields.io/badge/tests-411_passing-brightgreen" alt="tests">
19
+ <a href="https://coveralls.io/github/flyingsquirrel0419/layercache?branch=main"><img src="https://coveralls.io/repos/github/flyingsquirrel0419/layercache/badge.svg?branch=main&t=20260410" alt="Coveralls"></a>
20
20
  </p>
21
21
 
22
22
  <p align="center">
package/dist/cli.cjs CHANGED
@@ -465,6 +465,11 @@ function parseArgs(argv) {
465
465
  const token = rest[index];
466
466
  const value = rest[index + 1];
467
467
  if (token === "--redis") {
468
+ if (!value || value.startsWith("--")) {
469
+ process.stderr.write("Error: --redis requires a value (e.g. redis://localhost:6379)\n");
470
+ process.exitCode = 1;
471
+ return parsed;
472
+ }
468
473
  parsed.redisUrl = value;
469
474
  index += 1;
470
475
  } else if (token === "--pattern") {
@@ -484,6 +489,7 @@ function parseArgs(argv) {
484
489
  return parsed;
485
490
  }
486
491
  var BATCH_DELETE_SIZE = 500;
492
+ var SCAN_MAX_KEYS = 1e6;
487
493
  async function batchDelete(redis, keys) {
488
494
  for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
489
495
  const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
@@ -497,6 +503,13 @@ async function scanKeys(redis, pattern) {
497
503
  const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
498
504
  cursor = nextCursor;
499
505
  keys.push(...batch);
506
+ if (keys.length >= SCAN_MAX_KEYS) {
507
+ process.stderr.write(
508
+ `Warning: stopped scanning after ${SCAN_MAX_KEYS} keys. Use a more specific --pattern to narrow results.
509
+ `
510
+ );
511
+ return keys;
512
+ }
500
513
  } while (cursor !== "0");
501
514
  return keys;
502
515
  }
@@ -539,7 +552,7 @@ function maskRedisUrl(url) {
539
552
  return url.replace(/:([^@/]+)@/, ":***@");
540
553
  }
541
554
  }
542
- if (process.argv[1]?.includes("cli.")) {
555
+ if (process.argv[1]?.endsWith("cli.cjs") || process.argv[1]?.endsWith("cli.js")) {
543
556
  void main();
544
557
  }
545
558
  // Annotate the CommonJS export names for ESM import in node:
package/dist/cli.js CHANGED
@@ -123,6 +123,11 @@ function parseArgs(argv) {
123
123
  const token = rest[index];
124
124
  const value = rest[index + 1];
125
125
  if (token === "--redis") {
126
+ if (!value || value.startsWith("--")) {
127
+ process.stderr.write("Error: --redis requires a value (e.g. redis://localhost:6379)\n");
128
+ process.exitCode = 1;
129
+ return parsed;
130
+ }
126
131
  parsed.redisUrl = value;
127
132
  index += 1;
128
133
  } else if (token === "--pattern") {
@@ -142,6 +147,7 @@ function parseArgs(argv) {
142
147
  return parsed;
143
148
  }
144
149
  var BATCH_DELETE_SIZE = 500;
150
+ var SCAN_MAX_KEYS = 1e6;
145
151
  async function batchDelete(redis, keys) {
146
152
  for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
147
153
  const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
@@ -155,6 +161,13 @@ async function scanKeys(redis, pattern) {
155
161
  const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
156
162
  cursor = nextCursor;
157
163
  keys.push(...batch);
164
+ if (keys.length >= SCAN_MAX_KEYS) {
165
+ process.stderr.write(
166
+ `Warning: stopped scanning after ${SCAN_MAX_KEYS} keys. Use a more specific --pattern to narrow results.
167
+ `
168
+ );
169
+ return keys;
170
+ }
158
171
  } while (cursor !== "0");
159
172
  return keys;
160
173
  }
@@ -197,7 +210,7 @@ function maskRedisUrl(url) {
197
210
  return url.replace(/:([^@/]+)@/, ":***@");
198
211
  }
199
212
  }
200
- if (process.argv[1]?.includes("cli.")) {
213
+ if (process.argv[1]?.endsWith("cli.cjs") || process.argv[1]?.endsWith("cli.js")) {
201
214
  void main();
202
215
  }
203
216
  export {
@@ -556,6 +556,7 @@ declare class CacheStack extends EventEmitter {
556
556
  private readonly layerWriter;
557
557
  private readonly snapshots;
558
558
  private readonly backgroundRefreshes;
559
+ private readonly backgroundRefreshAbort;
559
560
  private readonly layerDegradedUntil;
560
561
  private readonly maintenance;
561
562
  private readonly ttlResolver;
@@ -556,6 +556,7 @@ declare class CacheStack extends EventEmitter {
556
556
  private readonly layerWriter;
557
557
  private readonly snapshots;
558
558
  private readonly backgroundRefreshes;
559
+ private readonly backgroundRefreshAbort;
559
560
  private readonly layerDegradedUntil;
560
561
  private readonly maintenance;
561
562
  private readonly ttlResolver;
package/dist/edge.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-DBs8Ko5W.cjs';
1
+ export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-BXWTKlI1.cjs';
2
2
  import 'node:events';
package/dist/edge.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-DBs8Ko5W.js';
1
+ export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-BXWTKlI1.js';
2
2
  import 'node:events';
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) {
@@ -2424,6 +2454,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
2424
2454
  tagIndex: this.tagIndex,
2425
2455
  snapshotSerializer: this.snapshotSerializer,
2426
2456
  readLayerEntry: this.readLayerEntry.bind(this),
2457
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2458
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2427
2459
  qualifyKey: this.qualifyKey.bind(this),
2428
2460
  stripQualifiedKey: this.stripQualifiedKey.bind(this),
2429
2461
  validateCacheKey,
@@ -2448,6 +2480,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2448
2480
  layerWriter;
2449
2481
  snapshots;
2450
2482
  backgroundRefreshes = /* @__PURE__ */ new Map();
2483
+ backgroundRefreshAbort = /* @__PURE__ */ new Map();
2451
2484
  layerDegradedUntil = /* @__PURE__ */ new Map();
2452
2485
  maintenance = new CacheStackMaintenance();
2453
2486
  ttlResolver;
@@ -2692,7 +2725,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2692
2725
  }
2693
2726
  for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2694
2727
  const layer = this.layers[layerIndex];
2695
- if (!layer) continue;
2728
+ if (!layer || this.shouldSkipLayer(layer)) continue;
2696
2729
  const keys = [...pending];
2697
2730
  if (keys.length === 0) {
2698
2731
  break;
@@ -2709,6 +2742,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2709
2742
  await layer.delete(key);
2710
2743
  continue;
2711
2744
  }
2745
+ if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
2746
+ this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2747
+ }
2712
2748
  await this.tagIndex.touch(key);
2713
2749
  await this.backfill(key, stored, layerIndex - 1);
2714
2750
  resultsByKey.set(key, resolved.value);
@@ -2964,7 +3000,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
2964
3000
  await this.unsubscribeInvalidation?.();
2965
3001
  await this.flushWriteBehindQueue();
2966
3002
  await this.maintenance.waitForGenerationCleanup();
2967
- await Promise.allSettled([...this.backgroundRefreshes.values()]);
3003
+ for (const key of this.backgroundRefreshAbort.keys()) {
3004
+ this.backgroundRefreshAbort.set(key, true);
3005
+ }
3006
+ await Promise.allSettled(
3007
+ [...this.backgroundRefreshes.values()].map((promise) => {
3008
+ let timer;
3009
+ return Promise.race([
3010
+ promise,
3011
+ new Promise((resolve2) => {
3012
+ timer = setTimeout(resolve2, 5e3);
3013
+ timer.unref?.();
3014
+ })
3015
+ ]).finally(() => {
3016
+ if (timer) clearTimeout(timer);
3017
+ });
3018
+ })
3019
+ );
3020
+ this.backgroundRefreshes.clear();
3021
+ this.backgroundRefreshAbort.clear();
2968
3022
  this.maintenance.disposeWriteBehindTimer();
2969
3023
  this.fetchRateLimiter.dispose();
2970
3024
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
@@ -3231,15 +3285,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
3231
3285
  }
3232
3286
  const clearEpoch = this.maintenance.currentClearEpoch();
3233
3287
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
3288
+ this.backgroundRefreshAbort.set(key, false);
3234
3289
  const refresh = (async () => {
3235
3290
  this.metricsCollector.increment("refreshes");
3236
3291
  try {
3292
+ if (this.backgroundRefreshAbort.get(key)) return;
3237
3293
  await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
3238
3294
  } catch (error) {
3295
+ if (this.backgroundRefreshAbort.get(key)) return;
3239
3296
  this.metricsCollector.increment("refreshErrors");
3240
3297
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
3241
3298
  } finally {
3242
3299
  this.backgroundRefreshes.delete(key);
3300
+ this.backgroundRefreshAbort.delete(key);
3243
3301
  }
3244
3302
  })();
3245
3303
  this.backgroundRefreshes.set(key, refresh);
@@ -3342,7 +3400,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3342
3400
  timer.unref?.();
3343
3401
  })
3344
3402
  ]);
3345
- if (result && typeof result === "object" && "kind" in result) {
3403
+ if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
3346
3404
  if (result.kind === "error") {
3347
3405
  throw result.error;
3348
3406
  }
@@ -3360,7 +3418,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3360
3418
  }
3361
3419
  async observeOperation(name, attributes, execute) {
3362
3420
  const id = this.nextOperationId;
3363
- this.nextOperationId += 1;
3421
+ this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
3364
3422
  this.emit("operation-start", { id, name, attributes });
3365
3423
  try {
3366
3424
  const result = await execute();
@@ -3596,6 +3654,7 @@ var RedisInvalidationBus = class {
3596
3654
  logger;
3597
3655
  handlers = /* @__PURE__ */ new Set();
3598
3656
  sharedListener;
3657
+ subscribePromise;
3599
3658
  constructor(options) {
3600
3659
  this.publisher = options.publisher;
3601
3660
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
@@ -3603,15 +3662,27 @@ var RedisInvalidationBus = class {
3603
3662
  this.logger = options.logger;
3604
3663
  }
3605
3664
  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);
3665
+ const previousPromise = this.subscribePromise;
3666
+ let resolveThis;
3667
+ this.subscribePromise = new Promise((resolve2) => {
3668
+ resolveThis = resolve2;
3669
+ });
3670
+ if (previousPromise) {
3671
+ await previousPromise;
3672
+ }
3673
+ try {
3674
+ if (this.handlers.size === 0) {
3675
+ const listener = (_channel, payload) => {
3676
+ void this.dispatchToHandlers(payload);
3677
+ };
3678
+ this.sharedListener = listener;
3679
+ this.subscriber.on("message", listener);
3680
+ await this.subscriber.subscribe(this.channel);
3681
+ }
3682
+ this.handlers.add(handler);
3683
+ } finally {
3684
+ resolveThis();
3613
3685
  }
3614
- this.handlers.add(handler);
3615
3686
  return async () => {
3616
3687
  this.handlers.delete(handler);
3617
3688
  if (this.handlers.size === 0 && this.sharedListener) {
@@ -4037,10 +4108,21 @@ function normalizeUrl2(url) {
4037
4108
  }
4038
4109
 
4039
4110
  // src/integrations/opentelemetry.ts
4111
+ var MAX_SPANS = 1e4;
4040
4112
  function createOpenTelemetryPlugin(cache, tracer) {
4041
4113
  const spans = /* @__PURE__ */ new Map();
4042
4114
  const onStart = (event) => {
4043
- spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
4115
+ try {
4116
+ if (spans.size >= MAX_SPANS) {
4117
+ const oldest = spans.keys().next().value;
4118
+ if (oldest !== void 0) {
4119
+ spans.get(oldest)?.end();
4120
+ spans.delete(oldest);
4121
+ }
4122
+ }
4123
+ spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
4124
+ } catch {
4125
+ }
4044
4126
  };
4045
4127
  const onEnd = (event) => {
4046
4128
  const span = spans.get(event.id);
@@ -4048,12 +4130,15 @@ function createOpenTelemetryPlugin(cache, tracer) {
4048
4130
  return;
4049
4131
  }
4050
4132
  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);
4133
+ try {
4134
+ span.setAttribute?.("layercache.success", event.success);
4135
+ if (event.result) {
4136
+ span.setAttribute?.("layercache.result", event.result);
4137
+ }
4138
+ if (event.error !== void 0) {
4139
+ span.recordException?.(event.error);
4140
+ }
4141
+ } catch {
4057
4142
  }
4058
4143
  span.end();
4059
4144
  };
@@ -4619,7 +4704,7 @@ var RedisLayer = class {
4619
4704
  };
4620
4705
 
4621
4706
  // src/layers/DiskLayer.ts
4622
- var import_node_crypto = require("crypto");
4707
+ var import_node_crypto2 = require("crypto");
4623
4708
  var import_node_fs2 = require("fs");
4624
4709
  var import_node_path2 = require("path");
4625
4710
  var FILE_SCAN_CONCURRENCY = 32;
@@ -4672,7 +4757,7 @@ var DiskLayer = class {
4672
4757
  };
4673
4758
  const payload = this.serializer.serialize(entry);
4674
4759
  const targetPath = this.keyToPath(key);
4675
- const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
4760
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
4676
4761
  try {
4677
4762
  await import_node_fs2.promises.writeFile(tempPath, payload);
4678
4763
  await import_node_fs2.promises.rename(tempPath, targetPath);
@@ -4772,7 +4857,7 @@ var DiskLayer = class {
4772
4857
  async dispose() {
4773
4858
  }
4774
4859
  keyToPath(key) {
4775
- const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
4860
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
4776
4861
  return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
4777
4862
  }
4778
4863
  resolveDirectory(directory) {
@@ -5050,7 +5135,7 @@ var MsgpackSerializer = class {
5050
5135
  };
5051
5136
 
5052
5137
  // src/singleflight/RedisSingleFlightCoordinator.ts
5053
- var import_node_crypto2 = require("crypto");
5138
+ var import_node_crypto3 = require("crypto");
5054
5139
  var RELEASE_SCRIPT = `
5055
5140
  if redis.call("get", KEYS[1]) == ARGV[1] then
5056
5141
  return redis.call("del", KEYS[1])
@@ -5072,7 +5157,7 @@ var RedisSingleFlightCoordinator = class {
5072
5157
  }
5073
5158
  async execute(key, options, worker, waiter) {
5074
5159
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
5075
- const token = (0, import_node_crypto2.randomUUID)();
5160
+ const token = (0, import_node_crypto3.randomUUID)();
5076
5161
  const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
5077
5162
  if (acquired === "OK") {
5078
5163
  const renewTimer = this.startLeaseRenewal(lockKey, token, options);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-DBs8Ko5W.cjs';
2
- export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-DBs8Ko5W.cjs';
1
+ import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-BXWTKlI1.cjs';
2
+ export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-BXWTKlI1.cjs';
3
3
  import Redis from 'ioredis';
4
4
  import 'node:events';
5
5
 
@@ -23,6 +23,7 @@ declare class RedisInvalidationBus implements InvalidationBus {
23
23
  private readonly logger?;
24
24
  private readonly handlers;
25
25
  private sharedListener?;
26
+ private subscribePromise;
26
27
  constructor(options: RedisInvalidationBusOptions);
27
28
  subscribe(handler: (message: InvalidationMessage) => Promise<void> | void): Promise<() => Promise<void>>;
28
29
  publish(message: InvalidationMessage): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-DBs8Ko5W.js';
2
- export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-DBs8Ko5W.js';
1
+ import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-BXWTKlI1.js';
2
+ export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-BXWTKlI1.js';
3
3
  import Redis from 'ioredis';
4
4
  import 'node:events';
5
5
 
@@ -23,6 +23,7 @@ declare class RedisInvalidationBus implements InvalidationBus {
23
23
  private readonly logger?;
24
24
  private readonly handlers;
25
25
  private sharedListener?;
26
+ private subscribePromise;
26
27
  constructor(options: RedisInvalidationBusOptions);
27
28
  subscribe(handler: (message: InvalidationMessage) => Promise<void> | void): Promise<() => Promise<void>>;
28
29
  publish(message: InvalidationMessage): Promise<void>;