@shaferllc/keel 0.66.0 → 0.74.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.
Files changed (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -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
+ ```