layercache 1.2.2 → 1.2.4

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/cli.cjs CHANGED
@@ -38,7 +38,30 @@ var import_ioredis = __toESM(require("ioredis"), 1);
38
38
 
39
39
  // src/internal/StoredValue.ts
40
40
  function isStoredValueEnvelope(value) {
41
- return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
41
+ if (typeof value !== "object" || value === null) {
42
+ return false;
43
+ }
44
+ const v = value;
45
+ if (v.__layercache !== 1) {
46
+ return false;
47
+ }
48
+ if (v.kind !== "value" && v.kind !== "empty") {
49
+ return false;
50
+ }
51
+ if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
52
+ return false;
53
+ }
54
+ if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
55
+ return false;
56
+ }
57
+ if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
58
+ return false;
59
+ }
60
+ const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
61
+ if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
62
+ return false;
63
+ }
64
+ return true;
42
65
  }
43
66
  function resolveStoredValue(stored, now = Date.now()) {
44
67
  if (!isStoredValueEnvelope(stored)) {
@@ -259,7 +282,7 @@ async function main(argv = process.argv.slice(2)) {
259
282
  const redisUrl = validateRedisUrl(args.redisUrl);
260
283
  if (!redisUrl) {
261
284
  process.stderr.write(
262
- `Error: invalid Redis URL "${args.redisUrl}". Expected format: redis://[user:password@]host[:port][/db]
285
+ `Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
263
286
  `
264
287
  );
265
288
  process.exitCode = 1;
@@ -273,7 +296,7 @@ async function main(argv = process.argv.slice(2)) {
273
296
  try {
274
297
  await redis.connect().catch((error) => {
275
298
  const message = error instanceof Error ? error.message : String(error);
276
- throw new Error(`Failed to connect to Redis at "${redisUrl}": ${message}`);
299
+ throw new Error(`Failed to connect to Redis at "${maskRedisUrl(redisUrl)}": ${message}`);
277
300
  });
278
301
  if (args.command === "stats") {
279
302
  const keys = await scanKeys(redis, args.pattern ?? "*");
@@ -428,6 +451,17 @@ function summarizeInspectableValue(value) {
428
451
  }
429
452
  return value;
430
453
  }
454
+ function maskRedisUrl(url) {
455
+ try {
456
+ const parsed = new URL(url);
457
+ if (parsed.password) {
458
+ parsed.password = "***";
459
+ }
460
+ return parsed.toString();
461
+ } catch {
462
+ return url.replace(/:([^@/]+)@/, ":***@");
463
+ }
464
+ }
431
465
  if (process.argv[1]?.includes("cli.")) {
432
466
  void main();
433
467
  }
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  RedisTagIndex
4
- } from "./chunk-IXCMHVHP.js";
4
+ } from "./chunk-QHWG7QS5.js";
5
5
  import {
6
6
  isStoredValueEnvelope,
7
7
  resolveStoredValue
8
- } from "./chunk-ZMDB5KOK.js";
8
+ } from "./chunk-7V7XAB74.js";
9
9
 
10
10
  // src/cli.ts
11
11
  import Redis from "ioredis";
@@ -20,7 +20,7 @@ async function main(argv = process.argv.slice(2)) {
20
20
  const redisUrl = validateRedisUrl(args.redisUrl);
21
21
  if (!redisUrl) {
22
22
  process.stderr.write(
23
- `Error: invalid Redis URL "${args.redisUrl}". Expected format: redis://[user:password@]host[:port][/db]
23
+ `Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
24
24
  `
25
25
  );
26
26
  process.exitCode = 1;
@@ -34,7 +34,7 @@ async function main(argv = process.argv.slice(2)) {
34
34
  try {
35
35
  await redis.connect().catch((error) => {
36
36
  const message = error instanceof Error ? error.message : String(error);
37
- throw new Error(`Failed to connect to Redis at "${redisUrl}": ${message}`);
37
+ throw new Error(`Failed to connect to Redis at "${maskRedisUrl(redisUrl)}": ${message}`);
38
38
  });
39
39
  if (args.command === "stats") {
40
40
  const keys = await scanKeys(redis, args.pattern ?? "*");
@@ -189,6 +189,17 @@ function summarizeInspectableValue(value) {
189
189
  }
190
190
  return value;
191
191
  }
192
+ function maskRedisUrl(url) {
193
+ try {
194
+ const parsed = new URL(url);
195
+ if (parsed.password) {
196
+ parsed.password = "***";
197
+ }
198
+ return parsed.toString();
199
+ } catch {
200
+ return url.replace(/:([^@/]+)@/, ":***@");
201
+ }
202
+ }
192
203
  if (process.argv[1]?.includes("cli.")) {
193
204
  void main();
194
205
  }
@@ -177,6 +177,7 @@ interface CacheStackOptions {
177
177
  invalidationBus?: InvalidationBus;
178
178
  tagIndex?: CacheTagIndex;
179
179
  generation?: number;
180
+ generationCleanup?: boolean | CacheGenerationCleanupOptions;
180
181
  broadcastL1Invalidation?: boolean;
181
182
  /**
182
183
  * @deprecated Use `broadcastL1Invalidation` instead.
@@ -195,11 +196,13 @@ interface CacheStackOptions {
195
196
  writeStrategy?: 'write-through' | 'write-behind';
196
197
  writeBehind?: CacheWriteBehindOptions;
197
198
  fetcherRateLimit?: CacheRateLimitOptions;
199
+ backgroundRefreshTimeoutMs?: number;
198
200
  singleFlightCoordinator?: CacheSingleFlightCoordinator;
199
201
  singleFlightLeaseMs?: number;
200
202
  singleFlightTimeoutMs?: number;
201
203
  singleFlightPollMs?: number;
202
204
  singleFlightRenewIntervalMs?: number;
205
+ snapshotBaseDir?: string | false;
203
206
  /**
204
207
  * Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
205
208
  * before the oldest entries are pruned. Prevents unbounded memory growth.
@@ -212,6 +215,9 @@ interface CacheAdaptiveTtlOptions {
212
215
  step?: number | LayerTtlMap;
213
216
  maxTtl?: number | LayerTtlMap;
214
217
  }
218
+ interface CacheGenerationCleanupOptions {
219
+ batchSize?: number;
220
+ }
215
221
  type CacheTtlPolicy = 'until-midnight' | 'next-hour' | {
216
222
  alignTo: number;
217
223
  } | ((context: CacheTtlPolicyContext) => number | undefined);
@@ -415,7 +421,7 @@ interface TagIndexOptions {
415
421
  /**
416
422
  * Maximum number of keys tracked in `knownKeys`. When exceeded, the oldest
417
423
  * 10 % of keys are pruned to keep memory bounded.
418
- * Defaults to unlimited.
424
+ * Defaults to 100,000.
419
425
  */
420
426
  maxKnownKeys?: number;
421
427
  }
@@ -424,6 +430,8 @@ declare class TagIndex implements CacheTagIndex {
424
430
  private readonly keyToTags;
425
431
  private readonly knownKeys;
426
432
  private readonly maxKnownKeys;
433
+ private nextNodeId;
434
+ private readonly root;
427
435
  constructor(options?: TagIndexOptions);
428
436
  touch(key: string): Promise<void>;
429
437
  track(key: string, tags: string[]): Promise<void>;
@@ -433,8 +441,14 @@ declare class TagIndex implements CacheTagIndex {
433
441
  tagsForKey(key: string): Promise<string[]>;
434
442
  matchPattern(pattern: string): Promise<string[]>;
435
443
  clear(): Promise<void>;
444
+ private createTrieNode;
445
+ private insertKnownKey;
446
+ private findNode;
447
+ private collectFromNode;
448
+ private collectPatternMatches;
436
449
  private pruneKnownKeysIfNeeded;
437
450
  private removeKey;
451
+ private removeKnownKey;
438
452
  }
439
453
 
440
454
  declare class CacheNamespace {
@@ -504,7 +518,9 @@ declare class CacheStack extends EventEmitter {
504
518
  private unsubscribeInvalidation?;
505
519
  private readonly logger;
506
520
  private readonly tagIndex;
521
+ private readonly keyDiscovery;
507
522
  private readonly fetchRateLimiter;
523
+ private readonly snapshotSerializer;
508
524
  private readonly backgroundRefreshes;
509
525
  private readonly layerDegradedUntil;
510
526
  private readonly ttlResolver;
@@ -513,6 +529,7 @@ declare class CacheStack extends EventEmitter {
513
529
  private readonly writeBehindQueue;
514
530
  private writeBehindTimer?;
515
531
  private writeBehindFlushPromise?;
532
+ private generationCleanupPromise?;
516
533
  private isDisconnecting;
517
534
  private disconnectPromise?;
518
535
  constructor(layers: CacheLayer[], options?: CacheStackOptions);
@@ -523,6 +540,7 @@ declare class CacheStack extends EventEmitter {
523
540
  * and no `fetcher` is provided.
524
541
  */
525
542
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
543
+ private getPrepared;
526
544
  /**
527
545
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
528
546
  * Fetches and caches the value if not already present.
@@ -581,6 +599,11 @@ declare class CacheStack extends EventEmitter {
581
599
  */
582
600
  getHitRate(): CacheHitRateSnapshot;
583
601
  healthCheck(): Promise<CacheHealthCheckResult[]>;
602
+ /**
603
+ * Rotates the active generation prefix used for all future cache keys.
604
+ * Previous-generation keys remain in the underlying layers until they expire,
605
+ * unless `generationCleanup` is enabled to prune them in the background.
606
+ */
584
607
  bumpGeneration(nextGeneration?: number): number;
585
608
  /**
586
609
  * Returns detailed metadata about a single cache key: which layers contain it,
@@ -608,6 +631,7 @@ declare class CacheStack extends EventEmitter {
608
631
  private resolveLayerSeconds;
609
632
  private shouldNegativeCache;
610
633
  private scheduleBackgroundRefresh;
634
+ private runBackgroundRefresh;
611
635
  private resolveSingleFlightOptions;
612
636
  private deleteKeys;
613
637
  private publishInvalidation;
@@ -615,7 +639,12 @@ declare class CacheStack extends EventEmitter {
615
639
  private getTagsForKey;
616
640
  private formatError;
617
641
  private sleep;
642
+ private withTimeout;
618
643
  private shouldBroadcastL1Invalidation;
644
+ private shouldCleanupGenerations;
645
+ private generationCleanupBatchSize;
646
+ private scheduleGenerationCleanup;
647
+ private cleanupGeneration;
619
648
  private initializeWriteBehind;
620
649
  private shouldWriteBehind;
621
650
  private enqueueWriteBehind;
@@ -643,12 +672,15 @@ declare class CacheStack extends EventEmitter {
643
672
  private applyFreshReadPolicies;
644
673
  private shouldSkipLayer;
645
674
  private handleLayerFailure;
675
+ private reportRecoverableLayerFailure;
646
676
  private isGracefulDegradationEnabled;
647
677
  private recordCircuitFailure;
648
678
  private isNegativeStoredValue;
649
679
  private emitError;
650
680
  private serializeKeyPart;
651
681
  private isCacheSnapshotEntries;
682
+ private sanitizeSnapshotValue;
683
+ private validateSnapshotFilePath;
652
684
  private normalizeForSerialization;
653
685
  }
654
686
 
@@ -177,6 +177,7 @@ interface CacheStackOptions {
177
177
  invalidationBus?: InvalidationBus;
178
178
  tagIndex?: CacheTagIndex;
179
179
  generation?: number;
180
+ generationCleanup?: boolean | CacheGenerationCleanupOptions;
180
181
  broadcastL1Invalidation?: boolean;
181
182
  /**
182
183
  * @deprecated Use `broadcastL1Invalidation` instead.
@@ -195,11 +196,13 @@ interface CacheStackOptions {
195
196
  writeStrategy?: 'write-through' | 'write-behind';
196
197
  writeBehind?: CacheWriteBehindOptions;
197
198
  fetcherRateLimit?: CacheRateLimitOptions;
199
+ backgroundRefreshTimeoutMs?: number;
198
200
  singleFlightCoordinator?: CacheSingleFlightCoordinator;
199
201
  singleFlightLeaseMs?: number;
200
202
  singleFlightTimeoutMs?: number;
201
203
  singleFlightPollMs?: number;
202
204
  singleFlightRenewIntervalMs?: number;
205
+ snapshotBaseDir?: string | false;
203
206
  /**
204
207
  * Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
205
208
  * before the oldest entries are pruned. Prevents unbounded memory growth.
@@ -212,6 +215,9 @@ interface CacheAdaptiveTtlOptions {
212
215
  step?: number | LayerTtlMap;
213
216
  maxTtl?: number | LayerTtlMap;
214
217
  }
218
+ interface CacheGenerationCleanupOptions {
219
+ batchSize?: number;
220
+ }
215
221
  type CacheTtlPolicy = 'until-midnight' | 'next-hour' | {
216
222
  alignTo: number;
217
223
  } | ((context: CacheTtlPolicyContext) => number | undefined);
@@ -415,7 +421,7 @@ interface TagIndexOptions {
415
421
  /**
416
422
  * Maximum number of keys tracked in `knownKeys`. When exceeded, the oldest
417
423
  * 10 % of keys are pruned to keep memory bounded.
418
- * Defaults to unlimited.
424
+ * Defaults to 100,000.
419
425
  */
420
426
  maxKnownKeys?: number;
421
427
  }
@@ -424,6 +430,8 @@ declare class TagIndex implements CacheTagIndex {
424
430
  private readonly keyToTags;
425
431
  private readonly knownKeys;
426
432
  private readonly maxKnownKeys;
433
+ private nextNodeId;
434
+ private readonly root;
427
435
  constructor(options?: TagIndexOptions);
428
436
  touch(key: string): Promise<void>;
429
437
  track(key: string, tags: string[]): Promise<void>;
@@ -433,8 +441,14 @@ declare class TagIndex implements CacheTagIndex {
433
441
  tagsForKey(key: string): Promise<string[]>;
434
442
  matchPattern(pattern: string): Promise<string[]>;
435
443
  clear(): Promise<void>;
444
+ private createTrieNode;
445
+ private insertKnownKey;
446
+ private findNode;
447
+ private collectFromNode;
448
+ private collectPatternMatches;
436
449
  private pruneKnownKeysIfNeeded;
437
450
  private removeKey;
451
+ private removeKnownKey;
438
452
  }
439
453
 
440
454
  declare class CacheNamespace {
@@ -504,7 +518,9 @@ declare class CacheStack extends EventEmitter {
504
518
  private unsubscribeInvalidation?;
505
519
  private readonly logger;
506
520
  private readonly tagIndex;
521
+ private readonly keyDiscovery;
507
522
  private readonly fetchRateLimiter;
523
+ private readonly snapshotSerializer;
508
524
  private readonly backgroundRefreshes;
509
525
  private readonly layerDegradedUntil;
510
526
  private readonly ttlResolver;
@@ -513,6 +529,7 @@ declare class CacheStack extends EventEmitter {
513
529
  private readonly writeBehindQueue;
514
530
  private writeBehindTimer?;
515
531
  private writeBehindFlushPromise?;
532
+ private generationCleanupPromise?;
516
533
  private isDisconnecting;
517
534
  private disconnectPromise?;
518
535
  constructor(layers: CacheLayer[], options?: CacheStackOptions);
@@ -523,6 +540,7 @@ declare class CacheStack extends EventEmitter {
523
540
  * and no `fetcher` is provided.
524
541
  */
525
542
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
543
+ private getPrepared;
526
544
  /**
527
545
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
528
546
  * Fetches and caches the value if not already present.
@@ -581,6 +599,11 @@ declare class CacheStack extends EventEmitter {
581
599
  */
582
600
  getHitRate(): CacheHitRateSnapshot;
583
601
  healthCheck(): Promise<CacheHealthCheckResult[]>;
602
+ /**
603
+ * Rotates the active generation prefix used for all future cache keys.
604
+ * Previous-generation keys remain in the underlying layers until they expire,
605
+ * unless `generationCleanup` is enabled to prune them in the background.
606
+ */
584
607
  bumpGeneration(nextGeneration?: number): number;
585
608
  /**
586
609
  * Returns detailed metadata about a single cache key: which layers contain it,
@@ -608,6 +631,7 @@ declare class CacheStack extends EventEmitter {
608
631
  private resolveLayerSeconds;
609
632
  private shouldNegativeCache;
610
633
  private scheduleBackgroundRefresh;
634
+ private runBackgroundRefresh;
611
635
  private resolveSingleFlightOptions;
612
636
  private deleteKeys;
613
637
  private publishInvalidation;
@@ -615,7 +639,12 @@ declare class CacheStack extends EventEmitter {
615
639
  private getTagsForKey;
616
640
  private formatError;
617
641
  private sleep;
642
+ private withTimeout;
618
643
  private shouldBroadcastL1Invalidation;
644
+ private shouldCleanupGenerations;
645
+ private generationCleanupBatchSize;
646
+ private scheduleGenerationCleanup;
647
+ private cleanupGeneration;
619
648
  private initializeWriteBehind;
620
649
  private shouldWriteBehind;
621
650
  private enqueueWriteBehind;
@@ -643,12 +672,15 @@ declare class CacheStack extends EventEmitter {
643
672
  private applyFreshReadPolicies;
644
673
  private shouldSkipLayer;
645
674
  private handleLayerFailure;
675
+ private reportRecoverableLayerFailure;
646
676
  private isGracefulDegradationEnabled;
647
677
  private recordCircuitFailure;
648
678
  private isNegativeStoredValue;
649
679
  private emitError;
650
680
  private serializeKeyPart;
651
681
  private isCacheSnapshotEntries;
682
+ private sanitizeSnapshotValue;
683
+ private validateSnapshotFilePath;
652
684
  private normalizeForSerialization;
653
685
  }
654
686
 
package/dist/edge.cjs CHANGED
@@ -29,7 +29,30 @@ module.exports = __toCommonJS(edge_exports);
29
29
 
30
30
  // src/internal/StoredValue.ts
31
31
  function isStoredValueEnvelope(value) {
32
- return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
32
+ if (typeof value !== "object" || value === null) {
33
+ return false;
34
+ }
35
+ const v = value;
36
+ if (v.__layercache !== 1) {
37
+ return false;
38
+ }
39
+ if (v.kind !== "value" && v.kind !== "empty") {
40
+ return false;
41
+ }
42
+ if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
43
+ return false;
44
+ }
45
+ if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
46
+ return false;
47
+ }
48
+ if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
49
+ return false;
50
+ }
51
+ const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
52
+ if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
53
+ return false;
54
+ }
55
+ return true;
33
56
  }
34
57
  function unwrapStoredValue(stored) {
35
58
  if (!isStoredValueEnvelope(stored)) {
@@ -87,16 +110,10 @@ var MemoryLayer = class {
87
110
  return entry.value;
88
111
  }
89
112
  async getMany(keys) {
90
- const values = [];
91
- for (const key of keys) {
92
- values.push(await this.getEntry(key));
93
- }
94
- return values;
113
+ return Promise.all(keys.map((key) => this.getEntry(key)));
95
114
  }
96
115
  async setMany(entries) {
97
- for (const entry of entries) {
98
- await this.set(entry.key, entry.value, entry.ttl);
99
- }
116
+ await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
100
117
  }
101
118
  async set(key, value, ttl = this.defaultTtl) {
102
119
  this.entries.delete(key);
@@ -283,15 +300,17 @@ var TagIndex = class {
283
300
  keyToTags = /* @__PURE__ */ new Map();
284
301
  knownKeys = /* @__PURE__ */ new Set();
285
302
  maxKnownKeys;
303
+ nextNodeId = 1;
304
+ root = this.createTrieNode();
286
305
  constructor(options = {}) {
287
- this.maxKnownKeys = options.maxKnownKeys;
306
+ this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
288
307
  }
289
308
  async touch(key) {
290
- this.knownKeys.add(key);
309
+ this.insertKnownKey(key);
291
310
  this.pruneKnownKeysIfNeeded();
292
311
  }
293
312
  async track(key, tags) {
294
- this.knownKeys.add(key);
313
+ this.insertKnownKey(key);
295
314
  this.pruneKnownKeysIfNeeded();
296
315
  if (tags.length === 0) {
297
316
  return;
@@ -317,18 +336,104 @@ var TagIndex = class {
317
336
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
318
337
  }
319
338
  async keysForPrefix(prefix) {
320
- return [...this.knownKeys].filter((key) => key.startsWith(prefix));
339
+ const node = this.findNode(prefix);
340
+ if (!node) {
341
+ return [];
342
+ }
343
+ const matches = [];
344
+ this.collectFromNode(node, prefix, matches);
345
+ return matches;
321
346
  }
322
347
  async tagsForKey(key) {
323
348
  return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
324
349
  }
325
350
  async matchPattern(pattern) {
326
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
351
+ const matches = /* @__PURE__ */ new Set();
352
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
353
+ return [...matches];
327
354
  }
328
355
  async clear() {
329
356
  this.tagToKeys.clear();
330
357
  this.keyToTags.clear();
331
358
  this.knownKeys.clear();
359
+ this.root.children.clear();
360
+ this.root.terminal = false;
361
+ this.nextNodeId = this.root.id + 1;
362
+ }
363
+ createTrieNode() {
364
+ return {
365
+ id: this.nextNodeId++,
366
+ terminal: false,
367
+ children: /* @__PURE__ */ new Map()
368
+ };
369
+ }
370
+ insertKnownKey(key) {
371
+ if (this.knownKeys.has(key)) {
372
+ return;
373
+ }
374
+ this.knownKeys.add(key);
375
+ let node = this.root;
376
+ for (const character of key) {
377
+ let child = node.children.get(character);
378
+ if (!child) {
379
+ child = this.createTrieNode();
380
+ node.children.set(character, child);
381
+ }
382
+ node = child;
383
+ }
384
+ node.terminal = true;
385
+ }
386
+ findNode(prefix) {
387
+ let node = this.root;
388
+ for (const character of prefix) {
389
+ node = node.children.get(character);
390
+ if (!node) {
391
+ return void 0;
392
+ }
393
+ }
394
+ return node;
395
+ }
396
+ collectFromNode(node, prefix, matches) {
397
+ if (node.terminal) {
398
+ matches.push(prefix);
399
+ }
400
+ for (const [character, child] of node.children) {
401
+ this.collectFromNode(child, `${prefix}${character}`, matches);
402
+ }
403
+ }
404
+ collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
405
+ const stateKey = `${node.id}:${patternIndex}`;
406
+ if (visited.has(stateKey)) {
407
+ return;
408
+ }
409
+ visited.add(stateKey);
410
+ if (patternIndex === pattern.length) {
411
+ if (node.terminal) {
412
+ matches.add(prefix);
413
+ }
414
+ return;
415
+ }
416
+ const patternChar = pattern[patternIndex];
417
+ if (patternChar === void 0) {
418
+ return;
419
+ }
420
+ if (patternChar === "*") {
421
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
422
+ for (const [character, child2] of node.children) {
423
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
424
+ }
425
+ return;
426
+ }
427
+ if (patternChar === "?") {
428
+ for (const [character, child2] of node.children) {
429
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
430
+ }
431
+ return;
432
+ }
433
+ const child = node.children.get(patternChar);
434
+ if (child) {
435
+ this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
436
+ }
332
437
  }
333
438
  pruneKnownKeysIfNeeded() {
334
439
  if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
@@ -345,7 +450,7 @@ var TagIndex = class {
345
450
  }
346
451
  }
347
452
  removeKey(key) {
348
- this.knownKeys.delete(key);
453
+ this.removeKnownKey(key);
349
454
  const tags = this.keyToTags.get(key);
350
455
  if (!tags) {
351
456
  return;
@@ -362,6 +467,34 @@ var TagIndex = class {
362
467
  }
363
468
  this.keyToTags.delete(key);
364
469
  }
470
+ removeKnownKey(key) {
471
+ if (!this.knownKeys.delete(key)) {
472
+ return;
473
+ }
474
+ const path = [];
475
+ let node = this.root;
476
+ for (const character of key) {
477
+ const child = node.children.get(character);
478
+ if (!child) {
479
+ return;
480
+ }
481
+ path.push([node, character]);
482
+ node = child;
483
+ }
484
+ node.terminal = false;
485
+ for (let index = path.length - 1; index >= 0; index -= 1) {
486
+ const entry = path[index];
487
+ if (!entry) {
488
+ continue;
489
+ }
490
+ const [parent, character] = entry;
491
+ const child = parent.children.get(character);
492
+ if (!child || child.terminal || child.children.size > 0) {
493
+ break;
494
+ }
495
+ parent.children.delete(character);
496
+ }
497
+ }
365
498
  };
366
499
 
367
500
  // src/integrations/hono.ts
@@ -373,7 +506,8 @@ function createHonoCacheMiddleware(cache, options = {}) {
373
506
  await next();
374
507
  return;
375
508
  }
376
- const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${context.req.path ?? context.req.url ?? "/"}`;
509
+ const rawPath = context.req.path ?? context.req.url ?? "/";
510
+ const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl(rawPath)}`;
377
511
  const cached = await cache.get(key, void 0, options);
378
512
  if (cached !== null) {
379
513
  context.header?.("x-cache", "HIT");
@@ -384,12 +518,26 @@ function createHonoCacheMiddleware(cache, options = {}) {
384
518
  const originalJson = context.json.bind(context);
385
519
  context.json = (body, status) => {
386
520
  context.header?.("x-cache", "MISS");
387
- void cache.set(key, body, options);
521
+ cache.set(key, body, options).catch((err) => {
522
+ cache.emit("error", {
523
+ operation: "set",
524
+ error: err instanceof Error ? err.message : String(err)
525
+ });
526
+ });
388
527
  return originalJson(body, status);
389
528
  };
390
529
  await next();
391
530
  };
392
531
  }
532
+ function normalizeUrl(url) {
533
+ try {
534
+ const parsed = new URL(url, "http://localhost");
535
+ parsed.searchParams.sort();
536
+ return decodeURIComponent(parsed.pathname) + parsed.search;
537
+ } catch {
538
+ return url;
539
+ }
540
+ }
393
541
  // Annotate the CommonJS export names for ESM import in node:
394
542
  0 && (module.exports = {
395
543
  MemoryLayer,
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-DLpdQN0W.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-Dw97n89L.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-DLpdQN0W.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-Dw97n89L.js';
2
2
  import 'node:events';
package/dist/edge.js CHANGED
@@ -2,10 +2,10 @@ import {
2
2
  MemoryLayer,
3
3
  TagIndex,
4
4
  createHonoCacheMiddleware
5
- } from "./chunk-46UH7LNM.js";
5
+ } from "./chunk-KOYGHLVP.js";
6
6
  import {
7
7
  PatternMatcher
8
- } from "./chunk-ZMDB5KOK.js";
8
+ } from "./chunk-7V7XAB74.js";
9
9
  export {
10
10
  MemoryLayer,
11
11
  PatternMatcher,