@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.
- package/AGENTS.md +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -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/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -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 +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -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 +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -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/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -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/pages.ts +82 -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/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -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/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -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/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -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 +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -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/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- package/package.json +42 -7
package/docs/helpers.md
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# Helpers
|
|
2
|
+
|
|
3
|
+
Keel gives you a handful of **global helper functions** so you can reach the
|
|
4
|
+
running application from anywhere — a route handler, a model, a plain function —
|
|
5
|
+
without threading a container reference through every call. `config('app.name')`,
|
|
6
|
+
`cache().get(…)`, `emit('user.registered', user)`: no `this.app`, no imports of
|
|
7
|
+
the container.
|
|
8
|
+
|
|
9
|
+
They all resolve against the **active application**, which registers itself the
|
|
10
|
+
moment an `Application` is constructed. In a normal single-app process — one Node
|
|
11
|
+
server, or one Worker isolate — that's exactly the app you mean, so the globals
|
|
12
|
+
just work.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { config, cache, emit, logger, view } from "@shaferllc/keel/core";
|
|
16
|
+
|
|
17
|
+
const name = config<string>("app.name", "Keel");
|
|
18
|
+
const stats = await cache().remember("stats", 60, () => computeStats());
|
|
19
|
+
await emit("user.registered", user);
|
|
20
|
+
logger().info("welcome sent", { userId: user.id });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How they resolve
|
|
24
|
+
|
|
25
|
+
Every helper is sugar over `app()` — the one helper that returns the active
|
|
26
|
+
`Application`. `config()` is `app().make(Config).get(…)`; `cache()` is
|
|
27
|
+
`app().make(Cache)`; `make()` is `app().make(…)`. So the whole set shares one
|
|
28
|
+
precondition: **an application must exist first.** Call any helper before
|
|
29
|
+
bootstrapping and `app()` throws:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
No Keel application has been bootstrapped. Create an Application first.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
In practice the application is created at boot, long before any request runs, so
|
|
36
|
+
you never see this outside of a bare unit test that forgot to construct one.
|
|
37
|
+
|
|
38
|
+
## The map
|
|
39
|
+
|
|
40
|
+
The helpers fall into groups, most of which have a dedicated guide. This page is
|
|
41
|
+
the quick index — reach for the deep doc when you need the full surface.
|
|
42
|
+
|
|
43
|
+
| Helper(s) | What it reaches | Deep dive |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `app` | the active `Application` | this page |
|
|
46
|
+
| `config` | configuration values | [configuration](./configuration.md) |
|
|
47
|
+
| `bind` `singleton` `instance` `make` `bound` | the service container | [container](./container.md) |
|
|
48
|
+
| `events` `emit` `listen` | the event emitter | [events](./events.md) |
|
|
49
|
+
| `cache` | the cache | [cache](./cache.md) |
|
|
50
|
+
| `logger` | the logger | [logger](./logger.md) |
|
|
51
|
+
| `view` | the view renderer | [views](./views.md) |
|
|
52
|
+
|
|
53
|
+
## Container helpers, up close
|
|
54
|
+
|
|
55
|
+
The five container helpers let you register and resolve services from anywhere,
|
|
56
|
+
exactly as `app().bind(…)` would — handy inside a factory or a helper function
|
|
57
|
+
that has no container reference of its own:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { singleton, make, bound } from "@shaferllc/keel/core";
|
|
61
|
+
|
|
62
|
+
singleton(Mailer, (app) => new Mailer(app.make(Config)));
|
|
63
|
+
const mailer = make(Mailer);
|
|
64
|
+
if (bound("clock")) { /* someone registered it */ }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The factory you pass to `bind`/`singleton` receives the container, so a service
|
|
68
|
+
can resolve its own dependencies. Unlike the `Container` methods (which return
|
|
69
|
+
`this` to chain), the `bind`/`singleton` **helpers return `void`** — there's no
|
|
70
|
+
builder to chain off of at the global level. See [container](./container.md) for
|
|
71
|
+
the binding lifecycle, auto-resolution, and tokens.
|
|
72
|
+
|
|
73
|
+
## Events, cache, logger
|
|
74
|
+
|
|
75
|
+
`events()`, `cache()`, and `logger()` each return the singleton service, so you
|
|
76
|
+
call methods on the result:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { events, cache, logger, listen, emit } from "@shaferllc/keel/core";
|
|
80
|
+
|
|
81
|
+
listen("order.paid", (order) => fulfil(order)); // subscribe
|
|
82
|
+
await emit("order.paid", order); // fan out, awaiting listeners
|
|
83
|
+
events().listenerCount("order.paid"); // the emitter itself
|
|
84
|
+
|
|
85
|
+
await cache().put("otp", code, 300);
|
|
86
|
+
logger().warn("retrying", { attempt: 2 });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`emit` and `listen` are shortcuts over `events().emit` / `events().on`, so you
|
|
90
|
+
rarely need `events()` directly — reach for it when you want `once`, `off`,
|
|
91
|
+
`listenerCount`, or `clear`. Full surface in [events](./events.md),
|
|
92
|
+
[cache](./cache.md), and [logger](./logger.md).
|
|
93
|
+
|
|
94
|
+
## Rendering a view
|
|
95
|
+
|
|
96
|
+
`view()` renders a component to a complete HTML document in one call — return it
|
|
97
|
+
straight from a handler. Props are type-checked against the component:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { view } from "@shaferllc/keel/core";
|
|
101
|
+
|
|
102
|
+
function Welcome({ appName }: { appName: string }) {
|
|
103
|
+
return `<h1>Welcome to ${appName}</h1>`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return view(Welcome, { appName: "Keel" }); // Promise<string>
|
|
107
|
+
return view(HomePage); // no props
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
See [views](./views.md) for the component contract and async (Suspense) rendering.
|
|
111
|
+
|
|
112
|
+
## Related
|
|
113
|
+
|
|
114
|
+
These globals are the front door to services documented in depth elsewhere:
|
|
115
|
+
[configuration](./configuration.md), the [container](./container.md),
|
|
116
|
+
[events](./events.md), [cache](./cache.md), [logger](./logger.md), and
|
|
117
|
+
[views](./views.md). Everything here is a thin, typed shortcut into one of those.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## API reference
|
|
122
|
+
|
|
123
|
+
Every helper below is exported from `@shaferllc/keel/core`. All of them resolve
|
|
124
|
+
against the active application and therefore throw
|
|
125
|
+
`No Keel application has been bootstrapped…` if called before one is created.
|
|
126
|
+
|
|
127
|
+
### `app()`
|
|
128
|
+
|
|
129
|
+
`app(): Application`
|
|
130
|
+
|
|
131
|
+
Returns the active `Application` — the container everything else resolves out of.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { app } from "@shaferllc/keel/core";
|
|
135
|
+
|
|
136
|
+
const port = app().config().get<number>("app.port", 3000);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Notes:** throws if no application has been constructed yet. Every other helper
|
|
140
|
+
on this page is built on `app()`, so this is the single point where a
|
|
141
|
+
"no application" error can originate.
|
|
142
|
+
|
|
143
|
+
### `config(key, fallback?)`
|
|
144
|
+
|
|
145
|
+
`config<T = unknown>(key: string, fallback?: T): T`
|
|
146
|
+
|
|
147
|
+
Reads a configuration value by dot-path, returning `fallback` when the path is
|
|
148
|
+
missing.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
config<string>("app.name");
|
|
152
|
+
config("app.port", 3000); // 3000 if unset
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Notes:** shorthand for `app().make(Config).get(key, fallback)`. Read-only —
|
|
156
|
+
use `app().make(Config).set(…)` to write. See [configuration](./configuration.md).
|
|
157
|
+
|
|
158
|
+
### `view(component, props?)`
|
|
159
|
+
|
|
160
|
+
`view<P>(component: (props: P, ...rest: any[]) => Renderable, props: P): Promise<string>`
|
|
161
|
+
`view(component: (...rest: any[]) => Renderable): Promise<string>`
|
|
162
|
+
|
|
163
|
+
Renders a component (with optional props) to a complete HTML document.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
return view(Welcome, { appName: "Keel" });
|
|
167
|
+
return view(HomePage);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Notes:** props are type-checked against the component's parameter. Resolves to
|
|
171
|
+
a `Promise<string>` (a full HTML document, doctype included) — return it directly
|
|
172
|
+
from a route handler. Sugar over `app().make(View).render(component(props))`. See
|
|
173
|
+
[views](./views.md).
|
|
174
|
+
|
|
175
|
+
### `bind(token, factory)`
|
|
176
|
+
|
|
177
|
+
`bind<T>(token: Token<T>, factory: Factory<T>): void`
|
|
178
|
+
|
|
179
|
+
Registers a **transient** binding — the factory runs on every `make`.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
bind("clock", () => new Date());
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Notes:** the factory receives the container. Returns `void` (the underlying
|
|
186
|
+
`Container.bind` returns `this`, but the helper does not). See
|
|
187
|
+
[container](./container.md).
|
|
188
|
+
|
|
189
|
+
### `singleton(token, factory)`
|
|
190
|
+
|
|
191
|
+
`singleton<T>(token: Token<T>, factory: Factory<T>): void`
|
|
192
|
+
|
|
193
|
+
Registers a **shared** binding — the factory runs once, then the value is cached.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
singleton(Mailer, (app) => new Mailer(app.make(Config)));
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Notes:** the cached value lives for the life of the application. Returns
|
|
200
|
+
`void`. See [container](./container.md).
|
|
201
|
+
|
|
202
|
+
### `instance(token, value)`
|
|
203
|
+
|
|
204
|
+
`instance<T>(token: Token<T>, value: T): T`
|
|
205
|
+
|
|
206
|
+
Registers an already-constructed value as a shared instance, and returns it.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const version = instance("app.version", "0.30.0");
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Notes:** unlike `bind`/`singleton`, this returns the value you passed in, so
|
|
213
|
+
you can register-and-use in one expression. See [container](./container.md).
|
|
214
|
+
|
|
215
|
+
### `make(token)`
|
|
216
|
+
|
|
217
|
+
`make<T>(token: Token<T>): T`
|
|
218
|
+
|
|
219
|
+
Resolves a token out of the container.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const mailer = make(Mailer);
|
|
223
|
+
const version = make<string>("app.version");
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Notes:** a zero-arg class token resolves even without an explicit binding (the
|
|
227
|
+
container builds it); an unbound string/symbol token throws
|
|
228
|
+
`Nothing bound in the container for […]`. See [container](./container.md).
|
|
229
|
+
|
|
230
|
+
### `bound(token)`
|
|
231
|
+
|
|
232
|
+
`bound(token: Token): boolean`
|
|
233
|
+
|
|
234
|
+
`true` if the token has a binding or a cached instance.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
if (bound("clock")) make<Date>("clock");
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Notes:** a guard for optional services. Note a class token that `make` could
|
|
241
|
+
auto-build still reports `false` here until it's explicitly bound. See
|
|
242
|
+
[container](./container.md).
|
|
243
|
+
|
|
244
|
+
### `events()`
|
|
245
|
+
|
|
246
|
+
`events(): Events`
|
|
247
|
+
|
|
248
|
+
Returns the application's event emitter singleton.
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
events().listenerCount("order.paid");
|
|
252
|
+
events().clear("order.paid");
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Notes:** use for `once`, `off`, `listenerCount`, and `clear`; for the common
|
|
256
|
+
subscribe/emit pair prefer `listen`/`emit` below. See [events](./events.md).
|
|
257
|
+
|
|
258
|
+
### `emit(event, payload?)`
|
|
259
|
+
|
|
260
|
+
`emit<T = unknown>(event: string, payload?: T): Promise<void>`
|
|
261
|
+
|
|
262
|
+
Emits an event, awaiting every listener in registration order.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
await emit("user.registered", user);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Notes:** shorthand for `events().emit(…)`. The returned promise resolves once
|
|
269
|
+
all listeners (including async ones) have run. No listeners → resolves
|
|
270
|
+
immediately. See [events](./events.md).
|
|
271
|
+
|
|
272
|
+
### `listen(event, listener)`
|
|
273
|
+
|
|
274
|
+
`listen<T = unknown>(event: string, listener: Listener<T>): () => void`
|
|
275
|
+
|
|
276
|
+
Subscribes to an event; returns an unsubscribe function.
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
const off = listen("user.registered", (user) => sendWelcome(user));
|
|
280
|
+
off(); // stop listening
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Notes:** shorthand for `events().on(…)`. The listener may be sync or async.
|
|
284
|
+
Call the returned function to remove it. See [events](./events.md).
|
|
285
|
+
|
|
286
|
+
### `cache()`
|
|
287
|
+
|
|
288
|
+
`cache(): Cache`
|
|
289
|
+
|
|
290
|
+
Returns the application's cache singleton.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const stats = await cache().remember("stats", 60, () => computeStats());
|
|
294
|
+
await cache().put("otp", code, 300);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Notes:** memory-backed per process/isolate by default; swap the store via a
|
|
298
|
+
`singleton(Cache, …)` binding. See [cache](./cache.md).
|
|
299
|
+
|
|
300
|
+
### `logger()`
|
|
301
|
+
|
|
302
|
+
`logger(): Logger`
|
|
303
|
+
|
|
304
|
+
Returns the application's logger singleton.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
logger().info("user registered", { userId: user.id });
|
|
308
|
+
logger().error("payment failed", { orderId });
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Notes:** structured JSON by default. `logger().child({ … })` returns a logger
|
|
312
|
+
with bound fields (e.g. a request id). See [logger](./logger.md).
|
|
313
|
+
|
|
314
|
+
### Interfaces & types
|
|
315
|
+
|
|
316
|
+
The helpers surface a few types from the services they front. You implement or
|
|
317
|
+
pass these; you rarely construct them here.
|
|
318
|
+
|
|
319
|
+
#### `Listener<T>`
|
|
320
|
+
|
|
321
|
+
`type Listener<T = unknown> = (payload: T) => void | Promise<void>`
|
|
322
|
+
|
|
323
|
+
The shape of a function passed to `listen`. Sync or async; the payload type
|
|
324
|
+
flows from `listen<T>`.
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
const onOrder: Listener<{ id: number }> = async (order) => fulfil(order.id);
|
|
328
|
+
listen("order.paid", onOrder);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### `Token<T>` / `Factory<T>`
|
|
332
|
+
|
|
333
|
+
`type Token<T = unknown> = string | symbol | Constructor<T>`
|
|
334
|
+
`type Factory<T> = (app: Container) => T`
|
|
335
|
+
|
|
336
|
+
The key and the factory used by `bind`/`singleton`/`instance`/`make`/`bound`. A
|
|
337
|
+
token is a string, symbol, or class constructor; a factory receives the container
|
|
338
|
+
so it can resolve its own dependencies. Documented in full under
|
|
339
|
+
[container](./container.md).
|
|
340
|
+
|
|
341
|
+
#### `Renderable`
|
|
342
|
+
|
|
343
|
+
`type Renderable = string | Promise<string> | { toString(): string | Promise<string> } | null | undefined`
|
|
344
|
+
|
|
345
|
+
What a component passed to `view()` may return — a string, a JSX node, a promise
|
|
346
|
+
of either, or nullish (renders empty). Documented in full under
|
|
347
|
+
[views](./views.md).
|
package/docs/hono.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Built on Hono
|
|
2
|
+
|
|
3
|
+
Keel's HTTP layer **is** [Hono](https://hono.dev) — an ultrafast, web-standard
|
|
4
|
+
router that runs on Node, Cloudflare Workers, Deno, Bun, and more. Keel adds the
|
|
5
|
+
container, providers, routing sugar, and helpers on top; everything Hono can do
|
|
6
|
+
is available to you underneath.
|
|
7
|
+
|
|
8
|
+
Keel's convenience helpers (`json()`, `param()`, `request`, `response`, `view()`)
|
|
9
|
+
are thin wrappers over Hono's context. You never have to use them — you can
|
|
10
|
+
always take the context (`c`) directly and use the full Hono API.
|
|
11
|
+
|
|
12
|
+
## What Hono provides, what Keel adds
|
|
13
|
+
|
|
14
|
+
The division of labor is worth holding in your head, because it tells you which
|
|
15
|
+
docs to reach for:
|
|
16
|
+
|
|
17
|
+
| Concern | Owned by |
|
|
18
|
+
|---------|----------|
|
|
19
|
+
| The `fetch` handler, request matching, method routing | **Hono** |
|
|
20
|
+
| `Context` — `c.req`, `c.json`, `c.html`, cookies, headers | **Hono** |
|
|
21
|
+
| JSX rendering (`hono/jsx`), streaming, SSE, WebSockets | **Hono** |
|
|
22
|
+
| Runtime adapters (Node, Workers, Deno, Bun, Lambda) | **Hono** |
|
|
23
|
+
| The service container, providers, config, the console | **Keel** |
|
|
24
|
+
| Fluent routing: names, groups, resources, param matchers, URL generation | **Keel** ([routing](./routing.md)) |
|
|
25
|
+
| `[Controller, method]` handlers resolved from the container | **Keel** ([controllers](./controllers.md)) |
|
|
26
|
+
| Request/response helpers that reach `c` without threading it | **Keel** ([request & response](./request-response.md)) |
|
|
27
|
+
| `view()`, error rendering, exceptions, validation | **Keel** |
|
|
28
|
+
|
|
29
|
+
Hono is the engine; Keel is the wheelhouse. Keel never hides Hono — it sits
|
|
30
|
+
beside it. When Keel wraps a Hono feature it's for ergonomics (fluent routes,
|
|
31
|
+
container DI, ambient helpers), and the raw feature is always one `c` away.
|
|
32
|
+
|
|
33
|
+
## The context (`c`)
|
|
34
|
+
|
|
35
|
+
Every closure handler receives Hono's `Context`, and controller methods can too:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
router.get("/users/:id", (c) => {
|
|
39
|
+
c.req.param("id"); // route param
|
|
40
|
+
c.req.query("q"); // query string
|
|
41
|
+
c.req.header("authorization");
|
|
42
|
+
await c.req.json(); // parse a JSON body
|
|
43
|
+
|
|
44
|
+
return c.json({ ok: true }); // c.text() · c.html() · c.body() · c.redirect()
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Keel's `Ctx` type is exactly Hono's `Context` — it's a re-export, not a wrapper:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import type { Ctx } from "@shaferllc/keel/core";
|
|
52
|
+
// type Ctx = import("hono").Context
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
So a Keel handler and a Hono handler have the identical signature. Anything that
|
|
56
|
+
accepts a Hono `Context` accepts a Keel `Ctx`, and vice versa — there is no
|
|
57
|
+
adapter, boxing, or conversion between the two. When a guide says "the request
|
|
58
|
+
context," it means this object.
|
|
59
|
+
|
|
60
|
+
Common context surface: `c.req.{param, query, header, json, parseBody, valid, path, method, url, raw}`,
|
|
61
|
+
`c.{json, text, html, body, redirect, status, header, notFound}`, `c.set/c.get`
|
|
62
|
+
for request-scoped variables, and on Workers `c.env` (bindings like D1/KV/R2) and
|
|
63
|
+
`c.executionCtx` (`waitUntil`). Full reference:
|
|
64
|
+
[hono.dev/docs/api/context](https://hono.dev/docs/api/context).
|
|
65
|
+
|
|
66
|
+
Keel does set a few request-scoped variables of its own on the context, which
|
|
67
|
+
you can read with `c.get(...)`:
|
|
68
|
+
|
|
69
|
+
- `c.get("app")` — the service container for this request.
|
|
70
|
+
- `c.get("route")` — the matched route (`{ name, pattern, methods }`).
|
|
71
|
+
- `c.get("subdomains")` — captured subdomain params on domain-bound routes.
|
|
72
|
+
|
|
73
|
+
These are exactly what the ambient [request helpers](./request-response.md) read
|
|
74
|
+
under the hood. That's the trade: the helpers are terse and don't need `c`
|
|
75
|
+
passed around, but they only work inside a request; `c` is explicit and works
|
|
76
|
+
anywhere you're handed it.
|
|
77
|
+
|
|
78
|
+
## Hono middleware works as-is
|
|
79
|
+
|
|
80
|
+
Any Hono middleware — built-in or third-party — drops straight into Keel's kernel
|
|
81
|
+
or onto a route, because Keel middleware **is** Hono middleware:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { cors } from "hono/cors";
|
|
85
|
+
import { secureHeaders } from "hono/secure-headers";
|
|
86
|
+
import { compress } from "hono/compress";
|
|
87
|
+
|
|
88
|
+
// app/Http/Kernel.ts
|
|
89
|
+
this.use(cors());
|
|
90
|
+
this.use(secureHeaders());
|
|
91
|
+
this.use(compress());
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Hono ships CORS, Secure Headers, Body Limit, Cache, Compress, ETag, Basic/Bearer
|
|
95
|
+
Auth, JWT, Logger, and more — see
|
|
96
|
+
[hono.dev/docs/middleware/builtin](https://hono.dev/docs/middleware/builtin).
|
|
97
|
+
Keel just gives you nicer places to attach them: global in the kernel, or
|
|
98
|
+
per-route and per-group via the fluent router. [Middleware](./middleware.md)
|
|
99
|
+
covers the ordering and named-middleware conveniences Keel layers on top.
|
|
100
|
+
|
|
101
|
+
## What else you get from Hono
|
|
102
|
+
|
|
103
|
+
Because Keel is Hono underneath, these are all available directly:
|
|
104
|
+
|
|
105
|
+
| Hono feature | Use it in Keel |
|
|
106
|
+
|--------------|----------------|
|
|
107
|
+
| **JSX** | Keel [views](./views.md) are Hono JSX (`hono/jsx`) |
|
|
108
|
+
| **Cookies** | `hono/cookie`; Keel wraps common cases in `request.cookie` / `response.cookie` |
|
|
109
|
+
| **Streaming / SSE** | `hono/streaming` — return a streamed `Response` from a handler |
|
|
110
|
+
| **WebSockets** | Hono's upgrade helpers on supported runtimes |
|
|
111
|
+
| **Testing** | `hono.request(path, init)` — exactly what Keel's own test suite uses |
|
|
112
|
+
| **Validators / RPC** | `hono/validator`, the `hc` typed client |
|
|
113
|
+
| **Runtime adapters** | Node (`@hono/node-server`), Workers, Deno, Bun, Lambda |
|
|
114
|
+
|
|
115
|
+
[Views](./views.md) are the clearest example of Keel building on a Hono
|
|
116
|
+
primitive: a Keel view is a Hono JSX function component, and `view()` just
|
|
117
|
+
renders it to a full HTML document through the `View` service. Drop the helper
|
|
118
|
+
and `return c.html(<Page />)` and you get the same result — Keel's version only
|
|
119
|
+
adds the doctype and props typing.
|
|
120
|
+
|
|
121
|
+
## When to drop to raw Hono
|
|
122
|
+
|
|
123
|
+
Reach for `c` and the Hono API directly when:
|
|
124
|
+
|
|
125
|
+
- You need a context feature Keel doesn't wrap — `c.executionCtx.waitUntil`,
|
|
126
|
+
streaming responses, content negotiation beyond the helpers, Workers bindings
|
|
127
|
+
on `c.env`.
|
|
128
|
+
- You're pulling in a Hono (or Hono-ecosystem) middleware — it already speaks
|
|
129
|
+
the native `Context`, so hand it `c` unchanged.
|
|
130
|
+
- You want the typed RPC client (`hc`) or `hono/validator`'s `c.req.valid(...)`.
|
|
131
|
+
|
|
132
|
+
Reach for Keel's helpers and fluent router when you want named routes, groups,
|
|
133
|
+
resource routes, container-resolved controllers, or the ambient
|
|
134
|
+
`request`/`response` accessors. The two mix freely in the same handler — start
|
|
135
|
+
with Keel's ergonomics and drop to `c` for the exact spot that needs it. Nothing
|
|
136
|
+
you do at the Hono level is "off the map"; it's the same object either way.
|
|
137
|
+
|
|
138
|
+
## Reaching the Hono app
|
|
139
|
+
|
|
140
|
+
The HTTP kernel compiles your routes onto a Hono instance and returns it —
|
|
141
|
+
that's the `fetch` handler you serve (Node) or export (Workers). If you need to
|
|
142
|
+
attach something at the Hono level, do it where you build the kernel:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const hono = new Kernel(app).build(); // a Hono instance
|
|
146
|
+
// hono.get(...), hono.use(...), export default hono, serve({ fetch: hono.fetch })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Because `build()` hands back a plain Hono app, the same kernel serves every
|
|
150
|
+
runtime — `serve({ fetch: hono.fetch })` under Node, `export default hono` on
|
|
151
|
+
Workers. That single return value is the seam that keeps Keel edge-portable (see
|
|
152
|
+
[Architecture](./architecture.md#edge-safe-by-design)).
|
|
153
|
+
|
|
154
|
+
For anything HTTP-layer that Keel doesn't wrap yet, drop down to Hono — the docs
|
|
155
|
+
at [hono.dev](https://hono.dev/docs/) apply directly.
|
|
156
|
+
|
|
157
|
+
## Serving over HTTP/2
|
|
158
|
+
|
|
159
|
+
There's nothing to configure in Keel for HTTP/2 — it's a transport concern, not a
|
|
160
|
+
framework one. Keel's handlers are Hono's fetch-based `Request`/`Response`, which
|
|
161
|
+
are HTTP-version-agnostic, so the protocol is decided entirely by whatever serves
|
|
162
|
+
the `fetch` handler:
|
|
163
|
+
|
|
164
|
+
- **On the edge** (Cloudflare Workers — the headline target), the platform
|
|
165
|
+
terminates HTTP/2 *and* HTTP/3 for you. Nothing to do, nothing to control from
|
|
166
|
+
app code.
|
|
167
|
+
- **In Node production**, HTTP/2 is almost always terminated at a reverse proxy
|
|
168
|
+
or CDN (nginx, Cloudflare, ALB) in front of the process — the usual setup.
|
|
169
|
+
- **In-process h2**, if you really want it, is a `@hono/node-server` option — hand
|
|
170
|
+
`serve()` a `node:http2` server. No Keel change:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { serve } from "@hono/node-server";
|
|
174
|
+
import { createSecureServer } from "node:http2";
|
|
175
|
+
import { readFileSync } from "node:fs";
|
|
176
|
+
|
|
177
|
+
const hono = new Kernel(app).build();
|
|
178
|
+
serve({
|
|
179
|
+
fetch: hono.fetch,
|
|
180
|
+
createServer: createSecureServer,
|
|
181
|
+
serverOptions: { key: readFileSync("key.pem"), cert: readFileSync("cert.pem") },
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
So HTTP/2 is available on every Keel deployment without the framework
|
|
186
|
+
implementing anything — the same `fetch` handler just gets served over it.
|
package/docs/hooks.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Lifecycle Hooks
|
|
2
|
+
|
|
3
|
+
Tap into the **application lifecycle** — run code once the app is ready, clean up
|
|
4
|
+
on shutdown, and observe route registration. Inspired by
|
|
5
|
+
[Fastify's hooks](https://fastify.dev/docs/latest/Reference/Hooks/), scoped to the
|
|
6
|
+
parts Keel doesn't already cover.
|
|
7
|
+
|
|
8
|
+
> **Request-lifecycle hooks** (before/after a request, on error) are
|
|
9
|
+
> [middleware](./middleware.md) in Keel — `HttpKernel.use()`, route/group
|
|
10
|
+
> `.middleware()`, and `onError()`. This page is the *application* lifecycle:
|
|
11
|
+
> ready, shutdown, and route registration.
|
|
12
|
+
|
|
13
|
+
## onReady
|
|
14
|
+
|
|
15
|
+
Run a callback once the application has finished booting (all providers
|
|
16
|
+
registered and booted). Register it before boot; if the app is already booted, it
|
|
17
|
+
runs immediately.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { onReady } from "@shaferllc/keel/core";
|
|
21
|
+
|
|
22
|
+
onReady(async (app) => {
|
|
23
|
+
await warmCaches();
|
|
24
|
+
logger().info("app ready");
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Graceful shutdown
|
|
29
|
+
|
|
30
|
+
`onShutdown` registers cleanup — closing database/Redis connections, flushing
|
|
31
|
+
queues, draining work. Hooks run **newest-first (LIFO)** when you call
|
|
32
|
+
`terminate()`, so teardown unwinds in the reverse order things were set up:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { onShutdown, terminate } from "@shaferllc/keel/core";
|
|
36
|
+
|
|
37
|
+
onShutdown(async () => {
|
|
38
|
+
await db().close?.();
|
|
39
|
+
await redis().flushAll();
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**`keel serve` already traps SIGINT and SIGTERM**, stops accepting connections,
|
|
44
|
+
and calls `terminate()` for you — so the hook above just runs. You only wire
|
|
45
|
+
signals by hand in a **custom entrypoint** that doesn't go through `keel serve`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
49
|
+
process.on(signal, async () => {
|
|
50
|
+
await terminate(); // runs every shutdown hook
|
|
51
|
+
process.exit(0);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A [service provider](./providers.md)'s `shutdown()` method joins the same queue,
|
|
57
|
+
so provider teardown and hand-registered `onShutdown` hooks unwind together.
|
|
58
|
+
`terminate()` is **idempotent** — a second call does nothing. A hook that throws
|
|
59
|
+
doesn't stop the others; the first error is re-thrown after all have run, so one
|
|
60
|
+
failing cleanup can't strand the rest.
|
|
61
|
+
|
|
62
|
+
## onRoute
|
|
63
|
+
|
|
64
|
+
Observe routes as they're registered — for request logging, an API map, or
|
|
65
|
+
metrics. The hook is called for each route added *after* registration, and
|
|
66
|
+
replayed for routes already registered, so you see them all regardless of order:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const router = app.make(Router);
|
|
70
|
+
|
|
71
|
+
router.onRoute((def) => {
|
|
72
|
+
logger().debug("route", { methods: def.methods, path: def.path, name: def.name });
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `def` is the live route definition, so reading it later reflects fluent
|
|
77
|
+
config applied after `add()` — `.name()`, `.middleware()`, and so on.
|
|
78
|
+
|
|
79
|
+
## API reference
|
|
80
|
+
|
|
81
|
+
### `onReady(hook)`
|
|
82
|
+
|
|
83
|
+
`onReady(hook: (app: Application) => void | Promise<void>): void`
|
|
84
|
+
|
|
85
|
+
Global helper — registers a ready hook on the active application. Runs after boot
|
|
86
|
+
(or immediately if already booted). Also available as `app.onReady(hook)`.
|
|
87
|
+
|
|
88
|
+
### `onShutdown(hook)`
|
|
89
|
+
|
|
90
|
+
`onShutdown(hook: (app: Application) => void | Promise<void>): void`
|
|
91
|
+
|
|
92
|
+
Registers a shutdown hook on the active application. Also `app.onShutdown(hook)`.
|
|
93
|
+
|
|
94
|
+
### `terminate()`
|
|
95
|
+
|
|
96
|
+
`terminate(): Promise<void>`
|
|
97
|
+
|
|
98
|
+
Gracefully shuts the active application down — runs every shutdown hook LIFO.
|
|
99
|
+
Idempotent. Re-throws the first hook error after running all. Also
|
|
100
|
+
`app.terminate()`.
|
|
101
|
+
|
|
102
|
+
### `Application.onReady` / `onShutdown` / `terminate`
|
|
103
|
+
|
|
104
|
+
The same three as methods on the `Application`, returning `this` (chainable) for
|
|
105
|
+
`onReady`/`onShutdown`. `app.isTerminated` reports whether `terminate()` has run.
|
|
106
|
+
|
|
107
|
+
### `Router.onRoute(hook)`
|
|
108
|
+
|
|
109
|
+
`onRoute(hook: (def: RouteDefinition) => void): this`
|
|
110
|
+
|
|
111
|
+
Called with each route's definition as it's registered, and replayed for existing
|
|
112
|
+
routes. Chainable.
|
|
113
|
+
|
|
114
|
+
### `LifecycleHook`
|
|
115
|
+
|
|
116
|
+
`type LifecycleHook = (app: Application) => void | Promise<void>`
|
|
117
|
+
|
|
118
|
+
The signature of `onReady` / `onShutdown` hooks.
|