@nwire/forge 0.12.0 → 0.13.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 (53) hide show
  1. package/README.md +100 -83
  2. package/dist/framework-events.d.ts +8 -37
  3. package/dist/framework-events.js +7 -3
  4. package/dist/helpers/cli-runner.js +21 -10
  5. package/dist/index.d.ts +8 -7
  6. package/dist/index.js +7 -6
  7. package/dist/plugins/actions-chain.d.ts +39 -22
  8. package/dist/plugins/actions-chain.js +117 -78
  9. package/dist/plugins/actions-plugin.d.ts +26 -23
  10. package/dist/plugins/actions-plugin.js +122 -44
  11. package/dist/plugins/actors-chain.d.ts +9 -2
  12. package/dist/plugins/actors-chain.js +62 -2
  13. package/dist/plugins/actors-plugin.d.ts +1 -1
  14. package/dist/plugins/actors-plugin.js +24 -14
  15. package/dist/plugins/external-calls-plugin.d.ts +28 -0
  16. package/dist/plugins/external-calls-plugin.js +136 -0
  17. package/dist/plugins/idempotency-plugin.d.ts +15 -1
  18. package/dist/plugins/idempotency-plugin.js +56 -11
  19. package/dist/plugins/projections-chain.d.ts +2 -2
  20. package/dist/plugins/projections-chain.js +2 -2
  21. package/dist/plugins/projections-plugin.d.ts +1 -1
  22. package/dist/plugins/projections-plugin.js +4 -13
  23. package/dist/plugins/queries-chain.d.ts +4 -3
  24. package/dist/plugins/queries-chain.js +8 -5
  25. package/dist/plugins/queries-plugin.d.ts +15 -29
  26. package/dist/plugins/queries-plugin.js +36 -49
  27. package/dist/plugins/workflows-chain.d.ts +9 -2
  28. package/dist/plugins/workflows-chain.js +19 -1
  29. package/dist/plugins/workflows-plugin.d.ts +1 -1
  30. package/dist/plugins/workflows-plugin.js +12 -20
  31. package/dist/primitives/define-action.d.ts +80 -115
  32. package/dist/primitives/define-action.js +111 -56
  33. package/dist/primitives/define-actor.d.ts +103 -214
  34. package/dist/primitives/define-actor.js +157 -216
  35. package/dist/primitives/define-handler.d.ts +42 -112
  36. package/dist/primitives/define-handler.js +14 -45
  37. package/dist/primitives/define-projection.d.ts +23 -28
  38. package/dist/primitives/define-projection.js +29 -32
  39. package/dist/primitives/define-query.d.ts +52 -42
  40. package/dist/primitives/define-query.js +65 -28
  41. package/dist/primitives/define-workflow.d.ts +8 -11
  42. package/dist/primitives/define-workflow.js +14 -8
  43. package/dist/runtime/forge-dispatcher.d.ts +30 -12
  44. package/dist/runtime/forge-dispatcher.js +199 -237
  45. package/dist/runtime/forge-plugin.d.ts +8 -0
  46. package/dist/runtime/forge-plugin.js +113 -17
  47. package/dist/runtime/forge-plugins.d.ts +55 -0
  48. package/dist/runtime/forge-plugins.js +57 -0
  49. package/dist/runtime/forge-types.d.ts +8 -2
  50. package/dist/runtime/with-forge.d.ts +8 -11
  51. package/dist/runtime/with-forge.js +9 -11
  52. package/dist/stores/idempotency-store.d.ts +1 -1
  53. package/package.json +12 -12
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @nwire/forge
2
2
 
3
- > Domain primitives: actions, actors, events, workflows, projections.
4
- > Plus the runtime that fires them.
3
+ > Domain primitives: actions, queries, actors, events, workflows, projections.
4
+ > The batteries you grow into, riding the one runtime.
5
5
 
6
6
  ```bash
7
7
  pnpm add @nwire/forge zod
@@ -11,162 +11,179 @@ pnpm add @nwire/forge zod
11
11
 
12
12
  ### 1. Define an action
13
13
 
14
+ An action is a handler — `defineAction = defineHandler.kind("action")`. It
15
+ carries a name, a zod `input`, and the handler body. The body takes one `ctx`
16
+ arg with the validated `input` on it.
17
+
14
18
  ```ts
15
19
  import { defineAction } from "@nwire/forge";
16
20
  import { z } from "zod";
17
21
 
18
22
  export const placeOrder = defineAction({
19
23
  name: "orders.place",
20
- schema: z.object({
24
+ input: z.object({
21
25
  customerId: z.string(),
22
26
  items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })),
23
27
  }),
28
+ handler: async ({ input, resolve }) => {
29
+ const orders = resolve<OrderRepo>("orders");
30
+ return orders.create(input);
31
+ },
24
32
  });
25
33
  ```
26
34
 
27
- An action is a typed contract — a name + zod schema. The runtime
28
- validates input on every dispatch.
35
+ `ctx` carries `input`, `resolve<T>(name)`, `envelope` (tenant / user /
36
+ correlation / causation), and the dispatch verbs `request` (ask + await),
37
+ `send` (fire-and-forget → `MessageRef`), `emit` (publish an event), `query`
38
+ (read a projection), and `actor` (load an actor view).
39
+
40
+ For a cross-module contract the local module doesn't handle, omit `handler` —
41
+ it's then a schema-only action you dispatch toward; the owning module registers
42
+ the handler.
29
43
 
30
- ### 2. Define a handler
44
+ ### 2. Define a query
45
+
46
+ A query is a handler too — `defineQuery = defineHandler.kind("query")` — so it
47
+ wires to a GET and dispatches like anything else. Projection form reads folded
48
+ state; handler form reaches a source directly.
31
49
 
32
50
  ```ts
33
- import { defineHandler } from "@nwire/forge";
51
+ import { defineQuery } from "@nwire/forge";
34
52
 
35
- export const placeOrderHandler = defineHandler(placeOrder, async (input, ctx) => {
36
- const orders = ctx.resolve<OrderRepo>("orders");
37
- return orders.create(input);
53
+ export const listOrders = defineQuery(OrdersDashboard, {
54
+ name: "orders.list",
55
+ input: z.object({ status: z.enum(["open", "shipped"]).optional() }),
56
+ execute: (state, { status }) =>
57
+ Object.values(state).filter((o) => !status || o.status === status),
38
58
  });
39
59
  ```
40
60
 
41
- `(input, ctx)` is the canonical handler shape. `ctx` carries:
42
-
43
- - `resolve<T>(name)` — pull bindings from the container
44
- - `envelope` — tenant, user, correlation, causation
45
- - `dispatch(action, input)` — fire other actions
46
- - `enqueue(queueName, payload)` — defer work to a queue
47
- - `emit(event, payload)` — emit a domain event
61
+ ### 3. Register + wire into an App
48
62
 
49
- ### 3. Wire it into an App
63
+ Registration is the app's job pass handlers (actions **and** queries) to
64
+ `createApp({ handlers })`. `forgePlugins()` adds the batteries (it scans the
65
+ runtime for action/query handlers and wires the command pipeline + read engine);
66
+ it does **not** take a handler list.
50
67
 
51
68
  ```ts
52
69
  import { createApp } from "@nwire/app";
53
- import { createForgePlugin } from "@nwire/forge";
54
- import { post } from "@nwire/wires/http";
70
+ import { forgePlugins } from "@nwire/forge";
71
+ import { post, get } from "@nwire/wires/http";
55
72
 
56
73
  const app = createApp({
57
74
  appName: "orders",
58
- plugins: [createForgePlugin({})],
59
- handlers: [placeOrderHandler],
75
+ handlers: [placeOrder, listOrders],
76
+ plugins: [...forgePlugins({ actors: [subscription], projections: [OrdersDashboard] })],
60
77
  });
61
78
 
62
- // HTTP route dispatches the same handler
63
- app.wire(post("/orders", { body: placeOrder.schema }), placeOrderHandler);
79
+ // HTTP routes wire the same handlers — the action/query IS the handler.
80
+ app.wire(post("/orders", { body: placeOrder.input }), placeOrder);
81
+ app.wire(get("/orders", { query: listOrders.input }), listOrders);
64
82
  ```
65
83
 
66
- `createForgePlugin({})` installs the dispatcher; handlers passed in
67
- `createApp({ handlers })` register on it at boot.
68
-
69
84
  ### 4. Dispatch from anywhere
70
85
 
71
86
  ```ts
72
- const dispatcher = app.container.resolve<ForgeDispatcher>("forge.dispatcher");
73
-
74
- await dispatcher.dispatch(placeOrder, {
87
+ await app.runtime.execute(placeOrder, {
75
88
  customerId: "c-1",
76
89
  items: [{ sku: "WIDGET", qty: 2 }],
77
90
  });
78
91
  ```
79
92
 
80
93
  The runtime validates input, opens an envelope, runs middleware, fires
81
- `action.before` hooks, executes the handler, runs `action.after` hooks,
82
- and surfaces telemetry. HTTP and queue adopters route through the same
83
- path so dispatch semantics never differ by transport.
94
+ `ActionDispatching` + per-action before/after hooks, runs the handler with
95
+ retry + DLQ, publishes returned events, and surfaces telemetry. HTTP and queue
96
+ adopters land on the same `runtime.execute`, so dispatch semantics never differ
97
+ by transport. Inside a handler, prefer `ctx.request` / `ctx.send`.
84
98
 
85
99
  ## Actors
86
100
 
87
101
  ```ts
88
- import { defineActor } from "@nwire/forge";
102
+ import { defineActor, defineSchema } from "@nwire/forge";
89
103
 
90
- export const subscription = defineActor({
104
+ const SubscriptionData = defineSchema({
91
105
  name: "subscription",
92
- schema: z.object({
106
+ key: "id",
107
+ fields: {
93
108
  id: z.string(),
94
109
  plan: z.enum(["free", "pro"]),
95
110
  status: z.enum(["active", "past_due", "suspended"]),
96
- }),
97
- methods: {
98
- suspend: ({ self }) => ({ ...self, status: "suspended" }),
99
- activate: ({ self }) => ({ ...self, status: "active" }),
111
+ },
112
+ states: {
113
+ active: { initial: true },
114
+ past_due: {},
115
+ suspended: { final: true },
100
116
  },
101
117
  });
118
+
119
+ export const subscription = defineActor(
120
+ "subscription",
121
+ ({ states, when }) => {
122
+ const { active, pastDue } = states;
123
+ active(() =>
124
+ when(PaymentFailed, (_e, { assign }) => {
125
+ assign({ status: "past_due" });
126
+ return pastDue;
127
+ }),
128
+ );
129
+ },
130
+ { schema: SubscriptionData },
131
+ );
102
132
  ```
103
133
 
104
- Actors carry state, version, and OCC. Methods receive the current state
105
- and return the next state pure transitions. The dispatcher persists
106
- through an `ActorStore` adapter.
134
+ Actors carry state, version, and OCC; events drive transitions. The store seam
135
+ is an `ActorStore` adapter. Pass `actors: [...]` to `forgePlugins`.
107
136
 
108
137
  ## Workflows
109
138
 
110
139
  ```ts
111
140
  import { defineWorkflow } from "@nwire/forge";
112
141
 
113
- export const paymentRenewal = defineWorkflow({
114
- name: "payment.renewal",
115
- on: { event: chargeFailed },
116
- steps: {
117
- holdGrace: async (ctx) => {
118
- await ctx.schedule(suspendSubscription, { in: "48h" });
119
- },
120
- },
142
+ export const paymentRenewal = defineWorkflow("payment.renewal", ({ when, schedule, timeout }) => {
143
+ const Overdue = timeout("overdue", "48h");
144
+ when(ChargeFailed, async (e, ctx) => {
145
+ await schedule(Overdue);
146
+ });
147
+ when(Overdue, async (_e, ctx) => {
148
+ await ctx.send(suspendSubscription, { id: _e.id });
149
+ });
121
150
  });
122
151
  ```
123
152
 
124
- Workflows react to events. `ctx.schedule(...)` arms timers, `ctx.dispatch(...)`
125
- fires actions; state persists between hops via the actor store.
153
+ Workflows react to events with the one verb, `when`; `schedule`/`timeout` arm
154
+ timers and `ctx.send(...)` dispatches actions. Declare `data`/`states` (3rd arg)
155
+ to make it a stateful saga. Pass `workflows: [...]` to `forgePlugins`.
126
156
 
127
157
  ## Projections
128
158
 
129
159
  ```ts
130
160
  import { defineProjection } from "@nwire/forge";
131
161
 
132
- export const queueDashboard = defineProjection({
133
- name: "queue.dashboard",
134
- on: { event: postSubmitted },
135
- apply: async (state, event) => {
136
- state.pending.push({ id: event.payload.postId, submittedAt: event.envelope.ts });
162
+ export const OrdersDashboard = defineProjection(
163
+ "orders.dashboard",
164
+ ({ when }) => {
165
+ when(orderPlaced, (state, event) => ({ ...state, [event.orderId]: event }));
137
166
  },
138
- });
167
+ { initial: () => ({}) },
168
+ );
139
169
  ```
140
170
 
141
- Projections build read models from events. Domain code reads them via
142
- `ctx.resolve<ProjectionStore>("projection.queue.dashboard")`.
143
-
144
- ## Plugin options
145
-
146
- `createForgePlugin({...})` accepts:
147
-
148
- ```ts
149
- createForgePlugin({
150
- workflows: [paymentRenewal],
151
- projections: [queueDashboard],
152
- queries: [listPending, queueTotals],
153
- actors: [subscription],
154
- externalCalls: [chargeStripe],
155
- });
156
- ```
171
+ Projections fold events into read models; queries read them. Pass
172
+ `projections: [...]` to `forgePlugins`.
157
173
 
158
174
  ## Surface
159
175
 
160
- - **Primitives** — `defineAction`, `defineActor`, `defineEvent`,
161
- `defineWorkflow`, `defineProjection`, `defineQuery`, `defineHandler`
162
- - **Plugin** `createForgePlugin(options)`
163
- - **Dispatcher** — `ForgeDispatcher` (resolved as `"forge.dispatcher"`)
164
- - **Helpers** — `eventFactory`, `defineResource`, `NotFound`,
165
- `defineUpcaster`, response builders
176
+ - **Primitives** — `defineAction`, `defineQuery`, `defineActor`, `defineEvent`,
177
+ `defineWorkflow`, `defineProjection`; plus `eventFactory`, `defineResource`,
178
+ `defineUpcaster`, response builders. (`defineAction`/`defineQuery` are
179
+ `defineHandler.kind(...)` from `@nwire/handler`.)
180
+ - **Plugins** — `forgePlugins(options)` (the set) or the à-la-carte concern
181
+ plugins: `actionsPlugin`, `queriesPlugin`, `actorsPlugin`, `projectionsPlugin`,
182
+ `workflowsPlugin`, `idempotencyPlugin`, `externalCallsPlugin`.
166
183
 
167
184
  ## Related
168
185
 
169
186
  - [`@nwire/app`](../core-app) — `createApp`, plugin lifecycle, runtime
170
- - [`@nwire/handler`](../core-handler) — handler contract + error envelope
187
+ - [`@nwire/handler`](../core-handler) — the handler primitive (`defineHandler`)
171
188
  - [`@nwire/messages`](../core-messages) — typed envelope + zod helpers
172
- - [`@nwire/koa`](../nwire-koa) — HTTP adopter that routes through the dispatcher
189
+ - [`@nwire/koa`](../nwire-koa) — HTTP adopter that routes through the runtime
@@ -14,26 +14,8 @@
14
14
  * domain-specific slots.
15
15
  */
16
16
  import type { Hook } from "@nwire/hooks";
17
- import type { MessageEnvelope } from "@nwire/envelope";
18
17
  import type { ActionDefinition } from "./primitives/define-action.js";
19
18
  import type { HandlerContext } from "./primitives/define-handler.js";
20
- import type { EventMessage } from "./messages/event-message.js";
21
- /**
22
- * Payload threaded through the `EventPublishing` hook chain. Each chain
23
- * step does its work for one event in order. Steps may set `deduped` to
24
- * short-circuit (idempotency), or read it to skip work when an earlier
25
- * step already deduplicated.
26
- *
27
- * Mutable so steps can flag deduplication without restructuring the
28
- * chain return shape.
29
- */
30
- export interface EventPublishingPayload {
31
- readonly event: EventMessage;
32
- readonly envelope: MessageEnvelope;
33
- readonly dedupKey: string;
34
- /** Set by the idempotency step when this event was seen before. */
35
- deduped: boolean;
36
- }
37
19
  declare module "@nwire/app" {
38
20
  interface FrameworkHooks {
39
21
  /** Fired before a dispatch reaches the handler. Veto by skipping next(). */
@@ -66,27 +48,16 @@ declare module "@nwire/app" {
66
48
  readonly eventName: string;
67
49
  readonly payload: unknown;
68
50
  }>;
69
- /**
70
- * The atomic event-publish chain. Each domain concern attaches one
71
- * `.use()` step at its priority slot; the hook engine enforces the
72
- * order. Priority slots used by forge:
73
- *
74
- * 1000 — idempotency (dedup gate; short-circuits when seen)
75
- * 800 — actor state transitions
76
- * 600 — projection folds
77
- * 400 — workflow correlation + fire
78
- * 200 — cross-process bus delivery (public events only)
79
- *
80
- * External plugins can attach at intermediate priorities to inject
81
- * domain-specific work (audit log, cross-tenant fan-out, etc.) while
82
- * preserving the forge ordering.
83
- */
84
- EventPublishing: Hook<EventPublishingPayload>;
85
51
  }
86
52
  }
87
- /** Forge-specific framework-hook slot names — materialised by `createForgePlugin`. */
88
- export declare const forgeFrameworkSlots: readonly ["ActionDispatching", "ActionCompleted", "ActionFailed", "EventRecording", "EventRecorded", "EventPublishing"];
89
- /** Priority slots forge's own chain participants attach at. */
53
+ /** Forge-specific framework-hook slot names — materialised by `actionsPlugin`. */
54
+ export declare const forgeFrameworkSlots: readonly ["ActionDispatching", "ActionCompleted", "ActionFailed", "EventRecording", "EventRecorded"];
55
+ /**
56
+ * Priority slots forge's fold steps attach at on the core `LocalDelivery`
57
+ * hook (idempotency → actors → projections → workflows). Bus + outbound
58
+ * sink are no longer chain steps — they're the outbound `publish` concern,
59
+ * applied by the dispatcher after `runtime.deliver`.
60
+ */
90
61
  export declare const EVENT_PUBLISHING_PRIORITIES: {
91
62
  readonly idempotency: 1000;
92
63
  readonly actors: 800;
@@ -13,16 +13,20 @@
13
13
  * are pre-instantiated on every Runtime; this file only adds forge's
14
14
  * domain-specific slots.
15
15
  */
16
- /** Forge-specific framework-hook slot names — materialised by `createForgePlugin`. */
16
+ /** Forge-specific framework-hook slot names — materialised by `actionsPlugin`. */
17
17
  export const forgeFrameworkSlots = [
18
18
  "ActionDispatching",
19
19
  "ActionCompleted",
20
20
  "ActionFailed",
21
21
  "EventRecording",
22
22
  "EventRecorded",
23
- "EventPublishing",
24
23
  ];
25
- /** Priority slots forge's own chain participants attach at. */
24
+ /**
25
+ * Priority slots forge's fold steps attach at on the core `LocalDelivery`
26
+ * hook (idempotency → actors → projections → workflows). Bus + outbound
27
+ * sink are no longer chain steps — they're the outbound `publish` concern,
28
+ * applied by the dispatcher after `runtime.deliver`.
29
+ */
26
30
  export const EVENT_PUBLISHING_PRIORITIES = {
27
31
  idempotency: 1000,
28
32
  actors: 800,
@@ -34,7 +34,16 @@
34
34
  * The CLI doesn't know about HTTP routes; it dispatches via the runtime.
35
35
  */
36
36
  import { seedEnvelope } from "@nwire/envelope";
37
- import { forgeDispatcher } from "../runtime/forge-plugin.js";
37
+ import { FORGE_ACTION_RUNNER_BINDING } from "../plugins/actions-plugin.js";
38
+ import { FORGE_QUERY_RUNNER_BINDING } from "../plugins/queries-plugin.js";
39
+ function actionRunnerOf(app) {
40
+ return app.container.resolve(FORGE_ACTION_RUNNER_BINDING);
41
+ }
42
+ function queryRunnerOf(app) {
43
+ return app.container.has(FORGE_QUERY_RUNNER_BINDING)
44
+ ? app.container.resolve(FORGE_QUERY_RUNNER_BINDING)
45
+ : undefined;
46
+ }
38
47
  export async function runCli(app, argv, options = {}) {
39
48
  const stdout = options.stdout ?? ((line) => console.log(line));
40
49
  const stderr = options.stderr ?? ((line) => console.error(line));
@@ -44,14 +53,15 @@ export async function runCli(app, argv, options = {}) {
44
53
  }
45
54
  const targetName = argv[0];
46
55
  const rest = argv.slice(1);
47
- const dispatcher = forgeDispatcher(app);
48
- const action = dispatcher.findActionByName(targetName);
56
+ const actions = actionRunnerOf(app);
57
+ const queries = queryRunnerOf(app);
58
+ const action = actions.findActionByName(targetName);
49
59
  if (action) {
50
60
  const { tenant, parsed } = parseFlags(rest);
51
61
  try {
52
- const validated = action.schema.parse(parsed);
62
+ const validated = action.input.parse(parsed);
53
63
  const envelope = tenant ? seedEnvelope({ tenant }) : undefined;
54
- await dispatcher.dispatch(action, validated, envelope);
64
+ await actions.dispatch(action, validated, envelope);
55
65
  stdout("OK");
56
66
  return 0;
57
67
  }
@@ -61,10 +71,10 @@ export async function runCli(app, argv, options = {}) {
61
71
  }
62
72
  }
63
73
  // Fall back to query.
64
- if (dispatcher.listQueries().includes(targetName)) {
74
+ if (queries?.listQueries().includes(targetName)) {
65
75
  const { tenant, parsed } = parseFlags(rest);
66
76
  try {
67
- const result = await dispatcher.query(targetName, parsed, tenant);
77
+ const result = await queries.run(targetName, parsed, tenant);
68
78
  stdout(JSON.stringify(result, null, 2));
69
79
  return 0;
70
80
  }
@@ -77,15 +87,16 @@ export async function runCli(app, argv, options = {}) {
77
87
  return 1;
78
88
  }
79
89
  function printHelp(app, stdout) {
80
- const dispatcher = forgeDispatcher(app);
90
+ const actions = actionRunnerOf(app);
91
+ const queries = queryRunnerOf(app);
81
92
  stdout("nwire — action CLI runner\n");
82
93
  stdout("Usage: amit <action-or-query-name> [--field value ...] [--tenant <id>]\n");
83
94
  stdout("Actions:");
84
- for (const name of dispatcher.listHandlers()) {
95
+ for (const name of actions.listHandlers()) {
85
96
  stdout(` ${name}`);
86
97
  }
87
98
  stdout("Queries:");
88
- for (const name of dispatcher.listQueries()) {
99
+ for (const name of queries?.listQueries() ?? []) {
89
100
  stdout(` ${name}`);
90
101
  }
91
102
  }
package/dist/index.d.ts CHANGED
@@ -13,8 +13,9 @@
13
13
  * const dispatcher = forgeDispatcher(app);
14
14
  * await dispatcher.dispatch(placeOrder, { … });
15
15
  */
16
- export { forgePlugin, createForgePlugin, forgeDispatcher, type ForgePluginOptions, FORGE_DISPATCHER_BINDING, } from "./runtime/forge-plugin.js";
16
+ export { forgePlugins, type ForgeOptions } from "./runtime/forge-plugins.js";
17
17
  export { withForge } from "./runtime/with-forge.js";
18
+ export { externalCallsPlugin, ExternalCallRunner, FORGE_EXTERNAL_CALLS_BINDING, } from "./plugins/external-calls-plugin.js";
18
19
  export { actorsPlugin, FORGE_ACTOR_CHAIN_BINDING, FORGE_ACTOR_STORE_BINDING, type ActorsPluginOptions, } from "./plugins/actors-plugin.js";
19
20
  export { ActorChainRunner, type ActorTransitionListener } from "./plugins/actors-chain.js";
20
21
  export { projectionsPlugin, FORGE_PROJECTION_CHAIN_BINDING, FORGE_PROJECTION_STORE_BINDING, type ProjectionsPluginOptions, } from "./plugins/projections-plugin.js";
@@ -23,21 +24,20 @@ export { workflowsPlugin, FORGE_WORKFLOW_CHAIN_BINDING, FORGE_WORKFLOW_TIMER_STO
23
24
  export { WorkflowChainRunner, type WorkflowEffectsFactory } from "./plugins/workflows-chain.js";
24
25
  export { queriesPlugin, FORGE_QUERY_RUNNER_BINDING } from "./plugins/queries-plugin.js";
25
26
  export { QueryRunner } from "./plugins/queries-chain.js";
26
- export { actionsPlugin, FORGE_ACTION_RUNNER_BINDING, FORGE_DEAD_LETTER_SINK_BINDING, type ActionsPluginOptions, } from "./plugins/actions-plugin.js";
27
- export { ActionRunner, type PublishEventsFn, type CtxAugmenter } from "./plugins/actions-chain.js";
27
+ export { actionsPlugin, FORGE_ACTION_RUNNER_BINDING, FORGE_DEAD_LETTER_SINK_BINDING, FORGE_PUBLISH_BINDING, FORGE_PUBLIC_EVENTS_BINDING, type ActionsPluginOptions, } from "./plugins/actions-plugin.js";
28
+ export { ActionRunner, type PublishEventsFn } from "./plugins/actions-chain.js";
28
29
  export { idempotencyPlugin, FORGE_IDEMPOTENCY_STORE_BINDING, type IdempotencyPluginOptions, } from "./plugins/idempotency-plugin.js";
29
30
  export { dlqPlugin, type DlqPluginOptions } from "./plugins/dlq-plugin.js";
30
- export { ForgeDispatcher, type ForgeDispatcherOptions } from "./runtime/forge-dispatcher.js";
31
31
  export type { ForgeTelemetry, DispatchOptions, ActionBeforeHookCtx, ActionAfterHookCtx, ActorTransitionHookCtx, WorkflowFireHookCtx, } from "./runtime/forge-types.js";
32
32
  export { defineAction, isCommandMessage, resolveDispatch, type ActionDefinition, type CallableActionDefinition, type CommandMessage, type ActionMeta, type ActionInput, type ActionResult, type ActionPolicy, type NwireActionRegistry, type RetryPolicy, } from "./primitives/define-action.js";
33
- export { defineActor, eventKey, type ActorDefinition, type ActorOptions, type ActorOptionsBound, type ActorReaction, type ActorStateConfig, type ActorTimerSpec, type ActorMethod, type ActorInstanceView, } from "./primitives/define-actor.js";
33
+ export { defineActor, eventKey, type ActorDefinition, type ActorOptions, type ActorBuilderContext, type StateRef, type ActorReaction, type ActorStateConfig, type ActorTimerSpec, type ActorMethod, type ActorInstanceView, } from "./primitives/define-actor.js";
34
34
  export { defineSchema, isSchemaDefinition, type SchemaDefinition, type SchemaOptions, type SchemaStateSpec, type SchemaStorageHints, } from "./primitives/define-schema.js";
35
35
  export { validate, isInvariantError, InvariantError, type Invariant } from "./helpers/validate.js";
36
- export { defineHandler, type HandlerContext, type HandlerDefinition, type HandlerReturn, } from "./primitives/define-handler.js";
36
+ export { type HandlerContext, type HandlerReturn } from "./primitives/define-handler.js";
37
37
  export { eventFactory, isEventMessage, normalizeEventReturn, type EventMessage, type EventFactory, } from "./messages/event-message.js";
38
38
  export { defineWorkflow, COMPLETE_EVENT_NAME, type WorkflowDefinition, type WorkflowContext, type StatelessWorkflowContext, type StatefulWorkflowContext, type WorkflowEffects, type WorkflowOptions, type StatelessWorkflowOptions, type StatefulWorkflowOptions, type WorkflowOnOptions, type WorkflowRetryPolicy, type WorkflowStateSpec, type WorkflowInstance, type WorkflowCorrelateMap, type FireContext, type CompleteEvent, type StateCallable, type TimerDefinition, } from "./primitives/define-workflow.js";
39
39
  export { defineProjection, type ProjectionDefinition, type ProjectionOptions, type ProjectionReducer, } from "./primitives/define-projection.js";
40
- export { defineQuery, type QueryDefinition, type QueryOptions } from "./primitives/define-query.js";
40
+ export { defineQuery, type QueryDefinition, type QueryOptions, type QueryContext, } from "./primitives/define-query.js";
41
41
  export { defineUpcaster, applyUpcasters, type UpcasterDefinition, } from "./primitives/define-upcaster.js";
42
42
  export { defineExternalCall, type ExternalCallDefinition, type ExternalCallMeta, type ExternalCallTarget, type ExternalCallSlo, type ExternalCallRetry, type ExternalCallExecutor, } from "./primitives/define-external-call.js";
43
43
  export { defineInboundWebhook, type InboundWebhookDefinition, type InboundWebhookMeta, type WebhookSignatureVerifier, type WebhookDedupe, } from "./primitives/define-inbound-webhook.js";
@@ -57,5 +57,6 @@ export { NoopLogger, ConsoleLogger, loggerForEnvelope, type Logger } from "@nwir
57
57
  export { InMemoryDeadLetterSink, buildDeadLetterEntry, type DeadLetterSink, type DeadLetterEntry, } from "@nwire/dead-letter";
58
58
  export { defineResource, isResourceDefinition, type ResourceDefinition, type DefineResourceOptions, } from "@nwire/handler";
59
59
  export { response, isResponseSpec, isResponseInstance, ok, created, accepted, noContent, notModified, gone, type ResponseSpec, type ListResponseSpec, type ResponseInstance, type ResponseKind, } from "./helpers/response.js";
60
+ export { messageRef, type MessageRef } from "@nwire/app";
60
61
  import "./framework-events.js";
61
62
  export type { FrameworkHooks, PluginKind } from "@nwire/app";
package/dist/index.js CHANGED
@@ -13,9 +13,10 @@
13
13
  * const dispatcher = forgeDispatcher(app);
14
14
  * await dispatcher.dispatch(placeOrder, { … });
15
15
  */
16
- // ─── Plugin + composition ──────────────────────────────────────────
17
- export { forgePlugin, createForgePlugin, forgeDispatcher, FORGE_DISPATCHER_BINDING, } from "./runtime/forge-plugin.js";
16
+ // ─── Plugin set + composition ──────────────────────────────────────
17
+ export { forgePlugins } from "./runtime/forge-plugins.js";
18
18
  export { withForge } from "./runtime/with-forge.js";
19
+ export { externalCallsPlugin, ExternalCallRunner, FORGE_EXTERNAL_CALLS_BINDING, } from "./plugins/external-calls-plugin.js";
19
20
  export { actorsPlugin, FORGE_ACTOR_CHAIN_BINDING, FORGE_ACTOR_STORE_BINDING, } from "./plugins/actors-plugin.js";
20
21
  export { ActorChainRunner } from "./plugins/actors-chain.js";
21
22
  export { projectionsPlugin, FORGE_PROJECTION_CHAIN_BINDING, FORGE_PROJECTION_STORE_BINDING, } from "./plugins/projections-plugin.js";
@@ -24,21 +25,19 @@ export { workflowsPlugin, FORGE_WORKFLOW_CHAIN_BINDING, FORGE_WORKFLOW_TIMER_STO
24
25
  export { WorkflowChainRunner } from "./plugins/workflows-chain.js";
25
26
  export { queriesPlugin, FORGE_QUERY_RUNNER_BINDING } from "./plugins/queries-plugin.js";
26
27
  export { QueryRunner } from "./plugins/queries-chain.js";
27
- export { actionsPlugin, FORGE_ACTION_RUNNER_BINDING, FORGE_DEAD_LETTER_SINK_BINDING, } from "./plugins/actions-plugin.js";
28
+ export { actionsPlugin, FORGE_ACTION_RUNNER_BINDING, FORGE_DEAD_LETTER_SINK_BINDING, FORGE_PUBLISH_BINDING, FORGE_PUBLIC_EVENTS_BINDING, } from "./plugins/actions-plugin.js";
28
29
  export { ActionRunner } from "./plugins/actions-chain.js";
29
30
  export { idempotencyPlugin, FORGE_IDEMPOTENCY_STORE_BINDING, } from "./plugins/idempotency-plugin.js";
30
31
  export { dlqPlugin } from "./plugins/dlq-plugin.js";
31
- export { ForgeDispatcher } from "./runtime/forge-dispatcher.js";
32
32
  // ─── Domain primitives ─────────────────────────────────────────────
33
33
  export { defineAction, isCommandMessage, resolveDispatch, } from "./primitives/define-action.js";
34
34
  export { defineActor, eventKey, } from "./primitives/define-actor.js";
35
35
  export { defineSchema, isSchemaDefinition, } from "./primitives/define-schema.js";
36
36
  export { validate, isInvariantError, InvariantError } from "./helpers/validate.js";
37
- export { defineHandler, } from "./primitives/define-handler.js";
38
37
  export { eventFactory, isEventMessage, normalizeEventReturn, } from "./messages/event-message.js";
39
38
  export { defineWorkflow, COMPLETE_EVENT_NAME, } from "./primitives/define-workflow.js";
40
39
  export { defineProjection, } from "./primitives/define-projection.js";
41
- export { defineQuery } from "./primitives/define-query.js";
40
+ export { defineQuery, } from "./primitives/define-query.js";
42
41
  export { defineUpcaster, applyUpcasters, } from "./primitives/define-upcaster.js";
43
42
  export { defineExternalCall, } from "./primitives/define-external-call.js";
44
43
  export { defineInboundWebhook, } from "./primitives/define-inbound-webhook.js";
@@ -61,6 +60,8 @@ export { NoopLogger, ConsoleLogger, loggerForEnvelope } from "@nwire/logger";
61
60
  export { InMemoryDeadLetterSink, buildDeadLetterEntry, } from "@nwire/dead-letter";
62
61
  export { defineResource, isResourceDefinition, } from "@nwire/handler";
63
62
  export { response, isResponseSpec, isResponseInstance, ok, created, accepted, noContent, notModified, gone, } from "./helpers/response.js";
63
+ // Message handle returned by the async dispatch verbs (`ctx.send`).
64
+ export { messageRef } from "@nwire/app";
64
65
  // Module augmentation side effect — adds Action* / Event* slots to
65
66
  // FrameworkHooks on @nwire/app's runtime.
66
67
  import "./framework-events.js";
@@ -1,48 +1,65 @@
1
1
  /**
2
- * Action runner — handler registry + dispatch loop.
2
+ * Action runner — the forge command pipeline, composed onto the real handler's
3
+ * own hook chain. There is no parallel dispatcher and no `run`-override:
4
+ * `install(action)` attaches the pipeline as a high-priority `.use()` step on
5
+ * the action handler's hook, so dispatching it through `runtime.execute` (from
6
+ * a wire, queue, cron, or another handler's `ctx.request`) runs retry / DLQ /
7
+ * ActionDispatching / before-after / telemetry / event-publish as the outermost
8
+ * step. The step invokes the handler body directly in its retry loop and
9
+ * short-circuits (`@nwire/hooks` `compose` forbids calling `next()` twice, so
10
+ * retry cannot loop the chain).
3
11
  *
4
- * Owns the per-action retry policy, DLQ integration, and the
5
- * `action.dispatched` / `action.completed` / `action.failed` / `dlq.recorded`
6
- * telemetry. Calls `runtime.hooks.ActionDispatching` (vetoable),
7
- * `ActionCompleted`, and `ActionFailed` at the right transitions.
8
- *
9
- * The runner builds a handler ctx with `resolve`, `request`, `send`, and
10
- * `query` wired through the container. Returned events from the handler
11
- * flow into the `EventPublishing` chain via the injected `publishEvents`
12
- * callback.
12
+ * The handler ctx (input, envelope, request/send/emit/query/actor/…) is built
13
+ * by `runtime.execute`'s capability ctx each forge concern plugin contributes
14
+ * its verb. This runner owns the pipeline + the per-action before/after hooks +
15
+ * a `dispatch` convenience; events a handler returns flow out via the injected
16
+ * `publishEvents` (local LocalDelivery fold + outbound sink for public).
13
17
  */
14
18
  import { type Runtime } from "@nwire/app";
15
- import { type MessageEnvelope } from "@nwire/envelope";
19
+ import type { MessageEnvelope } from "@nwire/envelope";
16
20
  import { type Hook } from "@nwire/hooks";
17
21
  import type { Container } from "@nwire/container";
18
22
  import { type DeadLetterSink } from "@nwire/dead-letter";
19
23
  import type { ActionDefinition, ActionInput, ActionResult } from "../primitives/define-action.js";
20
- import type { HandlerContext, HandlerDefinition } from "../primitives/define-handler.js";
24
+ import type { HandlerContext } from "../primitives/define-handler.js";
21
25
  import type { ActionBeforeHookCtx, ActionAfterHookCtx, DispatchOptions } from "../runtime/forge-types.js";
22
26
  import { type EventMessage } from "../messages/event-message.js";
23
- /** Publish callback — injected so the runner doesn't know about the chain hook directly. */
27
+ /** Publish callback — injected so the runner doesn't own the outbound concern. */
24
28
  export type PublishEventsFn = (events: readonly EventMessage[], envelope: MessageEnvelope) => Promise<void>;
25
- /** Optional ctx augmenter — actorsPlugin / externalCallsPlugin can extend ctx.use etc. */
26
- export type CtxAugmenter = (ctx: HandlerContext, envelope: MessageEnvelope) => void;
27
29
  export declare class ActionRunner {
28
30
  private readonly runtime;
29
31
  private readonly container;
30
32
  private readonly deadLetterSink;
31
33
  private readonly publishEvents;
32
- readonly handlers: Map<string, HandlerDefinition<any>>;
34
+ readonly handlers: Map<string, ActionDefinition<any>>;
33
35
  readonly beforeHooks: Map<string, Hook<ActionBeforeHookCtx>>;
34
36
  readonly afterHooks: Map<string, Hook<ActionAfterHookCtx>>;
35
- readonly augmenters: CtxAugmenter[];
36
37
  constructor(runtime: Runtime, container: Container, deadLetterSink: DeadLetterSink, publishEvents: PublishEventsFn);
37
- register(handler: HandlerDefinition<any>): void;
38
+ /**
39
+ * Compose the command pipeline onto an action's own hook chain. The action is
40
+ * already registered on the runtime (by the app); this only attaches behavior.
41
+ */
42
+ install(action: ActionDefinition<any>): void;
38
43
  /** Action handler names, in registration order. */
39
44
  listHandlers(): readonly string[];
40
45
  findActionByName(name: string): ActionDefinition<any> | undefined;
46
+ findHandler(name: string): ActionDefinition<any> | undefined;
47
+ hasHandler(name: string): boolean;
41
48
  ensureBeforeHook(name: string): Hook<ActionBeforeHookCtx>;
42
49
  ensureAfterHook(name: string): Hook<ActionAfterHookCtx>;
43
- /** Plugins can hang extra ctx members on each handler invocation. */
44
- registerCtxAugmenter(fn: CtxAugmenter): void;
50
+ /**
51
+ * Thin dispatch convenience — routes through `runtime.execute`, whose pinned
52
+ * core runs the action's hook chain (this pipeline). Used by `ctx.request`
53
+ * and any caller holding the action reference.
54
+ */
45
55
  dispatch<A extends ActionDefinition>(action: A, input: ActionInput<A>, parentEnvelope?: MessageEnvelope, opts?: DispatchOptions): Promise<ActionResult<A>>;
46
- /** Build the handler ctx for one dispatch. */
47
- private buildHandlerContext;
56
+ /**
57
+ * The command pipeline — runs as the action's outermost hook step under
58
+ * `runtime.execute`: `action.dispatched` telemetry, the `ActionDispatching`
59
+ * veto hook, per-action before/after hooks, retry + backoff invoking the
60
+ * handler body, dead-letter on exhaustion, completion/failure telemetry, and
61
+ * event-return publishing. `ctx` is execute's capability ctx; `rawHandler` is
62
+ * the action's adapted `(ctx)` body.
63
+ */
64
+ runActionPipeline(action: ActionDefinition<any>, rawHandler: (ctx: HandlerContext) => unknown, ctx: HandlerContext, signal: AbortSignal): Promise<unknown>;
48
65
  }