layercache 3.0.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.
- package/README.md +7 -3
- package/dist/{chunk-NBMG7DHT.js → chunk-L6L7QXYF.js} +10 -1
- package/dist/{chunk-5CIBABDH.js → chunk-XMUT66SH.js} +54 -71
- package/dist/cli.js +1 -1
- package/dist/{edge-BDyuPmIq.d.cts → edge-LBUuZAdr.d.cts} +56 -2
- package/dist/{edge-BDyuPmIq.d.ts → edge-LBUuZAdr.d.ts} +56 -2
- package/dist/edge.cjs +53 -71
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +266 -138
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +205 -68
- package/package.json +1 -1
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-
|
|
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=
|
|
22
|
+
<img src="https://img.shields.io/badge/tests-598_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
|
-
| **
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
@@ -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
|
|
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 {
|
|
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
|
|
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 {
|
|
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 };
|