@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/events.md
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# Events
|
|
2
|
+
|
|
3
|
+
A tiny event emitter for decoupling — fire an event in one place, handle it in
|
|
4
|
+
another. The emitter is a container singleton, reachable through the global
|
|
5
|
+
`emit()` / `listen()` helpers.
|
|
6
|
+
|
|
7
|
+
## Listen and emit
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { emit, listen } from "@shaferllc/keel/core";
|
|
11
|
+
|
|
12
|
+
// register a listener (usually in a provider's boot())
|
|
13
|
+
listen("user.registered", (user) => {
|
|
14
|
+
sendWelcomeEmail(user);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// fire it from anywhere — emit awaits every listener
|
|
18
|
+
await emit("user.registered", user);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`listen()` returns an unsubscribe function:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const off = listen("tick", () => {});
|
|
25
|
+
off(); // stop listening
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Typed payloads
|
|
29
|
+
|
|
30
|
+
Both `emit` and `listen` take a payload type parameter, so the value you fire and
|
|
31
|
+
the value your listener receives line up at compile time:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
type OrderPaid = { id: number; total: number };
|
|
35
|
+
|
|
36
|
+
listen<OrderPaid>("order.paid", (order) => {
|
|
37
|
+
order.total; // number
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await emit<OrderPaid>("order.paid", { id: 1, total: 4200 });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This is a convenience for the caller: you're passing the same type on both sides
|
|
44
|
+
by hand, and nothing checks that you did. Get it wrong in one place and the two
|
|
45
|
+
drift apart silently.
|
|
46
|
+
|
|
47
|
+
## The event registry
|
|
48
|
+
|
|
49
|
+
Declare an event once in `EventsList` and the emitter checks **both** sides of it
|
|
50
|
+
for you — no type argument to remember, and no way for the emitter and the
|
|
51
|
+
listener to disagree:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
declare module "@shaferllc/keel/core" {
|
|
55
|
+
interface EventsList {
|
|
56
|
+
"order.paid": { id: number; total: number };
|
|
57
|
+
"user.registered": User;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Put that in a `types/events.ts` (anywhere the compiler sees it). From then on:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
listen("order.paid", (order) => {
|
|
66
|
+
order.total; // number — inferred from the registry
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await emit("order.paid", { id: 1, total: 4200 }); // ✅
|
|
70
|
+
|
|
71
|
+
await emit("order.paid", { id: 1, total: "4200" }); // ❌ total must be a number
|
|
72
|
+
await emit("order.paid"); // ❌ this event requires a payload
|
|
73
|
+
listen("order.paid", (o: { nope: boolean }) => {}); // ❌ wrong listener payload
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Declaring events is **opt-in and incremental**. An event you haven't declared
|
|
77
|
+
behaves exactly as it always has — the payload type comes from the listener or an
|
|
78
|
+
explicit `listen<T>`, and falls back to `unknown` — so you can add entries to
|
|
79
|
+
`EventsList` one at a time.
|
|
80
|
+
|
|
81
|
+
Nothing validates the payload at *runtime*; this is a compile-time contract.
|
|
82
|
+
|
|
83
|
+
## Where to register listeners
|
|
84
|
+
|
|
85
|
+
Register listeners in a service provider's `boot()` method, so they're wired up
|
|
86
|
+
once when the app starts:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
export class EventServiceProvider extends ServiceProvider {
|
|
90
|
+
boot(): void {
|
|
91
|
+
listen("order.paid", (order) => this.app.make(Fulfillment).ship(order));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`boot()` runs after every provider has registered, so it's safe to resolve other
|
|
97
|
+
services there. Don't register listeners in `register()` — the services they
|
|
98
|
+
reach may not be bound yet.
|
|
99
|
+
|
|
100
|
+
## The full API
|
|
101
|
+
|
|
102
|
+
Reach the emitter directly with `events()` for `once`, `off`, and more:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { events } from "@shaferllc/keel/core";
|
|
106
|
+
|
|
107
|
+
events().once("boot", () => {}); // fire once, then auto-unsubscribe
|
|
108
|
+
events().off("tick", listener);
|
|
109
|
+
events().listenerCount("tick");
|
|
110
|
+
events().clear("tick"); // or clear() for everything
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Ordering and awaiting
|
|
114
|
+
|
|
115
|
+
Listeners for an event run in registration order, and `emit()` **awaits** each
|
|
116
|
+
one before starting the next — so a slow async listener finishes before the
|
|
117
|
+
following listener begins, and before `emit()` resolves:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
listen("deploy", async () => { await step1(); });
|
|
121
|
+
listen("deploy", async () => { await step2(); }); // runs after step1 settles
|
|
122
|
+
|
|
123
|
+
await emit("deploy"); // resolves only after both steps finish
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`emit()` snapshots the listener set before iterating, so a listener that
|
|
127
|
+
subscribes (or unsubscribes) another during the same `emit()` doesn't change who
|
|
128
|
+
runs this time around — the change takes effect on the next emission.
|
|
129
|
+
|
|
130
|
+
## Error behavior
|
|
131
|
+
|
|
132
|
+
**A listener that throws does not stop the others.** `emit()` runs every listener,
|
|
133
|
+
then reports what broke. That's the whole point of an emitter: the analytics
|
|
134
|
+
listener blowing up shouldn't silently cancel the welcome email.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
listen("user.registered", () => sendWelcomeEmail(user)); // runs
|
|
138
|
+
listen("user.registered", () => { throw new Error("boom"); }); // throws
|
|
139
|
+
listen("user.registered", () => trackSignup(user)); // still runs
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await emit("user.registered", user);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// every listener ran; err is the failure ("boom")
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Failures are never swallowed. With one failed listener `emit()` rejects with that
|
|
149
|
+
error; with several it rejects with an `AggregateError` whose `.errors` holds them
|
|
150
|
+
all.
|
|
151
|
+
|
|
152
|
+
### Handling failures centrally
|
|
153
|
+
|
|
154
|
+
Register an `onError` handler and `emit()` stops rejecting — each failure goes to
|
|
155
|
+
your handler instead, with the event name and the payload that triggered it. This
|
|
156
|
+
is how you keep a background listener's bug from taking down the request that
|
|
157
|
+
happened to fire the event:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
events().onError((event, error, payload) => {
|
|
161
|
+
logger().error("listener failed", { event, error, payload });
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Firing an event that has no listeners is a no-op — `emit()` resolves immediately.
|
|
166
|
+
|
|
167
|
+
## Observing every event
|
|
168
|
+
|
|
169
|
+
`onAny` subscribes to *all* events — for logging, metrics, and other
|
|
170
|
+
cross-cutting concerns. It runs before the event's own listeners and returns an
|
|
171
|
+
unsubscribe function:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const off = events().onAny((event, payload) => {
|
|
175
|
+
logger().debug(`event: ${event}`, { payload });
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Testing
|
|
180
|
+
|
|
181
|
+
`events().fake()` records emissions **instead of running listeners**, so a test
|
|
182
|
+
can assert an event fired without triggering its side effects — no welcome email,
|
|
183
|
+
no queued job. It returns a buffer to assert against; `restore()` puts the real
|
|
184
|
+
emitter back.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const buffer = events().fake();
|
|
188
|
+
|
|
189
|
+
await registerUser({ email: "a@b.com" });
|
|
190
|
+
|
|
191
|
+
buffer.assertEmitted("user.registered");
|
|
192
|
+
buffer.assertEmitted("order.paid", (o) => o.total === 4200); // with a predicate
|
|
193
|
+
buffer.assertEmittedCount("user.registered", 1);
|
|
194
|
+
buffer.assertNotEmitted("user.deleted");
|
|
195
|
+
buffer.assertNoneEmitted(); // nothing at all fired
|
|
196
|
+
|
|
197
|
+
events().restore();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Pass event names to fake only those — everything else dispatches for real:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const buffer = events().fake("user.registered"); // or ["a", "b"]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`buffer.all()` returns every recorded `{ event, payload }` in order, and
|
|
207
|
+
`buffer.payloadsFor("order.paid")` returns just that event's payloads (typed, if
|
|
208
|
+
the event is declared in `EventsList`).
|
|
209
|
+
|
|
210
|
+
## Notes
|
|
211
|
+
|
|
212
|
+
- A listener is stored once per event (the backing store is a `Set`), so
|
|
213
|
+
registering the exact same function twice for the same event only fires it
|
|
214
|
+
once. Register a fresh function each time if you want it to run twice.
|
|
215
|
+
- Events are in-process. For cross-process or durable events, have a listener
|
|
216
|
+
publish to your queue/broker of choice.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## API reference
|
|
221
|
+
|
|
222
|
+
### `Events`
|
|
223
|
+
|
|
224
|
+
The emitter itself — a container singleton. You rarely construct it; resolve it
|
|
225
|
+
with `events()` (or `app().make(Events)`). Events are keyed by string; each key
|
|
226
|
+
holds a `Set` of listeners.
|
|
227
|
+
|
|
228
|
+
#### `on(event, listener)`
|
|
229
|
+
|
|
230
|
+
`on<T = unknown>(event: string, listener: Listener<T>): () => void`
|
|
231
|
+
|
|
232
|
+
Subscribes `listener` to `event` and returns an unsubscribe function.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const off = events().on<{ id: number }>("user.deleted", (u) => audit(u.id));
|
|
236
|
+
off(); // later: stop listening
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Notes:** the returned function removes exactly this listener. The same function
|
|
240
|
+
reference is de-duplicated per event (Set-backed), so subscribing it twice still
|
|
241
|
+
only registers — and fires — it once. The global `listen()` helper is a thin
|
|
242
|
+
wrapper over this.
|
|
243
|
+
|
|
244
|
+
#### `once(event, listener)`
|
|
245
|
+
|
|
246
|
+
`once<T = unknown>(event: string, listener: Listener<T>): () => void`
|
|
247
|
+
|
|
248
|
+
Subscribes for a single emission: the listener runs on the next `emit`, then
|
|
249
|
+
auto-unsubscribes. Also returns an unsubscribe function to cancel it beforehand.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
events().once("boot", () => console.log("started"));
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Notes:** it unsubscribes itself *before* awaiting your callback, so a listener
|
|
256
|
+
that re-emits the same event won't re-trigger this one. The returned unsubscribe
|
|
257
|
+
removes the internal wrapper, so it works even if the event never fires.
|
|
258
|
+
|
|
259
|
+
#### `off(event, listener)`
|
|
260
|
+
|
|
261
|
+
`off<T = unknown>(event: string, listener: Listener<T>): void`
|
|
262
|
+
|
|
263
|
+
Removes `listener` from `event`. No-op if it wasn't subscribed.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
events().off("tick", handler);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Notes:** you must pass the same function reference you subscribed with —
|
|
270
|
+
anonymous inline functions can't be removed this way, so keep a reference (or use
|
|
271
|
+
the unsubscribe function `on`/`once` return). Removing the last listener leaves an
|
|
272
|
+
empty `Set` behind under that key; use `clear(event)` to drop the key entirely.
|
|
273
|
+
|
|
274
|
+
#### `emit(event, payload?)`
|
|
275
|
+
|
|
276
|
+
`emit<T = unknown>(event: string, payload?: T): Promise<void>`
|
|
277
|
+
|
|
278
|
+
Fires `event`, awaiting every listener in registration order with the given
|
|
279
|
+
payload. For an event declared in `EventsList`, the payload is required and
|
|
280
|
+
type-checked against the registry.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
await events().emit("order.paid", { id: 1, total: 4200 });
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Notes:** listeners run sequentially, each awaited before the next. A listener
|
|
287
|
+
that throws or rejects **does not** skip the rest — they all run, and `emit`
|
|
288
|
+
rejects afterwards with that error (or an `AggregateError` if more than one
|
|
289
|
+
failed), unless an `onError` handler is registered. No listeners means an
|
|
290
|
+
immediate resolve. Both listener sets are snapshotted up front, so subscriptions
|
|
291
|
+
made mid-emit apply only to later emissions.
|
|
292
|
+
|
|
293
|
+
#### `onAny(listener)`
|
|
294
|
+
|
|
295
|
+
`onAny(listener: AnyListener): () => void`
|
|
296
|
+
|
|
297
|
+
Subscribes to every event. The listener receives `(event, payload)`. Returns an
|
|
298
|
+
unsubscribe function.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const off = events().onAny((event, payload) => log(event, payload));
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Notes:** any-listeners run *before* the event's own listeners, so a logger sees
|
|
305
|
+
the event even if a listener later throws. They're subject to the same error
|
|
306
|
+
handling as ordinary listeners.
|
|
307
|
+
|
|
308
|
+
#### `onError(handler)`
|
|
309
|
+
|
|
310
|
+
`onError(handler: ErrorHandler): void`
|
|
311
|
+
|
|
312
|
+
Handles listener failures instead of letting `emit` reject. The handler receives
|
|
313
|
+
`(event, error, payload)`.
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
events().onError((event, error) => logger().error("listener failed", { event, error }));
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Notes:** only one handler is active — registering again replaces it. Without
|
|
320
|
+
one, failures surface by rejecting `emit`; they are never silently dropped.
|
|
321
|
+
|
|
322
|
+
#### `fake(only?)` / `restore()`
|
|
323
|
+
|
|
324
|
+
`fake(only?: EventName | EventName[]): EventBuffer` — record emissions instead of
|
|
325
|
+
running listeners, and return a buffer to assert against. `restore()` undoes it.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const buffer = events().fake();
|
|
329
|
+
await register(user);
|
|
330
|
+
buffer.assertEmitted("user.registered");
|
|
331
|
+
events().restore();
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Notes:** with no argument every event is faked; pass names to fake only those,
|
|
335
|
+
leaving the rest to dispatch for real. Each `fake()` returns a **fresh** buffer.
|
|
336
|
+
|
|
337
|
+
#### `clearAll()`
|
|
338
|
+
|
|
339
|
+
`clearAll(): void`
|
|
340
|
+
|
|
341
|
+
Drops every listener, every `onAny` listener, and the error handler. `clear()` only
|
|
342
|
+
drops ordinary listeners.
|
|
343
|
+
|
|
344
|
+
#### `listenerCount(event)`
|
|
345
|
+
|
|
346
|
+
`listenerCount(event: string): number`
|
|
347
|
+
|
|
348
|
+
Returns how many listeners are currently subscribed to `event`.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
if (events().listenerCount("tick") === 0) startClock();
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Notes:** returns `0` for an unknown event.
|
|
355
|
+
|
|
356
|
+
#### `clear(event?)`
|
|
357
|
+
|
|
358
|
+
`clear(event?: string): void`
|
|
359
|
+
|
|
360
|
+
Removes all listeners for `event`, or — with no argument — every listener for
|
|
361
|
+
every event.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
events().clear("tick"); // drop one event's listeners
|
|
365
|
+
events().clear(); // wipe everything (handy between tests)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Notes:** unlike `off`, this deletes the event's key outright, so it also cleans
|
|
369
|
+
up the empty-`Set` residue `off` can leave behind.
|
|
370
|
+
|
|
371
|
+
### Global helpers
|
|
372
|
+
|
|
373
|
+
Convenience functions that resolve the active application's `Events` singleton —
|
|
374
|
+
no need to thread the container around.
|
|
375
|
+
|
|
376
|
+
#### `events()`
|
|
377
|
+
|
|
378
|
+
`events(): Events`
|
|
379
|
+
|
|
380
|
+
Returns the application's `Events` instance.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { events } from "@shaferllc/keel/core";
|
|
384
|
+
events().once("boot", () => {});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Notes:** resolves `Events` from the active application container, so every call
|
|
388
|
+
returns the same singleton. Throws `No Keel application has been bootstrapped…`
|
|
389
|
+
if no `Application` has been created yet.
|
|
390
|
+
|
|
391
|
+
#### `emit(event, payload?)`
|
|
392
|
+
|
|
393
|
+
`emit<T = unknown>(event: string, payload?: T): Promise<void>`
|
|
394
|
+
|
|
395
|
+
Shorthand for `events().emit(event, payload)` — fire an event from anywhere.
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
import { emit } from "@shaferllc/keel/core";
|
|
399
|
+
await emit("user.registered", user);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Notes:** same awaiting/error semantics as `Events.emit`. Requires a bootstrapped
|
|
403
|
+
application.
|
|
404
|
+
|
|
405
|
+
#### `listen(event, listener)`
|
|
406
|
+
|
|
407
|
+
`listen<T = unknown>(event: string, listener: Listener<T>): () => void`
|
|
408
|
+
|
|
409
|
+
Shorthand for `events().on(event, listener)` — subscribe from anywhere; returns an
|
|
410
|
+
unsubscribe function.
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
import { listen } from "@shaferllc/keel/core";
|
|
414
|
+
const off = listen<{ id: number }>("user.deleted", (u) => audit(u.id));
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Notes:** wraps `on`, not `once`, so the listener stays subscribed until you call
|
|
418
|
+
the returned function. Requires a bootstrapped application.
|
|
419
|
+
|
|
420
|
+
### Interfaces & types
|
|
421
|
+
|
|
422
|
+
#### `Listener`
|
|
423
|
+
|
|
424
|
+
`type Listener<T = unknown> = (payload: T) => void | Promise<void>`
|
|
425
|
+
|
|
426
|
+
The shape of an event handler: a function taking the payload, optionally async.
|
|
427
|
+
Use it to type a handler you store or pass around before subscribing.
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
import { type Listener } from "@shaferllc/keel/core";
|
|
431
|
+
|
|
432
|
+
const onPaid: Listener<{ id: number }> = async (order) => {
|
|
433
|
+
await fulfill(order.id);
|
|
434
|
+
};
|
|
435
|
+
listen("order.paid", onPaid);
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Notes:** async listeners are fully awaited by `emit`. A listener that returns a
|
|
439
|
+
rejected promise is treated exactly like a synchronous throw — the other listeners
|
|
440
|
+
still run, and the failure is reported afterwards.
|
|
441
|
+
|
|
442
|
+
#### `EventsList`
|
|
443
|
+
|
|
444
|
+
`interface EventsList {}`
|
|
445
|
+
|
|
446
|
+
The registry of declared events, keyed by name. Empty by default; augment it from
|
|
447
|
+
your app to type an event's payload (see [The event registry](#the-event-registry)).
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
declare module "@shaferllc/keel/core" {
|
|
451
|
+
interface EventsList {
|
|
452
|
+
"order.paid": { id: number; total: number };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### `EventBuffer`
|
|
458
|
+
|
|
459
|
+
What `fake()` returns. Records every intercepted emission and asserts over them.
|
|
460
|
+
|
|
461
|
+
| Method | Signature |
|
|
462
|
+
|--------|-----------|
|
|
463
|
+
| `assertEmitted` | `(event, predicate?) => void` |
|
|
464
|
+
| `assertNotEmitted` | `(event) => void` |
|
|
465
|
+
| `assertEmittedCount` | `(event, count) => void` |
|
|
466
|
+
| `assertNoneEmitted` | `() => void` |
|
|
467
|
+
| `all` | `() => RecordedEvent[]` |
|
|
468
|
+
| `payloadsFor` | `(event) => PayloadOf<E>[]` |
|
|
469
|
+
|
|
470
|
+
Failed assertions throw with what actually fired.
|
|
471
|
+
|
|
472
|
+
#### `AnyListener`
|
|
473
|
+
|
|
474
|
+
`type AnyListener = (event: string, payload: unknown) => void | Promise<void>`
|
|
475
|
+
|
|
476
|
+
The shape of an `onAny` handler.
|
|
477
|
+
|
|
478
|
+
#### `ErrorHandler`
|
|
479
|
+
|
|
480
|
+
`type ErrorHandler = (event: string, error: unknown, payload: unknown) => void | Promise<void>`
|
|
481
|
+
|
|
482
|
+
The shape of an `onError` handler.
|
|
483
|
+
|
|
484
|
+
#### `RecordedEvent`
|
|
485
|
+
|
|
486
|
+
`interface RecordedEvent { event: string; payload: unknown }`
|
|
487
|
+
|
|
488
|
+
One emission captured by a fake.
|
|
489
|
+
|
|
490
|
+
#### `EventName` / `PayloadOf`
|
|
491
|
+
|
|
492
|
+
`type EventName = keyof EventsList | (string & {})` — a declared event name, or any
|
|
493
|
+
other string.
|
|
494
|
+
|
|
495
|
+
`type PayloadOf<E>` — the declared payload for an event, or `unknown` if it isn't in
|
|
496
|
+
`EventsList`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Type-check harness for the "application object" section of docs/architecture.md.
|
|
2
|
+
// Compile-only — never executed.
|
|
3
|
+
import { Application, type Configurator } from "@shaferllc/keel/core";
|
|
4
|
+
|
|
5
|
+
declare function sendWelcome(user: { id: number }): void;
|
|
6
|
+
declare function installBilling(app: Application): void;
|
|
7
|
+
|
|
8
|
+
export function configureApp(app: Application): Application {
|
|
9
|
+
const cfg: Configurator = (a) => a.set("mail.from", "hi@keel.dev");
|
|
10
|
+
return app.configure(cfg).configure(installBilling);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function settings(app: Application) {
|
|
14
|
+
app.set("db.url", "sqlite://x");
|
|
15
|
+
const url: unknown = app.get("db.url");
|
|
16
|
+
const name: string = app.get<string>("app.name", "Keel"); // typed fallback
|
|
17
|
+
return { url, name };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function events(app: Application) {
|
|
21
|
+
const off = app.on<{ id: number }>("user.registered", (user) => sendWelcome(user));
|
|
22
|
+
const offOnce = app.once("tick", () => {});
|
|
23
|
+
await app.emit("user.registered", { id: 1 });
|
|
24
|
+
off();
|
|
25
|
+
offOnce();
|
|
26
|
+
app.off("user.registered", () => {});
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Type-check harness for docs/authentication.md. Every type-checkable snippet in
|
|
2
|
+
// the reference is exercised here against the real exports, so a renamed method
|
|
3
|
+
// or wrong argument type fails `npm run typecheck:docs`. Compile-only — never
|
|
4
|
+
// executed.
|
|
5
|
+
import type { MiddlewareHandler } from "hono";
|
|
6
|
+
import {
|
|
7
|
+
Auth,
|
|
8
|
+
auth,
|
|
9
|
+
authGuard,
|
|
10
|
+
setUserProvider,
|
|
11
|
+
type UserProvider,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
// Externals the snippets reference but the auth module doesn't own.
|
|
15
|
+
declare const db: {
|
|
16
|
+
users: { find(id: number | string): Promise<{ id: number; email: string } | null> };
|
|
17
|
+
};
|
|
18
|
+
declare const userId: number;
|
|
19
|
+
|
|
20
|
+
export function provider() {
|
|
21
|
+
setUserProvider((id) => db.users.find(id));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function readingUser() {
|
|
25
|
+
auth().check();
|
|
26
|
+
auth().guest();
|
|
27
|
+
const uid: string | null = auth().id();
|
|
28
|
+
const anyUser = await auth().user();
|
|
29
|
+
|
|
30
|
+
type User = { id: number; email: string };
|
|
31
|
+
const user = await auth().user<User>(); // User | null
|
|
32
|
+
return { uid, anyUser, user };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loginLogout() {
|
|
36
|
+
auth().login(userId); // number
|
|
37
|
+
auth().login("42"); // or string
|
|
38
|
+
auth().logout();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function guards() {
|
|
42
|
+
const withRedirect: MiddlewareHandler = authGuard({ redirectTo: "/login" });
|
|
43
|
+
const api: MiddlewareHandler = authGuard(); // 401 JSON, no redirect
|
|
44
|
+
return { withRedirect, api };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function directAuth() {
|
|
48
|
+
if (new Auth().check()) {
|
|
49
|
+
return new Auth().id();
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Interface / type seam: implement UserProvider, then register it.
|
|
55
|
+
const userProvider: UserProvider = async (id) => {
|
|
56
|
+
// `id` is always a string; coerce if your keys are numeric.
|
|
57
|
+
return db.users.find(Number(id));
|
|
58
|
+
};
|
|
59
|
+
setUserProvider(userProvider);
|
|
60
|
+
|
|
61
|
+
export { userProvider };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Type-check harness for docs/authorization.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
define,
|
|
4
|
+
policy,
|
|
5
|
+
gateBefore,
|
|
6
|
+
setUserResolver,
|
|
7
|
+
clearAuthorization,
|
|
8
|
+
can,
|
|
9
|
+
cannot,
|
|
10
|
+
canFor,
|
|
11
|
+
authorize,
|
|
12
|
+
authorizeFor,
|
|
13
|
+
param,
|
|
14
|
+
type GateCallback,
|
|
15
|
+
type BeforeCallback,
|
|
16
|
+
} from "@shaferllc/keel/core";
|
|
17
|
+
|
|
18
|
+
type User = { id: number; role?: string; admin?: boolean };
|
|
19
|
+
class Post {
|
|
20
|
+
constructor(
|
|
21
|
+
public authorId: number,
|
|
22
|
+
public published = false,
|
|
23
|
+
) {}
|
|
24
|
+
}
|
|
25
|
+
declare const post: Post;
|
|
26
|
+
declare const otherUser: User;
|
|
27
|
+
|
|
28
|
+
export function gates() {
|
|
29
|
+
define("update-post", (user, p) => (p as Post).authorId === (user as User).id);
|
|
30
|
+
define("access-admin", (user) => (user as User).role === "admin");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function checks() {
|
|
34
|
+
if (await can("update-post", post)) {
|
|
35
|
+
// …
|
|
36
|
+
}
|
|
37
|
+
const no: boolean = await cannot("update-post", post);
|
|
38
|
+
await authorize("update-post", post);
|
|
39
|
+
return no;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function policies() {
|
|
43
|
+
class PostPolicy {
|
|
44
|
+
view(user: User, p: Post) {
|
|
45
|
+
return p.published || p.authorId === user.id;
|
|
46
|
+
}
|
|
47
|
+
update(user: User, p: Post) {
|
|
48
|
+
return p.authorId === user.id;
|
|
49
|
+
}
|
|
50
|
+
delete(user: User, p: Post) {
|
|
51
|
+
return Boolean(user.admin) || p.authorId === user.id;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
policy(Post, PostPolicy);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function beforeHook() {
|
|
58
|
+
gateBefore((user) => ((user as User).role === "superadmin" ? true : undefined));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function forUser() {
|
|
62
|
+
await canFor(otherUser, "update-post", post);
|
|
63
|
+
await authorizeFor(otherUser, "update-post", post);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolver() {
|
|
67
|
+
setUserResolver(() => otherUser);
|
|
68
|
+
clearAuthorization();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function inController() {
|
|
72
|
+
void param("id");
|
|
73
|
+
await authorize("update", post);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The type seams
|
|
77
|
+
const gate: GateCallback = (user, ...args) => Boolean(user) && args.length >= 0;
|
|
78
|
+
const before: BeforeCallback = (user) => (user ? undefined : false);
|
|
79
|
+
export { gate, before };
|