@nwire/handler 0.8.17 → 0.9.1

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/README.md CHANGED
@@ -1,88 +1,203 @@
1
1
  # @nwire/handler
2
2
 
3
- > Operation primitive — one shape for action / query / resolver, usable from any transport.
4
-
5
- `defineHandler(name, config)` describes a single operation: input
6
- schema, response shape, the function. `execute(handler, input, ctx?)`
7
- runs one. Every Nwire transport (`@nwire/http`, `@nwire/queue`,
8
- `@nwire/mcp`) speaks this exact shape; `@nwire/forge`'s `defineAction`
9
- and `defineQuery` are flavored sugar on top.
3
+ The operation primitive — one typed, callable, hookable value usable from every transport (HTTP, queue, MCP, CLI, direct test).
10
4
 
11
5
  ```bash
12
- pnpm add @nwire/handler zod
6
+ pnpm add @nwire/handler
7
+ ```
8
+
9
+ ## Why
10
+
11
+ Every backend ends up with the same shape: parse input, run a function with some deps, return a typed response or throw a typed error. Different teams wrap it in different abstractions (controllers, route handlers, command handlers, tRPC procedures, NestJS providers). They're all the same thing.
12
+
13
+ `@nwire/handler` is that thing, distilled: a callable, type-generic value. It's a `Hook` from `@nwire/hooks` underneath, so it gets `.use()` middleware, `.on()` listeners, `.tap()` observation, `signal` cancellation, and replay — for free.
14
+
15
+ Three rules:
16
+
17
+ 1. **Ctx is generic.** Whatever you pass to `.run(ctx)` is what the handler body destructures, fully typed.
18
+ 2. **App pins ctx once.** `defineHandlerWith<AppExtras>()` returns a factory you import everywhere; handlers never annotate ctx.
19
+ 3. **No global pollution.** Plugin contributions are imported types, intersected at the app boundary. Nothing is `declare module`-ed.
20
+
21
+ ## Surface
22
+
23
+ ```ts
24
+ // The primitive
25
+ function defineHandler<TInputSchema, TOutput, TExtras extends object = object>(
26
+ name: string,
27
+ config: HandlerConfig<TInputSchema, TOutput, TExtras>,
28
+ ): HandlerDefinition<TInputSchema, TOutput, TExtras>;
29
+
30
+ // The app-boundary factory — pins TExtras once
31
+ function defineHandlerWith<TExtras extends object>(): typeof defineHandler bound to TExtras;
32
+
33
+ // Type alias for declaring a handler's ctx shape inline
34
+ type Ctx<TInput, TExtras extends object = object> =
35
+ { input: TInput; signal: AbortSignal; set: … } & TExtras;
36
+
37
+ // Resources, errors, response factories
38
+ function defineResource(name, { schema, public, examples });
39
+ function defineError({ code, status, summary }); // callable: throw NotFound({ resourceId });
40
+ ok / created / accepted / noContent / notModified / gone
41
+
42
+ // Built-in errors
43
+ Unauthorized / Forbidden / NotFound / Conflict / Gone / BadRequest
44
+
45
+ // Type helpers
46
+ HandlerInput<H> / HandlerOutput<H> / HandlerExtras<H>
13
47
  ```
14
48
 
15
- ## Quick example
49
+ ## Consumer example
50
+
51
+ `app/define.ts` — one file, pinned once:
52
+
53
+ ```ts
54
+ import { defineHandlerWith, type Ctx } from "@nwire/handler";
55
+ import type { AuthCradle } from "@nwire/auth";
56
+ import type { DbCradle } from "@nwire/data-drizzle";
57
+
58
+ // Plugins export type fragments; app composes its cradle:
59
+ export type AppCradle = AuthCradle &
60
+ DbCradle & {
61
+ config: AppConfig;
62
+ logger: Logger;
63
+ };
64
+
65
+ // Extras the wires put onto ctx at request time:
66
+ type AppExtras = {
67
+ logger: Logger;
68
+ pg: PgClient;
69
+ user: { id: string; role: string; balance: number };
70
+ };
71
+
72
+ export const defineAction = defineHandlerWith<AppExtras>();
73
+ export type AppCtx<I = unknown> = Ctx<I, AppExtras>;
74
+ ```
75
+
76
+ `app/orders/buy-wash.action.ts` — a complete handler:
16
77
 
17
78
  ```ts
18
79
  import { z } from "zod";
19
- import {
20
- defineHandler,
21
- defineMiddleware,
22
- defineHook,
23
- pipe,
24
- execute,
25
- NotFound,
26
- ok,
27
- } from "@nwire/handler";
28
-
29
- const authenticate = defineMiddleware("authenticate", async (ctx, next) => {
30
- ctx.user = await verifyToken(ctx.header("authorization"));
80
+ import { defineResource, defineError, NotFound } from "@nwire/handler";
81
+ import { defineAction } from "@/define";
82
+
83
+ const Wash = defineResource("Wash", {
84
+ schema: z.object({ id: z.string(), name: z.string(), price: z.number() }),
85
+ public: ["id", "name", "price"],
86
+ });
87
+
88
+ const InsufficientFunds = defineError({
89
+ code: "INSUFFICIENT_FUNDS",
90
+ status: 402,
91
+ summary: "balance too low for this wash",
92
+ });
93
+
94
+ export const BuyWash = defineAction("buyWash", {
95
+ input: z.object({ washId: z.string() }),
96
+ returns: Wash,
97
+ errors: [NotFound, InsufficientFunds],
98
+ handler: async ({ input, logger, pg, user, signal }) => {
99
+ const row = await pg.query("SELECT * FROM washes WHERE id=$1", [input.washId], { signal });
100
+ if (!row) throw NotFound({ resourceId: input.washId });
101
+ if (user.balance < row.price) throw InsufficientFunds({ have: user.balance, need: row.price });
102
+ logger.log(`${user.id} bought ${row.id}`);
103
+ return row;
104
+ },
105
+ });
106
+
107
+ // Per-handler middleware via the hook substrate
108
+ BuyWash.use(async (ctx, next) => {
109
+ const t = performance.now();
31
110
  await next();
111
+ ctx.logger.log(`buyWash ${(performance.now() - t).toFixed(1)}ms`);
32
112
  });
113
+ ```
33
114
 
34
- const auditLog = defineHook("after", "audit", async (ctx, result) => {
35
- await audit.write({ user: ctx.user.id, result });
115
+ `wires/api.wire.ts` boot, container, request composition:
116
+
117
+ ```ts
118
+ import { createContainer } from "@nwire/container";
119
+ import type { AppCradle } from "@/define";
120
+ import { BuyWash } from "@/orders/buy-wash.action";
121
+
122
+ const root = createContainer<AppCradle>();
123
+ root.register("config", { port: 3000 });
124
+ root.register("logger", () => ({ log: (s) => console.log(s) }));
125
+ root.register("db.pg", () => new PgClient(root.cradle.config.port));
126
+
127
+ const server = createServer(async (req, res) => {
128
+ const user = await authenticate(req);
129
+
130
+ const result = await BuyWash(JSON.parse(await readBody(req))).run({
131
+ ctx: {
132
+ logger: root.cradle.logger,
133
+ pg: root.cradle["db.pg"],
134
+ user,
135
+ },
136
+ signal: req.signal,
137
+ });
138
+ res.writeHead(201).end(JSON.stringify(result));
36
139
  });
140
+ ```
37
141
 
38
- const userPipeline = pipe("user-pipeline", authenticate, auditLog);
142
+ `app/orders/buy-wash.test.ts` unit test, no container:
39
143
 
40
- const getUser = defineHandler("getUser", {
41
- input: z.object({ id: z.string() }),
42
- middleware: [userPipeline],
43
- handler: async ({ input, resolve }) => {
44
- const repo = resolve<UserRepo>("UserRepo");
45
- const user = await repo.findById(input.id);
46
- if (!user) throw NotFound("user", input.id);
47
- return ok(user);
48
- },
144
+ ```ts
145
+ import { expect, test } from "vitest";
146
+ import { BuyWash, InsufficientFunds } from "./buy-wash.action";
147
+
148
+ test("rejects when balance < price", async () => {
149
+ await expect(
150
+ BuyWash({ washId: "w1" }).run({
151
+ ctx: {
152
+ logger: { log: () => {} },
153
+ pg: { query: async () => ({ id: "w1", name: "Quick", price: 100 }) },
154
+ user: { id: "u-1", role: "user", balance: 5 },
155
+ },
156
+ }),
157
+ ).rejects.toMatchObject({
158
+ code: "INSUFFICIENT_FUNDS",
159
+ status: 402,
160
+ context: { have: 5, need: 100 },
161
+ });
49
162
  });
163
+ ```
164
+
165
+ ## The four contracts in one model
166
+
167
+ | Concern | How |
168
+ | ---------------- | ------------------------------------------------------------------------------------------- |
169
+ | Input validation | `input: zodSchema` — parsed before the handler body runs |
170
+ | Output shape | `returns: Resource \| ResponseSpec[]` — narrows the handler's return type, drives OpenAPI |
171
+ | Throws | `errors: [NotFound, …]` — typed throwables, drive 4xx OpenAPI bodies |
172
+ | Cancellation | `signal: AbortSignal` on ctx — forward to fetch/pg/etc., bail via `signal.throwIfAborted()` |
173
+
174
+ ## Cancellation in practice
50
175
 
51
- const res = await execute(getUser, { id: "u_1" }, { resolve });
176
+ The signal flows through nested `.run()` calls automatically:
177
+
178
+ ```ts
179
+ const ParentOp = defineAction("parent", {
180
+ handler: async ({ signal }) => ChildOp().run({ signal }),
181
+ });
182
+ // caller-supplied signal → ParentOp.run({ signal }) → ChildOp.run({ signal })
183
+ // abort the caller's controller → both bail
52
184
  ```
53
185
 
54
- ## Surface
186
+ When no signal is supplied, ctx gets a non-aborting placeholder — code can always pass `ctx.signal` through to `fetch`/`pg`/`redis` without null checks.
187
+
188
+ ## Errors
189
+
190
+ ```ts
191
+ throw NotFound; // bare value (it IS an Error)
192
+ throw NotFound({ resourceId: input.id }); // callable, contextualised clone
193
+ ```
194
+
195
+ Caught at the transport layer — REST → `status` + `{ code, message, context }`, GraphQL → `extensions.code`, CLI → exit status + stderr.
196
+
197
+ ## Built on @nwire/hooks
198
+
199
+ Every handler is a `Hook<HandlerRunCtx>` underneath. `.use()`, `.on()`, `.tap()`, `.runDetailed()` all work. The user's handler function is the innermost chain step, registered at construction with `Number.MIN_SAFE_INTEGER` priority so every `.use()` you add wraps around it. Telemetry, replay, observation come along for free.
200
+
201
+ ## Scope
55
202
 
56
- | Export | Role |
57
- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
58
- | `defineHandler(name, config)` | The operation primitive: `{ input, handler, returns?, errors?, summary?, policy?, middleware? }`. |
59
- | `execute(handler, input, ctx?)` | Run one handler; returns a response envelope. |
60
- | `defineMiddleware(name?, fn)` | Chain step `(ctx, next)`. Captures `$source`. Reusable. |
61
- | `defineHook("before" \| "after", name?, fn)` | Before-or-after-only step (cleaner than mw that forgets `next()`). |
62
- | `pipe(name?, ...steps)` | Compose middleware + hooks into a reusable pipe value. Attaches a per-pipe `$hook` (`@nwire/hooks`) so transports adopt it for telemetry. |
63
- | `unwindPipe(steps)` | Walk a pipe into `{ middlewares, beforeHooks, afterHooks }` — transports call this when mounting. |
64
- | `defineResource(name, opts)` | Public response shape (field allowlist, OpenAPI schema). |
65
- | `defineError(meta)` | Typed throwable with status code. |
66
- | Response factories | `ok` / `created` / `accepted` / `noContent` / `notModified` / `gone`. |
67
- | Framework errors | `Unauthorized` / `Forbidden` / `NotFound` / `Conflict` / `Gone` / `BadRequest`. |
68
- | Type guards | `isHandlerDefinition` / `isMiddleware` / `isHook` / `isPipe` / `isResourceDefinition` / `isNwireError` / `isResponseSpec` / `isResponseInstance`. |
69
-
70
- ### `$source` + `$hook`
71
-
72
- - Every `defineMiddleware` / `pipe` captures its call site as `$source`
73
- via `@nwire/messages.captureSourceLocation`, so Studio can render
74
- click-to-open chips.
75
- - Every `pipe(...)` creates a per-pipe `Hook<ResolverCtx>` (one
76
- `.use()` per middleware step). Runtimes adopt it via
77
- `runtime.adoptHook($hook)` to route taps into telemetry; consumers
78
- that don't know about `$hook` ignore the field.
79
-
80
- ## Related
81
-
82
- - `@nwire/forge` — re-exports this surface and layers `defineAction` (`defineHandler` + `emits`) + `defineQuery` on top.
83
- - `@nwire/hooks` — backs `pipe()`'s per-pipe `$hook` and the resolver-level dispatch chain.
84
- - `@nwire/http` — mounts handlers by `unwindPipe`-ing their middleware.
85
-
86
- ## Status
87
-
88
- v0.x — handler shape, middleware/hook/pipe primitives, response factories, and core errors are locked. Per-pipe `$hook` adoption is the canonical observation seam.
203
+ Standalone no DI, no events, no logger, no transport opinions. App composes ctx by value; transports compose ctx at boot. Forge / HTTP / queue / MCP wires layer on top.
@@ -7,14 +7,20 @@
7
7
  * summary: "Station already has an unfinished wash",
8
8
  * });
9
9
  *
10
- * // In a resolver handler:
11
- * if (active) throw WashInProgress;
10
+ * // Throw it with context — callable form, symmetric with defineEvent/defineAction:
11
+ * throw WashInProgress({ stationId: input.id });
12
12
  *
13
- * Each call site reuses the SAME instance (immutable). Transports detect
14
- * `instanceof NwireError`, read `.code` + `.status` + `.summary`, render
15
- * the right shape per medium:
16
- * - REST → JSON body `{ code, message }` with the declared status
17
- * - GraphQL → error with `extensions: { code }`
13
+ * // Throw it without context — works because the definition IS an Error:
14
+ * throw WashInProgress;
15
+ *
16
+ * Each `defineError(...)` result is BOTH:
17
+ * - an `Error` instance you can `throw` directly (zero context),
18
+ * - a callable factory `Boom(context)` that returns a contextualised clone.
19
+ *
20
+ * Transports detect `instanceof NwireError`, read `.code` + `.status` + `.summary`
21
+ * + `.context`, render the right shape per medium:
22
+ * - REST → JSON body `{ code, message, context }` with the declared status
23
+ * - GraphQL → error with `extensions: { code, context }`
18
24
  * - CLI → stderr message + exit status
19
25
  */
20
26
  export interface ErrorMeta {
@@ -24,36 +30,36 @@ export interface ErrorMeta {
24
30
  readonly description?: string;
25
31
  readonly tags?: readonly string[];
26
32
  }
27
- export interface ErrorDefinition extends ErrorMeta {
28
- readonly $kind: "error";
29
- }
30
33
  /**
31
34
  * Concrete Error subclass — thrown by domain code, caught by transport.
32
- * The `defineError(...)` result is an instance of this class.
35
+ * The `defineError(...)` result is an instance of this class, AND callable
36
+ * so `Boom(context)` returns a contextualised clone.
33
37
  */
34
- export declare class NwireError extends Error implements ErrorDefinition {
38
+ export declare class NwireError extends Error implements ErrorMeta {
35
39
  readonly $kind: "error";
36
40
  readonly code: string;
37
41
  readonly status: number;
38
42
  readonly summary: string;
39
43
  readonly description?: string;
40
44
  readonly tags?: readonly string[];
41
- constructor(meta: ErrorMeta);
42
- /**
43
- * Build a fresh error instance with the same metadata but additional
44
- * runtime context — e.g., the resource id that triggered it.
45
- */
46
- with(context: Record<string, unknown>): NwireError & {
47
- readonly context: Record<string, unknown>;
48
- };
45
+ readonly context?: Readonly<Record<string, unknown>>;
46
+ constructor(meta: ErrorMeta, context?: Readonly<Record<string, unknown>>);
47
+ }
48
+ /**
49
+ * The value returned by `defineError(meta)` — callable to build a contextual
50
+ * clone, and itself an Error instance you can throw directly.
51
+ */
52
+ export interface ErrorDefinition extends NwireError {
53
+ /** Build a contextualised clone — `throw NotFound({ resourceId: id })`. */
54
+ (context: Readonly<Record<string, unknown>>): NwireError;
49
55
  }
50
- export declare function defineError(meta: ErrorMeta): NwireError;
56
+ export declare function defineError(meta: ErrorMeta): ErrorDefinition;
51
57
  /** Type narrow. */
52
58
  export declare function isNwireError(x: unknown): x is NwireError;
53
- export declare const Unauthorized: NwireError;
54
- export declare const Forbidden: NwireError;
55
- export declare const NotFound: NwireError;
56
- export declare const Gone: NwireError;
57
- export declare const BadRequest: NwireError;
58
- export declare const Conflict: NwireError;
59
+ export declare const Unauthorized: ErrorDefinition;
60
+ export declare const Forbidden: ErrorDefinition;
61
+ export declare const NotFound: ErrorDefinition;
62
+ export declare const Gone: ErrorDefinition;
63
+ export declare const BadRequest: ErrorDefinition;
64
+ export declare const Conflict: ErrorDefinition;
59
65
  //# sourceMappingURL=define-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-error.d.ts","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CAKzB;AAED;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAM,YAAW,eAAe;IAC9D,QAAQ,CAAC,KAAK,EAAG,OAAO,CAAU;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;gBAEtB,IAAI,EAAE,SAAS;IAU3B;;;OAGG;IACH,IAAI,CACF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,UAAU,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE;CAK9D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,CAEvD;AAED,mBAAmB;AACnB,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAExD;AAGD,eAAO,MAAM,YAAY,YAIvB,CAAC;AAEH,eAAO,MAAM,SAAS,YAIpB,CAAC;AAEH,eAAO,MAAM,QAAQ,YAInB,CAAC;AAEH,eAAO,MAAM,IAAI,YAIf,CAAC;AAEH,eAAO,MAAM,UAAU,YAIrB,CAAC;AAEH,eAAO,MAAM,QAAQ,YAInB,CAAC"}
1
+ {"version":3,"file":"define-error.d.ts","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,KAAM,YAAW,SAAS;IACxD,QAAQ,CAAC,KAAK,EAAG,OAAO,CAAU;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;gBAEzC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAUzE;AAED;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD,2EAA2E;IAC3E,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,UAAU,CAAC;CAC1D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAW5D;AAED,mBAAmB;AACnB,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAExD;AAGD,eAAO,MAAM,YAAY,iBAIvB,CAAC;AAEH,eAAO,MAAM,SAAS,iBAIpB,CAAC;AAEH,eAAO,MAAM,QAAQ,iBAInB,CAAC;AAEH,eAAO,MAAM,IAAI,iBAIf,CAAC;AAEH,eAAO,MAAM,UAAU,iBAIrB,CAAC;AAEH,eAAO,MAAM,QAAQ,iBAInB,CAAC"}
@@ -7,19 +7,26 @@
7
7
  * summary: "Station already has an unfinished wash",
8
8
  * });
9
9
  *
10
- * // In a resolver handler:
11
- * if (active) throw WashInProgress;
10
+ * // Throw it with context — callable form, symmetric with defineEvent/defineAction:
11
+ * throw WashInProgress({ stationId: input.id });
12
12
  *
13
- * Each call site reuses the SAME instance (immutable). Transports detect
14
- * `instanceof NwireError`, read `.code` + `.status` + `.summary`, render
15
- * the right shape per medium:
16
- * - REST → JSON body `{ code, message }` with the declared status
17
- * - GraphQL → error with `extensions: { code }`
13
+ * // Throw it without context — works because the definition IS an Error:
14
+ * throw WashInProgress;
15
+ *
16
+ * Each `defineError(...)` result is BOTH:
17
+ * - an `Error` instance you can `throw` directly (zero context),
18
+ * - a callable factory `Boom(context)` that returns a contextualised clone.
19
+ *
20
+ * Transports detect `instanceof NwireError`, read `.code` + `.status` + `.summary`
21
+ * + `.context`, render the right shape per medium:
22
+ * - REST → JSON body `{ code, message, context }` with the declared status
23
+ * - GraphQL → error with `extensions: { code, context }`
18
24
  * - CLI → stderr message + exit status
19
25
  */
20
26
  /**
21
27
  * Concrete Error subclass — thrown by domain code, caught by transport.
22
- * The `defineError(...)` result is an instance of this class.
28
+ * The `defineError(...)` result is an instance of this class, AND callable
29
+ * so `Boom(context)` returns a contextualised clone.
23
30
  */
24
31
  export class NwireError extends Error {
25
32
  $kind = "error";
@@ -28,7 +35,8 @@ export class NwireError extends Error {
28
35
  summary;
29
36
  description;
30
37
  tags;
31
- constructor(meta) {
38
+ context;
39
+ constructor(meta, context) {
32
40
  super(meta.summary);
33
41
  this.name = meta.code;
34
42
  this.code = meta.code;
@@ -36,19 +44,19 @@ export class NwireError extends Error {
36
44
  this.summary = meta.summary;
37
45
  this.description = meta.description;
38
46
  this.tags = meta.tags;
39
- }
40
- /**
41
- * Build a fresh error instance with the same metadata but additional
42
- * runtime context — e.g., the resource id that triggered it.
43
- */
44
- with(context) {
45
- const next = new NwireError(this);
46
- Object.assign(next, { context });
47
- return next;
47
+ if (context !== undefined)
48
+ this.context = context;
48
49
  }
49
50
  }
50
51
  export function defineError(meta) {
51
- return new NwireError(meta);
52
+ // Build the canonical (no-context) instance + make it callable.
53
+ const base = new NwireError(meta);
54
+ const factory = ((context) => new NwireError(meta, context));
55
+ // Copy every own property + the prototype-resident fields onto the factory
56
+ // so `factory instanceof Error`, `factory.code`, `factory.message`, etc.
57
+ // all behave like the underlying NwireError instance.
58
+ Object.setPrototypeOf(factory, base);
59
+ return factory;
52
60
  }
53
61
  /** Type narrow. */
54
62
  export function isNwireError(x) {
@@ -1 +1 @@
1
- {"version":3,"file":"define-error.js","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAkBH;;;GAGG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,KAAK,GAAG,OAAgB,CAAC;IACzB,IAAI,CAAS;IACb,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,WAAW,CAAU;IACrB,IAAI,CAAqB;IAElC,YAAY,IAAe;QACzB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;IAED;;;OAGG;IACH,IAAI,CACF,OAAgC;QAEhC,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QACjC,OAAO,IAAkE,CAAC;IAC5E,CAAC;CACF;AAED,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,mBAAmB;AACnB,MAAM,UAAU,YAAY,CAAC,CAAU;IACrC,OAAO,CAAC,YAAY,UAAU,CAAC;AACjC,CAAC;AAED,+CAA+C;AAC/C,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;IACtC,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,0BAA0B;CACpC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;IACnC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,kDAAkD;CAC5D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;IAClC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,+BAA+B;CACzC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,WAAW,CAAC;IAC9B,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,iDAAiD;CAC3D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC;IACpC,IAAI,EAAE,aAAa;IACnB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,8EAA8E;CACxF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;IAClC,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,2DAA2D;CACrE,CAAC,CAAC"}
1
+ {"version":3,"file":"define-error.js","sourceRoot":"","sources":["../src/define-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAUH;;;;GAIG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,KAAK,GAAG,OAAgB,CAAC;IACzB,IAAI,CAAS;IACb,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,WAAW,CAAU;IACrB,IAAI,CAAqB;IACzB,OAAO,CAAqC;IAErD,YAAY,IAAe,EAAE,OAA2C;QACtE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACpD,CAAC;CACF;AAWD,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,gEAAgE;IAChE,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,OAAO,GAAoB,CAAC,CAAC,OAA0C,EAAE,EAAE,CAC/E,IAAI,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAA+B,CAAC;IAE/D,2EAA2E;IAC3E,yEAAyE;IACzE,sDAAsD;IACtD,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACrC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,mBAAmB;AACnB,MAAM,UAAU,YAAY,CAAC,CAAU;IACrC,OAAO,CAAC,YAAY,UAAU,CAAC;AACjC,CAAC;AAED,+CAA+C;AAC/C,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;IACtC,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,0BAA0B;CACpC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;IACnC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,kDAAkD;CAC5D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;IAClC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,+BAA+B;CACzC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,WAAW,CAAC;IAC9B,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,iDAAiD;CAC3D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC;IACpC,IAAI,EAAE,aAAa;IACnB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,8EAA8E;CACxF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;IAClC,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,2DAA2D;CACrE,CAAC,CAAC"}
@@ -3,166 +3,205 @@
3
3
  *
4
4
  * const GetStation = defineHandler("GetStation", {
5
5
  * input: z.object({ stationId: z.string() }),
6
+ * handler: async ({ input }) => loadStation(input.stationId),
6
7
  * returns: Station,
7
- * handler: async ({ input, db }) => db.stations.findById(input.stationId),
8
+ * errors: [NotFound],
8
9
  * });
9
10
  *
10
- * The shape is universal same primitive whether the operation is exposed
11
- * via HTTP, queue, MCP, CLI, or invoked directly via `execute(handler, input)`.
12
- * Every transport binds via its own `(binding, handler, options)` signature;
13
- * the binding narrows the input contract at the boundary, the handler is
14
- * the lowest-common-denominator inside.
11
+ * GetStation({ stationId: "abc" }) // mint a prepared op
12
+ * await GetStation({ stationId: "abc" }).run() // run it
13
+ * await GetStation.run({ input, signal }) // low-level hook surface
14
+ * GetStation.use(myMiddleware) // attach a hook chain step
15
+ * GetStation.on(observer) // attach a listener
15
16
  *
16
- * See: architecture-sketch.html §08.
17
+ * The handler IS a `Hook` from `@nwire/hooks`. `.use()` attaches chain steps;
18
+ * `.on()` attaches parallel listeners; `.run()` executes. The user's handler
19
+ * function is the innermost chain step — registered at construction with
20
+ * `Number.MIN_SAFE_INTEGER` priority so every `.use(...)` wraps around it.
17
21
  *
18
- * Field placement follows the freq table:
19
- * first-class input, handler, returns, errors, summary, policy, emits
20
- * meta — tags, description, version, deprecated, successor
21
- * settings — retry, timeout, cache, idempotency
22
+ * **Ctx is generic.** The handler's ctx is `BaseHookCtx & { input } & TExtras`
23
+ * where `TExtras` is inferred from the handler's param annotation, or pinned
24
+ * once via `defineHandlerWith<TExtras>()` at the app boundary. Whatever you
25
+ * pass to `.run(ctx)` is what reaches the handler body, fully typed. No
26
+ * declaration merging on ctx — composition by value.
22
27
  *
23
- * `meta` + `settings` are open bags; adapters extend them via TS declaration
24
- * merging without touching the core type.
25
- *
26
- * NOTE: forge has its own `defineHandler(action, fn)` that binds a handler
27
- * to an existing `ActionDefinition`. That signature stays inside `@nwire/forge`
28
- * because it depends on the action machinery. The standalone version lives
29
- * here because every transport needs it without the forge surface.
28
+ * `meta` + `settings` ARE globally augmentable open interfaces. Those are
29
+ * declarative per-handler tags (policy, retry, persona, SLO) that plugins
30
+ * must discover across every handler — different role from ctx, different
31
+ * mechanism by design.
30
32
  */
31
33
  import type { z } from "zod";
32
34
  import type { ZodTypeAny } from "@nwire/messages";
33
- import type { Container } from "@nwire/container";
34
- import { type Logger } from "@nwire/logger";
35
+ import { type BaseHookCtx, type ChainFn, type ListenerFn, type RunOptions, type RunResult, type UseOptions, type OnOptions, type TapFn } from "@nwire/hooks";
35
36
  import type { ErrorDefinition } from "./define-error.js";
36
37
  import type { ResponseSpec, ListResponseSpec } from "./response.js";
37
38
  import type { ResourceDefinition } from "./define-resource.js";
38
- /** Bags extensible via TS declaration merging — adapters add fields here. */
39
+ /**
40
+ * Empty bags — plugins extend via TS declaration merging.
41
+ *
42
+ * declare module "@nwire/handler" {
43
+ * interface HandlerSettings { retry?: { attempts: number } }
44
+ * }
45
+ *
46
+ * These hold *declarative config* (RBAC policies, retry strategies,
47
+ * SLO budgets, persona tags) — NOT runtime ctx. Different role, different
48
+ * mechanism: ctx is per-invocation runtime data composed by value;
49
+ * meta/settings are per-handler design-time tags plugins discover globally.
50
+ */
39
51
  export interface HandlerMeta {
40
- readonly tags?: readonly string[];
41
- readonly description?: string;
42
- readonly version?: number;
43
- readonly deprecated?: boolean;
44
- readonly successor?: string;
45
52
  }
46
53
  export interface HandlerSettings {
47
- readonly retry?: {
48
- attempts: number;
49
- backoff?: "linear" | "exponential";
50
- };
51
- readonly timeout?: number;
52
- readonly cache?: {
53
- ttl: number;
54
- };
55
- readonly idempotency?: (input: unknown) => string;
56
54
  }
55
+ /** Resolve a zod schema (or `undefined`) to the parsed input type. */
56
+ export type InferInput<S extends ZodTypeAny | undefined> = S extends ZodTypeAny ? z.output<S> : undefined;
57
57
  /**
58
- * Context delivered to every handler. The shape is intentionally minimal
59
- * adapters supply richer context via the container (resolve any registered
60
- * dep by name + destructure into the handler signature).
58
+ * The base ctx every handler receives hooks' `BaseHookCtx` (`signal` + typed
59
+ * `set`) plus the parsed `input`. Apps extend with their own `TExtras` via
60
+ * the `Ctx<TInput, TExtras>` alias below.
61
61
  */
62
- export interface HandlerContext<TInput = unknown> {
63
- /** Parsed + validated input (drawn from `config.input` schema). */
64
- readonly input: TInput;
65
- /** Look up a dependency by name. Most handlers destructure instead. */
66
- resolve<T = unknown>(name: string): T;
67
- /** Per-execution container scope. */
68
- readonly container: Container;
69
- /** Logger scoped to this execution (correlation/causation already attached). */
70
- readonly logger: Logger;
71
- /** Free-form bag for transport-specific extras (raw request, MCP progress, …). */
72
- readonly transport?: Record<string, unknown>;
62
+ export interface HandlerBaseCtx<TInput> extends BaseHookCtx {
63
+ input: TInput;
73
64
  }
65
+ /**
66
+ * Sugar for declaring a handler's ctx shape:
67
+ *
68
+ * handler: async ({ input, cradle, user }: Ctx<MyInput, { cradle: Cradle; user: User }>) => …
69
+ */
70
+ export type Ctx<TInput, TExtras extends object = object> = HandlerBaseCtx<TInput> & TExtras;
74
71
  /**
75
72
  * `returns` may be a single resource, a single response spec, or an array
76
73
  * of response specs (e.g. `[created(Station), accepted(StationHandle)]`).
77
74
  */
78
75
  export type HandlerReturnsSpec = ResourceDefinition | ResponseSpec | ListResponseSpec<ResourceDefinition> | ReadonlyArray<ResponseSpec | ListResponseSpec<ResourceDefinition>>;
79
- /**
80
- * Policy: a tag (`"admin"`), a list of tags (`["admin", "operator"]`), or
81
- * a tuple of `[action, subject]` for RBAC-style policies. Adapters
82
- * interpret the value per their authorization model.
83
- */
84
- export type HandlerPolicy = string | readonly string[] | readonly [string, string];
85
- export interface HandlerConfig<TInputSchema extends ZodTypeAny | undefined = ZodTypeAny | undefined, TOutput = unknown> {
76
+ export interface HandlerConfig<TInputSchema extends ZodTypeAny | undefined = undefined, TOutput = unknown, TExtras extends object = object> {
86
77
  /** Input schema. Optional for zero-arg handlers (health checks, listings without filters). */
87
78
  readonly input?: TInputSchema;
88
- /** The implementation. Receives the typed input + context. */
89
- readonly handler: (ctx: HandlerContext<TInputSchema extends ZodTypeAny ? z.output<TInputSchema> : undefined>) => TOutput | Promise<TOutput>;
79
+ /** The implementation. Receives the typed input + ctx fields contributed upstream. */
80
+ readonly handler: (ctx: Ctx<InferInput<TInputSchema>, TExtras>) => TOutput | Promise<TOutput>;
90
81
  /** Documented response shape(s). Drives OpenAPI + runtime body validation. */
91
82
  readonly returns?: HandlerReturnsSpec;
92
83
  /** Declared error values this handler may throw. */
93
84
  readonly errors?: readonly ErrorDefinition[];
94
- /** Events this handler may emit (action flavour). */
95
- readonly emits?: readonly unknown[];
96
- /** One-line intent. Surfaces in OpenAPI/MCP descriptions. */
85
+ /** One-line intent. Surfaces in OpenAPI / MCP / Studio. */
97
86
  readonly summary?: string;
98
- /** Authorization policy. Adapters (rbac, authz) consume this. */
99
- readonly policy?: HandlerPolicy;
100
- /** Rare-field bag — tags, description, version, deprecated, …. */
87
+ /** Multi-line prose. Surfaces in OpenAPI / MCP / Studio. */
88
+ readonly description?: string;
89
+ /** Open bag — plugins contribute fields via declaration merging. */
101
90
  readonly meta?: HandlerMeta;
102
- /** Behavior-tuning bag — retry, timeout, cache, idempotency. */
91
+ /** Open bag — plugins contribute fields via declaration merging. */
103
92
  readonly settings?: HandlerSettings;
104
93
  }
105
- export interface HandlerDefinition<TInputSchema extends ZodTypeAny | undefined = ZodTypeAny | undefined, TOutput = unknown> {
94
+ /**
95
+ * Hook ctx that flows through the chain when a handler runs. Extends the
96
+ * consumer's `TExtras` so middleware that wrote to ctx (or used `.set()`)
97
+ * is visible to subsequent steps + listeners.
98
+ */
99
+ export type HandlerRunCtx<TInput = unknown, TOutput = unknown, TExtras extends object = object> = Ctx<TInput, TExtras> & {
100
+ result?: TOutput;
101
+ };
102
+ /** Options passed to `PreparedOp.run()` — the high-level callable form. */
103
+ export interface PreparedOpRunOptions<TExtras extends object = object> {
104
+ readonly signal?: AbortSignal;
105
+ /**
106
+ * Extras to populate on ctx before the handler body runs. Required when
107
+ * `TExtras` declares fields the runtime must supply (`cradle`, `user`, …).
108
+ * Optional `{}` for handlers that declared no extras.
109
+ */
110
+ readonly ctx?: TExtras;
111
+ }
112
+ /**
113
+ * The value returned by calling a handler as a factory: `Handler(input)`.
114
+ * Holds the validated input and exposes `.run(opts?)` to execute.
115
+ */
116
+ export interface PreparedOp<TInput = unknown, TOutput = unknown, TExtras extends object = object> {
117
+ readonly input: TInput;
118
+ run(opts?: PreparedOpRunOptions<TExtras>): Promise<TOutput>;
119
+ }
120
+ /**
121
+ * The handler definition. Composes a `Hook<HandlerRunCtx>` for the chain
122
+ * surface (`.use` / `.on` / `.off` / `.run` / `.runDetailed` / `.tap`) and
123
+ * is callable as a factory that mints a `PreparedOp`.
124
+ */
125
+ export interface HandlerDefinition<TInputSchema extends ZodTypeAny | undefined = undefined, TOutput = unknown, TExtras extends object = object> {
106
126
  readonly $kind: "handler";
107
127
  readonly name: string;
108
- readonly config: HandlerConfig<TInputSchema, TOutput>;
128
+ readonly config: HandlerConfig<TInputSchema, TOutput, TExtras>;
129
+ (input: TInputSchema extends ZodTypeAny ? z.input<TInputSchema> : void): PreparedOp<InferInput<TInputSchema>, TOutput, TExtras>;
130
+ /** Hook chain attach — runs sequentially, can short-circuit. */
131
+ use(fn: ChainFn<HandlerRunCtx<InferInput<TInputSchema>, TOutput, TExtras>>, opts?: UseOptions): HandlerDefinition<TInputSchema, TOutput, TExtras>;
132
+ /** Listener attach — observes the final ctx, cannot mutate. */
133
+ on(fn: ListenerFn<HandlerRunCtx<InferInput<TInputSchema>, TOutput, TExtras>>, opts?: OnOptions): HandlerDefinition<TInputSchema, TOutput, TExtras>;
134
+ /** Detach a previously attached `.use()` or `.on()` fn. */
135
+ off(fn: ChainFn<HandlerRunCtx<InferInput<TInputSchema>, TOutput, TExtras>> | ListenerFn<HandlerRunCtx<InferInput<TInputSchema>, TOutput, TExtras>>): HandlerDefinition<TInputSchema, TOutput, TExtras>;
109
136
  /**
110
- * Convenience accessor equivalent to `config.handler`. Lets transports
111
- * write `handler.run(ctx)` without reaching into `.config`.
137
+ * Run the chain + listeners. The caller supplies the ctx — `input` +
138
+ * `signal` are filled in automatically; everything in `TExtras` must
139
+ * be present on the ctx object.
112
140
  */
113
- run(ctx: HandlerContext<TInputSchema extends ZodTypeAny ? z.output<TInputSchema> : undefined>): TOutput | Promise<TOutput>;
141
+ run(ctx: HandlerRunCtx<InferInput<TInputSchema>, TOutput, TExtras>, opts?: RunOptions): Promise<HandlerRunCtx<InferInput<TInputSchema>, TOutput, TExtras>>;
142
+ /** Same as `.run()` but returns the full observation trace. */
143
+ runDetailed(ctx: HandlerRunCtx<InferInput<TInputSchema>, TOutput, TExtras>, opts?: RunOptions): Promise<RunResult<HandlerRunCtx<InferInput<TInputSchema>, TOutput, TExtras>>>;
144
+ /** Subscribe to per-step observations. */
145
+ tap(observer: TapFn): () => void;
146
+ /** Step counts on the underlying hook. */
147
+ stepCounts(): {
148
+ readonly chain: number;
149
+ readonly listeners: number;
150
+ };
114
151
  }
152
+ /**
153
+ * `HandlerLike` — what `interface-builder.wire()` accepts.
154
+ *
155
+ * - a full `HandlerDefinition` (typed, hookable, has metadata) — the canonical path.
156
+ * - a bare async function (one-liners, health checks, escape hatch).
157
+ */
158
+ export type HandlerLike<TInput = unknown, TOutput = unknown> = HandlerDefinition<ZodTypeAny | undefined, TOutput, object> | ((input: TInput) => Promise<TOutput> | TOutput);
115
159
  /**
116
160
  * Define a handler — the operation primitive.
117
161
  *
118
162
  * const ListTodos = defineHandler("listTodos", {
119
163
  * input: z.object({ status: z.string().optional() }),
120
- * handler: async ({ input, resolve }) => {
121
- * const db = resolve<DB>("db");
122
- * return db.todos.findMany(input);
123
- * },
164
+ * handler: async ({ input }) => todos.findMany(input),
124
165
  * });
125
166
  *
126
167
  * // Direct invocation:
127
- * const todos = await execute(ListTodos, { status: "open" });
168
+ * const result = await ListTodos({ status: "open" }).run();
128
169
  *
129
- * // HTTP transport:
130
- * api.wire(get("/todos"), ListTodos);
170
+ * // Hook composition:
171
+ * ListTodos.use(withTimeout(5000));
172
+ * ListTodos.on(record => otel.span(...));
131
173
  *
132
- * // Queue transport (same handler):
133
- * workers.wire(job("listTodos"), ListTodos);
134
- */
135
- export declare function defineHandler<TInputSchema extends ZodTypeAny | undefined, TOutput>(name: string, config: HandlerConfig<TInputSchema, TOutput>): HandlerDefinition<TInputSchema, TOutput>;
136
- /** Type narrow. */
137
- export declare function isHandlerDefinition(x: unknown): x is HandlerDefinition;
138
- /**
139
- * Options for `execute()` — transport-agnostic. Adapters can pass a
140
- * container so the handler can `resolve()` its deps; if omitted, a
141
- * minimal stub container is supplied that throws on resolve.
174
+ * // Low-level hook surface (transports use this):
175
+ * await ListTodos.run({ input: { status: "open" }, signal });
142
176
  */
143
- export interface ExecuteOptions {
144
- readonly container?: Container;
145
- readonly logger?: Logger;
146
- readonly transport?: Record<string, unknown>;
147
- }
177
+ export declare function defineHandler<TInputSchema extends ZodTypeAny | undefined, TOutput, TExtras extends object = object>(name: string, config: HandlerConfig<TInputSchema, TOutput, TExtras>): HandlerDefinition<TInputSchema, TOutput, TExtras>;
148
178
  /**
149
- * Invoke a handler directly with input. The framework validates input
150
- * against the handler's `input` schema (if declared), constructs the
151
- * handler context, runs the handler, and returns its result.
179
+ * App-boundary sugar pin `TExtras` once so every handler in the app reads
180
+ * `ctx` as the bound shape without per-call annotations.
152
181
  *
153
- * const todos = await execute(ListTodos, { status: "open" });
182
+ * // app/action.ts
183
+ * type AppExtras = { cradle: AppCradle; user: User };
184
+ * export const action = defineHandlerWith<AppExtras>();
154
185
  *
155
- * This is the universal "any host can mount it" entry point. Transports
156
- * call into `execute()` after parsing the binding's payload; tests call
157
- * `execute()` directly to bypass transport machinery.
158
- */
159
- export declare function execute<TInputSchema extends ZodTypeAny | undefined, TOutput>(handler: HandlerDefinition<TInputSchema, TOutput>, input: TInputSchema extends ZodTypeAny ? z.input<TInputSchema> : any, options?: ExecuteOptions): Promise<TOutput>;
160
- /**
161
- * Type alias for a handler-or-callable. Transports `.wire()` accept either
162
- * a full `HandlerDefinition` (typed, validated) or a bare async function
163
- * (escape hatch for one-liners + health checks).
186
+ * // every handler file:
187
+ * import { action } from "@/action";
188
+ * const PlaceOrder = action("placeOrder", {
189
+ * input: z.object({ id: z.string() }),
190
+ * handler: async ({ input, cradle, user }) => … // fully typed, no annotation
191
+ * });
192
+ *
193
+ * // every transport / .run() call:
194
+ * PlaceOrder.run({ input, signal, cradle, user });
164
195
  */
165
- export type HandlerLike<TInput = unknown, TOutput = unknown> = HandlerDefinition<ZodTypeAny | undefined, TOutput> | ((input: TInput) => Promise<TOutput> | TOutput);
166
- /** Re-exports for the public surface. */
196
+ export declare function defineHandlerWith<TExtras extends object>(): <TInputSchema extends ZodTypeAny | undefined, TOutput>(name: string, config: HandlerConfig<TInputSchema, TOutput, TExtras>) => HandlerDefinition<TInputSchema, TOutput, TExtras>;
197
+ /** Type narrow. */
198
+ export declare function isHandlerDefinition(x: unknown): x is HandlerDefinition;
199
+ /** Extract the input type of a handler's schema (post-parse). */
200
+ export type HandlerInput<H> = H extends HandlerDefinition<infer S, unknown, object> ? InferInput<S> : never;
201
+ /** Extract the output type of a handler. */
202
+ export type HandlerOutput<H> = H extends HandlerDefinition<ZodTypeAny | undefined, infer O, object> ? O : never;
203
+ /** Extract the extras (TExtras) a handler requires on ctx. */
204
+ export type HandlerExtras<H> = H extends HandlerDefinition<ZodTypeAny | undefined, unknown, infer E> ? E : never;
205
+ /** Re-export from the response builders for ergonomics. */
167
206
  export type { ResponseInstance } from "./response.js";
168
207
  //# sourceMappingURL=define-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-handler.d.ts","sourceRoot":"","sources":["../src/define-handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"define-handler.d.ts","sourceRoot":"","sources":["../src/define-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,KAAK,EACX,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAoB,MAAM,eAAe,CAAC;AACtF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;CAAG;AAC/B,MAAM,WAAW,eAAe;CAAG;AAEnC,sEAAsE;AACtE,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,UAAU,GAAG,SAAS,IAAI,CAAC,SAAS,UAAU,GAC3E,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GACX,SAAS,CAAC;AAEd;;;;GAIG;AACH,MAAM,WAAW,cAAc,CAAC,MAAM,CAAE,SAAQ,WAAW;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,OAAO,SAAS,MAAM,GAAG,MAAM,IAAI,cAAc,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;AAE5F;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAC1B,kBAAkB,GAClB,YAAY,GACZ,gBAAgB,CAAC,kBAAkB,CAAC,GACpC,aAAa,CAAC,YAAY,GAAG,gBAAgB,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAEvE,MAAM,WAAW,aAAa,CAC5B,YAAY,SAAS,UAAU,GAAG,SAAS,GAAG,SAAS,EACvD,OAAO,GAAG,OAAO,EACjB,OAAO,SAAS,MAAM,GAAG,MAAM;IAE/B,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,sFAAsF;IACtF,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9F,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IACtC,oDAAoD;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;CACrC;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,CACvB,MAAM,GAAG,OAAO,EAChB,OAAO,GAAG,OAAO,EACjB,OAAO,SAAS,MAAM,GAAG,MAAM,IAC7B,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEhD,2EAA2E;AAC3E,MAAM,WAAW,oBAAoB,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,OAAO,SAAS,MAAM,GAAG,MAAM;IAC9F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,IAAI,CAAC,EAAE,oBAAoB,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7D;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB,CAChC,YAAY,SAAS,UAAU,GAAG,SAAS,GAAG,SAAS,EACvD,OAAO,GAAG,OAAO,EACjB,OAAO,SAAS,MAAM,GAAG,MAAM;IAE/B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAG/D,CACE,KAAK,EAAE,YAAY,SAAS,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,IAAI,GACpE,UAAU,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAE1D,gEAAgE;IAChE,GAAG,CACD,EAAE,EAAE,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,EACtE,IAAI,CAAC,EAAE,UAAU,GAChB,iBAAiB,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAErD,+DAA+D;IAC/D,EAAE,CACA,EAAE,EAAE,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,EACzE,IAAI,CAAC,EAAE,SAAS,GACf,iBAAiB,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAErD,2DAA2D;IAC3D,GAAG,CACD,EAAE,EACE,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,GAClE,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,GACxE,iBAAiB,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAErD;;;;OAIG;IACH,GAAG,CACD,GAAG,EAAE,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAC9D,IAAI,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAEtE,+DAA+D;IAC/D,WAAW,CACT,GAAG,EAAE,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAC9D,IAAI,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC,SAAS,CAAC,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAEjF,0CAA0C;IAC1C,GAAG,CAAC,QAAQ,EAAE,KAAK,GAAG,MAAM,IAAI,CAAC;IAEjC,0CAA0C;IAC1C,UAAU,IAAI;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACtE;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,IACvD,iBAAiB,CAAC,UAAU,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,GAC1D,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AASpD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,YAAY,SAAS,UAAU,GAAG,SAAS,EAC3C,OAAO,EACP,OAAO,SAAS,MAAM,GAAG,MAAM,EAE/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,GACpD,iBAAiB,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAkGnD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,MAC9C,YAAY,SAAS,UAAU,GAAG,SAAS,EAAE,OAAO,EAC1D,MAAM,MAAM,EACZ,QAAQ,aAAa,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,KACpD,iBAAiB,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAErD;AAED,mBAAmB;AACnB,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,iBAAiB,CAEtE;AAED,iEAAiE;AACjE,MAAM,MAAM,YAAY,CAAC,CAAC,IACxB,CAAC,SAAS,iBAAiB,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;AAEhF,4CAA4C;AAC5C,MAAM,MAAM,aAAa,CAAC,CAAC,IACzB,CAAC,SAAS,iBAAiB,CAAC,UAAU,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEnF,8DAA8D;AAC9D,MAAM,MAAM,aAAa,CAAC,CAAC,IACzB,CAAC,SAAS,iBAAiB,CAAC,UAAU,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEpF,2DAA2D;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
@@ -3,114 +3,170 @@
3
3
  *
4
4
  * const GetStation = defineHandler("GetStation", {
5
5
  * input: z.object({ stationId: z.string() }),
6
+ * handler: async ({ input }) => loadStation(input.stationId),
6
7
  * returns: Station,
7
- * handler: async ({ input, db }) => db.stations.findById(input.stationId),
8
+ * errors: [NotFound],
8
9
  * });
9
10
  *
10
- * The shape is universal same primitive whether the operation is exposed
11
- * via HTTP, queue, MCP, CLI, or invoked directly via `execute(handler, input)`.
12
- * Every transport binds via its own `(binding, handler, options)` signature;
13
- * the binding narrows the input contract at the boundary, the handler is
14
- * the lowest-common-denominator inside.
11
+ * GetStation({ stationId: "abc" }) // mint a prepared op
12
+ * await GetStation({ stationId: "abc" }).run() // run it
13
+ * await GetStation.run({ input, signal }) // low-level hook surface
14
+ * GetStation.use(myMiddleware) // attach a hook chain step
15
+ * GetStation.on(observer) // attach a listener
15
16
  *
16
- * See: architecture-sketch.html §08.
17
+ * The handler IS a `Hook` from `@nwire/hooks`. `.use()` attaches chain steps;
18
+ * `.on()` attaches parallel listeners; `.run()` executes. The user's handler
19
+ * function is the innermost chain step — registered at construction with
20
+ * `Number.MIN_SAFE_INTEGER` priority so every `.use(...)` wraps around it.
17
21
  *
18
- * Field placement follows the freq table:
19
- * first-class input, handler, returns, errors, summary, policy, emits
20
- * meta — tags, description, version, deprecated, successor
21
- * settings — retry, timeout, cache, idempotency
22
+ * **Ctx is generic.** The handler's ctx is `BaseHookCtx & { input } & TExtras`
23
+ * where `TExtras` is inferred from the handler's param annotation, or pinned
24
+ * once via `defineHandlerWith<TExtras>()` at the app boundary. Whatever you
25
+ * pass to `.run(ctx)` is what reaches the handler body, fully typed. No
26
+ * declaration merging on ctx — composition by value.
22
27
  *
23
- * `meta` + `settings` are open bags; adapters extend them via TS declaration
24
- * merging without touching the core type.
25
- *
26
- * NOTE: forge has its own `defineHandler(action, fn)` that binds a handler
27
- * to an existing `ActionDefinition`. That signature stays inside `@nwire/forge`
28
- * because it depends on the action machinery. The standalone version lives
29
- * here because every transport needs it without the forge surface.
28
+ * `meta` + `settings` ARE globally augmentable open interfaces. Those are
29
+ * declarative per-handler tags (policy, retry, persona, SLO) that plugins
30
+ * must discover across every handler — different role from ctx, different
31
+ * mechanism by design.
32
+ */
33
+ import { hook, } from "@nwire/hooks";
34
+ /**
35
+ * Tag for the terminal chain step that calls the user's handler function.
36
+ * Registered with `Number.MIN_SAFE_INTEGER` priority so every `.use()` from
37
+ * the consumer wraps around it (higher priority = runs first / outermost).
30
38
  */
31
- import { NoopLogger } from "@nwire/logger";
39
+ const TERMINAL_STEP_NAME = "@nwire/handler/terminal";
32
40
  /**
33
41
  * Define a handler — the operation primitive.
34
42
  *
35
43
  * const ListTodos = defineHandler("listTodos", {
36
44
  * input: z.object({ status: z.string().optional() }),
37
- * handler: async ({ input, resolve }) => {
38
- * const db = resolve<DB>("db");
39
- * return db.todos.findMany(input);
40
- * },
45
+ * handler: async ({ input }) => todos.findMany(input),
41
46
  * });
42
47
  *
43
48
  * // Direct invocation:
44
- * const todos = await execute(ListTodos, { status: "open" });
49
+ * const result = await ListTodos({ status: "open" }).run();
45
50
  *
46
- * // HTTP transport:
47
- * api.wire(get("/todos"), ListTodos);
51
+ * // Hook composition:
52
+ * ListTodos.use(withTimeout(5000));
53
+ * ListTodos.on(record => otel.span(...));
48
54
  *
49
- * // Queue transport (same handler):
50
- * workers.wire(job("listTodos"), ListTodos);
55
+ * // Low-level hook surface (transports use this):
56
+ * await ListTodos.run({ input: { status: "open" }, signal });
51
57
  */
52
58
  export function defineHandler(name, config) {
53
- return {
54
- $kind: "handler",
55
- name,
56
- config,
57
- run(ctx) {
58
- return config.handler(ctx);
59
- },
59
+ const h = hook(name);
60
+ // Innermost step — runs the user's handler after every `.use()` has had
61
+ // its chance to wrap. Validation happens here (parse against config.input)
62
+ // so middleware that mutates ctx.input does so BEFORE validation. The
63
+ // chain ctx is forwarded so middleware-contributed fields (TExtras —
64
+ // cradle, user, tenant, logger, …) flow through.
65
+ h.use(async (ctx, next) => {
66
+ ctx.signal?.throwIfAborted();
67
+ const parsed = config.input ? config.input.parse(ctx.input) : ctx.input;
68
+ ctx.input = parsed;
69
+ const result = await config.handler(ctx);
70
+ ctx.result = result;
71
+ await next();
72
+ }, {
73
+ name: TERMINAL_STEP_NAME,
74
+ priority: Number.MIN_SAFE_INTEGER,
75
+ });
76
+ // The factory is a callable function. `Object.defineProperty` attaches the
77
+ // metadata + delegated hook methods + the `$kind` discriminator.
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ const factory = (input) => {
80
+ const prepared = {
81
+ input: input,
82
+ async run(opts) {
83
+ const ctx = {
84
+ input,
85
+ ...(opts?.ctx ?? {}),
86
+ };
87
+ const out = await h.run(ctx, { signal: opts?.signal });
88
+ return out.result;
89
+ },
90
+ };
91
+ return prepared;
60
92
  };
61
- }
62
- /** Type narrow. */
63
- export function isHandlerDefinition(x) {
64
- return typeof x === "object" && x !== null && x.$kind === "handler";
93
+ Object.defineProperties(factory, {
94
+ $kind: { value: "handler", enumerable: true },
95
+ name: { value: name, enumerable: true, configurable: true },
96
+ config: { value: config, enumerable: true },
97
+ toString: { value: () => name, enumerable: false },
98
+ $hook: { value: h, enumerable: false, configurable: true },
99
+ use: {
100
+ value: function use(fn, opts) {
101
+ h.use(fn, opts);
102
+ return factory;
103
+ },
104
+ enumerable: false,
105
+ },
106
+ on: {
107
+ value: function on(fn, opts) {
108
+ h.on(fn, opts);
109
+ return factory;
110
+ },
111
+ enumerable: false,
112
+ },
113
+ off: {
114
+ value: function off(fn) {
115
+ h.off(fn);
116
+ return factory;
117
+ },
118
+ enumerable: false,
119
+ },
120
+ run: {
121
+ value: function run(ctx, opts) {
122
+ return h.run(ctx, opts);
123
+ },
124
+ enumerable: false,
125
+ },
126
+ runDetailed: {
127
+ value: function runDetailed(ctx, opts) {
128
+ return h.runDetailed(ctx, opts);
129
+ },
130
+ enumerable: false,
131
+ },
132
+ tap: {
133
+ value: function tap(observer) {
134
+ return h.tap(observer);
135
+ },
136
+ enumerable: false,
137
+ },
138
+ stepCounts: {
139
+ value: function stepCounts() {
140
+ return h.stepCounts();
141
+ },
142
+ enumerable: false,
143
+ },
144
+ });
145
+ return factory;
65
146
  }
66
147
  /**
67
- * Invoke a handler directly with input. The framework validates input
68
- * against the handler's `input` schema (if declared), constructs the
69
- * handler context, runs the handler, and returns its result.
148
+ * App-boundary sugar pin `TExtras` once so every handler in the app reads
149
+ * `ctx` as the bound shape without per-call annotations.
150
+ *
151
+ * // app/action.ts
152
+ * type AppExtras = { cradle: AppCradle; user: User };
153
+ * export const action = defineHandlerWith<AppExtras>();
70
154
  *
71
- * const todos = await execute(ListTodos, { status: "open" });
155
+ * // every handler file:
156
+ * import { action } from "@/action";
157
+ * const PlaceOrder = action("placeOrder", {
158
+ * input: z.object({ id: z.string() }),
159
+ * handler: async ({ input, cradle, user }) => … // fully typed, no annotation
160
+ * });
72
161
  *
73
- * This is the universal "any host can mount it" entry point. Transports
74
- * call into `execute()` after parsing the binding's payload; tests call
75
- * `execute()` directly to bypass transport machinery.
162
+ * // every transport / .run() call:
163
+ * PlaceOrder.run({ input, signal, cradle, user });
76
164
  */
77
- export async function execute(handler,
78
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
- input, options = {}) {
80
- const schema = handler.config.input;
81
- const parsed = schema ? schema.parse(input) : input;
82
- const container = options.container ?? minimalContainer();
83
- const logger = options.logger ?? minimalLogger();
84
- const ctx = {
85
- input: parsed,
86
- container,
87
- resolve: (name) => container.resolve(name),
88
- logger,
89
- transport: options.transport,
90
- };
91
- return handler.run(ctx);
92
- }
93
- /** Minimal stub container — every resolve throws. Used when no container supplied. */
94
- function minimalContainer() {
95
- const stub = {
96
- resolve(name) {
97
- throw new Error(`execute(): no container provided — cannot resolve "${name}". ` +
98
- `Pass options.container or use a transport that wires one.`);
99
- },
100
- register() {
101
- throw new Error("execute(): minimal container is read-only");
102
- },
103
- createScope() {
104
- return minimalContainer();
105
- },
106
- has() {
107
- return false;
108
- },
109
- };
110
- return stub;
165
+ export function defineHandlerWith() {
166
+ return (name, config) => defineHandler(name, config);
111
167
  }
112
- /** Minimal stub logger. No-op every method. */
113
- function minimalLogger() {
114
- return new NoopLogger();
168
+ /** Type narrow. */
169
+ export function isHandlerDefinition(x) {
170
+ return typeof x === "function" && x.$kind === "handler";
115
171
  }
116
172
  //# sourceMappingURL=define-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-handler.js","sourceRoot":"","sources":["../src/define-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,OAAO,EAAE,UAAU,EAAe,MAAM,eAAe,CAAC;AAkGxD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAY,EACZ,MAA4C;IAE5C,OAAO;QACL,KAAK,EAAE,SAAS;QAChB,IAAI;QACJ,MAAM;QACN,GAAG,CAAC,GAAG;YACL,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,mBAAmB;AACnB,MAAM,UAAU,mBAAmB,CAAC,CAAU;IAC5C,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAK,CAAyB,CAAC,KAAK,KAAK,SAAS,CAAC;AAC/F,CAAC;AAeD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,OAAiD;AACjD,8DAA8D;AAC9D,KAAoE,EACpE,UAA0B,EAAE;IAE5B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAEpD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,EAAE,CAAC;IAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,EAAE,CAAC;IAEjD,MAAM,GAAG,GAAmB;QAC1B,KAAK,EAAE,MAAM;QACb,SAAS;QACT,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;QAC1C,MAAM;QACN,SAAS,EAAE,OAAO,CAAC,SAAS;KAC7B,CAAC;IAEF,OAAO,OAAO,CAAC,GAAG,CAChB,GAA2F,CAC5F,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAS,gBAAgB;IACvB,MAAM,IAAI,GAAc;QACtB,OAAO,CAAI,IAAY;YACrB,MAAM,IAAI,KAAK,CACb,sDAAsD,IAAI,KAAK;gBAC7D,2DAA2D,CAC9D,CAAC;QACJ,CAAC;QACD,QAAQ;YACN,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC/D,CAAC;QACD,WAAW;YACT,OAAO,gBAAgB,EAAE,CAAC;QAC5B,CAAC;QACD,GAAG;YACD,OAAO,KAAK,CAAC;QACf,CAAC;KACF,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,+CAA+C;AAC/C,SAAS,aAAa;IACpB,OAAO,IAAI,UAAU,EAAE,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"define-handler.js","sourceRoot":"","sources":["../src/define-handler.ts"],"names":[],"mappings":"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"}
@@ -1,27 +1,20 @@
1
1
  /**
2
2
  * `@nwire/handler` — the operation primitive.
3
3
  *
4
- * Per the sealed architecture (§05), every transport (HTTP, queue,
5
- * MCP, CLI, …) speaks the same handler shape. This package ships that
6
- * shape the operation as value without dragging in forge's action /
7
- * actor / event machinery. A NestJS team can import `defineHandler`
8
- * from here, mount it on Express, and never touch `@nwire/forge`.
9
- *
10
- * `@nwire/forge` re-exports the same names so existing call sites keep
11
- * compiling unchanged. Forge layers `defineAction = defineHandler +
12
- * { flavor: "action", emits }` on top — same primitive, additional
13
- * semantics for the event-driven path.
4
+ * A handler is a typed, callable, hookable value built on `@nwire/hooks`.
5
+ * One value, every transport (HTTP, queue, MCP, CLI, direct test)
6
+ * standalone, no DI, no events, no logger, no transport opinions. Ctx is
7
+ * generic; the app pins `TExtras` via `defineHandlerWith<TExtras>()` once
8
+ * at the boot site and every handler reads `ctx` as the bound shape with
9
+ * full autocomplete, zero per-call annotations.
14
10
  *
15
11
  * Public surface:
16
12
  *
17
13
  * Primitives
18
- * defineHandler(name, config) — the operation primitive
19
- * execute(handler, input) direct invocation
20
- * defineError(meta) — typed throwable
21
- * defineResource(name, opts) — interface/response shape
22
- * defineMiddleware — chain step
23
- * defineHook — before/after-only step
24
- * pipe(...steps) — compose chain
14
+ * defineHandler(name, config) — the operation primitive
15
+ * defineHandlerWith<TExtras>() app-boundary factory binding
16
+ * defineError(meta) — typed throwable
17
+ * defineResource(name, opts) — interface/response shape
25
18
  *
26
19
  * Response factories
27
20
  * ok / ok.list / created / accepted / noContent / notModified / gone
@@ -30,12 +23,14 @@
30
23
  * Unauthorized / Forbidden / NotFound / Conflict / Gone / BadRequest
31
24
  *
32
25
  * Types
33
- * HandlerDefinition / HandlerConfig / HandlerContext / HandlerLike
34
- * ResponseInstance / ResponseSpec / ChainFn / NwireError
26
+ * HandlerDefinition / HandlerConfig / Ctx / HandlerBaseCtx
27
+ * HandlerRunCtx / PreparedOp / HandlerLike
28
+ * HandlerMeta / HandlerSettings (empty bags — extend via declaration merging)
29
+ * HandlerInput<H> / HandlerOutput<H> / HandlerExtras<H>
30
+ * ResponseInstance / ResponseSpec / NwireError
35
31
  */
36
- export { defineHandler, execute, isHandlerDefinition, type HandlerDefinition, type HandlerConfig, type HandlerContext, type HandlerMeta, type HandlerSettings, type HandlerReturnsSpec, type HandlerPolicy, type HandlerLike, type ExecuteOptions, } from "./define-handler.js";
32
+ export { defineHandler, defineHandlerWith, isHandlerDefinition, type HandlerDefinition, type HandlerConfig, type HandlerBaseCtx, type Ctx, type HandlerMeta, type HandlerSettings, type HandlerReturnsSpec, type HandlerRunCtx, type HandlerLike, type PreparedOp, type PreparedOpRunOptions, type HandlerInput, type HandlerOutput, type HandlerExtras, type InferInput, type ResponseInstance, } from "./define-handler.js";
37
33
  export { defineError, isNwireError, NwireError, Unauthorized, Forbidden, NotFound, Gone, BadRequest, Conflict, type ErrorDefinition, type ErrorMeta, } from "./define-error.js";
38
34
  export { defineResource, isResourceDefinition, type ResourceDefinition, type DefineResourceOptions, } from "./define-resource.js";
39
- export { defineMiddleware, defineHook, pipe, unwindPipe, isMiddleware, isHook, isPipe, type MiddlewareDefinition, type HookDefinition, type MiddlewarePipe, type PipeStep, type ChainFn, type ResolverCtx, type Next, } from "./define-middleware.js";
40
- export { response, isResponseSpec, isResponseInstance, ok, created, accepted, noContent, notModified, gone, type ResponseSpec, type ListResponseSpec, type ResponseInstance, type ResponseSpecBase, type ResponseKind, } from "./response.js";
35
+ export { response, isResponseSpec, isResponseInstance, ok, created, accepted, noContent, notModified, gone, type ResponseSpec, type ListResponseSpec, type ResponseSpecBase, type ResponseKind, } from "./response.js";
41
36
  //# sourceMappingURL=handler-index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler-index.d.ts","sourceRoot":"","sources":["../src/handler-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EACL,aAAa,EACb,OAAO,EACP,mBAAmB,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,UAAU,EACV,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,SAAS,GACf,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,IAAI,EACJ,UAAU,EACV,YAAY,EACZ,MAAM,EACN,MAAM,EACN,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,IAAI,GACV,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,EAAE,EACF,OAAO,EACP,QAAQ,EACR,SAAS,EACT,WAAW,EACX,IAAI,EACJ,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"handler-index.d.ts","sourceRoot":"","sources":["../src/handler-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,GAAG,EACR,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,UAAU,EACV,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,SAAS,GACf,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,EAAE,EACF,OAAO,EACP,QAAQ,EACR,SAAS,EACT,WAAW,EACX,IAAI,EACJ,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC"}
@@ -1,27 +1,20 @@
1
1
  /**
2
2
  * `@nwire/handler` — the operation primitive.
3
3
  *
4
- * Per the sealed architecture (§05), every transport (HTTP, queue,
5
- * MCP, CLI, …) speaks the same handler shape. This package ships that
6
- * shape the operation as value without dragging in forge's action /
7
- * actor / event machinery. A NestJS team can import `defineHandler`
8
- * from here, mount it on Express, and never touch `@nwire/forge`.
9
- *
10
- * `@nwire/forge` re-exports the same names so existing call sites keep
11
- * compiling unchanged. Forge layers `defineAction = defineHandler +
12
- * { flavor: "action", emits }` on top — same primitive, additional
13
- * semantics for the event-driven path.
4
+ * A handler is a typed, callable, hookable value built on `@nwire/hooks`.
5
+ * One value, every transport (HTTP, queue, MCP, CLI, direct test)
6
+ * standalone, no DI, no events, no logger, no transport opinions. Ctx is
7
+ * generic; the app pins `TExtras` via `defineHandlerWith<TExtras>()` once
8
+ * at the boot site and every handler reads `ctx` as the bound shape with
9
+ * full autocomplete, zero per-call annotations.
14
10
  *
15
11
  * Public surface:
16
12
  *
17
13
  * Primitives
18
- * defineHandler(name, config) — the operation primitive
19
- * execute(handler, input) direct invocation
20
- * defineError(meta) — typed throwable
21
- * defineResource(name, opts) — interface/response shape
22
- * defineMiddleware — chain step
23
- * defineHook — before/after-only step
24
- * pipe(...steps) — compose chain
14
+ * defineHandler(name, config) — the operation primitive
15
+ * defineHandlerWith<TExtras>() app-boundary factory binding
16
+ * defineError(meta) — typed throwable
17
+ * defineResource(name, opts) — interface/response shape
25
18
  *
26
19
  * Response factories
27
20
  * ok / ok.list / created / accepted / noContent / notModified / gone
@@ -30,12 +23,14 @@
30
23
  * Unauthorized / Forbidden / NotFound / Conflict / Gone / BadRequest
31
24
  *
32
25
  * Types
33
- * HandlerDefinition / HandlerConfig / HandlerContext / HandlerLike
34
- * ResponseInstance / ResponseSpec / ChainFn / NwireError
26
+ * HandlerDefinition / HandlerConfig / Ctx / HandlerBaseCtx
27
+ * HandlerRunCtx / PreparedOp / HandlerLike
28
+ * HandlerMeta / HandlerSettings (empty bags — extend via declaration merging)
29
+ * HandlerInput<H> / HandlerOutput<H> / HandlerExtras<H>
30
+ * ResponseInstance / ResponseSpec / NwireError
35
31
  */
36
- export { defineHandler, execute, isHandlerDefinition, } from "./define-handler.js";
32
+ export { defineHandler, defineHandlerWith, isHandlerDefinition, } from "./define-handler.js";
37
33
  export { defineError, isNwireError, NwireError, Unauthorized, Forbidden, NotFound, Gone, BadRequest, Conflict, } from "./define-error.js";
38
34
  export { defineResource, isResourceDefinition, } from "./define-resource.js";
39
- export { defineMiddleware, defineHook, pipe, unwindPipe, isMiddleware, isHook, isPipe, } from "./define-middleware.js";
40
35
  export { response, isResponseSpec, isResponseInstance, ok, created, accepted, noContent, notModified, gone, } from "./response.js";
41
36
  //# sourceMappingURL=handler-index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler-index.js","sourceRoot":"","sources":["../src/handler-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EACL,aAAa,EACb,OAAO,EACP,mBAAmB,GAUpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,UAAU,EACV,QAAQ,GAGT,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,cAAc,EACd,oBAAoB,GAGrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,IAAI,EACJ,UAAU,EACV,YAAY,EACZ,MAAM,EACN,MAAM,GAQP,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,EAAE,EACF,OAAO,EACP,QAAQ,EACR,SAAS,EACT,WAAW,EACX,IAAI,GAML,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"handler-index.js","sourceRoot":"","sources":["../src/handler-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,mBAAmB,GAiBpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,UAAU,EACV,QAAQ,GAGT,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,cAAc,EACd,oBAAoB,GAGrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,EAAE,EACF,OAAO,EACP,QAAQ,EACR,SAAS,EACT,WAAW,EACX,IAAI,GAKL,MAAM,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nwire/handler",
3
- "version": "0.8.17",
4
- "description": "Nwire — the operation primitive. defineHandler / defineError / defineResource / defineMiddleware / pipe / response factories / typed framework errors. Standalone depend on this without pulling forge. Every transport (HTTP, queue, MCP, ) speaks the same handler shape.",
3
+ "version": "0.9.1",
4
+ "description": "Nwire — the operation primitive. Typed, callable, hookable value built on @nwire/hooks. defineHandler / defineError / defineResource / response factories / framework errors. Standalone; every transport (HTTP, queue, MCP, CLI) speaks the same handler shape.",
5
5
  "keywords": [
6
6
  "execute",
7
7
  "handler",
@@ -28,11 +28,8 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "zod": "^4.0.0",
31
- "@nwire/container": "0.8.17",
32
- "@nwire/envelope": "0.8.17",
33
- "@nwire/hooks": "0.8.17",
34
- "@nwire/logger": "0.8.17",
35
- "@nwire/messages": "0.8.17"
31
+ "@nwire/messages": "0.9.1",
32
+ "@nwire/hooks": "0.9.1"
36
33
  },
37
34
  "devDependencies": {
38
35
  "@types/node": "^22.19.9",