@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 +7 -7
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +41 -3
- package/dist/cache.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/types.d.ts +39 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +2 -2
- package/src/cache.ts +47 -3
- package/src/index.ts +3 -1
- package/src/memory-cache.ts +1 -1
- package/src/types.ts +41 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @nifrajs/cache
|
|
2
2
|
|
|
3
|
-
Typed KV cache for nifra
|
|
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
|
|
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)`**
|
|
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
|
|
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`
|
|
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? })`
|
|
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)
|
|
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
|
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,EAAE,KAAK,EAAE,YAAY,EAAuC,MAAM,YAAY,CAAA;AAE1F,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG,KAAK,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
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,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
|
|
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
|
*
|
package/dist/memory-cache.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-process {@link CacheStore}
|
|
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
|
|
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
|
|
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
|
|
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
|
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,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;
|
|
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
|
|
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.
|
|
4
|
-
"description": "Typed KV cache for nifra
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
package/src/memory-cache.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-process {@link CacheStore}
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|