@nifrajs/cache 3.3.0 → 4.0.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
@@ -48,10 +48,36 @@ const cache = createCache({ store: new RedisCacheStore(redis) })
48
48
  On **Cloudflare Workers** the in-memory cache is per-isolate and short-lived - back it with **CF KV** (or
49
49
  the Cache API) via a `CacheStore` for anything that should survive across requests/instances.
50
50
 
51
+ ## Observing operations and tracing
52
+
53
+ `observer` receives one event per operation after it settles: `{ op, outcome, startedAt, durationMs,
54
+ key, tag, tagCount, context }`. `outcome` is `hit`, `stale` or `miss` for reads, `ok` for writes and a
55
+ finished background refresh (`op: "revalidate"`), `error` when the store or loader threw. An observer
56
+ that throws is swallowed; it never changes a result. Without an observer the cache reads no extra clock.
57
+
58
+ `cache.for(c)` binds a request (or job) context, and every event carries it. That is how
59
+ `cacheTracing()` from `@nifrajs/otel/cache` turns each operation into a child span of `c.trace`:
60
+
61
+ ```ts
62
+ import { cacheTracing } from "@nifrajs/otel/cache"
63
+
64
+ const cache = createCache({ observer: cacheTracing({ exporter }) })
65
+ app.use(tracing({ exporter })).get("/orders/:user", (c) =>
66
+ cache.for(c).wrap(`orders:${c.params.user}`, () => loadOrders(c.params.user)), // span "cache wrap"
67
+ )
68
+ ```
69
+
70
+ The raw key goes to the in-process observer only. `cacheTracing` exports the key prefix (the token
71
+ before the first `:`, when it is a short lowercase name) unless `keyAttribute` says otherwise
72
+ (`"none"`, or a function that owns the redaction). A stale read's background refresh is its own trace,
73
+ linked to the request span. `for(c)` needs a `beacon`, an `observer`, or both; the beacon is enforced
74
+ whenever it is set.
75
+
51
76
  ## API
52
77
 
53
- - `createCache(options?)` → `Cache` - `{ store?, defaultTtlMs?, now?, onError? }`.
78
+ - `createCache(options?)` → `Cache` - `{ store?, defaultTtlMs?, now?, onError?, observer?, beacon?, capabilities? }`.
54
79
  - `cache.wrap(key, loader, { ttlMs?, swrMs?, tags? })` · `get` · `has` · `set` · `delete` · `invalidateTag` · `clear`.
80
+ - `cache.for(context)` → the same surface bound to a request or job context.
55
81
  - `MemoryCache({ maxEntries?, now? })` - the default store; implement `CacheStore` for your own.
56
82
 
57
83
  ## For AI agents
@@ -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;AAQ1F,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG,KAAK,CAgI7D"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,KAAK,EAIL,YAAY,EAMb,MAAM,YAAY,CAAA;AAuCnB,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG,KAAK,CA6O7D"}
package/dist/cache.js CHANGED
@@ -16,6 +16,28 @@ function assertDuration(value, name) {
16
16
  throw new RangeError(`[nifra/cache] ${name} must be a finite non-negative number`);
17
17
  }
18
18
  }
19
+ function notify(observer, event) {
20
+ try {
21
+ const result = observer(event);
22
+ if (result instanceof Promise)
23
+ result.catch(() => undefined);
24
+ }
25
+ catch {
26
+ // An observer can never change a cache result.
27
+ }
28
+ }
29
+ function report(observer, op, outcome, started, key, tag, tagCount, context) {
30
+ notify(observer, {
31
+ op,
32
+ outcome,
33
+ startedAt: performance.timeOrigin + started,
34
+ durationMs: performance.now() - started,
35
+ key,
36
+ tag,
37
+ tagCount,
38
+ context,
39
+ });
40
+ }
19
41
  /** Create a cache over the given (or a fresh in-memory) store. */
20
42
  export function createCache(options = {}) {
21
43
  const defaultTtlMs = options.defaultTtlMs ?? 60_000;
@@ -25,6 +47,7 @@ export function createCache(options = {}) {
25
47
  const store = options.store ?? new MemoryCache({ now });
26
48
  const onError = options.onError ??
27
49
  ((error, key) => console.error(`[nifra/cache] revalidate ${JSON.stringify(key)} failed:`, error));
50
+ const observer = options.observer;
28
51
  // Single-flight: concurrent loads for the same key share one promise (miss stampede + SWR dedup).
29
52
  const inflight = new Map();
30
53
  async function get(key) {
@@ -57,10 +80,16 @@ export function createCache(options = {}) {
57
80
  inflight.set(key, p);
58
81
  return p;
59
82
  }
60
- function revalidate(key, loader, opts) {
83
+ function revalidate(key, loader, opts, context) {
61
84
  if (inflight.has(key))
62
85
  return; // a refresh is already running
63
- void load(key, loader, opts).catch((error) => {
86
+ const started = observer === undefined ? 0 : performance.now();
87
+ const refresh = load(key, loader, opts);
88
+ if (observer !== undefined) {
89
+ const tagCount = opts.tags?.length ?? 0;
90
+ refresh.then(() => report(observer, "revalidate", "ok", started, key, undefined, tagCount, context), () => report(observer, "revalidate", "error", started, key, undefined, tagCount, context));
91
+ }
92
+ void refresh.catch((error) => {
64
93
  try {
65
94
  onError(error, key);
66
95
  }
@@ -74,30 +103,96 @@ export function createCache(options = {}) {
74
103
  if (entry !== undefined) {
75
104
  if (now() < entry.staleAt)
76
105
  return entry.value; // fresh hit
77
- revalidate(key, loader, opts); // stale-but-live → serve stale, refresh in background
106
+ revalidate(key, loader, opts, undefined); // stale-but-live → serve stale, refresh in background
78
107
  return entry.value;
79
108
  }
80
109
  return (await load(key, loader, opts)); // miss → load (single-flight)
81
110
  }
111
+ const readOutcome = (entry) => entry === undefined ? "miss" : now() < entry.staleAt ? "hit" : "stale";
112
+ // A separate surface rather than branches in the plain one, so a cache without an observer runs
113
+ // exactly the code it ran before observers existed.
114
+ function observed(observer, context) {
115
+ const settle = async (op, key, tag, tagCount, run, success) => {
116
+ const started = performance.now();
117
+ let outcome = "error";
118
+ try {
119
+ const value = await run();
120
+ outcome = success;
121
+ return value;
122
+ }
123
+ finally {
124
+ report(observer, op, outcome, started, key, tag, tagCount, context);
125
+ }
126
+ };
127
+ const read = async (op, key, pick) => {
128
+ const started = performance.now();
129
+ let outcome = "error";
130
+ try {
131
+ const entry = await store.get(key);
132
+ outcome = readOutcome(entry);
133
+ return pick(entry);
134
+ }
135
+ finally {
136
+ report(observer, op, outcome, started, key, undefined, 0, context);
137
+ }
138
+ };
139
+ return {
140
+ get: (key) => read("get", key, (entry) =>
141
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the store keeps values untyped; T is the caller's claim for this key, exactly as in the plain get().
142
+ entry === undefined ? undefined : entry.value),
143
+ has: (key) => read("has", key, (entry) => entry !== undefined),
144
+ set: (key, value, opts) => settle("set", key, undefined, opts?.tags?.length ?? 0, () => set(key, value, opts), "ok"),
145
+ async wrap(key, loader, opts = {}) {
146
+ const started = performance.now();
147
+ let outcome = "error";
148
+ try {
149
+ const entry = await store.get(key);
150
+ if (entry !== undefined) {
151
+ outcome = readOutcome(entry);
152
+ if (outcome === "stale")
153
+ revalidate(key, loader, opts, context);
154
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the store keeps values untyped; a wrap of this key stored what a loader of the caller's T returned.
155
+ return entry.value;
156
+ }
157
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: load() resolves to what this loader returned, or what a concurrent wrap of the same key loaded for the same T.
158
+ const value = (await load(key, loader, opts));
159
+ outcome = "miss";
160
+ return value;
161
+ }
162
+ finally {
163
+ report(observer, "wrap", outcome, started, key, undefined, opts.tags?.length ?? 0, context);
164
+ }
165
+ },
166
+ delete: (key) => settle("delete", key, undefined, 0, () => Promise.resolve(store.delete(key)), "ok"),
167
+ invalidateTag: (tag) => settle("invalidateTag", undefined, tag, 1, () => Promise.resolve(store.invalidateTag(tag)), "ok"),
168
+ clear: () => settle("clear", undefined, undefined, 0, () => Promise.resolve(store.clear()), "ok"),
169
+ for: bind,
170
+ };
171
+ }
82
172
  const readToken = options.capabilities?.read ?? "cache.read";
83
173
  const writeToken = options.capabilities?.write ?? "cache.write";
84
- const plain = {
85
- get,
86
- has,
87
- set,
88
- wrap,
89
- delete: (key) => Promise.resolve(store.delete(key)),
90
- invalidateTag: (tag) => Promise.resolve(store.invalidateTag(tag)),
91
- clear: () => Promise.resolve(store.clear()),
92
- for: bind,
93
- };
174
+ const plain = observer === undefined
175
+ ? {
176
+ get,
177
+ has,
178
+ set,
179
+ wrap,
180
+ delete: (key) => Promise.resolve(store.delete(key)),
181
+ invalidateTag: (tag) => Promise.resolve(store.invalidateTag(tag)),
182
+ clear: () => Promise.resolve(store.clear()),
183
+ for: bind,
184
+ }
185
+ : observed(observer, undefined);
94
186
  // Built per call rather than cached per context: a cache keyed by context would hold every request's
95
187
  // context object for the process lifetime, which is a leak with a request body attached to it.
96
188
  function bind(context) {
97
189
  const beacon = options.beacon;
98
- if (beacon === undefined) {
99
- throw new Error("@nifrajs/cache: for(context) needs a beacon - pass `beacon: useCapability` (from @nifrajs/core/capabilities) to createCache");
190
+ if (beacon === undefined && observer === undefined) {
191
+ throw new Error("@nifrajs/cache: for(context) needs a beacon or an observer - pass `beacon: useCapability` (from @nifrajs/core/capabilities) or `observer` to createCache");
100
192
  }
193
+ const base = observer === undefined ? plain : observed(observer, context);
194
+ if (beacon === undefined)
195
+ return base;
101
196
  // A refused capability has to surface as a REJECTION, not a synchronous throw: every method here
102
197
  // returns a promise, and a caller who writes `.catch(…)` instead of `try` would otherwise miss it
103
198
  // entirely - turning a fail-closed gate into an unhandled crash.
@@ -114,14 +209,14 @@ export function createCache(options = {}) {
114
209
  const READ = [readToken];
115
210
  const WRITE = [writeToken];
116
211
  return {
117
- get: (key) => guard(READ, () => plain.get(key)),
118
- has: (key) => guard(READ, () => plain.has(key)),
119
- set: (key, value, opts) => guard(WRITE, () => plain.set(key, value, opts)),
212
+ get: (key) => guard(READ, () => base.get(key)),
213
+ has: (key) => guard(READ, () => base.has(key)),
214
+ set: (key, value, opts) => guard(WRITE, () => base.set(key, value, opts)),
120
215
  // Both, because a miss writes and which one happens is not knowable before the call.
121
- wrap: (key, loader, opts) => guard([readToken, writeToken], () => plain.wrap(key, loader, opts)),
122
- delete: (key) => guard(WRITE, () => plain.delete(key)),
123
- invalidateTag: (tag) => guard(WRITE, () => plain.invalidateTag(tag)),
124
- clear: () => guard(WRITE, () => plain.clear()),
216
+ wrap: (key, loader, opts) => guard([readToken, writeToken], () => base.wrap(key, loader, opts)),
217
+ delete: (key) => guard(WRITE, () => base.delete(key)),
218
+ invalidateTag: (tag) => guard(WRITE, () => base.invalidateTag(tag)),
219
+ clear: () => guard(WRITE, () => base.clear()),
125
220
  for: bind,
126
221
  };
127
222
  }
package/dist/cache.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAG/C,SAAS,cAAc,CAAC,KAAa,EAAE,IAAY;IACjD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,UAAU,CAAC,iBAAiB,IAAI,uCAAuC,CAAC,CAAA;IACpF,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,WAAW,CAAC,UAAwB,EAAE;IACpD,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,MAAM,CAAA;IACnD,cAAc,CAAC,YAAY,EAAE,cAAc,CAAC,CAAA;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IAC7C,uGAAuG;IACvG,MAAM,KAAK,GAAe,OAAO,CAAC,KAAK,IAAI,IAAI,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;IACnE,MAAM,OAAO,GACX,OAAO,CAAC,OAAO;QACf,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CACd,OAAO,CAAC,KAAK,CAAC,4BAA4B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAA;IAEpF,kGAAkG;IAClG,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAA;IAEpD,KAAK,UAAU,GAAG,CAAc,GAAW;QACzC,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,KAAK,CAAC,KAAW,CAAA;IAC7D,CAAC;IAED,KAAK,UAAU,GAAG,CAAC,GAAW;QAC5B,OAAO,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,CAAA;IAC7C,CAAC;IAED,KAAK,UAAU,GAAG,CAAI,GAAW,EAAE,KAAQ,EAAE,OAAmB,EAAE;QAChE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,YAAY,CAAA;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAA;QAC7B,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC9B,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC9B,MAAM,CAAC,GAAG,GAAG,EAAE,CAAA;QACf,MAAM,KAAK,CAAC,GAAG,CACb,GAAG,EACH,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,EAAE,CAAC,GAAG,KAAK,GAAG,KAAK,EAAE,EAC3D,IAAI,CAAC,IAAI,IAAI,EAAE,CAChB,CAAA;IACH,CAAC;IAED,SAAS,IAAI,CAAC,GAAW,EAAE,MAAqB,EAAE,IAAiB;QACjE,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAA;QAC3C,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YACpB,MAAM,KAAK,GAAG,MAAM,MAAM,EAAE,CAAA;YAC5B,MAAM,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;YAC3B,OAAO,KAAK,CAAA;QACd,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAChB,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACnD,CAAC,CAAC,CAAA;QACF,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACpB,OAAO,CAAC,CAAA;IACV,CAAC;IAED,SAAS,UAAU,CAAC,GAAW,EAAE,MAAqB,EAAE,IAAiB;QACvE,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAM,CAAC,+BAA+B;QAC7D,KAAK,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YAC3C,IAAI,CAAC;gBACH,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YACrB,CAAC;YAAC,MAAM,CAAC;gBACP,yCAAyC;YAC3C,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,UAAU,IAAI,CACjB,GAAW,EACX,MAAe,EACf,OAAoB,EAAE;QAEtB,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO;gBAAE,OAAO,KAAK,CAAC,KAAmB,CAAA,CAAC,YAAY;YACxE,UAAU,CAAC,GAAG,EAAE,MAAuB,EAAE,IAAI,CAAC,CAAA,CAAC,sDAAsD;YACrG,OAAO,KAAK,CAAC,KAAmB,CAAA;QAClC,CAAC;QACD,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,MAAuB,EAAE,IAAI,CAAC,CAAe,CAAA,CAAC,8BAA8B;IACtG,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,IAAI,IAAI,YAAY,CAAA;IAC5D,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,EAAE,KAAK,IAAI,aAAa,CAAA;IAE/D,MAAM,KAAK,GAAU;QACnB,GAAG;QACH,GAAG;QACH,GAAG;QACH,IAAI;QACJ,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnD,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QACjE,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QAC3C,GAAG,EAAE,IAAI;KACV,CAAA;IAED,qGAAqG;IACrG,+FAA+F;IAC/F,SAAS,IAAI,CAAC,OAAe;QAC3B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;QAC7B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CACb,6HAA6H,CAC9H,CAAA;QACH,CAAC;QACD,iGAAiG;QACjG,kGAAkG;QAClG,iEAAiE;QACjE,MAAM,KAAK,GAAG,CAAI,YAA+B,EAAE,GAAqB,EAAc,EAAE;YACtF,IAAI,CAAC;gBACH,KAAK,MAAM,UAAU,IAAI,YAAY;oBAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;YACpE,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YAC9B,CAAC;YACD,OAAO,GAAG,EAAE,CAAA;QACd,CAAC,CAAA;QACD,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC,CAAA;QACxB,MAAM,KAAK,GAAG,CAAC,UAAU,CAAC,CAAA;QAC1B,OAAO;YACL,GAAG,EAAE,CAAc,GAAW,EAA0B,EAAE,CACxD,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAI,GAAG,CAAC,CAAC;YACtC,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/C,GAAG,EAAE,CAAI,GAAW,EAAE,KAAQ,EAAE,IAAiB,EAAiB,EAAE,CAClE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YACjD,qFAAqF;YACrF,IAAI,EAAE,CAAI,GAAW,EAAE,MAAe,EAAE,IAAkB,EAAuB,EAAE,CACjF,KAAK,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YACrE,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACtD,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YACpE,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YAC9C,GAAG,EAAE,IAAI;SACV,CAAA;IACH,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAc/C,SAAS,cAAc,CAAC,KAAa,EAAE,IAAY;IACjD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,UAAU,CAAC,iBAAiB,IAAI,uCAAuC,CAAC,CAAA;IACpF,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CAAC,QAAuB,EAAE,KAAiB;IACxD,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvC,IAAI,MAAM,YAAY,OAAO;YAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,+CAA+C;IACjD,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CACb,QAAuB,EACvB,EAAkB,EAClB,OAAqB,EACrB,OAAe,EACf,GAAuB,EACvB,GAAuB,EACvB,QAAgB,EAChB,OAA2B;IAE3B,MAAM,CAAC,QAAQ,EAAE;QACf,EAAE;QACF,OAAO;QACP,SAAS,EAAE,WAAW,CAAC,UAAU,GAAG,OAAO;QAC3C,UAAU,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO;QACvC,GAAG;QACH,GAAG;QACH,QAAQ;QACR,OAAO;KACR,CAAC,CAAA;AACJ,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,WAAW,CAAC,UAAwB,EAAE;IACpD,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,MAAM,CAAA;IACnD,cAAc,CAAC,YAAY,EAAE,cAAc,CAAC,CAAA;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IAC7C,uGAAuG;IACvG,MAAM,KAAK,GAAe,OAAO,CAAC,KAAK,IAAI,IAAI,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;IACnE,MAAM,OAAO,GACX,OAAO,CAAC,OAAO;QACf,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CACd,OAAO,CAAC,KAAK,CAAC,4BAA4B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAA;IAEpF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;IACjC,kGAAkG;IAClG,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAA;IAEpD,KAAK,UAAU,GAAG,CAAc,GAAW;QACzC,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,KAAK,CAAC,KAAW,CAAA;IAC7D,CAAC;IAED,KAAK,UAAU,GAAG,CAAC,GAAW;QAC5B,OAAO,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,CAAA;IAC7C,CAAC;IAED,KAAK,UAAU,GAAG,CAAI,GAAW,EAAE,KAAQ,EAAE,OAAmB,EAAE;QAChE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,YAAY,CAAA;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAA;QAC7B,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC9B,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC9B,MAAM,CAAC,GAAG,GAAG,EAAE,CAAA;QACf,MAAM,KAAK,CAAC,GAAG,CACb,GAAG,EACH,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,EAAE,CAAC,GAAG,KAAK,GAAG,KAAK,EAAE,EAC3D,IAAI,CAAC,IAAI,IAAI,EAAE,CAChB,CAAA;IACH,CAAC;IAED,SAAS,IAAI,CAAC,GAAW,EAAE,MAAqB,EAAE,IAAiB;QACjE,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAA;QAC3C,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YACpB,MAAM,KAAK,GAAG,MAAM,MAAM,EAAE,CAAA;YAC5B,MAAM,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;YAC3B,OAAO,KAAK,CAAA;QACd,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAChB,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACnD,CAAC,CAAC,CAAA;QACF,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACpB,OAAO,CAAC,CAAA;IACV,CAAC;IAED,SAAS,UAAU,CACjB,GAAW,EACX,MAAqB,EACrB,IAAiB,EACjB,OAA2B;QAE3B,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAM,CAAC,+BAA+B;QAC7D,MAAM,OAAO,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,CAAA;QAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;QACvC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,CAAA;YACvC,OAAO,CAAC,IAAI,CACV,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,EACtF,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAC1F,CAAA;QACH,CAAC;QACD,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YAC3B,IAAI,CAAC;gBACH,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YACrB,CAAC;YAAC,MAAM,CAAC;gBACP,yCAAyC;YAC3C,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,UAAU,IAAI,CACjB,GAAW,EACX,MAAe,EACf,OAAoB,EAAE;QAEtB,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO;gBAAE,OAAO,KAAK,CAAC,KAAmB,CAAA,CAAC,YAAY;YACxE,UAAU,CAAC,GAAG,EAAE,MAAuB,EAAE,IAAI,EAAE,SAAS,CAAC,CAAA,CAAC,sDAAsD;YAChH,OAAO,KAAK,CAAC,KAAmB,CAAA;QAClC,CAAC;QACD,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,MAAuB,EAAE,IAAI,CAAC,CAAe,CAAA,CAAC,8BAA8B;IACtG,CAAC;IAED,MAAM,WAAW,GAAG,CAAC,KAA8B,EAAgB,EAAE,CACnE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAA;IAExE,gGAAgG;IAChG,oDAAoD;IACpD,SAAS,QAAQ,CAAC,QAAuB,EAAE,OAA2B;QACpE,MAAM,MAAM,GAAG,KAAK,EAClB,EAAkB,EAClB,GAAuB,EACvB,GAAuB,EACvB,QAAgB,EAChB,GAAqB,EACrB,OAAqB,EACT,EAAE;YACd,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;YACjC,IAAI,OAAO,GAAiB,OAAO,CAAA;YACnC,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,MAAM,GAAG,EAAE,CAAA;gBACzB,OAAO,GAAG,OAAO,CAAA;gBACjB,OAAO,KAAK,CAAA;YACd,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;YACrE,CAAC;QACH,CAAC,CAAA;QACD,MAAM,IAAI,GAAG,KAAK,EAAK,EAAiB,EAAE,GAAW,EAAE,IAA4B,EAAE,EAAE;YACrF,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;YACjC,IAAI,OAAO,GAAiB,OAAO,CAAA;YACnC,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBAClC,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;gBAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAA;YACpB,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,CAAC,CAAA;YACpE,CAAC;QACH,CAAC,CAAA;QACD,OAAO;YACL,GAAG,EAAE,CAAc,GAAW,EAA0B,EAAE,CACxD,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE;YACzB,sKAAsK;YACtK,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,KAAK,CAAC,KAAW,CACrD;YACH,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;YAC9D,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CACxB,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;YAC3F,KAAK,CAAC,IAAI,CAAI,GAAW,EAAE,MAAe,EAAE,OAAoB,EAAE;gBAChE,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;gBACjC,IAAI,OAAO,GAAiB,OAAO,CAAA;gBACnC,IAAI,CAAC;oBACH,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;oBAClC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;wBACxB,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;wBAC5B,IAAI,OAAO,KAAK,OAAO;4BAAE,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;wBAC/D,qKAAqK;wBACrK,OAAO,KAAK,CAAC,KAAmB,CAAA;oBAClC,CAAC;oBACD,gLAAgL;oBAChL,MAAM,KAAK,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAe,CAAA;oBAC3D,OAAO,GAAG,MAAM,CAAA;oBAChB,OAAO,KAAK,CAAA;gBACd,CAAC;wBAAS,CAAC;oBACT,MAAM,CACJ,QAAQ,EACR,MAAM,EACN,OAAO,EACP,OAAO,EACP,GAAG,EACH,SAAS,EACT,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,EACtB,OAAO,CACR,CAAA;gBACH,CAAC;YACH,CAAC;YACD,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CACd,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC;YACrF,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE,CACrB,MAAM,CACJ,eAAe,EACf,SAAS,EACT,GAAG,EACH,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,EAC/C,IAAI,CACL;YACH,KAAK,EAAE,GAAG,EAAE,CACV,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC;YACtF,GAAG,EAAE,IAAI;SACV,CAAA;IACH,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,IAAI,IAAI,YAAY,CAAA;IAC5D,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,EAAE,KAAK,IAAI,aAAa,CAAA;IAE/D,MAAM,KAAK,GACT,QAAQ,KAAK,SAAS;QACpB,CAAC,CAAC;YACE,GAAG;YACH,GAAG;YACH,GAAG;YACH,IAAI;YACJ,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnD,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YACjE,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YAC3C,GAAG,EAAE,IAAI;SACV;QACH,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;IAEnC,qGAAqG;IACrG,+FAA+F;IAC/F,SAAS,IAAI,CAAC,OAAe;QAC3B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;QAC7B,IAAI,MAAM,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CACb,0JAA0J,CAC3J,CAAA;QACH,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QACzE,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,IAAI,CAAA;QACrC,iGAAiG;QACjG,kGAAkG;QAClG,iEAAiE;QACjE,MAAM,KAAK,GAAG,CAAI,YAA+B,EAAE,GAAqB,EAAc,EAAE;YACtF,IAAI,CAAC;gBACH,KAAK,MAAM,UAAU,IAAI,YAAY;oBAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;YACpE,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YAC9B,CAAC;YACD,OAAO,GAAG,EAAE,CAAA;QACd,CAAC,CAAA;QACD,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC,CAAA;QACxB,MAAM,KAAK,GAAG,CAAC,UAAU,CAAC,CAAA;QAC1B,OAAO;YACL,GAAG,EAAE,CAAc,GAAW,EAA0B,EAAE,CACxD,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAI,GAAG,CAAC,CAAC;YACrC,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC9C,GAAG,EAAE,CAAI,GAAW,EAAE,KAAQ,EAAE,IAAiB,EAAiB,EAAE,CAClE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YAChD,qFAAqF;YACrF,IAAI,EAAE,CAAI,GAAW,EAAE,MAAe,EAAE,IAAkB,EAAuB,EAAE,CACjF,KAAK,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YACpE,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACrD,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YACnE,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAC7C,GAAG,EAAE,IAAI;SACV,CAAA;IACH,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC"}
package/dist/index.d.ts CHANGED
@@ -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, CacheCapabilities, CacheOptions, CacheStore, CapabilityBeacon, SetOptions, StoredEntry, WrapOptions, } from "./types.js";
12
+ export type { Cache, CacheCapabilities, CacheEvent, CacheObserver, CacheOperation, CacheOptions, CacheOutcome, 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,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,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,UAAU,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,GACZ,MAAM,YAAY,CAAA"}
package/dist/types.d.ts CHANGED
@@ -47,6 +47,41 @@ export interface CacheCapabilities {
47
47
  readonly read?: string;
48
48
  readonly write?: string;
49
49
  }
50
+ /** A cache operation reported to a {@link CacheObserver}. `revalidate` is the background SWR refresh. */
51
+ export type CacheOperation = "get" | "has" | "set" | "wrap" | "delete" | "invalidateTag" | "clear" | "revalidate";
52
+ /**
53
+ * How an operation ended. Reads report `hit`, `stale` (served from the SWR window) or `miss`; writes and
54
+ * a completed revalidation report `ok`; `error` means the store or the loader threw.
55
+ */
56
+ export type CacheOutcome = "hit" | "stale" | "miss" | "ok" | "error";
57
+ /** One settled cache operation, as an observer sees it. */
58
+ export interface CacheEvent {
59
+ readonly op: CacheOperation;
60
+ readonly outcome: CacheOutcome;
61
+ /** Epoch ms the operation started. */
62
+ readonly startedAt: number;
63
+ /** Monotonic duration in ms. */
64
+ readonly durationMs: number;
65
+ /**
66
+ * The raw key. Keys often hold ids or emails: this value is for in-process use, and an exporter
67
+ * decides what (if anything) of it leaves the process. Absent for `clear` and `invalidateTag`.
68
+ */
69
+ readonly key: string | undefined;
70
+ /** The tag passed to `invalidateTag`, with the same caution as `key`. */
71
+ readonly tag: string | undefined;
72
+ /** Tags written by `set`, `wrap` and `revalidate`; 1 for `invalidateTag`; otherwise 0. */
73
+ readonly tagCount: number;
74
+ /**
75
+ * The context bound with `for(context)`, or `undefined` for the unbound cache. For `revalidate` it is
76
+ * the context whose read found the stale entry.
77
+ */
78
+ readonly context: object | undefined;
79
+ }
80
+ /**
81
+ * Called once per operation after it settles, never before the caller sees the result. A throwing (or
82
+ * rejecting) observer is swallowed: it cannot change a cache result.
83
+ */
84
+ export type CacheObserver = (event: CacheEvent) => void;
50
85
  export interface CacheOptions {
51
86
  /** Storage adapter. Default: a fresh {@link MemoryCache}. */
52
87
  readonly store?: CacheStore;
@@ -69,6 +104,11 @@ export interface CacheOptions {
69
104
  readonly now?: () => number;
70
105
  /** Called when a background SWR revalidation throws. Default: `console.error`. */
71
106
  readonly onError?: (error: unknown, key: string) => void;
107
+ /**
108
+ * Receives one {@link CacheEvent} per operation - the seam `cacheTracing()` from `@nifrajs/otel/cache`
109
+ * plugs into. Without one the cache reads no extra clock and allocates no event.
110
+ */
111
+ readonly observer?: CacheObserver;
72
112
  }
73
113
  export interface Cache {
74
114
  /** The cached value for `key`, or `undefined` if missing/expired. Pass `T` for the value type. */
@@ -88,15 +128,17 @@ export interface Cache {
88
128
  */
89
129
  wrap<T>(key: string, loader: () => T, options?: WrapOptions): Promise<Awaited<T>>;
90
130
  /**
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.
131
+ * A view bound to a request (or job) context. With a `beacon`, every operation announces its
132
+ * capability first and fails closed when the route did not declare it. With an `observer`, every
133
+ * event carries `context`, which is how a tracer parents the span to `context.trace`.
93
134
  *
94
135
  * `wrap` announces BOTH read and write, because a miss writes and which one happens is not known
95
136
  * before the call. A declaration describes what a route may do, so the conservative answer is the
96
137
  * correct one.
97
138
  *
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.
139
+ * Requires `beacon` or `observer`. With neither this throws rather than handing back a view that
140
+ * quietly does nothing - a beacon nobody notices is missing is the exact failure the beacon exists
141
+ * to prevent.
100
142
  */
101
143
  for(context: object): Cache;
102
144
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;AAE5E,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,yGAAyG;AACzG,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,KAAK,GACL,KAAK,GACL,MAAM,GACN,QAAQ,GACR,eAAe,GACf,OAAO,GACP,YAAY,CAAA;AAEhB;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAA;AAEpE,2DAA2D;AAC3D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,sCAAsC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,gCAAgC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;CACrC;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;AAEvD,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAA;IAC3B;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAA;IAClC,qFAAqF;IACrF,QAAQ,CAAC,YAAY,CAAC,EAAE,iBAAiB,CAAA;IACzC,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;IACxD;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAA;CAClC;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;IACjF;;;;;;;;;;;;OAYG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAA;CAC5B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/cache",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
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",
@@ -13,6 +13,9 @@
13
13
  "cache-tags"
14
14
  ],
15
15
  "type": "module",
16
+ "nifra": {
17
+ "environment": "library"
18
+ },
16
19
  "main": "./dist/index.js",
17
20
  "module": "./dist/index.js",
18
21
  "types": "./dist/index.d.ts",
package/src/cache.ts CHANGED
@@ -11,7 +11,18 @@
11
11
  * await cache.invalidateTag(`user:${id}`) // on write
12
12
  */
13
13
  import { MemoryCache } from "./memory-cache.ts"
14
- import type { Cache, CacheOptions, CacheStore, SetOptions, WrapOptions } from "./types.ts"
14
+ import type {
15
+ Cache,
16
+ CacheEvent,
17
+ CacheObserver,
18
+ CacheOperation,
19
+ CacheOptions,
20
+ CacheOutcome,
21
+ CacheStore,
22
+ SetOptions,
23
+ StoredEntry,
24
+ WrapOptions,
25
+ } from "./types.ts"
15
26
 
16
27
  function assertDuration(value: number, name: string): void {
17
28
  if (!Number.isFinite(value) || value < 0) {
@@ -19,6 +30,37 @@ function assertDuration(value: number, name: string): void {
19
30
  }
20
31
  }
21
32
 
33
+ function notify(observer: CacheObserver, event: CacheEvent): void {
34
+ try {
35
+ const result: unknown = observer(event)
36
+ if (result instanceof Promise) result.catch(() => undefined)
37
+ } catch {
38
+ // An observer can never change a cache result.
39
+ }
40
+ }
41
+
42
+ function report(
43
+ observer: CacheObserver,
44
+ op: CacheOperation,
45
+ outcome: CacheOutcome,
46
+ started: number,
47
+ key: string | undefined,
48
+ tag: string | undefined,
49
+ tagCount: number,
50
+ context: object | undefined,
51
+ ): void {
52
+ notify(observer, {
53
+ op,
54
+ outcome,
55
+ startedAt: performance.timeOrigin + started,
56
+ durationMs: performance.now() - started,
57
+ key,
58
+ tag,
59
+ tagCount,
60
+ context,
61
+ })
62
+ }
63
+
22
64
  /** Create a cache over the given (or a fresh in-memory) store. */
23
65
  export function createCache(options: CacheOptions = {}): Cache {
24
66
  const defaultTtlMs = options.defaultTtlMs ?? 60_000
@@ -31,6 +73,7 @@ export function createCache(options: CacheOptions = {}): Cache {
31
73
  ((error, key) =>
32
74
  console.error(`[nifra/cache] revalidate ${JSON.stringify(key)} failed:`, error))
33
75
 
76
+ const observer = options.observer
34
77
  // Single-flight: concurrent loads for the same key share one promise (miss stampede + SWR dedup).
35
78
  const inflight = new Map<string, Promise<unknown>>()
36
79
 
@@ -70,9 +113,23 @@ export function createCache(options: CacheOptions = {}): Cache {
70
113
  return p
71
114
  }
72
115
 
73
- function revalidate(key: string, loader: () => unknown, opts: WrapOptions): void {
116
+ function revalidate(
117
+ key: string,
118
+ loader: () => unknown,
119
+ opts: WrapOptions,
120
+ context: object | undefined,
121
+ ): void {
74
122
  if (inflight.has(key)) return // a refresh is already running
75
- void load(key, loader, opts).catch((error) => {
123
+ const started = observer === undefined ? 0 : performance.now()
124
+ const refresh = load(key, loader, opts)
125
+ if (observer !== undefined) {
126
+ const tagCount = opts.tags?.length ?? 0
127
+ refresh.then(
128
+ () => report(observer, "revalidate", "ok", started, key, undefined, tagCount, context),
129
+ () => report(observer, "revalidate", "error", started, key, undefined, tagCount, context),
130
+ )
131
+ }
132
+ void refresh.catch((error) => {
76
133
  try {
77
134
  onError(error, key)
78
135
  } catch {
@@ -89,35 +146,129 @@ export function createCache(options: CacheOptions = {}): Cache {
89
146
  const entry = await store.get(key)
90
147
  if (entry !== undefined) {
91
148
  if (now() < entry.staleAt) return entry.value as Awaited<T> // fresh hit
92
- revalidate(key, loader as () => unknown, opts) // stale-but-live → serve stale, refresh in background
149
+ revalidate(key, loader as () => unknown, opts, undefined) // stale-but-live → serve stale, refresh in background
93
150
  return entry.value as Awaited<T>
94
151
  }
95
152
  return (await load(key, loader as () => unknown, opts)) as Awaited<T> // miss → load (single-flight)
96
153
  }
97
154
 
155
+ const readOutcome = (entry: StoredEntry | undefined): CacheOutcome =>
156
+ entry === undefined ? "miss" : now() < entry.staleAt ? "hit" : "stale"
157
+
158
+ // A separate surface rather than branches in the plain one, so a cache without an observer runs
159
+ // exactly the code it ran before observers existed.
160
+ function observed(observer: CacheObserver, context: object | undefined): Cache {
161
+ const settle = async <T>(
162
+ op: CacheOperation,
163
+ key: string | undefined,
164
+ tag: string | undefined,
165
+ tagCount: number,
166
+ run: () => Promise<T>,
167
+ success: CacheOutcome,
168
+ ): Promise<T> => {
169
+ const started = performance.now()
170
+ let outcome: CacheOutcome = "error"
171
+ try {
172
+ const value = await run()
173
+ outcome = success
174
+ return value
175
+ } finally {
176
+ report(observer, op, outcome, started, key, tag, tagCount, context)
177
+ }
178
+ }
179
+ const read = async <T>(op: "get" | "has", key: string, pick: (e?: StoredEntry) => T) => {
180
+ const started = performance.now()
181
+ let outcome: CacheOutcome = "error"
182
+ try {
183
+ const entry = await store.get(key)
184
+ outcome = readOutcome(entry)
185
+ return pick(entry)
186
+ } finally {
187
+ report(observer, op, outcome, started, key, undefined, 0, context)
188
+ }
189
+ }
190
+ return {
191
+ get: <T = unknown>(key: string): Promise<T | undefined> =>
192
+ read("get", key, (entry) =>
193
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the store keeps values untyped; T is the caller's claim for this key, exactly as in the plain get().
194
+ entry === undefined ? undefined : (entry.value as T),
195
+ ),
196
+ has: (key) => read("has", key, (entry) => entry !== undefined),
197
+ set: (key, value, opts) =>
198
+ settle("set", key, undefined, opts?.tags?.length ?? 0, () => set(key, value, opts), "ok"),
199
+ async wrap<T>(key: string, loader: () => T, opts: WrapOptions = {}): Promise<Awaited<T>> {
200
+ const started = performance.now()
201
+ let outcome: CacheOutcome = "error"
202
+ try {
203
+ const entry = await store.get(key)
204
+ if (entry !== undefined) {
205
+ outcome = readOutcome(entry)
206
+ if (outcome === "stale") revalidate(key, loader, opts, context)
207
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the store keeps values untyped; a wrap of this key stored what a loader of the caller's T returned.
208
+ return entry.value as Awaited<T>
209
+ }
210
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: load() resolves to what this loader returned, or what a concurrent wrap of the same key loaded for the same T.
211
+ const value = (await load(key, loader, opts)) as Awaited<T>
212
+ outcome = "miss"
213
+ return value
214
+ } finally {
215
+ report(
216
+ observer,
217
+ "wrap",
218
+ outcome,
219
+ started,
220
+ key,
221
+ undefined,
222
+ opts.tags?.length ?? 0,
223
+ context,
224
+ )
225
+ }
226
+ },
227
+ delete: (key) =>
228
+ settle("delete", key, undefined, 0, () => Promise.resolve(store.delete(key)), "ok"),
229
+ invalidateTag: (tag) =>
230
+ settle(
231
+ "invalidateTag",
232
+ undefined,
233
+ tag,
234
+ 1,
235
+ () => Promise.resolve(store.invalidateTag(tag)),
236
+ "ok",
237
+ ),
238
+ clear: () =>
239
+ settle("clear", undefined, undefined, 0, () => Promise.resolve(store.clear()), "ok"),
240
+ for: bind,
241
+ }
242
+ }
243
+
98
244
  const readToken = options.capabilities?.read ?? "cache.read"
99
245
  const writeToken = options.capabilities?.write ?? "cache.write"
100
246
 
101
- const plain: Cache = {
102
- get,
103
- has,
104
- set,
105
- wrap,
106
- delete: (key) => Promise.resolve(store.delete(key)),
107
- invalidateTag: (tag) => Promise.resolve(store.invalidateTag(tag)),
108
- clear: () => Promise.resolve(store.clear()),
109
- for: bind,
110
- }
247
+ const plain: Cache =
248
+ observer === undefined
249
+ ? {
250
+ get,
251
+ has,
252
+ set,
253
+ wrap,
254
+ delete: (key) => Promise.resolve(store.delete(key)),
255
+ invalidateTag: (tag) => Promise.resolve(store.invalidateTag(tag)),
256
+ clear: () => Promise.resolve(store.clear()),
257
+ for: bind,
258
+ }
259
+ : observed(observer, undefined)
111
260
 
112
261
  // Built per call rather than cached per context: a cache keyed by context would hold every request's
113
262
  // context object for the process lifetime, which is a leak with a request body attached to it.
114
263
  function bind(context: object): Cache {
115
264
  const beacon = options.beacon
116
- if (beacon === undefined) {
265
+ if (beacon === undefined && observer === undefined) {
117
266
  throw new Error(
118
- "@nifrajs/cache: for(context) needs a beacon - pass `beacon: useCapability` (from @nifrajs/core/capabilities) to createCache",
267
+ "@nifrajs/cache: for(context) needs a beacon or an observer - pass `beacon: useCapability` (from @nifrajs/core/capabilities) or `observer` to createCache",
119
268
  )
120
269
  }
270
+ const base = observer === undefined ? plain : observed(observer, context)
271
+ if (beacon === undefined) return base
121
272
  // A refused capability has to surface as a REJECTION, not a synchronous throw: every method here
122
273
  // returns a promise, and a caller who writes `.catch(…)` instead of `try` would otherwise miss it
123
274
  // entirely - turning a fail-closed gate into an unhandled crash.
@@ -133,16 +284,16 @@ export function createCache(options: CacheOptions = {}): Cache {
133
284
  const WRITE = [writeToken]
134
285
  return {
135
286
  get: <T = unknown>(key: string): Promise<T | undefined> =>
136
- guard(READ, () => plain.get<T>(key)),
137
- has: (key) => guard(READ, () => plain.has(key)),
287
+ guard(READ, () => base.get<T>(key)),
288
+ has: (key) => guard(READ, () => base.has(key)),
138
289
  set: <T>(key: string, value: T, opts?: SetOptions): Promise<void> =>
139
- guard(WRITE, () => plain.set(key, value, opts)),
290
+ guard(WRITE, () => base.set(key, value, opts)),
140
291
  // Both, because a miss writes and which one happens is not knowable before the call.
141
292
  wrap: <T>(key: string, loader: () => T, opts?: WrapOptions): Promise<Awaited<T>> =>
142
- guard([readToken, writeToken], () => plain.wrap(key, loader, opts)),
143
- delete: (key) => guard(WRITE, () => plain.delete(key)),
144
- invalidateTag: (tag) => guard(WRITE, () => plain.invalidateTag(tag)),
145
- clear: () => guard(WRITE, () => plain.clear()),
293
+ guard([readToken, writeToken], () => base.wrap(key, loader, opts)),
294
+ delete: (key) => guard(WRITE, () => base.delete(key)),
295
+ invalidateTag: (tag) => guard(WRITE, () => base.invalidateTag(tag)),
296
+ clear: () => guard(WRITE, () => base.clear()),
146
297
  for: bind,
147
298
  }
148
299
  }
package/src/index.ts CHANGED
@@ -13,7 +13,11 @@ export { MemoryCache, type MemoryCacheOptions } from "./memory-cache.ts"
13
13
  export type {
14
14
  Cache,
15
15
  CacheCapabilities,
16
+ CacheEvent,
17
+ CacheObserver,
18
+ CacheOperation,
16
19
  CacheOptions,
20
+ CacheOutcome,
17
21
  CacheStore,
18
22
  CapabilityBeacon,
19
23
  SetOptions,
package/src/types.ts CHANGED
@@ -54,6 +54,53 @@ export interface CacheCapabilities {
54
54
  readonly write?: string
55
55
  }
56
56
 
57
+ /** A cache operation reported to a {@link CacheObserver}. `revalidate` is the background SWR refresh. */
58
+ export type CacheOperation =
59
+ | "get"
60
+ | "has"
61
+ | "set"
62
+ | "wrap"
63
+ | "delete"
64
+ | "invalidateTag"
65
+ | "clear"
66
+ | "revalidate"
67
+
68
+ /**
69
+ * How an operation ended. Reads report `hit`, `stale` (served from the SWR window) or `miss`; writes and
70
+ * a completed revalidation report `ok`; `error` means the store or the loader threw.
71
+ */
72
+ export type CacheOutcome = "hit" | "stale" | "miss" | "ok" | "error"
73
+
74
+ /** One settled cache operation, as an observer sees it. */
75
+ export interface CacheEvent {
76
+ readonly op: CacheOperation
77
+ readonly outcome: CacheOutcome
78
+ /** Epoch ms the operation started. */
79
+ readonly startedAt: number
80
+ /** Monotonic duration in ms. */
81
+ readonly durationMs: number
82
+ /**
83
+ * The raw key. Keys often hold ids or emails: this value is for in-process use, and an exporter
84
+ * decides what (if anything) of it leaves the process. Absent for `clear` and `invalidateTag`.
85
+ */
86
+ readonly key: string | undefined
87
+ /** The tag passed to `invalidateTag`, with the same caution as `key`. */
88
+ readonly tag: string | undefined
89
+ /** Tags written by `set`, `wrap` and `revalidate`; 1 for `invalidateTag`; otherwise 0. */
90
+ readonly tagCount: number
91
+ /**
92
+ * The context bound with `for(context)`, or `undefined` for the unbound cache. For `revalidate` it is
93
+ * the context whose read found the stale entry.
94
+ */
95
+ readonly context: object | undefined
96
+ }
97
+
98
+ /**
99
+ * Called once per operation after it settles, never before the caller sees the result. A throwing (or
100
+ * rejecting) observer is swallowed: it cannot change a cache result.
101
+ */
102
+ export type CacheObserver = (event: CacheEvent) => void
103
+
57
104
  export interface CacheOptions {
58
105
  /** Storage adapter. Default: a fresh {@link MemoryCache}. */
59
106
  readonly store?: CacheStore
@@ -76,6 +123,11 @@ export interface CacheOptions {
76
123
  readonly now?: () => number
77
124
  /** Called when a background SWR revalidation throws. Default: `console.error`. */
78
125
  readonly onError?: (error: unknown, key: string) => void
126
+ /**
127
+ * Receives one {@link CacheEvent} per operation - the seam `cacheTracing()` from `@nifrajs/otel/cache`
128
+ * plugs into. Without one the cache reads no extra clock and allocates no event.
129
+ */
130
+ readonly observer?: CacheObserver
79
131
  }
80
132
 
81
133
  export interface Cache {
@@ -96,15 +148,17 @@ export interface Cache {
96
148
  */
97
149
  wrap<T>(key: string, loader: () => T, options?: WrapOptions): Promise<Awaited<T>>
98
150
  /**
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.
151
+ * A view bound to a request (or job) context. With a `beacon`, every operation announces its
152
+ * capability first and fails closed when the route did not declare it. With an `observer`, every
153
+ * event carries `context`, which is how a tracer parents the span to `context.trace`.
101
154
  *
102
155
  * `wrap` announces BOTH read and write, because a miss writes and which one happens is not known
103
156
  * before the call. A declaration describes what a route may do, so the conservative answer is the
104
157
  * correct one.
105
158
  *
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.
159
+ * Requires `beacon` or `observer`. With neither this throws rather than handing back a view that
160
+ * quietly does nothing - a beacon nobody notices is missing is the exact failure the beacon exists
161
+ * to prevent.
108
162
  */
109
163
  for(context: object): Cache
110
164
  }