layercache 2.1.0 → 3.1.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.
@@ -47,6 +47,8 @@ interface CacheContextOptionsContext {
47
47
  interface CacheWriteOptions extends CacheEntryWriteOptions {
48
48
  /** Cache `null` fetcher results using `negativeTtl` instead of treating them as misses. */
49
49
  negativeCache?: boolean;
50
+ /** Cache `null` fetcher results as regular values instead of negative/empty entries. */
51
+ cacheNullValues?: boolean;
50
52
  /** Extend a key's TTL on fresh reads. */
51
53
  slidingTtl?: boolean;
52
54
  /** Refresh in the background when the remaining TTL is at or below this threshold in milliseconds. */
@@ -71,6 +73,7 @@ interface CacheWriteOptions extends CacheEntryWriteOptions {
71
73
  *
72
74
  * Returned values override any static entry options already present on the
73
75
  * same object. Fetch controls like `shouldCache`, `negativeCache`,
76
+ * `cacheNullValues`,
74
77
  * `refreshAhead`, or `circuitBreaker` are not affected.
75
78
  *
76
79
  * @example
@@ -340,6 +343,8 @@ interface CacheStackOptions {
340
343
  publishSetInvalidation?: boolean;
341
344
  /** Cache null fetcher results as negative entries. */
342
345
  negativeCaching?: boolean;
346
+ /** Cache null fetcher results as regular values instead of negative/empty entries. */
347
+ cacheNullValues?: boolean;
343
348
  /** Default negative-cache TTL in milliseconds. */
344
349
  negativeTtl?: number | LayerTtlMap;
345
350
  /** Default stale-while-revalidate window in milliseconds. */
@@ -422,6 +427,13 @@ interface CacheCircuitBreakerOptions {
422
427
  failureThreshold?: number;
423
428
  /** Milliseconds before an open circuit allows another attempt. */
424
429
  cooldownMs?: number;
430
+ /**
431
+ * Failure scope. `key` preserves the historical per-cache-key behavior.
432
+ * `shared` uses one breaker for all fetches using these options.
433
+ */
434
+ scope?: 'key' | 'shared';
435
+ /** Custom breaker id for grouping related fetches, such as one backend dependency. */
436
+ breakerKey?: string;
425
437
  }
426
438
  /** Graceful degradation settings for temporarily unhealthy layers. */
427
439
  interface CacheDegradationOptions {
@@ -440,6 +452,11 @@ interface CacheRateLimitOptions {
440
452
  scope?: 'global' | 'key' | 'fetcher';
441
453
  /** Custom bucket id used to group otherwise unrelated fetches. */
442
454
  bucketKey?: string;
455
+ /**
456
+ * Behavior when a bucket queue reaches its internal safety limit.
457
+ * Defaults to `reject` so overflow is explicit instead of bypassing limits.
458
+ */
459
+ queueOverflow?: 'reject' | 'bypass';
443
460
  }
444
461
  /** Queue controls for write-behind mode. */
445
462
  interface CacheWriteBehindOptions {
@@ -539,6 +556,19 @@ interface CacheInspectResult {
539
556
  /** Tags associated with this key (from the TagIndex). */
540
557
  tags: string[];
541
558
  }
559
+ /** Cached entry result that can distinguish a stored `null` from a miss. */
560
+ interface CacheEntryResult<T = unknown> {
561
+ /** User-facing cache key. */
562
+ key: string;
563
+ /** Unwrapped cached value. May be `null` when `kind` is `value` or `empty`. */
564
+ value: T | null;
565
+ /** Whether this entry stores a normal value or a negative-cache empty marker. */
566
+ kind: 'value' | 'empty';
567
+ /** Fresh/stale state currently available for this entry. */
568
+ state: 'fresh' | 'stale-while-revalidate' | 'stale-if-error';
569
+ /** First layer that supplied the entry. */
570
+ layer: string;
571
+ }
542
572
  /** All events emitted by CacheStack and their payload shapes. */
543
573
  interface CacheStackEvents {
544
574
  /** Fired on every cache hit. */
@@ -746,12 +776,18 @@ interface TagIndexOptions {
746
776
  * Defaults to 100,000.
747
777
  */
748
778
  maxKnownKeys?: number;
779
+ /**
780
+ * Minimum age before an existing key touch refreshes LRU order.
781
+ * Defaults to 1000ms to avoid delete/set churn on cache-hit hot paths.
782
+ */
783
+ touchRefreshIntervalMs?: number;
749
784
  }
750
785
  declare class TagIndex implements CacheTagIndex {
751
786
  private readonly tagToKeys;
752
787
  private readonly keyToTags;
753
788
  private readonly knownKeys;
754
789
  private readonly maxKnownKeys;
790
+ private readonly touchRefreshIntervalMs;
755
791
  private nextNodeId;
756
792
  private readonly root;
757
793
  constructor(options?: TagIndexOptions);
@@ -804,7 +840,7 @@ declare class TagIndex implements CacheTagIndex {
804
840
  private findNode;
805
841
  private collectFromNode;
806
842
  private visitFromNode;
807
- private collectPatternMatches;
843
+ private literalPrefix;
808
844
  private pruneKnownKeysIfNeeded;
809
845
  private removeKey;
810
846
  private removeKnownKey;
@@ -834,6 +870,11 @@ declare class CacheNamespace {
834
870
  * Alias for `get(key, fetcher, options)` that makes the get-or-set behavior explicit.
835
871
  */
836
872
  getOrSet<T>(key: string, fetcher: CacheFetcher<T>, options?: CacheGetOptions): Promise<T | null>;
873
+ /**
874
+ * Returns a namespaced cache entry, or `null` on miss.
875
+ * Unlike `get()`, this distinguishes a stored `null` value from an absent key.
876
+ */
877
+ getEntry<T>(key: string): Promise<CacheEntryResult<T> | null>;
837
878
  /**
838
879
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
839
880
  */
@@ -1024,6 +1065,11 @@ declare class CacheStack extends EventEmitter {
1024
1065
  * Fetches and caches the value if not already present.
1025
1066
  */
1026
1067
  getOrSet<T>(key: string, fetcher: CacheFetcher<T>, options?: CacheGetOptions): Promise<T | null>;
1068
+ /**
1069
+ * Returns a discriminated cache entry, or `null` on miss.
1070
+ * Unlike `get()`, this distinguishes a stored `null` value from an absent key.
1071
+ */
1072
+ getEntry<T>(key: string): Promise<CacheEntryResult<T> | null>;
1027
1073
  /**
1028
1074
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
1029
1075
  * Useful when the value is expected to exist or the fetcher is expected to
@@ -1139,6 +1185,14 @@ declare class CacheStack extends EventEmitter {
1139
1185
  * Returns cumulative cache metrics since startup or the last `resetMetrics()`.
1140
1186
  */
1141
1187
  getMetrics(): CacheMetricsSnapshot;
1188
+ /**
1189
+ * Runs an operation while collecting only the metrics emitted by its async context.
1190
+ * Used by namespaces so metrics tracking does not serialize the operation itself.
1191
+ */
1192
+ captureMetrics<T>(operation: () => Promise<T>): Promise<{
1193
+ result: T;
1194
+ metrics: CacheMetricsSnapshot;
1195
+ }>;
1142
1196
  /**
1143
1197
  * Returns metrics plus layer degradation state and active background refresh count.
1144
1198
  */
@@ -1162,6 +1216,10 @@ declare class CacheStack extends EventEmitter {
1162
1216
  * unless `generationCleanup` is enabled to prune them in the background.
1163
1217
  */
1164
1218
  bumpGeneration(nextGeneration?: number): number;
1219
+ /**
1220
+ * Returns the active generation prefix number used for future cache keys.
1221
+ */
1222
+ getGeneration(): number | undefined;
1165
1223
  /**
1166
1224
  * Returns detailed metadata about a single cache key: which layers contain it,
1167
1225
  * remaining fresh/stale/error TTLs, and associated tags.
@@ -1246,6 +1304,7 @@ interface HonoLikeRequest {
1246
1304
  interface HonoLikeContext {
1247
1305
  req: HonoLikeRequest;
1248
1306
  header?: (name: string, value: string) => void;
1307
+ status?: (status: number) => unknown;
1249
1308
  json: (body: unknown, status?: number) => Response | Promise<Response> | unknown;
1250
1309
  }
1251
1310
  interface HonoCacheMiddlewareOptions extends CacheGetOptions {
@@ -1264,4 +1323,4 @@ interface HonoCacheMiddlewareOptions extends CacheGetOptions {
1264
1323
  */
1265
1324
  declare function createHonoCacheMiddleware(cache: CacheStack, options?: HonoCacheMiddlewareOptions): (context: HonoLikeContext, next: () => Promise<void>) => Promise<unknown>;
1266
1325
 
1267
- export { CacheNamespace as A, type CacheRateLimitOptions as B, type CacheLogger as C, type CacheSnapshotEntry as D, type CacheStackEvents as E, type CacheStackOptions as F, type CacheStatsSnapshot as G, type CacheTtlPolicy as H, type InvalidationBus as I, type CacheTtlPolicyContext as J, type CacheWarmEntry as K, type CacheWarmOptions as L, type CacheWarmProgress as M, type CacheWriteBehindOptions as N, type CacheWriteOptions as O, type EvictionPolicy as P, type LayerTtlMap as Q, MemoryLayer as R, type MemoryLayerOptions as S, type MemoryLayerSnapshotEntry as T, PatternMatcher as U, TagIndex as V, createHonoCacheMiddleware as W, type InvalidationMessage as a, type CacheTagIndex as b, CacheStack as c, type CacheWrapOptions as d, type CacheGetOptions as e, type CacheLayer as f, type CacheSerializer as g, type CacheLayerSetManyEntry as h, type CacheSingleFlightCoordinator as i, type CacheSingleFlightExecutionOptions as j, type CacheAdaptiveTtlOptions as k, type CacheCircuitBreakerOptions as l, type CacheContextOptionsContext as m, type CacheDegradationOptions as n, type CacheEntryWriteKind as o, type CacheEntryWriteOptions as p, type CacheFetcher as q, type CacheFetcherContext as r, type CacheHealthCheckResult as s, type CacheHitRateSnapshot as t, type CacheInspectResult as u, type CacheLayerLatency as v, type CacheMGetEntry as w, type CacheMSetEntry as x, type CacheMetricsSnapshot as y, CacheMissError as z };
1326
+ export { CacheMissError as A, CacheNamespace as B, type CacheLogger as C, type CacheRateLimitOptions as D, type CacheSnapshotEntry as E, type CacheStackEvents as F, type CacheStackOptions as G, type CacheStatsSnapshot as H, type InvalidationBus as I, type CacheTtlPolicy as J, type CacheTtlPolicyContext as K, type CacheWarmEntry as L, type CacheWarmOptions as M, type CacheWarmProgress as N, type CacheWriteBehindOptions as O, type CacheWriteOptions as P, type EvictionPolicy as Q, type LayerTtlMap as R, MemoryLayer as S, type MemoryLayerOptions as T, type MemoryLayerSnapshotEntry as U, PatternMatcher as V, TagIndex as W, createHonoCacheMiddleware as X, type InvalidationMessage as a, type CacheTagIndex as b, CacheStack as c, type CacheWrapOptions as d, type CacheGetOptions as e, type CacheLayer as f, type CacheSerializer as g, type CacheLayerSetManyEntry as h, type CacheSingleFlightCoordinator as i, type CacheSingleFlightExecutionOptions as j, type CacheAdaptiveTtlOptions as k, type CacheCircuitBreakerOptions as l, type CacheContextOptionsContext as m, type CacheDegradationOptions as n, type CacheEntryResult as o, type CacheEntryWriteKind as p, type CacheEntryWriteOptions as q, type CacheFetcher as r, type CacheFetcherContext as s, type CacheHealthCheckResult as t, type CacheHitRateSnapshot as u, type CacheInspectResult as v, type CacheLayerLatency as w, type CacheMGetEntry as x, type CacheMSetEntry as y, type CacheMetricsSnapshot as z };
@@ -47,6 +47,8 @@ interface CacheContextOptionsContext {
47
47
  interface CacheWriteOptions extends CacheEntryWriteOptions {
48
48
  /** Cache `null` fetcher results using `negativeTtl` instead of treating them as misses. */
49
49
  negativeCache?: boolean;
50
+ /** Cache `null` fetcher results as regular values instead of negative/empty entries. */
51
+ cacheNullValues?: boolean;
50
52
  /** Extend a key's TTL on fresh reads. */
51
53
  slidingTtl?: boolean;
52
54
  /** Refresh in the background when the remaining TTL is at or below this threshold in milliseconds. */
@@ -71,6 +73,7 @@ interface CacheWriteOptions extends CacheEntryWriteOptions {
71
73
  *
72
74
  * Returned values override any static entry options already present on the
73
75
  * same object. Fetch controls like `shouldCache`, `negativeCache`,
76
+ * `cacheNullValues`,
74
77
  * `refreshAhead`, or `circuitBreaker` are not affected.
75
78
  *
76
79
  * @example
@@ -340,6 +343,8 @@ interface CacheStackOptions {
340
343
  publishSetInvalidation?: boolean;
341
344
  /** Cache null fetcher results as negative entries. */
342
345
  negativeCaching?: boolean;
346
+ /** Cache null fetcher results as regular values instead of negative/empty entries. */
347
+ cacheNullValues?: boolean;
343
348
  /** Default negative-cache TTL in milliseconds. */
344
349
  negativeTtl?: number | LayerTtlMap;
345
350
  /** Default stale-while-revalidate window in milliseconds. */
@@ -422,6 +427,13 @@ interface CacheCircuitBreakerOptions {
422
427
  failureThreshold?: number;
423
428
  /** Milliseconds before an open circuit allows another attempt. */
424
429
  cooldownMs?: number;
430
+ /**
431
+ * Failure scope. `key` preserves the historical per-cache-key behavior.
432
+ * `shared` uses one breaker for all fetches using these options.
433
+ */
434
+ scope?: 'key' | 'shared';
435
+ /** Custom breaker id for grouping related fetches, such as one backend dependency. */
436
+ breakerKey?: string;
425
437
  }
426
438
  /** Graceful degradation settings for temporarily unhealthy layers. */
427
439
  interface CacheDegradationOptions {
@@ -440,6 +452,11 @@ interface CacheRateLimitOptions {
440
452
  scope?: 'global' | 'key' | 'fetcher';
441
453
  /** Custom bucket id used to group otherwise unrelated fetches. */
442
454
  bucketKey?: string;
455
+ /**
456
+ * Behavior when a bucket queue reaches its internal safety limit.
457
+ * Defaults to `reject` so overflow is explicit instead of bypassing limits.
458
+ */
459
+ queueOverflow?: 'reject' | 'bypass';
443
460
  }
444
461
  /** Queue controls for write-behind mode. */
445
462
  interface CacheWriteBehindOptions {
@@ -539,6 +556,19 @@ interface CacheInspectResult {
539
556
  /** Tags associated with this key (from the TagIndex). */
540
557
  tags: string[];
541
558
  }
559
+ /** Cached entry result that can distinguish a stored `null` from a miss. */
560
+ interface CacheEntryResult<T = unknown> {
561
+ /** User-facing cache key. */
562
+ key: string;
563
+ /** Unwrapped cached value. May be `null` when `kind` is `value` or `empty`. */
564
+ value: T | null;
565
+ /** Whether this entry stores a normal value or a negative-cache empty marker. */
566
+ kind: 'value' | 'empty';
567
+ /** Fresh/stale state currently available for this entry. */
568
+ state: 'fresh' | 'stale-while-revalidate' | 'stale-if-error';
569
+ /** First layer that supplied the entry. */
570
+ layer: string;
571
+ }
542
572
  /** All events emitted by CacheStack and their payload shapes. */
543
573
  interface CacheStackEvents {
544
574
  /** Fired on every cache hit. */
@@ -746,12 +776,18 @@ interface TagIndexOptions {
746
776
  * Defaults to 100,000.
747
777
  */
748
778
  maxKnownKeys?: number;
779
+ /**
780
+ * Minimum age before an existing key touch refreshes LRU order.
781
+ * Defaults to 1000ms to avoid delete/set churn on cache-hit hot paths.
782
+ */
783
+ touchRefreshIntervalMs?: number;
749
784
  }
750
785
  declare class TagIndex implements CacheTagIndex {
751
786
  private readonly tagToKeys;
752
787
  private readonly keyToTags;
753
788
  private readonly knownKeys;
754
789
  private readonly maxKnownKeys;
790
+ private readonly touchRefreshIntervalMs;
755
791
  private nextNodeId;
756
792
  private readonly root;
757
793
  constructor(options?: TagIndexOptions);
@@ -804,7 +840,7 @@ declare class TagIndex implements CacheTagIndex {
804
840
  private findNode;
805
841
  private collectFromNode;
806
842
  private visitFromNode;
807
- private collectPatternMatches;
843
+ private literalPrefix;
808
844
  private pruneKnownKeysIfNeeded;
809
845
  private removeKey;
810
846
  private removeKnownKey;
@@ -834,6 +870,11 @@ declare class CacheNamespace {
834
870
  * Alias for `get(key, fetcher, options)` that makes the get-or-set behavior explicit.
835
871
  */
836
872
  getOrSet<T>(key: string, fetcher: CacheFetcher<T>, options?: CacheGetOptions): Promise<T | null>;
873
+ /**
874
+ * Returns a namespaced cache entry, or `null` on miss.
875
+ * Unlike `get()`, this distinguishes a stored `null` value from an absent key.
876
+ */
877
+ getEntry<T>(key: string): Promise<CacheEntryResult<T> | null>;
837
878
  /**
838
879
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
839
880
  */
@@ -1024,6 +1065,11 @@ declare class CacheStack extends EventEmitter {
1024
1065
  * Fetches and caches the value if not already present.
1025
1066
  */
1026
1067
  getOrSet<T>(key: string, fetcher: CacheFetcher<T>, options?: CacheGetOptions): Promise<T | null>;
1068
+ /**
1069
+ * Returns a discriminated cache entry, or `null` on miss.
1070
+ * Unlike `get()`, this distinguishes a stored `null` value from an absent key.
1071
+ */
1072
+ getEntry<T>(key: string): Promise<CacheEntryResult<T> | null>;
1027
1073
  /**
1028
1074
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
1029
1075
  * Useful when the value is expected to exist or the fetcher is expected to
@@ -1139,6 +1185,14 @@ declare class CacheStack extends EventEmitter {
1139
1185
  * Returns cumulative cache metrics since startup or the last `resetMetrics()`.
1140
1186
  */
1141
1187
  getMetrics(): CacheMetricsSnapshot;
1188
+ /**
1189
+ * Runs an operation while collecting only the metrics emitted by its async context.
1190
+ * Used by namespaces so metrics tracking does not serialize the operation itself.
1191
+ */
1192
+ captureMetrics<T>(operation: () => Promise<T>): Promise<{
1193
+ result: T;
1194
+ metrics: CacheMetricsSnapshot;
1195
+ }>;
1142
1196
  /**
1143
1197
  * Returns metrics plus layer degradation state and active background refresh count.
1144
1198
  */
@@ -1162,6 +1216,10 @@ declare class CacheStack extends EventEmitter {
1162
1216
  * unless `generationCleanup` is enabled to prune them in the background.
1163
1217
  */
1164
1218
  bumpGeneration(nextGeneration?: number): number;
1219
+ /**
1220
+ * Returns the active generation prefix number used for future cache keys.
1221
+ */
1222
+ getGeneration(): number | undefined;
1165
1223
  /**
1166
1224
  * Returns detailed metadata about a single cache key: which layers contain it,
1167
1225
  * remaining fresh/stale/error TTLs, and associated tags.
@@ -1246,6 +1304,7 @@ interface HonoLikeRequest {
1246
1304
  interface HonoLikeContext {
1247
1305
  req: HonoLikeRequest;
1248
1306
  header?: (name: string, value: string) => void;
1307
+ status?: (status: number) => unknown;
1249
1308
  json: (body: unknown, status?: number) => Response | Promise<Response> | unknown;
1250
1309
  }
1251
1310
  interface HonoCacheMiddlewareOptions extends CacheGetOptions {
@@ -1264,4 +1323,4 @@ interface HonoCacheMiddlewareOptions extends CacheGetOptions {
1264
1323
  */
1265
1324
  declare function createHonoCacheMiddleware(cache: CacheStack, options?: HonoCacheMiddlewareOptions): (context: HonoLikeContext, next: () => Promise<void>) => Promise<unknown>;
1266
1325
 
1267
- export { CacheNamespace as A, type CacheRateLimitOptions as B, type CacheLogger as C, type CacheSnapshotEntry as D, type CacheStackEvents as E, type CacheStackOptions as F, type CacheStatsSnapshot as G, type CacheTtlPolicy as H, type InvalidationBus as I, type CacheTtlPolicyContext as J, type CacheWarmEntry as K, type CacheWarmOptions as L, type CacheWarmProgress as M, type CacheWriteBehindOptions as N, type CacheWriteOptions as O, type EvictionPolicy as P, type LayerTtlMap as Q, MemoryLayer as R, type MemoryLayerOptions as S, type MemoryLayerSnapshotEntry as T, PatternMatcher as U, TagIndex as V, createHonoCacheMiddleware as W, type InvalidationMessage as a, type CacheTagIndex as b, CacheStack as c, type CacheWrapOptions as d, type CacheGetOptions as e, type CacheLayer as f, type CacheSerializer as g, type CacheLayerSetManyEntry as h, type CacheSingleFlightCoordinator as i, type CacheSingleFlightExecutionOptions as j, type CacheAdaptiveTtlOptions as k, type CacheCircuitBreakerOptions as l, type CacheContextOptionsContext as m, type CacheDegradationOptions as n, type CacheEntryWriteKind as o, type CacheEntryWriteOptions as p, type CacheFetcher as q, type CacheFetcherContext as r, type CacheHealthCheckResult as s, type CacheHitRateSnapshot as t, type CacheInspectResult as u, type CacheLayerLatency as v, type CacheMGetEntry as w, type CacheMSetEntry as x, type CacheMetricsSnapshot as y, CacheMissError as z };
1326
+ export { CacheMissError as A, CacheNamespace as B, type CacheLogger as C, type CacheRateLimitOptions as D, type CacheSnapshotEntry as E, type CacheStackEvents as F, type CacheStackOptions as G, type CacheStatsSnapshot as H, type InvalidationBus as I, type CacheTtlPolicy as J, type CacheTtlPolicyContext as K, type CacheWarmEntry as L, type CacheWarmOptions as M, type CacheWarmProgress as N, type CacheWriteBehindOptions as O, type CacheWriteOptions as P, type EvictionPolicy as Q, type LayerTtlMap as R, MemoryLayer as S, type MemoryLayerOptions as T, type MemoryLayerSnapshotEntry as U, PatternMatcher as V, TagIndex as W, createHonoCacheMiddleware as X, type InvalidationMessage as a, type CacheTagIndex as b, CacheStack as c, type CacheWrapOptions as d, type CacheGetOptions as e, type CacheLayer as f, type CacheSerializer as g, type CacheLayerSetManyEntry as h, type CacheSingleFlightCoordinator as i, type CacheSingleFlightExecutionOptions as j, type CacheAdaptiveTtlOptions as k, type CacheCircuitBreakerOptions as l, type CacheContextOptionsContext as m, type CacheDegradationOptions as n, type CacheEntryResult as o, type CacheEntryWriteKind as p, type CacheEntryWriteOptions as q, type CacheFetcher as r, type CacheFetcherContext as s, type CacheHealthCheckResult as t, type CacheHitRateSnapshot as u, type CacheInspectResult as v, type CacheLayerLatency as w, type CacheMGetEntry as x, type CacheMSetEntry as y, type CacheMetricsSnapshot as z };
package/dist/edge.cjs CHANGED
@@ -389,30 +389,34 @@ var PatternMatcher = class _PatternMatcher {
389
389
  };
390
390
 
391
391
  // src/invalidation/TagIndex.ts
392
- var MAX_PATTERN_RECURSION_DEPTH = 500;
392
+ var DEFAULT_TOUCH_REFRESH_INTERVAL_MS = 1e3;
393
393
  var TagIndex = class {
394
394
  tagToKeys = /* @__PURE__ */ new Map();
395
395
  keyToTags = /* @__PURE__ */ new Map();
396
396
  knownKeys = /* @__PURE__ */ new Map();
397
397
  maxKnownKeys;
398
+ touchRefreshIntervalMs;
398
399
  nextNodeId = 1;
399
400
  root = this.createTrieNode();
400
401
  constructor(options = {}) {
401
402
  this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
403
+ this.touchRefreshIntervalMs = options.touchRefreshIntervalMs ?? DEFAULT_TOUCH_REFRESH_INTERVAL_MS;
402
404
  }
403
405
  /**
404
406
  * Records a key as known without changing tag assignments.
405
407
  */
406
408
  async touch(key) {
407
- this.insertKnownKey(key);
408
- this.pruneKnownKeysIfNeeded();
409
+ if (this.insertKnownKey(key)) {
410
+ this.pruneKnownKeysIfNeeded();
411
+ }
409
412
  }
410
413
  /**
411
414
  * Replaces the tags associated with a key and records the key as known.
412
415
  */
413
416
  async track(key, tags) {
414
- this.insertKnownKey(key);
415
- this.pruneKnownKeysIfNeeded();
417
+ if (this.insertKnownKey(key)) {
418
+ this.pruneKnownKeysIfNeeded();
419
+ }
416
420
  if (tags.length === 0) {
417
421
  return;
418
422
  }
@@ -482,9 +486,14 @@ var TagIndex = class {
482
486
  * Returns known keys matching a wildcard pattern.
483
487
  */
484
488
  async matchPattern(pattern) {
485
- const matches = /* @__PURE__ */ new Set();
486
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
487
- return [...matches];
489
+ const literalPrefix = this.literalPrefix(pattern);
490
+ const node = this.findNode(literalPrefix);
491
+ if (!node) {
492
+ return [];
493
+ }
494
+ const candidates = [];
495
+ this.collectFromNode(node, literalPrefix, candidates);
496
+ return candidates.filter((key) => PatternMatcher.matches(pattern, key));
488
497
  }
489
498
  /**
490
499
  * Visits known keys matching a wildcard pattern.
@@ -514,10 +523,18 @@ var TagIndex = class {
514
523
  };
515
524
  }
516
525
  insertKnownKey(key) {
517
- const isNew = !this.knownKeys.has(key);
518
- this.knownKeys.set(key, Date.now());
526
+ const previousTouch = this.knownKeys.get(key);
527
+ const isNew = previousTouch === void 0;
528
+ const now = Date.now();
529
+ if (!isNew && now - previousTouch < this.touchRefreshIntervalMs) {
530
+ return false;
531
+ }
519
532
  if (!isNew) {
520
- return;
533
+ this.knownKeys.delete(key);
534
+ }
535
+ this.knownKeys.set(key, now);
536
+ if (!isNew) {
537
+ return true;
521
538
  }
522
539
  let node = this.root;
523
540
  for (const character of key) {
@@ -529,6 +546,7 @@ var TagIndex = class {
529
546
  node = child;
530
547
  }
531
548
  node.terminal = true;
549
+ return true;
532
550
  }
533
551
  findNode(prefix) {
534
552
  let node = this.root;
@@ -541,85 +559,52 @@ var TagIndex = class {
541
559
  return node;
542
560
  }
543
561
  collectFromNode(node, prefix, matches) {
544
- if (node.terminal) {
545
- matches.push(prefix);
546
- }
547
- for (const [character, child] of node.children) {
548
- this.collectFromNode(child, `${prefix}${character}`, matches);
562
+ const stack = [{ node, prefix }];
563
+ while (stack.length > 0) {
564
+ const current = stack.pop();
565
+ if (!current) {
566
+ continue;
567
+ }
568
+ if (current.node.terminal) {
569
+ matches.push(current.prefix);
570
+ }
571
+ const children = [...current.node.children].reverse();
572
+ for (const [character, child] of children) {
573
+ stack.push({ node: child, prefix: `${current.prefix}${character}` });
574
+ }
549
575
  }
550
576
  }
551
577
  async visitFromNode(node, prefix, visitor) {
552
- if (node.terminal) {
553
- await visitor(prefix);
554
- }
555
- for (const [character, child] of node.children) {
556
- await this.visitFromNode(child, `${prefix}${character}`, visitor);
557
- }
558
- }
559
- collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
560
- if (depth > MAX_PATTERN_RECURSION_DEPTH) {
561
- return;
562
- }
563
- const stateKey = `${node.id}:${patternIndex}`;
564
- if (visited.has(stateKey)) {
565
- return;
566
- }
567
- visited.add(stateKey);
568
- if (patternIndex === pattern.length) {
569
- if (node.terminal) {
570
- matches.add(prefix);
578
+ const stack = [{ node, prefix }];
579
+ while (stack.length > 0) {
580
+ const current = stack.pop();
581
+ if (!current) {
582
+ continue;
571
583
  }
572
- return;
573
- }
574
- const patternChar = pattern[patternIndex];
575
- if (patternChar === void 0) {
576
- return;
577
- }
578
- if (patternChar === "*") {
579
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
580
- for (const [character, child2] of node.children) {
581
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
584
+ if (current.node.terminal) {
585
+ await visitor(current.prefix);
582
586
  }
583
- return;
584
- }
585
- if (patternChar === "?") {
586
- for (const [character, child2] of node.children) {
587
- this.collectPatternMatches(
588
- child2,
589
- `${prefix}${character}`,
590
- pattern,
591
- patternIndex + 1,
592
- matches,
593
- visited,
594
- depth + 1
595
- );
587
+ const children = [...current.node.children].reverse();
588
+ for (const [character, child] of children) {
589
+ stack.push({ node: child, prefix: `${current.prefix}${character}` });
596
590
  }
597
- return;
598
- }
599
- const child = node.children.get(patternChar);
600
- if (child) {
601
- this.collectPatternMatches(
602
- child,
603
- `${prefix}${patternChar}`,
604
- pattern,
605
- patternIndex + 1,
606
- matches,
607
- visited,
608
- depth + 1
609
- );
610
591
  }
611
592
  }
593
+ literalPrefix(pattern) {
594
+ const wildcardIndex = pattern.search(/[*?]/);
595
+ return wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
596
+ }
612
597
  pruneKnownKeysIfNeeded() {
613
598
  if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
614
599
  return;
615
600
  }
616
- const sorted = [...this.knownKeys.entries()].sort((a, b) => a[1] - b[1]);
617
601
  const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
618
- for (let i = 0; i < toRemove && i < sorted.length; i += 1) {
619
- const entry = sorted[i];
620
- if (entry) {
621
- this.removeKey(entry[0]);
602
+ for (let i = 0; i < toRemove; i += 1) {
603
+ const oldestKey = this.knownKeys.keys().next().value;
604
+ if (oldestKey === void 0) {
605
+ break;
622
606
  }
607
+ this.removeKnownKey(oldestKey);
623
608
  }
624
609
  }
625
610
  removeKey(key) {
@@ -670,6 +655,41 @@ var TagIndex = class {
670
655
  }
671
656
  };
672
657
 
658
+ // src/integrations/httpCacheKeys.ts
659
+ var SENSITIVE_QUERY_PARAMETERS = /* @__PURE__ */ new Set([
660
+ "access_token",
661
+ "api_key",
662
+ "apikey",
663
+ "auth",
664
+ "authorization",
665
+ "code",
666
+ "credentials",
667
+ "id_token",
668
+ "jwt",
669
+ "password",
670
+ "private_key",
671
+ "refresh_token",
672
+ "secret",
673
+ "session",
674
+ "sessionid",
675
+ "session_id",
676
+ "token"
677
+ ]);
678
+ function normalizeHttpCacheUrl(url) {
679
+ try {
680
+ const parsed = new URL(url, "http://localhost");
681
+ for (const name of [...parsed.searchParams.keys()]) {
682
+ if (SENSITIVE_QUERY_PARAMETERS.has(name.toLowerCase())) {
683
+ parsed.searchParams.delete(name);
684
+ }
685
+ }
686
+ parsed.searchParams.sort();
687
+ return parsed.pathname + parsed.search;
688
+ } catch {
689
+ return url;
690
+ }
691
+ }
692
+
673
693
  // src/integrations/hono.ts
674
694
  function createHonoCacheMiddleware(cache, options = {}) {
675
695
  const allowedMethods = new Set((options.methods ?? ["GET"]).map((method) => method.toUpperCase()));
@@ -684,35 +704,39 @@ function createHonoCacheMiddleware(cache, options = {}) {
684
704
  return;
685
705
  }
686
706
  const rawPath = context.req.path ?? context.req.url ?? "/";
687
- const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl(rawPath)}`;
707
+ const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeHttpCacheUrl(rawPath)}`;
688
708
  const cached = await cache.get(key, void 0, options);
689
709
  if (cached !== null) {
690
710
  context.header?.("x-cache", "HIT");
691
711
  context.header?.("content-type", "application/json; charset=utf-8");
692
712
  return context.json(cached);
693
713
  }
714
+ let currentStatus;
715
+ const originalStatus = context.status?.bind(context);
716
+ if (originalStatus) {
717
+ context.status = (status) => {
718
+ currentStatus = status;
719
+ return originalStatus(status);
720
+ };
721
+ }
694
722
  const originalJson = context.json.bind(context);
695
723
  context.json = (body, status) => {
696
724
  context.header?.("x-cache", "MISS");
697
- cache.set(key, body, options).catch((err) => {
698
- cache.emit("error", {
699
- operation: "set",
700
- error: err instanceof Error ? err.message : String(err)
725
+ if (isSuccessfulStatus(status ?? currentStatus)) {
726
+ cache.set(key, body, options).catch((err) => {
727
+ cache.emit("error", {
728
+ operation: "set",
729
+ error: err instanceof Error ? err.message : String(err)
730
+ });
701
731
  });
702
- });
732
+ }
703
733
  return originalJson(body, status);
704
734
  };
705
735
  await next();
706
736
  };
707
737
  }
708
- function normalizeUrl(url) {
709
- try {
710
- const parsed = new URL(url, "http://localhost");
711
- parsed.searchParams.sort();
712
- return parsed.pathname + parsed.search;
713
- } catch {
714
- return url;
715
- }
738
+ function isSuccessfulStatus(statusCode) {
739
+ return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
716
740
  }
717
741
  // Annotate the CommonJS export names for ESM import in node:
718
742
  0 && (module.exports = {
package/dist/edge.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- export { m as CacheContextOptionsContext, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, y as CacheMetricsSnapshot, B as CacheRateLimitOptions, H as CacheTtlPolicy, J as CacheTtlPolicyContext, O as CacheWriteOptions, P as EvictionPolicy, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-BCU8D-Yd.cjs';
1
+ export { m as CacheContextOptionsContext, p as CacheEntryWriteKind, q as CacheEntryWriteOptions, e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, z as CacheMetricsSnapshot, D as CacheRateLimitOptions, J as CacheTtlPolicy, K as CacheTtlPolicyContext, P as CacheWriteOptions, Q as EvictionPolicy, S as MemoryLayer, T as MemoryLayerOptions, U as MemoryLayerSnapshotEntry, V as PatternMatcher, W as TagIndex, X as createHonoCacheMiddleware } from './edge-LBUuZAdr.cjs';
2
2
  import 'node:events';
package/dist/edge.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { m as CacheContextOptionsContext, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, y as CacheMetricsSnapshot, B as CacheRateLimitOptions, H as CacheTtlPolicy, J as CacheTtlPolicyContext, O as CacheWriteOptions, P as EvictionPolicy, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-BCU8D-Yd.js';
1
+ export { m as CacheContextOptionsContext, p as CacheEntryWriteKind, q as CacheEntryWriteOptions, e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, z as CacheMetricsSnapshot, D as CacheRateLimitOptions, J as CacheTtlPolicy, K as CacheTtlPolicyContext, P as CacheWriteOptions, Q as EvictionPolicy, S as MemoryLayer, T as MemoryLayerOptions, U as MemoryLayerSnapshotEntry, V as PatternMatcher, W as TagIndex, X as createHonoCacheMiddleware } from './edge-LBUuZAdr.js';
2
2
  import 'node:events';
package/dist/edge.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  MemoryLayer,
3
3
  TagIndex,
4
4
  createHonoCacheMiddleware
5
- } from "./chunk-IVX6ABFX.js";
5
+ } from "./chunk-XMUT66SH.js";
6
6
  import {
7
7
  PatternMatcher
8
8
  } from "./chunk-KJDFYE5T.js";