@nifrajs/cache 3.5.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 +27 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +117 -22
- package/dist/cache.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +46 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/cache.ts +174 -23
- package/src/index.ts +4 -0
- package/src/types.ts +58 -4
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
|
package/dist/cache.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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, () =>
|
|
118
|
-
has: (key) => guard(READ, () =>
|
|
119
|
-
set: (key, value, opts) => guard(WRITE, () =>
|
|
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], () =>
|
|
122
|
-
delete: (key) => guard(WRITE, () =>
|
|
123
|
-
invalidateTag: (tag) => guard(WRITE, () =>
|
|
124
|
-
clear: () => guard(WRITE, () =>
|
|
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;
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
|
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`.
|
|
99
|
-
*
|
|
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
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;;;;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,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;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/cache",
|
|
3
|
-
"version": "
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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, () =>
|
|
137
|
-
has: (key) => guard(READ, () =>
|
|
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, () =>
|
|
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], () =>
|
|
143
|
-
delete: (key) => guard(WRITE, () =>
|
|
144
|
-
invalidateTag: (tag) => guard(WRITE, () =>
|
|
145
|
-
clear: () => guard(WRITE, () =>
|
|
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
|
|
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`.
|
|
107
|
-
*
|
|
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
|
}
|