@shaferllc/keel 0.66.0 → 0.68.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/AGENTS.md +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +2 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/index.d.ts +15 -10
- package/dist/core/index.js +7 -4
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +26 -7
package/docs/cache.md
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# Cache
|
|
2
|
+
|
|
3
|
+
A small cache with TTLs and the `remember` pattern. Memory-backed by default
|
|
4
|
+
(per-process, or per-isolate on the edge), with a pluggable store so you can
|
|
5
|
+
swap in Redis, KV, or anything else. Reach it with the global `cache()` helper.
|
|
6
|
+
|
|
7
|
+
## Basics
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { cache } from "@shaferllc/keel/core";
|
|
11
|
+
|
|
12
|
+
await cache().put("user:1", user); // forever
|
|
13
|
+
await cache().put("otp", code, 300); // expires in 300s
|
|
14
|
+
await cache().add("otp", code, 300); // write only if absent → boolean
|
|
15
|
+
await cache().get("user:1");
|
|
16
|
+
await cache().get("missing", fallback);
|
|
17
|
+
await cache().has("otp");
|
|
18
|
+
await cache().missing("otp"); // the inverse of has
|
|
19
|
+
await cache().forget("otp");
|
|
20
|
+
await cache().forgetMany(["otp", "user:1"]); // forget several
|
|
21
|
+
await cache().pull("otp"); // get + forget
|
|
22
|
+
await cache().flush(); // clear everything
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`put` takes a TTL in **seconds** (converted to milliseconds for the store);
|
|
26
|
+
omit it to cache forever. `get` returns `undefined` on a miss unless you pass a
|
|
27
|
+
fallback, in which case the fallback comes back instead — it's only returned,
|
|
28
|
+
never written to the cache. `add` writes only when the key is absent and returns
|
|
29
|
+
whether it did — a lightweight "claim this key" for one-shot work.
|
|
30
|
+
|
|
31
|
+
## remember
|
|
32
|
+
|
|
33
|
+
The common pattern — return the cached value, or compute, cache, and return it:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const stats = await cache().remember("dashboard.stats", 60, async () => {
|
|
37
|
+
return computeExpensiveStats(); // runs only on a cache miss
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const config = await cache().rememberForever("app.config", () => loadConfig());
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The factory runs **only on a miss**. On a hit the cached value is returned and
|
|
44
|
+
the factory is never called, so it's the right place for an expensive query, an
|
|
45
|
+
upstream API call, or anything you'd rather do once per TTL window. The factory
|
|
46
|
+
may be sync or async — both are awaited.
|
|
47
|
+
|
|
48
|
+
### Stampede protection
|
|
49
|
+
|
|
50
|
+
When a hot key expires, many requests can hit the miss at once and each run the
|
|
51
|
+
factory — a "cache stampede" that hammers the thing you were trying to protect.
|
|
52
|
+
`remember` guards against this automatically: **concurrent calls for the same
|
|
53
|
+
key share a single factory run** and all receive its result. You don't opt in;
|
|
54
|
+
it's just how `remember` and `rememberForever` behave.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// 100 concurrent requests, one cold key → the query runs ONCE.
|
|
58
|
+
await Promise.all(
|
|
59
|
+
requests.map(() => cache().remember("report", 300, runExpensiveReport)),
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This is per-isolate (no cross-node lock), which matches keel's single-store
|
|
64
|
+
model — it collapses the dog-pile within a process/worker, the case that
|
|
65
|
+
actually melts a server.
|
|
66
|
+
|
|
67
|
+
### Grace: serve stale on error
|
|
68
|
+
|
|
69
|
+
Pass a `grace` window (seconds) and an expired value is **retained past its TTL**
|
|
70
|
+
and served if the refreshing factory throws. A flaky upstream then degrades to
|
|
71
|
+
slightly-stale data instead of a hard error:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const rates = await cache().remember("fx.rates", 60, fetchRates, { grace: 3600 });
|
|
75
|
+
// For up to an hour after the 60s TTL lapses, if fetchRates() throws the last
|
|
76
|
+
// good rates are returned. A successful refresh replaces them and resets the window.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Grace only rescues a *failing* refresh — a normal `get()` on an expired key still
|
|
80
|
+
reports a miss, so stale data never leaks through the plain read path. If the
|
|
81
|
+
factory succeeds, the fresh value is cached and the grace window restarts.
|
|
82
|
+
|
|
83
|
+
## Read-through, then invalidate
|
|
84
|
+
|
|
85
|
+
`pull` reads and forgets in one step — handy for one-shot values like a
|
|
86
|
+
password-reset token or a flash message you want to survive exactly one read:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const token = await cache().pull<string>("reset:jane", ""); // read, then delete
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Pair `remember` with `forget` to invalidate a derived value when its inputs
|
|
93
|
+
change:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
await db("users").where("id", id).update({ name });
|
|
97
|
+
await cache().forget(`user:${id}`); // next read recomputes
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Tags
|
|
101
|
+
|
|
102
|
+
When one change should invalidate a *group* of unrelated keys, tag them and drop
|
|
103
|
+
the whole group with `deleteByTag`. Pass `tags` on any write:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
await cache().put("post:1", post, 600, { tags: ["posts"] });
|
|
107
|
+
await cache().remember("feed:home", 300, buildFeed, { tags: ["posts"] });
|
|
108
|
+
await cache().put("post:2", post, 600, { tags: ["posts", "featured"] });
|
|
109
|
+
|
|
110
|
+
// A new post lands — blow away everything tagged "posts" in one call:
|
|
111
|
+
await cache().deleteByTag(["posts"]);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`deleteByTag` is **O(number of tags)**, not O(number of keys): each tag carries a
|
|
115
|
+
version counter, every entry records the counter it was written at, and
|
|
116
|
+
`deleteByTag` just bumps it — so any entry on the old version reads as a miss on
|
|
117
|
+
its next access. There's no key scan and nothing to clean up; invalidated
|
|
118
|
+
entries fall out on their own TTL. Because it's a hard invalidation, a
|
|
119
|
+
tag-dropped entry is **not** grace-eligible — `remember` recomputes it rather
|
|
120
|
+
than serving it stale.
|
|
121
|
+
|
|
122
|
+
## Namespaces
|
|
123
|
+
|
|
124
|
+
`namespace(prefix)` returns a cache scoped under a key prefix. Keys written
|
|
125
|
+
through it live at `prefix:key`, so two namespaces can reuse the same logical key
|
|
126
|
+
without colliding — and `flush()` on a namespace clears **only** that namespace,
|
|
127
|
+
leaving the rest of the store intact:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const users = cache().namespace("users");
|
|
131
|
+
const posts = cache().namespace("posts");
|
|
132
|
+
|
|
133
|
+
await users.put("1", user); // stored at "users:1"
|
|
134
|
+
await posts.put("1", post); // stored at "posts:1" — no collision
|
|
135
|
+
|
|
136
|
+
await users.flush(); // clears the users namespace only
|
|
137
|
+
await posts.get("1"); // still there
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Namespaces nest (`cache().namespace("org").namespace("team")`) and carry the full
|
|
141
|
+
API — `remember`, `grace`, `tags`, everything. Scoped `flush()` uses the same
|
|
142
|
+
version-stamp trick as tags (a namespace is an implicit tag), so it's O(1) and
|
|
143
|
+
needs no key scanning — the deliberate trade-off is that flushed entries are
|
|
144
|
+
invalidated rather than physically removed, and expire on their TTL.
|
|
145
|
+
|
|
146
|
+
## TTLs and expiry
|
|
147
|
+
|
|
148
|
+
TTLs are lazy in the memory store: an expired entry isn't purged on a timer, it's
|
|
149
|
+
dropped the next time you `get` (or `has`) it. So an untouched expired key still
|
|
150
|
+
occupies memory until it's read again or you `flush()`. A `ttlSeconds` of `0` (or
|
|
151
|
+
omitted) means no expiry — the entry lives until it's forgotten or flushed.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await cache().put("otp", code, 300); // gone 300s after this write
|
|
155
|
+
await cache().put("app.config", cfg); // no TTL — lives until forgotten
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Custom stores
|
|
159
|
+
|
|
160
|
+
The default is in-memory. To persist elsewhere, implement `CacheStore` and bind
|
|
161
|
+
your own `Cache` in a provider:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { Cache, singleton, type CacheStore } from "@shaferllc/keel/core";
|
|
165
|
+
|
|
166
|
+
class RedisStore implements CacheStore {
|
|
167
|
+
async get(key: string) { /* … */ }
|
|
168
|
+
async set(key: string, value: unknown, ttlMs?: number) { /* … */ }
|
|
169
|
+
async delete(key: string) { /* … */ }
|
|
170
|
+
async clear() { /* … */ }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
singleton(Cache, () => new Cache(new RedisStore()));
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The store speaks **milliseconds** (`ttlMs`), while the `Cache` façade takes
|
|
177
|
+
seconds — `Cache` does the conversion, so your store never sees the seconds unit.
|
|
178
|
+
Every `CacheStore` method may return a value or a promise; `Cache` awaits both,
|
|
179
|
+
so a synchronous in-memory store and an async network store are interchangeable
|
|
180
|
+
behind the same API.
|
|
181
|
+
|
|
182
|
+
## Notes
|
|
183
|
+
|
|
184
|
+
- The in-memory store is ephemeral: it clears on restart and isn't shared across
|
|
185
|
+
processes or Worker isolates. Use a custom store for anything durable or
|
|
186
|
+
shared.
|
|
187
|
+
- Every `Cache` method is async, so the same code works whether the store is
|
|
188
|
+
in-memory or over the network.
|
|
189
|
+
- Cache keys are plain strings — namespace them yourself (`user:1`,
|
|
190
|
+
`dashboard.stats`) to avoid collisions.
|
|
191
|
+
|
|
192
|
+
## Related
|
|
193
|
+
|
|
194
|
+
`cache()` resolves the `Cache` singleton out of the application container, the
|
|
195
|
+
same way `config()` and `logger()` reach their services.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## API reference
|
|
200
|
+
|
|
201
|
+
### `cache()`
|
|
202
|
+
|
|
203
|
+
`cache(): Cache`
|
|
204
|
+
|
|
205
|
+
Resolves the application's `Cache` singleton from the container — the global
|
|
206
|
+
entry point used everywhere else on this page.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { cache } from "@shaferllc/keel/core";
|
|
210
|
+
|
|
211
|
+
await cache().put("user:1", user);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Notes:** throws if no `Application` has been bootstrapped (it goes through
|
|
215
|
+
`app()` internally). The instance is a singleton, so every call returns the same
|
|
216
|
+
`Cache` — bind a replacement with `singleton(Cache, …)` to swap the store.
|
|
217
|
+
|
|
218
|
+
### `Cache`
|
|
219
|
+
|
|
220
|
+
The cache façade. Construct it with a `CacheStore` (defaults to `MemoryStore`),
|
|
221
|
+
or reach the app-bound instance with `cache()`. Every method is async and awaits
|
|
222
|
+
the underlying store.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { Cache, MemoryStore } from "@shaferllc/keel/core";
|
|
226
|
+
|
|
227
|
+
const c = new Cache(); // MemoryStore by default
|
|
228
|
+
const r = new Cache(new MemoryStore()); // explicit store
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### `get(key, fallback?)`
|
|
232
|
+
|
|
233
|
+
`get<T = unknown>(key: string, fallback?: T): Promise<T>`
|
|
234
|
+
|
|
235
|
+
Reads a value, returning `fallback` (or `undefined`) when the key is missing.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const user = await cache().get<User>("user:1");
|
|
239
|
+
const port = await cache().get("app.port", 3000); // 3000 on a miss
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Notes:** a miss is detected by `=== undefined`, so a stored `null`, `0`, `""`,
|
|
243
|
+
or `false` counts as a hit and is returned as-is. The `fallback` is only returned,
|
|
244
|
+
never written back to the cache. The type parameter `T` is a compile-time
|
|
245
|
+
convenience — the value isn't validated at runtime.
|
|
246
|
+
|
|
247
|
+
#### `put(key, value, ttlSeconds?, options?)`
|
|
248
|
+
|
|
249
|
+
`put(key: string, value: unknown, ttlSeconds?: number, options?: PutOptions): Promise<void>`
|
|
250
|
+
|
|
251
|
+
Stores a value, optionally expiring it after `ttlSeconds` and joining it to
|
|
252
|
+
`options.tags`.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
await cache().put("otp", code, 300); // expires in 5 minutes
|
|
256
|
+
await cache().put("user:1", user); // no TTL — cached forever
|
|
257
|
+
await cache().put("post:1", post, 600, { tags: ["posts"] }); // tagged
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Notes:** `ttlSeconds` is **seconds** and is converted to milliseconds for the
|
|
261
|
+
store. Omitting it (or passing `0`) means no expiry. Overwrites any existing value
|
|
262
|
+
at `key`. `options.tags` associates the entry with those tags for `deleteByTag`.
|
|
263
|
+
|
|
264
|
+
#### `add(key, value, ttlSeconds?, options?)`
|
|
265
|
+
|
|
266
|
+
`add(key: string, value: unknown, ttlSeconds?: number, options?: PutOptions): Promise<boolean>`
|
|
267
|
+
|
|
268
|
+
Stores a value **only if the key is absent**, returning `true` when it wrote and
|
|
269
|
+
`false` when the key already existed.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
if (await cache().add("job:lock", 1, 30)) {
|
|
273
|
+
await runJobOnce(); // we claimed the key
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Notes:** a best-effort "claim" — a read-then-write, not an atomic compare-and-set
|
|
278
|
+
(keel has no lock driver), so treat it as coordination within one isolate, not a
|
|
279
|
+
distributed mutex. Accepts the same `{ tags }` option as `put`.
|
|
280
|
+
|
|
281
|
+
#### `has(key)`
|
|
282
|
+
|
|
283
|
+
`has(key: string): Promise<boolean>`
|
|
284
|
+
|
|
285
|
+
`true` when a live (non-expired) value exists at `key`.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
if (await cache().has("otp")) { /* still valid */ }
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Notes:** reads through the store, so in the memory store it also triggers the
|
|
292
|
+
lazy purge of an expired entry. A stored `undefined` reads as absent.
|
|
293
|
+
|
|
294
|
+
#### `missing(key)`
|
|
295
|
+
|
|
296
|
+
`missing(key: string): Promise<boolean>`
|
|
297
|
+
|
|
298
|
+
The inverse of `has` — `true` when the key is absent or expired.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
if (await cache().missing("profile:1")) await warmProfile(1);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### `forget(key)`
|
|
305
|
+
|
|
306
|
+
`forget(key: string): Promise<void>`
|
|
307
|
+
|
|
308
|
+
Removes a single key.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
await cache().forget("user:1"); // next read recomputes
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Notes:** a no-op if the key isn't present — never throws on a miss.
|
|
315
|
+
|
|
316
|
+
#### `forgetMany(keys)`
|
|
317
|
+
|
|
318
|
+
`forgetMany(keys: string[]): Promise<void>`
|
|
319
|
+
|
|
320
|
+
Removes several keys at once.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
await cache().forgetMany(["user:1", "user:1:posts", "user:1:stats"]);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Notes:** deletes run concurrently; missing keys are skipped harmlessly.
|
|
327
|
+
|
|
328
|
+
#### `pull(key, fallback?)`
|
|
329
|
+
|
|
330
|
+
`pull<T = unknown>(key: string, fallback?: T): Promise<T>`
|
|
331
|
+
|
|
332
|
+
Reads a value and forgets it in one step — a `get` followed by a `forget`.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
const token = await cache().pull<string>("reset:jane", "");
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Notes:** returns `fallback` (or `undefined`) on a miss, then still calls
|
|
339
|
+
`forget` (harmless). Use it for single-use values like one-time tokens or flash
|
|
340
|
+
messages.
|
|
341
|
+
|
|
342
|
+
#### `flush()`
|
|
343
|
+
|
|
344
|
+
`flush(): Promise<void>`
|
|
345
|
+
|
|
346
|
+
Clears the cache. On the root cache this wipes the whole store; on a
|
|
347
|
+
[namespace](#namespacename) it clears only that namespace.
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
await cache().flush(); // everything
|
|
351
|
+
await cache().namespace("users").flush(); // just the users namespace
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Notes:** the root delegates to the store's `clear()` — wipes every key, not
|
|
355
|
+
just the ones you set through this `Cache`. In a shared store that's every
|
|
356
|
+
consumer's keys. A namespace flush is a scoped invalidation (version bump), so
|
|
357
|
+
entries are logically gone but reclaimed on their TTL.
|
|
358
|
+
|
|
359
|
+
#### `deleteByTag(tags)`
|
|
360
|
+
|
|
361
|
+
`deleteByTag(tags: string[]): Promise<void>`
|
|
362
|
+
|
|
363
|
+
Invalidates every entry tagged with any of `tags` (via `put`/`add`/`remember`'s
|
|
364
|
+
`{ tags }` option).
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
await cache().put("post:1", post, 600, { tags: ["posts"] });
|
|
368
|
+
await cache().deleteByTag(["posts"]); // post:1 (and any other "posts" entry) gone
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Notes:** O(number of tags) — bumps a per-tag version counter, so entries on the
|
|
372
|
+
old version read as a miss; no key scan. A hard invalidation, so tag-dropped
|
|
373
|
+
entries are **not** grace-eligible. Invalidated entries occupy space until their
|
|
374
|
+
TTL evicts them.
|
|
375
|
+
|
|
376
|
+
#### `namespace(name)`
|
|
377
|
+
|
|
378
|
+
`namespace(name: string): Cache`
|
|
379
|
+
|
|
380
|
+
Returns a cache scoped under the `name:` key prefix, sharing the same store.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
const users = cache().namespace("users");
|
|
384
|
+
await users.put("1", user); // stored at "users:1"
|
|
385
|
+
await users.flush(); // clears only this namespace
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Notes:** carries the full `Cache` API (`remember`, `grace`, `tags`, …) and
|
|
389
|
+
nests (`namespace("a").namespace("b")` → prefix `a:b:`). Scoped `flush()` uses the
|
|
390
|
+
same version-stamp mechanism as tags, so it's O(1) with no key scan.
|
|
391
|
+
|
|
392
|
+
#### `remember(key, ttlSeconds, factory, options?)`
|
|
393
|
+
|
|
394
|
+
`remember<T>(key: string, ttlSeconds: number, factory: () => T | Promise<T>, options?: RememberOptions): Promise<T>`
|
|
395
|
+
|
|
396
|
+
Returns the cached value, or runs `factory`, caches its result for `ttlSeconds`,
|
|
397
|
+
and returns it. **Stampede-protected**: concurrent calls for the same cold key
|
|
398
|
+
share one factory run.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
const stats = await cache().remember("dashboard.stats", 60, () =>
|
|
402
|
+
computeExpensiveStats(),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// With grace: serve the last good value for up to an hour if a refresh throws.
|
|
406
|
+
const rates = await cache().remember("fx.rates", 60, fetchRates, { grace: 3600 });
|
|
407
|
+
|
|
408
|
+
// With tags: invalidate later via deleteByTag(["feeds"]).
|
|
409
|
+
const feed = await cache().remember("feed:home", 300, buildFeed, { tags: ["feeds"] });
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Notes:** `factory` runs **only on a miss** and may be sync or async (both are
|
|
413
|
+
awaited). A stored `undefined` is treated as a miss, so `factory` re-runs. The
|
|
414
|
+
`ttlSeconds` argument is required here (unlike `put`); use `rememberForever` for
|
|
415
|
+
no expiry. `options.grace` (seconds) retains an expired value that much longer
|
|
416
|
+
and returns it if the refreshing `factory` throws — a normal `get` still reports
|
|
417
|
+
the expired key as a miss, so stale data never leaks through the plain read path.
|
|
418
|
+
`options.tags` joins the cached value to those tags for `deleteByTag`. A failing
|
|
419
|
+
factory **without** grace rejects and is not cached.
|
|
420
|
+
|
|
421
|
+
#### `rememberForever(key, factory, options?)`
|
|
422
|
+
|
|
423
|
+
`rememberForever<T>(key: string, factory: () => T | Promise<T>, options?: PutOptions): Promise<T>`
|
|
424
|
+
|
|
425
|
+
Like `remember`, but caches with no TTL. Also stampede-protected, and accepts
|
|
426
|
+
`{ tags }`.
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
const config = await cache().rememberForever("app.config", () => loadConfig());
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Notes:** same miss semantics as `remember` — `factory` runs once, then the
|
|
433
|
+
value is served until it's forgotten or flushed. No TTL means grace doesn't
|
|
434
|
+
apply (there's nothing to expire).
|
|
435
|
+
|
|
436
|
+
### `MemoryStore`
|
|
437
|
+
|
|
438
|
+
The default `CacheStore` — an in-process `Map` with lazy TTL expiry. Used
|
|
439
|
+
automatically when you construct a `Cache` with no store; construct it directly
|
|
440
|
+
only to pass it explicitly or to inspect it in tests.
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import { Cache, MemoryStore } from "@shaferllc/keel/core";
|
|
444
|
+
|
|
445
|
+
const c = new Cache(new MemoryStore());
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### `get(key)`
|
|
449
|
+
|
|
450
|
+
`get(key: string): unknown`
|
|
451
|
+
|
|
452
|
+
Returns the stored value, or `undefined` if absent or expired.
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
const store = new MemoryStore();
|
|
456
|
+
store.set("k", 1, 1000);
|
|
457
|
+
store.get("k"); // 1
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Notes:** synchronous. Expiry is checked on read — an expired entry is deleted
|
|
461
|
+
in-line and returns `undefined`, so `get` is what actually purges stale keys.
|
|
462
|
+
|
|
463
|
+
#### `set(key, value, ttlMs?)`
|
|
464
|
+
|
|
465
|
+
`set(key: string, value: unknown, ttlMs?: number): void`
|
|
466
|
+
|
|
467
|
+
Stores a value with an optional TTL in **milliseconds**.
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
store.set("otp", code, 300_000); // 5 minutes
|
|
471
|
+
store.set("cfg", config); // no expiry
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Notes:** synchronous, and takes `ttlMs` (milliseconds), not seconds — the
|
|
475
|
+
`Cache` façade does the seconds→ms conversion before calling this. Omitting
|
|
476
|
+
`ttlMs` (or `0`) stores with `expires: 0`, meaning no expiry.
|
|
477
|
+
|
|
478
|
+
#### `delete(key)`
|
|
479
|
+
|
|
480
|
+
`delete(key: string): void`
|
|
481
|
+
|
|
482
|
+
Removes a single key. Synchronous; a no-op if absent.
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
store.delete("otp");
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### `clear()`
|
|
489
|
+
|
|
490
|
+
`clear(): void`
|
|
491
|
+
|
|
492
|
+
Empties the whole map. Synchronous.
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
store.clear();
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Interfaces & types
|
|
499
|
+
|
|
500
|
+
#### `CacheStore`
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
interface CacheStore {
|
|
504
|
+
get(key: string): Promise<unknown> | unknown;
|
|
505
|
+
set(key: string, value: unknown, ttlMs?: number): Promise<void> | void;
|
|
506
|
+
delete(key: string): Promise<void> | void;
|
|
507
|
+
clear(): Promise<void> | void;
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
The seam between `Cache` and its backing store. Implement it to persist
|
|
512
|
+
elsewhere (Redis, Cloudflare KV, a database) and bind a `Cache` around it. Each
|
|
513
|
+
method may return synchronously or as a promise — `Cache` awaits either, so a
|
|
514
|
+
plain in-memory map and an async network client satisfy the same interface.
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
import { Cache, singleton, type CacheStore } from "@shaferllc/keel/core";
|
|
518
|
+
|
|
519
|
+
class KVStore implements CacheStore {
|
|
520
|
+
constructor(private kv: KV) {}
|
|
521
|
+
async get(key: string) {
|
|
522
|
+
return (await this.kv.get(key)) ?? undefined;
|
|
523
|
+
}
|
|
524
|
+
async set(key: string, value: unknown, ttlMs?: number) {
|
|
525
|
+
await this.kv.put(key, JSON.stringify(value), ttlMs);
|
|
526
|
+
}
|
|
527
|
+
async delete(key: string) { await this.kv.delete(key); }
|
|
528
|
+
async clear() { /* KV has no bulk clear — list + delete, or skip */ }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
singleton(Cache, () => new Cache(new KVStore(kv)));
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Notes:** TTLs reach your store in **milliseconds** (`ttlMs`). A missing key must
|
|
535
|
+
resolve to `undefined` — that's how `Cache` distinguishes a miss from a stored
|
|
536
|
+
value in `get`, `has`, `pull`, and the `remember` family. `Cache` writes an
|
|
537
|
+
opaque envelope (value + expiry + tag stamps) as the store value — treat stored
|
|
538
|
+
values as blobs to round-trip, not to read directly.
|
|
539
|
+
|
|
540
|
+
#### `PutOptions` / `RememberOptions`
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
interface PutOptions {
|
|
544
|
+
tags?: string[]; // associate the entry with tags, for deleteByTag
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
interface RememberOptions extends PutOptions {
|
|
548
|
+
grace?: number; // seconds to retain an expired value for stale-on-error
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
`PutOptions` is the trailing options bag on `put`/`add`/`rememberForever`;
|
|
553
|
+
`RememberOptions` adds `grace` for `remember`. Both are optional.
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
await cache().put("post:1", post, 600, { tags: ["posts"] });
|
|
557
|
+
await cache().remember("feed", 300, build, { grace: 60, tags: ["posts"] });
|
|
558
|
+
```
|