@shaferllc/keel 0.66.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +2 -2
  6. package/dist/core/cache.d.ts +82 -5
  7. package/dist/core/cache.js +181 -23
  8. package/dist/core/cli/stubs.d.ts +12 -0
  9. package/dist/core/cli/stubs.js +120 -0
  10. package/dist/core/events.d.ts +129 -5
  11. package/dist/core/events.js +165 -7
  12. package/dist/core/health.d.ts +141 -0
  13. package/dist/core/health.js +226 -0
  14. package/dist/core/helpers.d.ts +9 -3
  15. package/dist/core/helpers.js +11 -3
  16. package/dist/core/index.d.ts +15 -10
  17. package/dist/core/index.js +7 -4
  18. package/dist/core/lock.d.ts +139 -0
  19. package/dist/core/lock.js +215 -0
  20. package/dist/core/logger.d.ts +82 -4
  21. package/dist/core/logger.js +141 -23
  22. package/dist/core/mail.d.ts +128 -7
  23. package/dist/core/mail.js +264 -16
  24. package/dist/core/queue.d.ts +134 -9
  25. package/dist/core/queue.js +304 -14
  26. package/dist/core/storage.d.ts +159 -6
  27. package/dist/core/storage.js +287 -7
  28. package/dist/mcp/server.d.ts +19 -0
  29. package/dist/mcp/server.js +355 -0
  30. package/docs/ai-manifest.json +2472 -0
  31. package/docs/ai.md +128 -0
  32. package/docs/architecture.md +331 -0
  33. package/docs/authentication.md +453 -0
  34. package/docs/authorization.md +167 -0
  35. package/docs/broadcasting.md +137 -0
  36. package/docs/broker.md +500 -0
  37. package/docs/cache.md +558 -0
  38. package/docs/configuration.md +311 -0
  39. package/docs/console.md +356 -0
  40. package/docs/container.md +467 -0
  41. package/docs/controllers.md +265 -0
  42. package/docs/cors.md +51 -0
  43. package/docs/database.md +530 -0
  44. package/docs/debugging.md +129 -0
  45. package/docs/decorators.md +127 -0
  46. package/docs/errors.md +395 -0
  47. package/docs/events.md +496 -0
  48. package/docs/examples/architecture-app.ts +27 -0
  49. package/docs/examples/authentication.ts +61 -0
  50. package/docs/examples/authorization.ts +79 -0
  51. package/docs/examples/broadcasting.ts +60 -0
  52. package/docs/examples/broker-cache-validate.ts +34 -0
  53. package/docs/examples/broker-fault-tolerance.ts +29 -0
  54. package/docs/examples/broker-middleware.ts +27 -0
  55. package/docs/examples/broker.ts +203 -0
  56. package/docs/examples/cache.ts +222 -0
  57. package/docs/examples/configuration.ts +81 -0
  58. package/docs/examples/container.ts +134 -0
  59. package/docs/examples/controllers.ts +86 -0
  60. package/docs/examples/database.ts +118 -0
  61. package/docs/examples/debugging.ts +41 -0
  62. package/docs/examples/decorators.ts +40 -0
  63. package/docs/examples/errors.ts +121 -0
  64. package/docs/examples/events.ts +204 -0
  65. package/docs/examples/factories.ts +84 -0
  66. package/docs/examples/hashing.ts +71 -0
  67. package/docs/examples/health.ts +94 -0
  68. package/docs/examples/helpers.ts +171 -0
  69. package/docs/examples/hooks.ts +54 -0
  70. package/docs/examples/inertia.ts +81 -0
  71. package/docs/examples/locks.ts +120 -0
  72. package/docs/examples/logger.ts +92 -0
  73. package/docs/examples/mail.ts +160 -0
  74. package/docs/examples/middleware.ts +119 -0
  75. package/docs/examples/migrations.ts +126 -0
  76. package/docs/examples/models.ts +239 -0
  77. package/docs/examples/notification.ts +124 -0
  78. package/docs/examples/providers.ts +123 -0
  79. package/docs/examples/queues.ts +254 -0
  80. package/docs/examples/rate-limiting.ts +42 -0
  81. package/docs/examples/redis.ts +99 -0
  82. package/docs/examples/request-response.ts +197 -0
  83. package/docs/examples/routing.ts +186 -0
  84. package/docs/examples/scheduling.ts +62 -0
  85. package/docs/examples/sessions.ts +102 -0
  86. package/docs/examples/static-files.ts +63 -0
  87. package/docs/examples/storage.ts +132 -0
  88. package/docs/examples/templates.ts +58 -0
  89. package/docs/examples/testing.ts +66 -0
  90. package/docs/examples/transformer.ts +141 -0
  91. package/docs/examples/transformers.ts +49 -0
  92. package/docs/examples/url-builder.ts +86 -0
  93. package/docs/examples/validation.ts +102 -0
  94. package/docs/examples/views.tsx +62 -0
  95. package/docs/examples/vite.ts +106 -0
  96. package/docs/factories.md +166 -0
  97. package/docs/getting-started.md +290 -0
  98. package/docs/hashing.md +259 -0
  99. package/docs/health.md +225 -0
  100. package/docs/helpers.md +347 -0
  101. package/docs/hono.md +186 -0
  102. package/docs/hooks.md +118 -0
  103. package/docs/inertia.md +241 -0
  104. package/docs/locks.md +323 -0
  105. package/docs/logger.md +290 -0
  106. package/docs/mail.md +678 -0
  107. package/docs/middleware.md +425 -0
  108. package/docs/migrations.md +476 -0
  109. package/docs/models.md +810 -0
  110. package/docs/notifications.md +474 -0
  111. package/docs/providers.md +363 -0
  112. package/docs/queues.md +679 -0
  113. package/docs/rate-limiting.md +155 -0
  114. package/docs/redis.md +178 -0
  115. package/docs/request-response.md +953 -0
  116. package/docs/routing.md +804 -0
  117. package/docs/scheduling.md +110 -0
  118. package/docs/security.md +85 -0
  119. package/docs/sessions.md +354 -0
  120. package/docs/social-auth.md +174 -0
  121. package/docs/static-files.md +211 -0
  122. package/docs/storage.md +450 -0
  123. package/docs/templates.md +315 -0
  124. package/docs/testing.md +125 -0
  125. package/docs/transformers.md +381 -0
  126. package/docs/url-builder.md +295 -0
  127. package/docs/validation.md +288 -0
  128. package/docs/views.md +267 -0
  129. package/docs/vite.md +434 -0
  130. package/llms-full.txt +17694 -0
  131. package/llms.txt +116 -0
  132. package/package.json +26 -7
@@ -0,0 +1,120 @@
1
+ /** Code-generation templates for `keel make:*`. */
2
+ export function controllerStub(name) {
3
+ return `import type { Ctx } from "@shaferllc/keel/core";
4
+
5
+ export class ${name} {
6
+ index(c: Ctx) {
7
+ return c.json({ controller: "${name}", action: "index" });
8
+ }
9
+ }
10
+ `;
11
+ }
12
+ export function resourceControllerStub(name) {
13
+ const actions = ["index", "create", "store", "show", "edit", "update", "destroy"];
14
+ const body = actions
15
+ .map((a) => ` ${a}(c: Ctx) {\n return c.json({ action: "${a}" });\n }`)
16
+ .join("\n\n");
17
+ return `import type { Ctx } from "@shaferllc/keel/core";
18
+
19
+ export class ${name} {
20
+ ${body}
21
+ }
22
+ `;
23
+ }
24
+ export function providerStub(name) {
25
+ return `import { ServiceProvider } from "@shaferllc/keel/core";
26
+
27
+ export class ${name} extends ServiceProvider {
28
+ register(): void {
29
+ // Bind services into the container here.
30
+ }
31
+
32
+ boot(): void {
33
+ // Resolve and wire things up here.
34
+ }
35
+ }
36
+ `;
37
+ }
38
+ export function middlewareStub(name) {
39
+ const fn = name.charAt(0).toLowerCase() + name.slice(1);
40
+ return `import type { MiddlewareHandler } from "hono";
41
+
42
+ export const ${fn}: MiddlewareHandler = async (c, next) => {
43
+ // ...before
44
+ await next();
45
+ // ...after
46
+ };
47
+ `;
48
+ }
49
+ /** `name` is the model class (e.g. "User") the factory builds. */
50
+ export function factoryStub(model) {
51
+ return `import { factory } from "@shaferllc/keel/core";
52
+ import { ${model} } from "../../app/Models/${model}.js";
53
+
54
+ export const ${model.toLowerCase()}Factory = factory(${model}, (f) => ({
55
+ // Describe one ${model}'s attributes; \`f\` is a Faker.
56
+ name: f.name(),
57
+ email: f.email(),
58
+ }));
59
+ `;
60
+ }
61
+ export function seederStub(name) {
62
+ return `import { Seeder } from "@shaferllc/keel/core";
63
+
64
+ export class ${name} extends Seeder {
65
+ async run(): Promise<void> {
66
+ // Populate the database, e.g.:
67
+ // await userFactory.count(10).create();
68
+ }
69
+ }
70
+ `;
71
+ }
72
+ export function jobStub(name) {
73
+ return `import { Job } from "@shaferllc/keel/core";
74
+
75
+ export class ${name} extends Job {
76
+ constructor(/* pass the data this job needs */) {
77
+ super();
78
+ }
79
+
80
+ async handle(): Promise<void> {
81
+ // Do the background work here.
82
+ }
83
+ }
84
+ `;
85
+ }
86
+ export function notificationStub(name) {
87
+ return `import { Notification, type Notifiable, type MailContent } from "@shaferllc/keel/core";
88
+
89
+ export class ${name} extends Notification {
90
+ via(_notifiable: Notifiable): string[] {
91
+ return ["mail"];
92
+ }
93
+
94
+ toMail(_notifiable: Notifiable): MailContent {
95
+ return {
96
+ subject: "${name}",
97
+ text: "Notification body.",
98
+ };
99
+ }
100
+ }
101
+ `;
102
+ }
103
+ /** `name` is the class (e.g. "UserTransformer"); `model` is the value it maps. */
104
+ export function transformerStub(name, model) {
105
+ return `import { Transformer, type Attributes } from "@shaferllc/keel/core";
106
+ // import { ${model} } from "../Models/${model}.js";
107
+
108
+ export class ${name} extends Transformer</* ${model} */ any> {
109
+ transform(item: /* ${model} */ any): Attributes {
110
+ // Map the value to the exact shape your API exposes.
111
+ return {
112
+ id: item.id,
113
+ // name: item.name,
114
+ // email: this.when(canSeeEmail, item.email),
115
+ // posts: this.whenLoaded(item, "posts", new PostTransformer()),
116
+ };
117
+ }
118
+ }
119
+ `;
120
+ }
@@ -5,17 +5,141 @@
5
5
  *
6
6
  * listen("user.registered", (user) => sendWelcome(user));
7
7
  * await emit("user.registered", user);
8
+ *
9
+ * Declare an event in `EventsList` and the emitter checks both sides of it —
10
+ * the payload you fire and the payload your listener receives:
11
+ *
12
+ * declare module "@shaferllc/keel/core" {
13
+ * interface EventsList {
14
+ * "order.paid": { id: number; total: number };
15
+ * }
16
+ * }
17
+ *
18
+ * One listener's failure never stops another's: `emit` runs them all, then
19
+ * reports what broke — to `onError()` if you registered one, otherwise by
20
+ * rejecting.
21
+ */
22
+ /**
23
+ * The registry of known events, keyed by name. Empty by default — augment it
24
+ * from your app to type an event's payload:
25
+ *
26
+ * declare module "@shaferllc/keel/core" {
27
+ * interface EventsList {
28
+ * "user.registered": User;
29
+ * }
30
+ * }
31
+ */
32
+ export interface EventsList {
33
+ }
34
+ /** A declared event name, or any other string. */
35
+ export type EventName = keyof EventsList | (string & {});
36
+ /**
37
+ * The declared payload for an event, or `unknown` for an undeclared one.
38
+ *
39
+ * The `[E]` tuple wrapper stops the conditional from distributing: `EventName`
40
+ * is itself a union, and a naked `E` would resolve to a *union* of every
41
+ * declared payload rather than the one event's.
42
+ */
43
+ export type PayloadOf<E> = [E] extends [keyof EventsList] ? EventsList[E & keyof EventsList] : unknown;
44
+ /**
45
+ * `emit`'s payload argument. Declared events must be fired with their payload;
46
+ * undeclared (and void) ones may be fired without.
8
47
  */
48
+ export type EmitArgs<P> = [P] extends [void | undefined] ? [payload?: P] : unknown extends P ? [payload?: P] : [payload: P];
49
+ /**
50
+ * The payload type for an event.
51
+ *
52
+ * A **declared** event takes its payload from `EventsList`, and `NoInfer` is
53
+ * load-bearing there: without it TypeScript would infer the payload type from
54
+ * the value you pass, so `emit("order.paid", anythingAtAll)` would type-check by
55
+ * inferring it to whatever you handed over — and the registry would enforce
56
+ * nothing.
57
+ *
58
+ * An **undeclared** event behaves as it always has: the payload type is inferred
59
+ * from the listener, or given explicitly as `listen<Order>("order.paid", …)`,
60
+ * and falls back to `unknown`.
61
+ */
62
+ export type Resolve<T, E> = [E] extends [keyof EventsList] ? NoInfer<EventsList[E & keyof EventsList]> : T;
9
63
  export type Listener<T = unknown> = (payload: T) => void | Promise<void>;
64
+ /** A listener for *every* event, as registered by `onAny`. */
65
+ export type AnyListener = (event: string, payload: unknown) => void | Promise<void>;
66
+ /** Called when a listener throws, instead of `emit` rejecting. */
67
+ export type ErrorHandler = (event: string, error: unknown, payload: unknown) => void | Promise<void>;
68
+ /** One recorded emission, as captured by a fake. */
69
+ export interface RecordedEvent {
70
+ event: string;
71
+ payload: unknown;
72
+ }
10
73
  export declare class Events {
11
74
  private listeners;
75
+ private anyListeners;
76
+ private errorHandler?;
77
+ /** Non-null while faking: the events to intercept (`true` = all of them). */
78
+ private faked?;
79
+ private buffer?;
12
80
  /** Subscribe to an event. Returns an unsubscribe function. */
13
- on<T = unknown>(event: string, listener: Listener<T>): () => void;
81
+ on<T = unknown, E extends EventName = EventName>(event: E, listener: Listener<Resolve<T, E>>): () => void;
14
82
  /** Subscribe for a single emission. */
15
- once<T = unknown>(event: string, listener: Listener<T>): () => void;
16
- off<T = unknown>(event: string, listener: Listener<T>): void;
17
- /** Emit an event, awaiting every listener in order. */
18
- emit<T = unknown>(event: string, payload?: T): Promise<void>;
83
+ once<T = unknown, E extends EventName = EventName>(event: E, listener: Listener<Resolve<T, E>>): () => void;
84
+ off<T = unknown, E extends EventName = EventName>(event: E, listener: Listener<Resolve<T, E>>): void;
85
+ /**
86
+ * Subscribe to *every* event for logging and other cross-cutting concerns.
87
+ * These run before the event's own listeners. Returns an unsubscribe function.
88
+ */
89
+ onAny(listener: AnyListener): () => void;
90
+ /**
91
+ * Handle listener failures instead of letting `emit` reject. Without one, an
92
+ * `emit` whose listeners threw rejects (with an `AggregateError` if more than
93
+ * one did) — errors are never silently swallowed.
94
+ */
95
+ onError(handler: ErrorHandler): void;
96
+ /**
97
+ * Emit an event, awaiting every listener in registration order.
98
+ *
99
+ * A listener that throws does not stop the others — they all run, and the
100
+ * failures are reported afterwards. That's the point of an emitter: one
101
+ * subscriber's bug shouldn't silently cancel an unrelated subscriber's work.
102
+ */
103
+ emit<T = unknown, E extends EventName = EventName>(event: E, ...args: EmitArgs<Resolve<T, E>>): Promise<void>;
19
104
  listenerCount(event: string): number;
20
105
  clear(event?: string): void;
106
+ /** Drop every `onAny` listener and the error handler too. */
107
+ clearAll(): void;
108
+ /**
109
+ * Record emissions instead of running listeners, so a test can assert an event
110
+ * fired without triggering its side effects. Undo with `restore()`.
111
+ *
112
+ * const events = events().fake();
113
+ * await register(user);
114
+ * events.assertEmitted("user.registered");
115
+ *
116
+ * Pass one or more event names to fake only those — everything else dispatches
117
+ * for real.
118
+ */
119
+ fake(only?: EventName | EventName[]): EventBuffer;
120
+ /** Stop faking; listeners run for real again. */
121
+ restore(): void;
122
+ }
123
+ /** What a fake records, plus assertions over it. */
124
+ export declare class EventBuffer {
125
+ private events;
126
+ /** @internal — called by `Events.emit` while faking. */
127
+ record(event: string, payload: unknown): void;
128
+ /** Every emission recorded so far, in order. */
129
+ all(): RecordedEvent[];
130
+ /** The payloads recorded for one event. */
131
+ payloadsFor<E extends EventName>(event: E): PayloadOf<E>[];
132
+ /**
133
+ * Assert the event fired — optionally only counting emissions whose payload
134
+ * satisfies `predicate`.
135
+ *
136
+ * buffer.assertEmitted("order.paid", (o) => o.total === 4200);
137
+ */
138
+ assertEmitted<E extends EventName>(event: E, predicate?: (payload: PayloadOf<E>) => boolean): void;
139
+ assertNotEmitted<E extends EventName>(event: E): void;
140
+ assertEmittedCount<E extends EventName>(event: E, expected: number): void;
141
+ /** Assert nothing at all was emitted. */
142
+ assertNoneEmitted(): void;
143
+ private listenerCountFor;
144
+ private summary;
21
145
  }
@@ -5,9 +5,27 @@
5
5
  *
6
6
  * listen("user.registered", (user) => sendWelcome(user));
7
7
  * await emit("user.registered", user);
8
+ *
9
+ * Declare an event in `EventsList` and the emitter checks both sides of it —
10
+ * the payload you fire and the payload your listener receives:
11
+ *
12
+ * declare module "@shaferllc/keel/core" {
13
+ * interface EventsList {
14
+ * "order.paid": { id: number; total: number };
15
+ * }
16
+ * }
17
+ *
18
+ * One listener's failure never stops another's: `emit` runs them all, then
19
+ * reports what broke — to `onError()` if you registered one, otherwise by
20
+ * rejecting.
8
21
  */
9
22
  export class Events {
10
23
  listeners = new Map();
24
+ anyListeners = new Set();
25
+ errorHandler;
26
+ /** Non-null while faking: the events to intercept (`true` = all of them). */
27
+ faked;
28
+ buffer;
11
29
  /** Subscribe to an event. Returns an unsubscribe function. */
12
30
  on(event, listener) {
13
31
  const set = this.listeners.get(event) ?? new Set();
@@ -18,7 +36,9 @@ export class Events {
18
36
  /** Subscribe for a single emission. */
19
37
  once(event, listener) {
20
38
  const wrapper = async (payload) => {
21
- this.off(event, wrapper);
39
+ // Unsubscribe before awaiting, so a listener that re-emits the same event
40
+ // doesn't re-trigger this one.
41
+ this.listeners.get(event)?.delete(wrapper);
22
42
  await listener(payload);
23
43
  };
24
44
  return this.on(event, wrapper);
@@ -26,13 +46,66 @@ export class Events {
26
46
  off(event, listener) {
27
47
  this.listeners.get(event)?.delete(listener);
28
48
  }
29
- /** Emit an event, awaiting every listener in order. */
30
- async emit(event, payload) {
31
- const set = this.listeners.get(event);
32
- if (!set)
49
+ /**
50
+ * Subscribe to *every* event for logging and other cross-cutting concerns.
51
+ * These run before the event's own listeners. Returns an unsubscribe function.
52
+ */
53
+ onAny(listener) {
54
+ this.anyListeners.add(listener);
55
+ return () => {
56
+ this.anyListeners.delete(listener);
57
+ };
58
+ }
59
+ /**
60
+ * Handle listener failures instead of letting `emit` reject. Without one, an
61
+ * `emit` whose listeners threw rejects (with an `AggregateError` if more than
62
+ * one did) — errors are never silently swallowed.
63
+ */
64
+ onError(handler) {
65
+ this.errorHandler = handler;
66
+ }
67
+ /**
68
+ * Emit an event, awaiting every listener in registration order.
69
+ *
70
+ * A listener that throws does not stop the others — they all run, and the
71
+ * failures are reported afterwards. That's the point of an emitter: one
72
+ * subscriber's bug shouldn't silently cancel an unrelated subscriber's work.
73
+ */
74
+ async emit(event, ...args) {
75
+ const payload = args[0];
76
+ if (this.faked && (this.faked === true || this.faked.has(event))) {
77
+ this.buffer?.record(event, payload);
33
78
  return;
34
- for (const listener of [...set])
35
- await listener(payload);
79
+ }
80
+ const errors = [];
81
+ // Snapshot both sets: a listener that (un)subscribes mid-emit affects only
82
+ // the next emission, not this one.
83
+ for (const listener of [...this.anyListeners]) {
84
+ try {
85
+ await listener(event, payload);
86
+ }
87
+ catch (error) {
88
+ errors.push(error);
89
+ }
90
+ }
91
+ for (const listener of [...(this.listeners.get(event) ?? [])]) {
92
+ try {
93
+ await listener(payload);
94
+ }
95
+ catch (error) {
96
+ errors.push(error);
97
+ }
98
+ }
99
+ if (!errors.length)
100
+ return;
101
+ if (this.errorHandler) {
102
+ for (const error of errors)
103
+ await this.errorHandler(event, error, payload);
104
+ return;
105
+ }
106
+ if (errors.length === 1)
107
+ throw errors[0];
108
+ throw new AggregateError(errors, `${errors.length} listeners for "${event}" failed.`);
36
109
  }
37
110
  listenerCount(event) {
38
111
  return this.listeners.get(event)?.size ?? 0;
@@ -43,4 +116,89 @@ export class Events {
43
116
  else
44
117
  this.listeners.clear();
45
118
  }
119
+ /** Drop every `onAny` listener and the error handler too. */
120
+ clearAll() {
121
+ this.listeners.clear();
122
+ this.anyListeners.clear();
123
+ this.errorHandler = undefined;
124
+ }
125
+ /**
126
+ * Record emissions instead of running listeners, so a test can assert an event
127
+ * fired without triggering its side effects. Undo with `restore()`.
128
+ *
129
+ * const events = events().fake();
130
+ * await register(user);
131
+ * events.assertEmitted("user.registered");
132
+ *
133
+ * Pass one or more event names to fake only those — everything else dispatches
134
+ * for real.
135
+ */
136
+ fake(only) {
137
+ this.faked = only === undefined ? true : new Set(Array.isArray(only) ? only : [only]);
138
+ this.buffer = new EventBuffer();
139
+ return this.buffer;
140
+ }
141
+ /** Stop faking; listeners run for real again. */
142
+ restore() {
143
+ this.faked = undefined;
144
+ this.buffer = undefined;
145
+ }
146
+ }
147
+ /* ------------------------------ event buffer ------------------------------ */
148
+ /** What a fake records, plus assertions over it. */
149
+ export class EventBuffer {
150
+ events = [];
151
+ /** @internal — called by `Events.emit` while faking. */
152
+ record(event, payload) {
153
+ this.events.push({ event, payload });
154
+ }
155
+ /** Every emission recorded so far, in order. */
156
+ all() {
157
+ return [...this.events];
158
+ }
159
+ /** The payloads recorded for one event. */
160
+ payloadsFor(event) {
161
+ return this.events.filter((e) => e.event === event).map((e) => e.payload);
162
+ }
163
+ /**
164
+ * Assert the event fired — optionally only counting emissions whose payload
165
+ * satisfies `predicate`.
166
+ *
167
+ * buffer.assertEmitted("order.paid", (o) => o.total === 4200);
168
+ */
169
+ assertEmitted(event, predicate) {
170
+ const matches = this.payloadsFor(event).filter((p) => predicate?.(p) ?? true);
171
+ if (matches.length)
172
+ return;
173
+ const fired = this.listenerCountFor(event);
174
+ throw new Error(predicate && fired
175
+ ? `Expected "${event}" to be emitted with a matching payload. It fired ${fired} time(s), but none matched.`
176
+ : `Expected "${event}" to be emitted, but it was not. ${this.summary()}`);
177
+ }
178
+ assertNotEmitted(event) {
179
+ const fired = this.listenerCountFor(event);
180
+ if (fired)
181
+ throw new Error(`Expected "${event}" not to be emitted, but it fired ${fired} time(s).`);
182
+ }
183
+ assertEmittedCount(event, expected) {
184
+ const fired = this.listenerCountFor(event);
185
+ if (fired !== expected) {
186
+ throw new Error(`Expected "${event}" to be emitted ${expected} time(s), but it fired ${fired}.`);
187
+ }
188
+ }
189
+ /** Assert nothing at all was emitted. */
190
+ assertNoneEmitted() {
191
+ if (this.events.length) {
192
+ throw new Error(`Expected no events, but ${this.events.length} fired. ${this.summary()}`);
193
+ }
194
+ }
195
+ listenerCountFor(event) {
196
+ return this.events.filter((e) => e.event === event).length;
197
+ }
198
+ summary() {
199
+ if (!this.events.length)
200
+ return "No events were emitted.";
201
+ const names = [...new Set(this.events.map((e) => e.event))].join(", ");
202
+ return `Emitted: ${names}.`;
203
+ }
46
204
  }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Health checks — the two endpoints an orchestrator (Kubernetes, Fly, Railway,
3
+ * a load balancer) asks about before it sends you traffic:
4
+ *
5
+ * /health/live is the process up? — answers instantly, checks nothing
6
+ * /health/ready can it serve requests? — runs every registered check
7
+ *
8
+ * A liveness probe that touched the database would restart a healthy app during a
9
+ * database blip, so it deliberately checks nothing. Readiness is where the checks
10
+ * live: it reports 200 while everything it depends on is reachable, and 503 when
11
+ * something isn't — which pulls the instance out of the pool without killing it.
12
+ *
13
+ * health().register([new DatabaseCheck(), new RedisCheck()]);
14
+ * this.use(healthCheck()); // serves both endpoints
15
+ *
16
+ * Keel ships the checks that mean something wherever it runs — a database, Redis,
17
+ * the cache. Deliberately absent are AdonisJS's disk-space and heap/RSS checks:
18
+ * they measure a Node process, and on Workers there isn't one.
19
+ */
20
+ import type { MiddlewareHandler } from "hono";
21
+ export type HealthStatus = "ok" | "warning" | "error";
22
+ /** The outcome of a single check. Build one with `Result.ok/warning/failed`. */
23
+ export declare class Result {
24
+ readonly status: HealthStatus;
25
+ readonly message: string;
26
+ readonly error?: unknown | undefined;
27
+ meta?: Record<string, unknown> | undefined;
28
+ private constructor();
29
+ /** Healthy. */
30
+ static ok(message: string): Result;
31
+ /**
32
+ * Working, but degraded — worth paging someone, not worth pulling the instance
33
+ * out of the pool. A warning keeps readiness at 200.
34
+ */
35
+ static warning(message: string): Result;
36
+ /** Broken. Any failed check takes readiness to 503. */
37
+ static failed(message: string, error?: unknown): Result;
38
+ /** Attach arbitrary detail — connection counts, latencies, versions. */
39
+ withMeta(meta: Record<string, unknown>): this;
40
+ }
41
+ /** One thing worth knowing about before serving traffic. */
42
+ export declare abstract class BaseCheck {
43
+ abstract readonly name: string;
44
+ private ttl;
45
+ private cached?;
46
+ abstract run(): Promise<Result>;
47
+ /**
48
+ * Reuse this check's last result for `seconds`, so a probe every few seconds
49
+ * doesn't hammer the thing it's checking. The report marks a reused result
50
+ * with `isCached: true`.
51
+ */
52
+ cacheFor(seconds: number): this;
53
+ /**
54
+ * Run the check, honouring `cacheFor`, and never throw: a check that blows up
55
+ * is itself a failure, and one broken check must not take down the report.
56
+ *
57
+ * @internal — called by `HealthChecks.run()`.
58
+ */
59
+ execute(): Promise<{
60
+ result: Result;
61
+ isCached: boolean;
62
+ }>;
63
+ }
64
+ /**
65
+ * A check from a plain function — the escape hatch, for anything Keel doesn't
66
+ * ship a check for.
67
+ *
68
+ * health().register([
69
+ * check("stripe", async () => {
70
+ * const res = await fetch("https://api.stripe.com/healthcheck");
71
+ * return res.ok ? Result.ok("Stripe is reachable") : Result.failed("Stripe is down");
72
+ * }),
73
+ * ]);
74
+ */
75
+ export declare function check(name: string, run: () => Promise<Result> | Result): BaseCheck;
76
+ /** Is the database reachable? Runs `SELECT 1` on a connection. */
77
+ export declare class DatabaseCheck extends BaseCheck {
78
+ private connectionName?;
79
+ readonly name: string;
80
+ constructor(connectionName?: string | undefined);
81
+ run(): Promise<Result>;
82
+ }
83
+ /** Is Redis reachable? Reads a key — a failed read means a broken connection. */
84
+ export declare class RedisCheck extends BaseCheck {
85
+ readonly name = "redis";
86
+ run(): Promise<Result>;
87
+ }
88
+ /** Does the cache round-trip? Writes a key, reads it back, deletes it. */
89
+ export declare class CacheCheck extends BaseCheck {
90
+ readonly name = "cache";
91
+ run(): Promise<Result>;
92
+ }
93
+ export interface CheckReport {
94
+ name: string;
95
+ status: HealthStatus;
96
+ message: string;
97
+ /** Whether this result was reused from a `cacheFor()` window. */
98
+ isCached: boolean;
99
+ finishedAt: string;
100
+ meta?: Record<string, unknown>;
101
+ }
102
+ export interface HealthReport {
103
+ /** True unless a check failed. A warning is still healthy. */
104
+ isHealthy: boolean;
105
+ /** The worst status any check reported. */
106
+ status: HealthStatus;
107
+ finishedAt: string;
108
+ checks: CheckReport[];
109
+ }
110
+ export declare class HealthChecks {
111
+ private checks;
112
+ /** Add checks. Call it once, in a provider's `boot()`. */
113
+ register(checks: BaseCheck[]): this;
114
+ /** Every registered check. */
115
+ all(): BaseCheck[];
116
+ clear(): this;
117
+ /** Run every check — concurrently, since they're independent I/O. */
118
+ run(): Promise<HealthReport>;
119
+ }
120
+ /** The application's health-check registry. */
121
+ export declare function health(): HealthChecks;
122
+ export interface HealthCheckOptions {
123
+ /** URL prefix for the two endpoints. Default: `"/health"`. */
124
+ basePath?: string;
125
+ /**
126
+ * Require `Authorization: Bearer <secret>` on the readiness endpoint. The
127
+ * report names your infrastructure, so don't publish it — set this whenever
128
+ * the endpoint is reachable from outside your network.
129
+ */
130
+ secret?: string;
131
+ /** The registry to run. Defaults to the global one from `health()`. */
132
+ checks?: HealthChecks;
133
+ }
134
+ /**
135
+ * Serve `/health/live` and `/health/ready`. Anything else falls through to your
136
+ * routes.
137
+ *
138
+ * this.use(healthCheck());
139
+ * this.use(healthCheck({ secret: env.HEALTH_SECRET }));
140
+ */
141
+ export declare function healthCheck(options?: HealthCheckOptions): MiddlewareHandler;