layercache 3.0.0 → 3.1.1

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
@@ -19,8 +19,8 @@
19
19
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache_2.0-green" alt="license"></a>
20
20
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-first-3178C6?logo=typescript&logoColor=white" alt="TypeScript"></a>
21
21
  <img src="https://img.shields.io/badge/Node.js-%E2%89%A5_20-339933?logo=nodedotjs&logoColor=white" alt="Node.js >= 20">
22
- <img src="https://img.shields.io/badge/tests-549_passing-brightgreen" alt="tests">
23
- <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>
22
+ <img src="https://img.shields.io/badge/tests-601_passing-brightgreen" alt="tests">
23
+ <a href="https://coveralls.io/github/flyingsquirrel0419/layercache?branch=main"><img src="https://coveralls.io/repos/github/flyingsquirrel0419/layercache/badge.svg?branch=main&t=20260517" alt="Coveralls"></a>
24
24
  </p>
25
25
 
26
26
  <p align="center">
@@ -262,6 +262,8 @@ const cache = new CacheStack([
262
262
  | **Namespaces** | Scoped cache views with hierarchical prefix support |
263
263
  | **Cache warming** | Pre-populate layers at startup with priority-based loading |
264
264
  | **Negative caching** | Cache misses (e.g., "user not found") for short TTLs |
265
+ | **Stored null values** | `cacheNullValues` keeps intentional `null` values distinct from misses |
266
+ | **Entry introspection** | `getEntry()` reports value, kind, state, key, and source layer |
265
267
 
266
268
  ### Invalidation & Freshness
267
269
 
@@ -285,9 +287,11 @@ const cache = new CacheStack([
285
287
  |---|---|
286
288
  | **Graceful degradation** | Skip failed layers temporarily, keep cache available |
287
289
  | **Circuit breaker** | Stop hammering broken upstreams after repeated failures |
288
- | **Fetcher rate limiting** | Scoped to global, per-key, or per-fetcher with custom buckets |
290
+ | **Shared circuit breaker scopes** | Group failures by backend dependency with `scope: 'shared'` and `breakerKey` |
291
+ | **Fetcher rate limiting** | Scoped to global, per-key, or per-fetcher; `queueOverflow: 'reject'` rejects saturated queues and `'bypass'` runs overflow work directly |
289
292
  | **Write policies** | `strict` (fail if any layer fails) or `best-effort` |
290
293
  | **Write-behind** | Batch writes with configurable flush interval |
294
+ | **Bounded disk writes** | `DiskLayer.maxWriteQueueDepth` prevents unbounded serialized write buildup |
291
295
  | **Compression** | gzip / brotli in RedisLayer with configurable threshold |
292
296
  | **MessagePack** | Pluggable serializers (JSON default, MessagePack alternative) |
293
297
  | **Persistence** | Export/import snapshots to memory or disk |
@@ -41,9 +41,12 @@ function validateRateLimitOptions(name, options) {
41
41
  validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
42
42
  validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
43
43
  validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
44
- if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
44
+ if (options.scope !== void 0 && !["global", "key", "fetcher"].includes(options.scope)) {
45
45
  throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
46
46
  }
47
+ if (options.queueOverflow !== void 0 && !["reject", "bypass"].includes(options.queueOverflow)) {
48
+ throw new Error(`${name}.queueOverflow must be one of "reject" or "bypass".`);
49
+ }
47
50
  if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
48
51
  throw new Error(`${name}.bucketKey must not be empty.`);
49
52
  }
@@ -124,6 +127,12 @@ function validateCircuitBreakerOptions(options) {
124
127
  }
125
128
  validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
126
129
  validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
130
+ if (options.scope !== void 0 && !["key", "shared"].includes(options.scope)) {
131
+ throw new Error('circuitBreaker.scope must be one of "key" or "shared".');
132
+ }
133
+ if (options.breakerKey !== void 0 && options.breakerKey.length === 0) {
134
+ throw new Error("circuitBreaker.breakerKey must not be empty.");
135
+ }
127
136
  }
128
137
  function validateContextEntryOptions(name, options) {
129
138
  if (!options) {
@@ -1,4 +1,5 @@
1
1
  import {
2
+ PatternMatcher,
2
3
  unwrapStoredValue
3
4
  } from "./chunk-KJDFYE5T.js";
4
5
 
@@ -245,30 +246,34 @@ var MemoryLayer = class {
245
246
  };
246
247
 
247
248
  // src/invalidation/TagIndex.ts
248
- var MAX_PATTERN_RECURSION_DEPTH = 500;
249
+ var DEFAULT_TOUCH_REFRESH_INTERVAL_MS = 1e3;
249
250
  var TagIndex = class {
250
251
  tagToKeys = /* @__PURE__ */ new Map();
251
252
  keyToTags = /* @__PURE__ */ new Map();
252
253
  knownKeys = /* @__PURE__ */ new Map();
253
254
  maxKnownKeys;
255
+ touchRefreshIntervalMs;
254
256
  nextNodeId = 1;
255
257
  root = this.createTrieNode();
256
258
  constructor(options = {}) {
257
259
  this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
260
+ this.touchRefreshIntervalMs = options.touchRefreshIntervalMs ?? DEFAULT_TOUCH_REFRESH_INTERVAL_MS;
258
261
  }
259
262
  /**
260
263
  * Records a key as known without changing tag assignments.
261
264
  */
262
265
  async touch(key) {
263
- this.insertKnownKey(key);
264
- this.pruneKnownKeysIfNeeded();
266
+ if (this.insertKnownKey(key)) {
267
+ this.pruneKnownKeysIfNeeded();
268
+ }
265
269
  }
266
270
  /**
267
271
  * Replaces the tags associated with a key and records the key as known.
268
272
  */
269
273
  async track(key, tags) {
270
- this.insertKnownKey(key);
271
- this.pruneKnownKeysIfNeeded();
274
+ if (this.insertKnownKey(key)) {
275
+ this.pruneKnownKeysIfNeeded();
276
+ }
272
277
  if (tags.length === 0) {
273
278
  return;
274
279
  }
@@ -338,9 +343,14 @@ var TagIndex = class {
338
343
  * Returns known keys matching a wildcard pattern.
339
344
  */
340
345
  async matchPattern(pattern) {
341
- const matches = /* @__PURE__ */ new Set();
342
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
343
- return [...matches];
346
+ const literalPrefix = this.literalPrefix(pattern);
347
+ const node = this.findNode(literalPrefix);
348
+ if (!node) {
349
+ return [];
350
+ }
351
+ const candidates = [];
352
+ this.collectFromNode(node, literalPrefix, candidates);
353
+ return candidates.filter((key) => PatternMatcher.matches(pattern, key));
344
354
  }
345
355
  /**
346
356
  * Visits known keys matching a wildcard pattern.
@@ -370,13 +380,18 @@ var TagIndex = class {
370
380
  };
371
381
  }
372
382
  insertKnownKey(key) {
373
- const isNew = !this.knownKeys.has(key);
383
+ const previousTouch = this.knownKeys.get(key);
384
+ const isNew = previousTouch === void 0;
385
+ const now = Date.now();
386
+ if (!isNew && now - previousTouch < this.touchRefreshIntervalMs) {
387
+ return false;
388
+ }
374
389
  if (!isNew) {
375
390
  this.knownKeys.delete(key);
376
391
  }
377
- this.knownKeys.set(key, Date.now());
392
+ this.knownKeys.set(key, now);
378
393
  if (!isNew) {
379
- return;
394
+ return true;
380
395
  }
381
396
  let node = this.root;
382
397
  for (const character of key) {
@@ -388,6 +403,7 @@ var TagIndex = class {
388
403
  node = child;
389
404
  }
390
405
  node.terminal = true;
406
+ return true;
391
407
  }
392
408
  findNode(prefix) {
393
409
  let node = this.root;
@@ -400,74 +416,41 @@ var TagIndex = class {
400
416
  return node;
401
417
  }
402
418
  collectFromNode(node, prefix, matches) {
403
- if (node.terminal) {
404
- matches.push(prefix);
405
- }
406
- for (const [character, child] of node.children) {
407
- this.collectFromNode(child, `${prefix}${character}`, matches);
419
+ const stack = [{ node, prefix }];
420
+ while (stack.length > 0) {
421
+ const current = stack.pop();
422
+ if (!current) {
423
+ continue;
424
+ }
425
+ if (current.node.terminal) {
426
+ matches.push(current.prefix);
427
+ }
428
+ const children = [...current.node.children].reverse();
429
+ for (const [character, child] of children) {
430
+ stack.push({ node: child, prefix: `${current.prefix}${character}` });
431
+ }
408
432
  }
409
433
  }
410
434
  async visitFromNode(node, prefix, visitor) {
411
- if (node.terminal) {
412
- await visitor(prefix);
413
- }
414
- for (const [character, child] of node.children) {
415
- await this.visitFromNode(child, `${prefix}${character}`, visitor);
416
- }
417
- }
418
- collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
419
- if (depth > MAX_PATTERN_RECURSION_DEPTH) {
420
- return;
421
- }
422
- const stateKey = `${node.id}:${patternIndex}`;
423
- if (visited.has(stateKey)) {
424
- return;
425
- }
426
- visited.add(stateKey);
427
- if (patternIndex === pattern.length) {
428
- if (node.terminal) {
429
- matches.add(prefix);
435
+ const stack = [{ node, prefix }];
436
+ while (stack.length > 0) {
437
+ const current = stack.pop();
438
+ if (!current) {
439
+ continue;
430
440
  }
431
- return;
432
- }
433
- const patternChar = pattern[patternIndex];
434
- if (patternChar === void 0) {
435
- return;
436
- }
437
- if (patternChar === "*") {
438
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
439
- for (const [character, child2] of node.children) {
440
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
441
+ if (current.node.terminal) {
442
+ await visitor(current.prefix);
441
443
  }
442
- return;
443
- }
444
- if (patternChar === "?") {
445
- for (const [character, child2] of node.children) {
446
- this.collectPatternMatches(
447
- child2,
448
- `${prefix}${character}`,
449
- pattern,
450
- patternIndex + 1,
451
- matches,
452
- visited,
453
- depth + 1
454
- );
444
+ const children = [...current.node.children].reverse();
445
+ for (const [character, child] of children) {
446
+ stack.push({ node: child, prefix: `${current.prefix}${character}` });
455
447
  }
456
- return;
457
- }
458
- const child = node.children.get(patternChar);
459
- if (child) {
460
- this.collectPatternMatches(
461
- child,
462
- `${prefix}${patternChar}`,
463
- pattern,
464
- patternIndex + 1,
465
- matches,
466
- visited,
467
- depth + 1
468
- );
469
448
  }
470
449
  }
450
+ literalPrefix(pattern) {
451
+ const wildcardIndex = pattern.search(/[*?]/);
452
+ return wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
453
+ }
471
454
  pruneKnownKeysIfNeeded() {
472
455
  if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
473
456
  return;
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  validateCacheKey,
5
5
  validatePattern,
6
6
  validateTag
7
- } from "./chunk-NBMG7DHT.js";
7
+ } from "./chunk-L6L7QXYF.js";
8
8
  import {
9
9
  isStoredValueEnvelope,
10
10
  resolveStoredValue
@@ -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
  */
@@ -1269,4 +1323,4 @@ interface HonoCacheMiddlewareOptions extends CacheGetOptions {
1269
1323
  */
1270
1324
  declare function createHonoCacheMiddleware(cache: CacheStack, options?: HonoCacheMiddlewareOptions): (context: HonoLikeContext, next: () => Promise<void>) => Promise<unknown>;
1271
1325
 
1272
- 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
  */
@@ -1269,4 +1323,4 @@ interface HonoCacheMiddlewareOptions extends CacheGetOptions {
1269
1323
  */
1270
1324
  declare function createHonoCacheMiddleware(cache: CacheStack, options?: HonoCacheMiddlewareOptions): (context: HonoLikeContext, next: () => Promise<void>) => Promise<unknown>;
1271
1325
 
1272
- 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 };