@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.
- package/README.md +100 -83
- package/dist/framework-events.d.ts +8 -37
- package/dist/framework-events.js +7 -3
- package/dist/helpers/cli-runner.js +21 -10
- package/dist/index.d.ts +8 -7
- package/dist/index.js +7 -6
- package/dist/plugins/actions-chain.d.ts +39 -22
- package/dist/plugins/actions-chain.js +117 -78
- package/dist/plugins/actions-plugin.d.ts +26 -23
- package/dist/plugins/actions-plugin.js +122 -44
- package/dist/plugins/actors-chain.d.ts +9 -2
- package/dist/plugins/actors-chain.js +62 -2
- package/dist/plugins/actors-plugin.d.ts +1 -1
- package/dist/plugins/actors-plugin.js +24 -14
- package/dist/plugins/external-calls-plugin.d.ts +28 -0
- package/dist/plugins/external-calls-plugin.js +136 -0
- package/dist/plugins/idempotency-plugin.d.ts +15 -1
- package/dist/plugins/idempotency-plugin.js +56 -11
- package/dist/plugins/projections-chain.d.ts +2 -2
- package/dist/plugins/projections-chain.js +2 -2
- package/dist/plugins/projections-plugin.d.ts +1 -1
- package/dist/plugins/projections-plugin.js +4 -13
- package/dist/plugins/queries-chain.d.ts +4 -3
- package/dist/plugins/queries-chain.js +8 -5
- package/dist/plugins/queries-plugin.d.ts +15 -29
- package/dist/plugins/queries-plugin.js +36 -49
- package/dist/plugins/workflows-chain.d.ts +9 -2
- package/dist/plugins/workflows-chain.js +19 -1
- package/dist/plugins/workflows-plugin.d.ts +1 -1
- package/dist/plugins/workflows-plugin.js +12 -20
- package/dist/primitives/define-action.d.ts +80 -115
- package/dist/primitives/define-action.js +111 -56
- package/dist/primitives/define-actor.d.ts +103 -214
- package/dist/primitives/define-actor.js +157 -216
- package/dist/primitives/define-handler.d.ts +42 -112
- package/dist/primitives/define-handler.js +14 -45
- package/dist/primitives/define-projection.d.ts +23 -28
- package/dist/primitives/define-projection.js +29 -32
- package/dist/primitives/define-query.d.ts +52 -42
- package/dist/primitives/define-query.js +65 -28
- package/dist/primitives/define-workflow.d.ts +8 -11
- package/dist/primitives/define-workflow.js +14 -8
- package/dist/runtime/forge-dispatcher.d.ts +30 -12
- package/dist/runtime/forge-dispatcher.js +199 -237
- package/dist/runtime/forge-plugin.d.ts +8 -0
- package/dist/runtime/forge-plugin.js +113 -17
- package/dist/runtime/forge-plugins.d.ts +55 -0
- package/dist/runtime/forge-plugins.js +57 -0
- package/dist/runtime/forge-types.d.ts +8 -2
- package/dist/runtime/with-forge.d.ts +8 -11
- package/dist/runtime/with-forge.js +9 -11
- package/dist/stores/idempotency-store.d.ts +1 -1
- 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
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
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 {
|
|
51
|
+
import { defineQuery } from "@nwire/forge";
|
|
34
52
|
|
|
35
|
-
export const
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
59
|
-
|
|
75
|
+
handlers: [placeOrder, listOrders],
|
|
76
|
+
plugins: [...forgePlugins({ actors: [subscription], projections: [OrdersDashboard] })],
|
|
60
77
|
});
|
|
61
78
|
|
|
62
|
-
// HTTP
|
|
63
|
-
app.wire(post("/orders", { body: placeOrder.
|
|
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
|
-
|
|
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
|
|
82
|
-
and surfaces telemetry. HTTP and queue
|
|
83
|
-
|
|
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
|
-
|
|
104
|
+
const SubscriptionData = defineSchema({
|
|
91
105
|
name: "subscription",
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
105
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
125
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
142
|
-
`
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
- **
|
|
165
|
-
`
|
|
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
|
|
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
|
|
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 `
|
|
88
|
-
export declare const forgeFrameworkSlots: readonly ["ActionDispatching", "ActionCompleted", "ActionFailed", "EventRecording", "EventRecorded"
|
|
89
|
-
/**
|
|
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;
|
package/dist/framework-events.js
CHANGED
|
@@ -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 `
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
|
48
|
-
const
|
|
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.
|
|
62
|
+
const validated = action.input.parse(parsed);
|
|
53
63
|
const envelope = tenant ? seedEnvelope({ tenant }) : undefined;
|
|
54
|
-
await
|
|
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 (
|
|
74
|
+
if (queries?.listQueries().includes(targetName)) {
|
|
65
75
|
const { tenant, parsed } = parseFlags(rest);
|
|
66
76
|
try {
|
|
67
|
-
const result = await
|
|
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
|
|
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
|
|
95
|
+
for (const name of actions.listHandlers()) {
|
|
85
96
|
stdout(` ${name}`);
|
|
86
97
|
}
|
|
87
98
|
stdout("Queries:");
|
|
88
|
-
for (const name of
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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 —
|
|
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
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
/**
|
|
44
|
-
|
|
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
|
-
/**
|
|
47
|
-
|
|
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
|
}
|