@nifrajs/cache 2.1.0 → 2.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @nifrajs/cache
2
2
 
3
- Typed KV cache for nifra — **TTL**, **stale-while-revalidate**, **tag invalidation**, and **single-flight
3
+ Typed KV cache for nifra - **TTL**, **stale-while-revalidate**, **tag invalidation**, and **single-flight
4
4
  stampede protection** on a **pluggable store** (in-memory by default; bring CF KV / Redis for a shared
5
5
  cache). **Dependency-free**; runs on Bun/Node/Deno/Workers.
6
6
 
@@ -9,7 +9,7 @@ import { createCache } from "@nifrajs/cache"
9
9
 
10
10
  const cache = createCache({ defaultTtlMs: 30_000 })
11
11
 
12
- // Cache-aside in a loader — one DB hit per key per TTL, even under a stampede:
12
+ // Cache-aside in a loader - one DB hit per key per TTL, even under a stampede:
13
13
  export async function loader({ params }) {
14
14
  const user = await cache.wrap(
15
15
  `user:${params.id}`,
@@ -25,7 +25,7 @@ await cache.invalidateTag(`user:${id}`)
25
25
 
26
26
  ## Semantics
27
27
 
28
- - **`wrap(key, loader, opts)`** — returns the cached value, or runs `loader`, stores, and returns it.
28
+ - **`wrap(key, loader, opts)`** - returns the cached value, or runs `loader`, stores, and returns it.
29
29
  - **Fresh** (`now < staleAt`): the cached value, no loader call.
30
30
  - **Stale-but-live** (`staleAt ≤ now < expiresAt`, i.e. within `swrMs`): the stale value is returned
31
31
  **immediately** while a **background** refresh runs (deduped). Latency stays flat; data self-heals.
@@ -44,18 +44,18 @@ The default `MemoryCache` is in-process with lazy expiry, a tag index, and an op
44
44
  const cache = createCache({ store: new RedisCacheStore(redis) })
45
45
  ```
46
46
 
47
- On **Cloudflare Workers** the in-memory cache is per-isolate and short-lived — back it with **CF KV** (or
47
+ On **Cloudflare Workers** the in-memory cache is per-isolate and short-lived - back it with **CF KV** (or
48
48
  the Cache API) via a `CacheStore` for anything that should survive across requests/instances.
49
49
 
50
50
  ## API
51
51
 
52
- - `createCache(options?)` → `Cache` — `{ store?, defaultTtlMs?, now?, onError? }`.
52
+ - `createCache(options?)` → `Cache` - `{ store?, defaultTtlMs?, now?, onError? }`.
53
53
  - `cache.wrap(key, loader, { ttlMs?, swrMs?, tags? })` · `get` · `has` · `set` · `delete` · `invalidateTag` · `clear`.
54
- - `MemoryCache({ maxEntries?, now? })` — the default store; implement `CacheStore` for your own.
54
+ - `MemoryCache({ maxEntries?, now? })` - the default store; implement `CacheStore` for your own.
55
55
 
56
56
  ## For AI agents
57
57
 
58
- Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
58
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
59
59
  one cheap read instead of the whole corpus. For the wider framework: the repo's
60
60
  [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
61
61
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAuC,MAAM,YAAY,CAAA;AAE1F,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG,KAAK,CAiF7D"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAuC,MAAM,YAAY,CAAA;AAE1F,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG,KAAK,CA6H7D"}
package/dist/cache.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * The cache facade — typed get/set/wrap over a {@link CacheStore}, with TTL, stale-while-revalidate, and
2
+ * The cache facade - typed get/set/wrap over a {@link CacheStore}, with TTL, stale-while-revalidate, and
3
3
  * single-flight stampede protection. Mirrors the other nifra primitives: a factory, an injectable clock,
4
4
  * and error isolation (a background revalidation that throws goes to `onError`, never rejects the caller).
5
5
  *
6
6
  * import { createCache } from "@nifrajs/cache"
7
7
  *
8
8
  * const cache = createCache({ defaultTtlMs: 30_000 })
9
- * // Cache-aside in a loader — one DB hit per 30s per key, stampede-safe:
9
+ * // Cache-aside in a loader - one DB hit per 30s per key, stampede-safe:
10
10
  * const user = await cache.wrap(`user:${id}`, () => db.user(id), { ttlMs: 30_000, swrMs: 60_000, tags: [`user:${id}`] })
11
11
  * await cache.invalidateTag(`user:${id}`) // on write
12
12
  */
@@ -71,7 +71,9 @@ export function createCache(options = {}) {
71
71
  }
72
72
  return (await load(key, loader, opts)); // miss → load (single-flight)
73
73
  }
74
- return {
74
+ const readToken = options.capabilities?.read ?? "cache.read";
75
+ const writeToken = options.capabilities?.write ?? "cache.write";
76
+ const plain = {
75
77
  get,
76
78
  has,
77
79
  set,
@@ -79,6 +81,42 @@ export function createCache(options = {}) {
79
81
  delete: (key) => Promise.resolve(store.delete(key)),
80
82
  invalidateTag: (tag) => Promise.resolve(store.invalidateTag(tag)),
81
83
  clear: () => Promise.resolve(store.clear()),
84
+ for: bind,
82
85
  };
86
+ // Built per call rather than cached per context: a cache keyed by context would hold every request's
87
+ // context object for the process lifetime, which is a leak with a request body attached to it.
88
+ function bind(context) {
89
+ const beacon = options.beacon;
90
+ if (beacon === undefined) {
91
+ throw new Error("@nifrajs/cache: for(context) needs a beacon - pass `beacon: useCapability` (from @nifrajs/core/capabilities) to createCache");
92
+ }
93
+ // A refused capability has to surface as a REJECTION, not a synchronous throw: every method here
94
+ // returns a promise, and a caller who writes `.catch(…)` instead of `try` would otherwise miss it
95
+ // entirely - turning a fail-closed gate into an unhandled crash.
96
+ const guard = (capabilities, run) => {
97
+ try {
98
+ for (const capability of capabilities)
99
+ beacon(context, capability);
100
+ }
101
+ catch (error) {
102
+ return Promise.reject(error);
103
+ }
104
+ return run();
105
+ };
106
+ const READ = [readToken];
107
+ const WRITE = [writeToken];
108
+ return {
109
+ get: (key) => guard(READ, () => plain.get(key)),
110
+ has: (key) => guard(READ, () => plain.has(key)),
111
+ set: (key, value, opts) => guard(WRITE, () => plain.set(key, value, opts)),
112
+ // Both, because a miss writes and which one happens is not knowable before the call.
113
+ wrap: (key, loader, opts) => guard([readToken, writeToken], () => plain.wrap(key, loader, opts)),
114
+ delete: (key) => guard(WRITE, () => plain.delete(key)),
115
+ invalidateTag: (tag) => guard(WRITE, () => plain.invalidateTag(tag)),
116
+ clear: () => guard(WRITE, () => plain.clear()),
117
+ for: bind,
118
+ };
119
+ }
120
+ return plain;
83
121
  }
84
122
  //# sourceMappingURL=cache.js.map
package/dist/cache.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nifrajs/cache — a typed KV cache for nifra: TTL, stale-while-revalidate, tag invalidation, and
2
+ * @nifrajs/cache - a typed KV cache for nifra: TTL, stale-while-revalidate, tag invalidation, and
3
3
  * single-flight stampede protection, on a pluggable store (in-memory by default; bring CF KV / Redis for
4
4
  * a shared cache). Dependency-free; runs on Bun/Node/Deno/Workers.
5
5
  *
@@ -9,5 +9,5 @@
9
9
  */
10
10
  export { createCache } from "./cache.js";
11
11
  export { MemoryCache, type MemoryCacheOptions } from "./memory-cache.js";
12
- export type { Cache, CacheOptions, CacheStore, SetOptions, StoredEntry, WrapOptions, } from "./types.js";
12
+ export type { Cache, CacheCapabilities, CacheOptions, CacheStore, CapabilityBeacon, SetOptions, StoredEntry, WrapOptions, } from "./types.js";
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AACxE,YAAY,EACV,KAAK,EACL,YAAY,EACZ,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,GACZ,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AACxE,YAAY,EACV,KAAK,EACL,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,GACZ,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nifrajs/cache — a typed KV cache for nifra: TTL, stale-while-revalidate, tag invalidation, and
2
+ * @nifrajs/cache - a typed KV cache for nifra: TTL, stale-while-revalidate, tag invalidation, and
3
3
  * single-flight stampede protection, on a pluggable store (in-memory by default; bring CF KV / Redis for
4
4
  * a shared cache). Dependency-free; runs on Bun/Node/Deno/Workers.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * In-process {@link CacheStore} — a Map with lazy expiry, a tag index for group invalidation, and an
2
+ * In-process {@link CacheStore} - a Map with lazy expiry, a tag index for group invalidation, and an
3
3
  * optional LRU cap. Correct for a single instance; for a cache shared across instances/workers, implement
4
4
  * `CacheStore` over CF KV / Redis. Not persisted (a restart empties it).
5
5
  */
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nifrajs/cache — types for the typed KV cache.
2
+ * @nifrajs/cache - types for the typed KV cache.
3
3
  *
4
4
  * The facade ({@link Cache}) is typed + owns TTL/SWR/stampede logic; the {@link CacheStore} is a raw
5
5
  * key→entry adapter (memory by default; bring CF KV / Redis for shared or durable caching). Dependency-free.
@@ -7,7 +7,7 @@
7
7
  /** A cached entry as the store holds it. */
8
8
  export interface StoredEntry {
9
9
  readonly value: unknown;
10
- /** Epoch-ms after which the entry is gone — a miss. */
10
+ /** Epoch-ms after which the entry is gone - a miss. */
11
11
  readonly expiresAt: number;
12
12
  /** Epoch-ms after which the entry is stale: still served during the SWR window while a refresh runs.
13
13
  * Equal to `expiresAt` when there's no stale-while-revalidate window. */
@@ -15,7 +15,7 @@ export interface StoredEntry {
15
15
  }
16
16
  /**
17
17
  * Raw key→entry storage. The default {@link MemoryCache} is in-process; implement this over CF KV /
18
- * Redis / etc. for a cache shared across instances. All methods may be sync or async — the cache awaits them.
18
+ * Redis / etc. for a cache shared across instances. All methods may be sync or async - the cache awaits them.
19
19
  */
20
20
  export interface CacheStore {
21
21
  /** The live entry for `key`, or `undefined` if missing or hard-expired (`now >= expiresAt`). */
@@ -36,9 +36,33 @@ export interface SetOptions {
36
36
  readonly tags?: readonly string[];
37
37
  }
38
38
  export type WrapOptions = SetOptions;
39
+ /**
40
+ * `useCapability` from `@nifrajs/core/capabilities`, taken as a parameter rather than imported so this
41
+ * package keeps its zero dependencies - a cache should not drag the server into a bundle that only
42
+ * wanted a cache. Wiring it is one line where the cache is created.
43
+ */
44
+ export type CapabilityBeacon = (context: object, capability: string) => void;
45
+ /** Capability tokens this cache announces. Defaults: `cache.read` and `cache.write`. */
46
+ export interface CacheCapabilities {
47
+ readonly read?: string;
48
+ readonly write?: string;
49
+ }
39
50
  export interface CacheOptions {
40
51
  /** Storage adapter. Default: a fresh {@link MemoryCache}. */
41
52
  readonly store?: CacheStore;
53
+ /**
54
+ * Make cache operations produce capability evidence:
55
+ *
56
+ * import { useCapability } from "@nifrajs/core/capabilities"
57
+ * const cache = createCache({ beacon: useCapability })
58
+ *
59
+ * Then `cache.for(context)` announces its capability before each operation. Evidence from the CALL is
60
+ * per-route and exact, where evidence from an import is per-module and therefore as broad as the
61
+ * module - which is why a beacon can say something a provenance rule cannot.
62
+ */
63
+ readonly beacon?: CapabilityBeacon;
64
+ /** Override the announced tokens when the app names its capabilities differently. */
65
+ readonly capabilities?: CacheCapabilities;
42
66
  /** Default TTL (ms) for `set`/`wrap` when none is given. Default 60_000. */
43
67
  readonly defaultTtlMs?: number;
44
68
  /** Injectable clock (tests). Default `() => Date.now()`. */
@@ -63,5 +87,17 @@ export interface Cache {
63
87
  * returned immediately while a deduped background refresh runs). A throwing `loader` is NOT cached.
64
88
  */
65
89
  wrap<T>(key: string, loader: () => T, options?: WrapOptions): Promise<Awaited<T>>;
90
+ /**
91
+ * A view bound to a request context: every operation announces its capability first, and fails closed
92
+ * when the route did not declare it.
93
+ *
94
+ * `wrap` announces BOTH read and write, because a miss writes and which one happens is not known
95
+ * before the call. A declaration describes what a route may do, so the conservative answer is the
96
+ * correct one.
97
+ *
98
+ * Requires `beacon`. Without one this throws rather than handing back a cache that quietly produces
99
+ * no evidence - a beacon nobody notices is missing is the exact failure the beacon exists to prevent.
100
+ */
101
+ for(context: object): Cache;
66
102
  }
67
103
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,uDAAuD;IACvD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B;6EACyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,gGAAgG;IAChG,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;IAC5E,yFAAyF;IACzF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzC,uCAAuC;IACvC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,0FAA0F;IAC1F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,2GAA2G;IAC3G,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,CAAA;AAEpC,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAA;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAC3B,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CACzD;AAED,MAAM,WAAW,KAAK;IACpB,kGAAkG;IAClG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IACrD,iDAAiD;IACjD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAClC,iCAAiC;IACjC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,qCAAqC;IACrC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB;;;;OAIG;IACH,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;CAClF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nifrajs/cache — types for the typed KV cache.
2
+ * @nifrajs/cache - types for the typed KV cache.
3
3
  *
4
4
  * The facade ({@link Cache}) is typed + owns TTL/SWR/stampede logic; the {@link CacheStore} is a raw
5
5
  * key→entry adapter (memory by default; bring CF KV / Redis for shared or durable caching). Dependency-free.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/cache",
3
- "version": "2.1.0",
4
- "description": "Typed KV cache for nifra — TTL, stale-while-revalidate, tag invalidation, and single-flight stampede protection on a pluggable store (in-memory default; bring CF KV / Redis). Dependency-free, runs on Bun/Node/Deno/Workers.",
3
+ "version": "2.3.0",
4
+ "description": "Typed KV cache for nifra - TTL, stale-while-revalidate, tag invalidation, and single-flight stampede protection on a pluggable store (in-memory default; bring CF KV / Redis). Dependency-free, runs on Bun/Node/Deno/Workers.",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
package/src/cache.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * The cache facade — typed get/set/wrap over a {@link CacheStore}, with TTL, stale-while-revalidate, and
2
+ * The cache facade - typed get/set/wrap over a {@link CacheStore}, with TTL, stale-while-revalidate, and
3
3
  * single-flight stampede protection. Mirrors the other nifra primitives: a factory, an injectable clock,
4
4
  * and error isolation (a background revalidation that throws goes to `onError`, never rejects the caller).
5
5
  *
6
6
  * import { createCache } from "@nifrajs/cache"
7
7
  *
8
8
  * const cache = createCache({ defaultTtlMs: 30_000 })
9
- * // Cache-aside in a loader — one DB hit per 30s per key, stampede-safe:
9
+ * // Cache-aside in a loader - one DB hit per 30s per key, stampede-safe:
10
10
  * const user = await cache.wrap(`user:${id}`, () => db.user(id), { ttlMs: 30_000, swrMs: 60_000, tags: [`user:${id}`] })
11
11
  * await cache.invalidateTag(`user:${id}`) // on write
12
12
  */
@@ -86,7 +86,10 @@ export function createCache(options: CacheOptions = {}): Cache {
86
86
  return (await load(key, loader as () => unknown, opts)) as Awaited<T> // miss → load (single-flight)
87
87
  }
88
88
 
89
- return {
89
+ const readToken = options.capabilities?.read ?? "cache.read"
90
+ const writeToken = options.capabilities?.write ?? "cache.write"
91
+
92
+ const plain: Cache = {
90
93
  get,
91
94
  has,
92
95
  set,
@@ -94,5 +97,46 @@ export function createCache(options: CacheOptions = {}): Cache {
94
97
  delete: (key) => Promise.resolve(store.delete(key)),
95
98
  invalidateTag: (tag) => Promise.resolve(store.invalidateTag(tag)),
96
99
  clear: () => Promise.resolve(store.clear()),
100
+ for: bind,
97
101
  }
102
+
103
+ // Built per call rather than cached per context: a cache keyed by context would hold every request's
104
+ // context object for the process lifetime, which is a leak with a request body attached to it.
105
+ function bind(context: object): Cache {
106
+ const beacon = options.beacon
107
+ if (beacon === undefined) {
108
+ throw new Error(
109
+ "@nifrajs/cache: for(context) needs a beacon - pass `beacon: useCapability` (from @nifrajs/core/capabilities) to createCache",
110
+ )
111
+ }
112
+ // A refused capability has to surface as a REJECTION, not a synchronous throw: every method here
113
+ // returns a promise, and a caller who writes `.catch(…)` instead of `try` would otherwise miss it
114
+ // entirely - turning a fail-closed gate into an unhandled crash.
115
+ const guard = <T>(capabilities: readonly string[], run: () => Promise<T>): Promise<T> => {
116
+ try {
117
+ for (const capability of capabilities) beacon(context, capability)
118
+ } catch (error) {
119
+ return Promise.reject(error)
120
+ }
121
+ return run()
122
+ }
123
+ const READ = [readToken]
124
+ const WRITE = [writeToken]
125
+ return {
126
+ get: <T = unknown>(key: string): Promise<T | undefined> =>
127
+ guard(READ, () => plain.get<T>(key)),
128
+ has: (key) => guard(READ, () => plain.has(key)),
129
+ set: <T>(key: string, value: T, opts?: SetOptions): Promise<void> =>
130
+ guard(WRITE, () => plain.set(key, value, opts)),
131
+ // Both, because a miss writes and which one happens is not knowable before the call.
132
+ wrap: <T>(key: string, loader: () => T, opts?: WrapOptions): Promise<Awaited<T>> =>
133
+ guard([readToken, writeToken], () => plain.wrap(key, loader, opts)),
134
+ delete: (key) => guard(WRITE, () => plain.delete(key)),
135
+ invalidateTag: (tag) => guard(WRITE, () => plain.invalidateTag(tag)),
136
+ clear: () => guard(WRITE, () => plain.clear()),
137
+ for: bind,
138
+ }
139
+ }
140
+
141
+ return plain
98
142
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nifrajs/cache — a typed KV cache for nifra: TTL, stale-while-revalidate, tag invalidation, and
2
+ * @nifrajs/cache - a typed KV cache for nifra: TTL, stale-while-revalidate, tag invalidation, and
3
3
  * single-flight stampede protection, on a pluggable store (in-memory by default; bring CF KV / Redis for
4
4
  * a shared cache). Dependency-free; runs on Bun/Node/Deno/Workers.
5
5
  *
@@ -12,8 +12,10 @@ export { createCache } from "./cache.ts"
12
12
  export { MemoryCache, type MemoryCacheOptions } from "./memory-cache.ts"
13
13
  export type {
14
14
  Cache,
15
+ CacheCapabilities,
15
16
  CacheOptions,
16
17
  CacheStore,
18
+ CapabilityBeacon,
17
19
  SetOptions,
18
20
  StoredEntry,
19
21
  WrapOptions,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * In-process {@link CacheStore} — a Map with lazy expiry, a tag index for group invalidation, and an
2
+ * In-process {@link CacheStore} - a Map with lazy expiry, a tag index for group invalidation, and an
3
3
  * optional LRU cap. Correct for a single instance; for a cache shared across instances/workers, implement
4
4
  * `CacheStore` over CF KV / Redis. Not persisted (a restart empties it).
5
5
  */
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nifrajs/cache — types for the typed KV cache.
2
+ * @nifrajs/cache - types for the typed KV cache.
3
3
  *
4
4
  * The facade ({@link Cache}) is typed + owns TTL/SWR/stampede logic; the {@link CacheStore} is a raw
5
5
  * key→entry adapter (memory by default; bring CF KV / Redis for shared or durable caching). Dependency-free.
@@ -8,7 +8,7 @@
8
8
  /** A cached entry as the store holds it. */
9
9
  export interface StoredEntry {
10
10
  readonly value: unknown
11
- /** Epoch-ms after which the entry is gone — a miss. */
11
+ /** Epoch-ms after which the entry is gone - a miss. */
12
12
  readonly expiresAt: number
13
13
  /** Epoch-ms after which the entry is stale: still served during the SWR window while a refresh runs.
14
14
  * Equal to `expiresAt` when there's no stale-while-revalidate window. */
@@ -17,7 +17,7 @@ export interface StoredEntry {
17
17
 
18
18
  /**
19
19
  * Raw key→entry storage. The default {@link MemoryCache} is in-process; implement this over CF KV /
20
- * Redis / etc. for a cache shared across instances. All methods may be sync or async — the cache awaits them.
20
+ * Redis / etc. for a cache shared across instances. All methods may be sync or async - the cache awaits them.
21
21
  */
22
22
  export interface CacheStore {
23
23
  /** The live entry for `key`, or `undefined` if missing or hard-expired (`now >= expiresAt`). */
@@ -41,9 +41,35 @@ export interface SetOptions {
41
41
 
42
42
  export type WrapOptions = SetOptions
43
43
 
44
+ /**
45
+ * `useCapability` from `@nifrajs/core/capabilities`, taken as a parameter rather than imported so this
46
+ * package keeps its zero dependencies - a cache should not drag the server into a bundle that only
47
+ * wanted a cache. Wiring it is one line where the cache is created.
48
+ */
49
+ export type CapabilityBeacon = (context: object, capability: string) => void
50
+
51
+ /** Capability tokens this cache announces. Defaults: `cache.read` and `cache.write`. */
52
+ export interface CacheCapabilities {
53
+ readonly read?: string
54
+ readonly write?: string
55
+ }
56
+
44
57
  export interface CacheOptions {
45
58
  /** Storage adapter. Default: a fresh {@link MemoryCache}. */
46
59
  readonly store?: CacheStore
60
+ /**
61
+ * Make cache operations produce capability evidence:
62
+ *
63
+ * import { useCapability } from "@nifrajs/core/capabilities"
64
+ * const cache = createCache({ beacon: useCapability })
65
+ *
66
+ * Then `cache.for(context)` announces its capability before each operation. Evidence from the CALL is
67
+ * per-route and exact, where evidence from an import is per-module and therefore as broad as the
68
+ * module - which is why a beacon can say something a provenance rule cannot.
69
+ */
70
+ readonly beacon?: CapabilityBeacon
71
+ /** Override the announced tokens when the app names its capabilities differently. */
72
+ readonly capabilities?: CacheCapabilities
47
73
  /** Default TTL (ms) for `set`/`wrap` when none is given. Default 60_000. */
48
74
  readonly defaultTtlMs?: number
49
75
  /** Injectable clock (tests). Default `() => Date.now()`. */
@@ -69,4 +95,16 @@ export interface Cache {
69
95
  * returned immediately while a deduped background refresh runs). A throwing `loader` is NOT cached.
70
96
  */
71
97
  wrap<T>(key: string, loader: () => T, options?: WrapOptions): Promise<Awaited<T>>
98
+ /**
99
+ * A view bound to a request context: every operation announces its capability first, and fails closed
100
+ * when the route did not declare it.
101
+ *
102
+ * `wrap` announces BOTH read and write, because a miss writes and which one happens is not known
103
+ * before the call. A declaration describes what a route may do, so the conservative answer is the
104
+ * correct one.
105
+ *
106
+ * Requires `beacon`. Without one this throws rather than handing back a cache that quietly produces
107
+ * no evidence - a beacon nobody notices is missing is the exact failure the beacon exists to prevent.
108
+ */
109
+ for(context: object): Cache
72
110
  }