@nwire/forge 0.12.1 → 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 -31
- 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
|
@@ -1,41 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `queriesPlugin` —
|
|
2
|
+
* `queriesPlugin` — the forge read concern as a runtime-native plugin.
|
|
3
3
|
*
|
|
4
4
|
* import { createApp } from "@nwire/app";
|
|
5
|
-
* import {
|
|
6
|
-
* projectionsPlugin,
|
|
7
|
-
* queriesPlugin,
|
|
8
|
-
* forgePlugin,
|
|
9
|
-
* } from "@nwire/forge";
|
|
5
|
+
* import { projectionsPlugin, queriesPlugin } from "@nwire/forge";
|
|
10
6
|
*
|
|
11
7
|
* const app = createApp({
|
|
12
8
|
* appName: "orders",
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* projectionsPlugin([OrdersDashboard]),
|
|
16
|
-
* queriesPlugin([listOrders, getOrderById]),
|
|
17
|
-
* ],
|
|
9
|
+
* handlers: [listOrders, getOrderById], // the app registers queries
|
|
10
|
+
* plugins: [projectionsPlugin([OrdersDashboard]), queriesPlugin()],
|
|
18
11
|
* });
|
|
19
12
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Queries don't participate in the EventPublishing chain. They execute
|
|
27
|
-
* on demand against either a projection's per-tenant state (when
|
|
28
|
-
* `projection + execute` is set) or a handler closure. Projection state
|
|
29
|
-
* comes from the `forge.projectionStore` binding, which `projectionsPlugin`
|
|
30
|
-
* or `forgePlugin` provides.
|
|
13
|
+
* It owns no query list. A query IS a handler (`defineQuery =
|
|
14
|
+
* defineHandler.kind("query")`), registered by the app via `createApp({
|
|
15
|
+
* handlers })`. At setup this plugin scans the runtime for `config.kind ===
|
|
16
|
+
* "query"` and registers each into the read engine (`QueryRunner`), which loads
|
|
17
|
+
* projection state or runs the handler closure on demand. Queries do not
|
|
18
|
+
* participate in the LocalDelivery chain.
|
|
31
19
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* the
|
|
36
|
-
* flags the duplicate install.
|
|
20
|
+
* Owns:
|
|
21
|
+
* - the `QueryRunner` (read registry + execution) and the
|
|
22
|
+
* `forge.queryRunner` container binding.
|
|
23
|
+
* - the `ctx.query` verb — a light read scoped to the envelope's tenant.
|
|
37
24
|
*/
|
|
38
25
|
import type { PluginDefinition } from "@nwire/app";
|
|
39
|
-
import type { QueryDefinition } from "../primitives/define-query.js";
|
|
40
26
|
export declare const FORGE_QUERY_RUNNER_BINDING: "forge.queryRunner";
|
|
41
|
-
export declare function queriesPlugin(
|
|
27
|
+
export declare function queriesPlugin(): PluginDefinition;
|
|
@@ -1,73 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `queriesPlugin` —
|
|
2
|
+
* `queriesPlugin` — the forge read concern as a runtime-native plugin.
|
|
3
3
|
*
|
|
4
4
|
* import { createApp } from "@nwire/app";
|
|
5
|
-
* import {
|
|
6
|
-
* projectionsPlugin,
|
|
7
|
-
* queriesPlugin,
|
|
8
|
-
* forgePlugin,
|
|
9
|
-
* } from "@nwire/forge";
|
|
5
|
+
* import { projectionsPlugin, queriesPlugin } from "@nwire/forge";
|
|
10
6
|
*
|
|
11
7
|
* const app = createApp({
|
|
12
8
|
* appName: "orders",
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* projectionsPlugin([OrdersDashboard]),
|
|
16
|
-
* queriesPlugin([listOrders, getOrderById]),
|
|
17
|
-
* ],
|
|
9
|
+
* handlers: [listOrders, getOrderById], // the app registers queries
|
|
10
|
+
* plugins: [projectionsPlugin([OrdersDashboard]), queriesPlugin()],
|
|
18
11
|
* });
|
|
19
12
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Queries don't participate in the EventPublishing chain. They execute
|
|
27
|
-
* on demand against either a projection's per-tenant state (when
|
|
28
|
-
* `projection + execute` is set) or a handler closure. Projection state
|
|
29
|
-
* comes from the `forge.projectionStore` binding, which `projectionsPlugin`
|
|
30
|
-
* or `forgePlugin` provides.
|
|
13
|
+
* It owns no query list. A query IS a handler (`defineQuery =
|
|
14
|
+
* defineHandler.kind("query")`), registered by the app via `createApp({
|
|
15
|
+
* handlers })`. At setup this plugin scans the runtime for `config.kind ===
|
|
16
|
+
* "query"` and registers each into the read engine (`QueryRunner`), which loads
|
|
17
|
+
* projection state or runs the handler closure on demand. Queries do not
|
|
18
|
+
* participate in the LocalDelivery chain.
|
|
31
19
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* the
|
|
36
|
-
* flags the duplicate install.
|
|
20
|
+
* Owns:
|
|
21
|
+
* - the `QueryRunner` (read registry + execution) and the
|
|
22
|
+
* `forge.queryRunner` container binding.
|
|
23
|
+
* - the `ctx.query` verb — a light read scoped to the envelope's tenant.
|
|
37
24
|
*/
|
|
38
25
|
import { FORGE_PROJECTION_STORE_BINDING } from "./projections-plugin.js";
|
|
39
26
|
import { QueryRunner } from "./queries-chain.js";
|
|
40
27
|
export const FORGE_QUERY_RUNNER_BINDING = "forge.queryRunner";
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
/** Is this registered handler a forge query? */
|
|
29
|
+
function isQueryHandler(h) {
|
|
30
|
+
return (h != null &&
|
|
31
|
+
(typeof h === "object" || typeof h === "function") &&
|
|
32
|
+
h.config?.kind === "query");
|
|
33
|
+
}
|
|
34
|
+
export function queriesPlugin() {
|
|
44
35
|
return {
|
|
45
36
|
name: "forge.queries",
|
|
46
|
-
setup({ runtime, container
|
|
37
|
+
setup({ runtime, container }) {
|
|
47
38
|
if (!container.has(FORGE_PROJECTION_STORE_BINDING)) {
|
|
48
39
|
throw new Error(`queriesPlugin: ${FORGE_PROJECTION_STORE_BINDING} is not bound. ` +
|
|
49
|
-
`Install projectionsPlugin
|
|
40
|
+
`Install projectionsPlugin before queriesPlugin so the projection store exists.`);
|
|
50
41
|
}
|
|
51
42
|
const projectionStore = container.resolve(FORGE_PROJECTION_STORE_BINDING);
|
|
52
43
|
const runner = new QueryRunner(runtime, container, projectionStore);
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
// Scan the runtime's registered handlers — the app owns registration.
|
|
45
|
+
for (const name of runtime.listHandlers()) {
|
|
46
|
+
const handler = runtime.getHandler(name);
|
|
47
|
+
if (isQueryHandler(handler))
|
|
48
|
+
runner.register(handler);
|
|
49
|
+
}
|
|
55
50
|
container.register(FORGE_QUERY_RUNNER_BINDING, runner);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: undefined;
|
|
64
|
-
const bundledNames = dispatcher?.listQueries?.() ?? [];
|
|
65
|
-
const overlap = runner.listQueries().filter((n) => bundledNames.includes(n));
|
|
66
|
-
if (overlap.length > 0) {
|
|
67
|
-
// eslint-disable-next-line no-console
|
|
68
|
-
console.warn(`queriesPlugin: ${overlap.length} query name(s) (${overlap.join(", ")}) are also registered on forgePlugin's bundled queries. ` +
|
|
69
|
-
`Install either queriesPlugin OR forgePlugin's options.queries path, not both.`);
|
|
70
|
-
}
|
|
51
|
+
// Contribute the `ctx.query` verb — a light read scoped to the envelope's
|
|
52
|
+
// tenant; the runner routes projection-form vs handler-form.
|
|
53
|
+
runtime.add({
|
|
54
|
+
name: "forge.query-ctx",
|
|
55
|
+
provideCtx: ({ envelope }) => ({
|
|
56
|
+
query: (queryDef, input) => runner.run(queryDef.name, input, envelope.tenant ?? "", envelope),
|
|
57
|
+
}),
|
|
71
58
|
});
|
|
72
59
|
},
|
|
73
60
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow chain runner — the
|
|
2
|
+
* Workflow chain runner — the LocalDelivery step that drives every
|
|
3
3
|
* registered workflow whose subscribed events include the current one.
|
|
4
4
|
*
|
|
5
5
|
* Self-contained: takes its dependencies (runtime, timer store, effects
|
|
@@ -22,7 +22,7 @@ import { type Runtime } from "@nwire/app";
|
|
|
22
22
|
import type { WorkflowDefinition, WorkflowInstance, WorkflowEffects } from "../primitives/define-workflow.js";
|
|
23
23
|
import { type EventMessage } from "../messages/event-message.js";
|
|
24
24
|
import type { WorkflowFireHookCtx } from "../runtime/forge-types.js";
|
|
25
|
-
import type
|
|
25
|
+
import { type WorkflowTimerStore } from "../stores/workflow-timer-store.js";
|
|
26
26
|
/**
|
|
27
27
|
* Resolves the dispatch effects (send / enqueue / publish) the workflow
|
|
28
28
|
* sees on each fire. Returned in a fresh closure per event so envelope
|
|
@@ -48,4 +48,11 @@ export declare class WorkflowChainRunner {
|
|
|
48
48
|
instanceStore(workflowName: string): Map<string, WorkflowInstance>;
|
|
49
49
|
/** Fire every workflow that subscribes to this event. */
|
|
50
50
|
apply(event: EventMessage, envelope: MessageEnvelope, correlationKeyOverride?: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Drain every saga timer due at `now` and route each back into its
|
|
53
|
+
* workflow. Each fire reuses the normal `apply` path with the timer's
|
|
54
|
+
* `correlationKey` as the override, so the timer reaches the saga instance
|
|
55
|
+
* that scheduled it. Returns the number of timers fired.
|
|
56
|
+
*/
|
|
57
|
+
fireDueWorkflowTimers(now?: Date): Promise<number>;
|
|
51
58
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow chain runner — the
|
|
2
|
+
* Workflow chain runner — the LocalDelivery step that drives every
|
|
3
3
|
* registered workflow whose subscribed events include the current one.
|
|
4
4
|
*
|
|
5
5
|
* Self-contained: takes its dependencies (runtime, timer store, effects
|
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { hook } from "@nwire/hooks";
|
|
21
|
+
import { seedEnvelope } from "@nwire/envelope";
|
|
21
22
|
import { loggerForEnvelope } from "@nwire/logger";
|
|
22
23
|
import { serializeError } from "@nwire/app";
|
|
23
24
|
import { eventFactory } from "../messages/event-message.js";
|
|
25
|
+
import { timerEventName } from "../stores/workflow-timer-store.js";
|
|
24
26
|
import { computeBackoff, parseDelay, sleep } from "../helpers/retry-helpers.js";
|
|
25
27
|
export class WorkflowChainRunner {
|
|
26
28
|
runtime;
|
|
@@ -205,4 +207,20 @@ export class WorkflowChainRunner {
|
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Drain every saga timer due at `now` and route each back into its
|
|
212
|
+
* workflow. Each fire reuses the normal `apply` path with the timer's
|
|
213
|
+
* `correlationKey` as the override, so the timer reaches the saga instance
|
|
214
|
+
* that scheduled it. Returns the number of timers fired.
|
|
215
|
+
*/
|
|
216
|
+
async fireDueWorkflowTimers(now = new Date()) {
|
|
217
|
+
let fired = 0;
|
|
218
|
+
const envelope = seedEnvelope({});
|
|
219
|
+
for await (const timer of this.timerStore.drainDue(now)) {
|
|
220
|
+
const eventName = timerEventName(timer.workflowName, timer.timerName);
|
|
221
|
+
await this.apply({ eventName, payload: timer.payload }, envelope, timer.correlationKey);
|
|
222
|
+
fired++;
|
|
223
|
+
}
|
|
224
|
+
return fired;
|
|
225
|
+
}
|
|
208
226
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Owns:
|
|
16
16
|
* - the `forge.workflowTimerStore` container binding
|
|
17
17
|
* - the workflow registry (private to the plugin's `WorkflowChainRunner`)
|
|
18
|
-
* - the `forge.publish.workflows` step on the
|
|
18
|
+
* - the `forge.publish.workflows` step on the LocalDelivery chain
|
|
19
19
|
* (priority 400 — between projections and bus delivery)
|
|
20
20
|
* - the `forge.workflowChain` container binding so other plugins can
|
|
21
21
|
* resolve the runner (the timer scheduler fires through it)
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Owns:
|
|
16
16
|
* - the `forge.workflowTimerStore` container binding
|
|
17
17
|
* - the workflow registry (private to the plugin's `WorkflowChainRunner`)
|
|
18
|
-
* - the `forge.publish.workflows` step on the
|
|
18
|
+
* - the `forge.publish.workflows` step on the LocalDelivery chain
|
|
19
19
|
* (priority 400 — between projections and bus delivery)
|
|
20
20
|
* - the `forge.workflowChain` container binding so other plugins can
|
|
21
21
|
* resolve the runner (the timer scheduler fires through it)
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* fires at `AppReady` when both attachments are present.
|
|
30
30
|
*/
|
|
31
31
|
import { InMemoryWorkflowTimerStore, } from "../stores/workflow-timer-store.js";
|
|
32
|
-
import {
|
|
32
|
+
import { FORGE_ACTION_RUNNER_BINDING, FORGE_PUBLISH_BINDING } from "./actions-plugin.js";
|
|
33
33
|
import { EVENT_PUBLISHING_PRIORITIES } from "../framework-events.js";
|
|
34
34
|
import { WorkflowChainRunner } from "./workflows-chain.js";
|
|
35
35
|
export const FORGE_WORKFLOW_CHAIN_BINDING = "forge.workflowChain";
|
|
@@ -41,21 +41,22 @@ export function workflowsPlugin(workflows, opts = {}) {
|
|
|
41
41
|
const store = opts.workflowTimerStore ?? new InMemoryWorkflowTimerStore();
|
|
42
42
|
bind(FORGE_WORKFLOW_TIMER_STORE_BINDING, () => store);
|
|
43
43
|
},
|
|
44
|
-
setup({ runtime, container
|
|
44
|
+
setup({ runtime, container }) {
|
|
45
45
|
const timerStore = container.resolve(FORGE_WORKFLOW_TIMER_STORE_BINDING);
|
|
46
46
|
const chain = new WorkflowChainRunner(runtime, timerStore, (envelope) => {
|
|
47
|
-
// Effects resolve the
|
|
48
|
-
// the time a workflow actually fires.
|
|
49
|
-
const
|
|
47
|
+
// Effects resolve the action runner + shared publish lazily so the
|
|
48
|
+
// bindings exist by the time a workflow actually fires.
|
|
49
|
+
const runner = container.resolve(FORGE_ACTION_RUNNER_BINDING);
|
|
50
|
+
const publish = container.resolve(FORGE_PUBLISH_BINDING);
|
|
50
51
|
const effects = {
|
|
51
52
|
async send(action, input) {
|
|
52
|
-
return
|
|
53
|
+
return runner.dispatch(action, input, envelope);
|
|
53
54
|
},
|
|
54
55
|
async enqueue(action, input) {
|
|
55
|
-
void
|
|
56
|
+
void runner.dispatch(action, input, envelope);
|
|
56
57
|
},
|
|
57
58
|
async publish(eventMsg) {
|
|
58
|
-
await
|
|
59
|
+
await publish([eventMsg], envelope);
|
|
59
60
|
},
|
|
60
61
|
};
|
|
61
62
|
return effects;
|
|
@@ -63,19 +64,10 @@ export function workflowsPlugin(workflows, opts = {}) {
|
|
|
63
64
|
for (const workflow of workflows)
|
|
64
65
|
chain.register(workflow);
|
|
65
66
|
container.register(FORGE_WORKFLOW_CHAIN_BINDING, chain);
|
|
66
|
-
runtime.hooks.
|
|
67
|
-
await chain.apply(payload.
|
|
67
|
+
runtime.hooks.LocalDelivery.use(async (payload, next) => {
|
|
68
|
+
await chain.apply({ eventName: payload.eventName, payload: payload.payload }, payload.envelope);
|
|
68
69
|
await next();
|
|
69
70
|
}, { name: "forge.publish.workflows", priority: EVENT_PUBLISHING_PRIORITIES.workflows });
|
|
70
|
-
on("AppReady", () => {
|
|
71
|
-
const hookChain = runtime.hooks.EventPublishing;
|
|
72
|
-
const steps = (hookChain.chain ?? []).filter((s) => s.name === "forge.publish.workflows");
|
|
73
|
-
if (steps.length > 1) {
|
|
74
|
-
// eslint-disable-next-line no-console
|
|
75
|
-
console.warn(`workflowsPlugin: detected ${steps.length} "forge.publish.workflows" steps on the EventPublishing chain. ` +
|
|
76
|
-
`Install either workflowsPlugin OR forgePlugin's options.workflows path, not both.`);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
71
|
},
|
|
80
72
|
};
|
|
81
73
|
}
|
|
@@ -1,41 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `defineAction` — declares a typed
|
|
3
|
-
*
|
|
4
|
-
* transport concerns. Resolvers wrap actions for HTTP/GraphQL/CLI; workflows
|
|
5
|
-
* dispatch actions in response to events.
|
|
2
|
+
* `defineAction` — declares a typed command the system can handle. It is sugar
|
|
3
|
+
* over the one operation primitive:
|
|
6
4
|
*
|
|
7
|
-
* export const
|
|
8
|
-
* name: 'submissions.submit-answer',
|
|
9
|
-
* description: 'Avi submits his answer to an exercise.',
|
|
10
|
-
* schema: SubmitAnswerInput,
|
|
11
|
-
* })
|
|
5
|
+
* export const defineAction = defineHandler.kind("action");
|
|
12
6
|
*
|
|
13
|
-
* An action
|
|
14
|
-
*
|
|
7
|
+
* An action IS a handler (`@nwire/handler`) with `config.kind === "action"`,
|
|
8
|
+
* plus a thin forge decoration: it mints a typed `CommandMessage` when called,
|
|
9
|
+
* carries the command-side metadata (`retry`, `emits`, `policy`, persona /
|
|
10
|
+
* journey / SLO for Studio), and exposes `.public()`. There is no separate
|
|
11
|
+
* `.handler` object — the action object itself is what `runtime.execute`
|
|
12
|
+
* dispatches and what the app registers.
|
|
15
13
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* export const submitAnswer = defineAction({
|
|
15
|
+
* name: "submissions.submit-answer",
|
|
16
|
+
* input: SubmitAnswerInput,
|
|
17
|
+
* emits: [AnswerSubmitted],
|
|
18
|
+
* handler: async ({ input, request }) => {
|
|
19
|
+
* const grade = await request(evaluateGrade, { ... })
|
|
20
|
+
* return grade.ok ? AnswerSubmitted({ ... }) : undefined
|
|
21
|
+
* },
|
|
22
|
+
* })
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* execute(submitAnswer({ ... })) // dispatch via minted CommandMessage
|
|
25
|
+
* ctx.request(submitAnswer, input) // dispatch by reference
|
|
26
|
+
* submitAnswer.name // "submissions.submit-answer"
|
|
23
27
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* lives elsewhere, leave `handler` undefined
|
|
28
|
+
* The handler body takes one `ctx` arg — `{ input, request, send, emit, query,
|
|
29
|
+
* actor, … }` — the same shape as the kernel handler. For cross-module
|
|
30
|
+
* orchestration where the handler lives elsewhere, leave `handler` undefined —
|
|
31
|
+
* the action is then a schema-only contract you dispatch toward.
|
|
27
32
|
*/
|
|
28
33
|
import type { z } from "zod";
|
|
29
34
|
import type { ZodTypeAny } from "@nwire/messages";
|
|
30
|
-
import type { EventDefinition
|
|
31
|
-
import {
|
|
35
|
+
import type { EventDefinition } from "@nwire/messages";
|
|
36
|
+
import type { HandlerContext, HandlerReturn } from "./define-handler.js";
|
|
32
37
|
/** RBAC policy tag — string, string list, or CASL-style `[action, subject]` tuple. */
|
|
33
38
|
export type ActionPolicy = string | readonly string[] | readonly [action: string, subject: string];
|
|
34
39
|
/**
|
|
35
40
|
* Retry policy for an action's handler. When a handler throws, the runtime
|
|
36
41
|
* retries up to `max` times with `backoff` strategy between attempts. After
|
|
37
|
-
* `max` attempts fail, the failure goes to the DLQ (dead-letter queue
|
|
38
|
-
* currently a sink in the runtime; production wires plug in BullMQ DLQ).
|
|
42
|
+
* `max` attempts fail, the failure goes to the DLQ (dead-letter queue).
|
|
39
43
|
*/
|
|
40
44
|
export interface RetryPolicy {
|
|
41
45
|
/** Maximum retry attempts after the initial try. Default: 0 (no retry). */
|
|
@@ -48,10 +52,8 @@ export interface RetryPolicy {
|
|
|
48
52
|
readonly maxDelayMs?: number;
|
|
49
53
|
}
|
|
50
54
|
/**
|
|
51
|
-
* Service-level objective declaration for an action. Studio shows the
|
|
52
|
-
*
|
|
53
|
-
* telemetry stream, success rate from action.completed vs .failed counts).
|
|
54
|
-
* All fields optional; missing fields just don't get scored.
|
|
55
|
+
* Service-level objective declaration for an action. Studio shows the declared
|
|
56
|
+
* target side-by-side with observed reality.
|
|
55
57
|
*/
|
|
56
58
|
export interface ActionSlo {
|
|
57
59
|
/** Declared p95 latency target in milliseconds. */
|
|
@@ -64,29 +66,27 @@ export interface ActionSlo {
|
|
|
64
66
|
*
|
|
65
67
|
* const cmd = submitAnswer({ ... }) // { $kind, actionName, input }
|
|
66
68
|
* execute(cmd) // dispatch
|
|
67
|
-
*
|
|
68
|
-
* The runtime's `execute`/`send`/`dispatch` accept either this command
|
|
69
|
-
* message OR the legacy `(action, input)` positional form. Both routes
|
|
70
|
-
* end at the same handler.
|
|
71
69
|
*/
|
|
72
70
|
export interface CommandMessage<TSchema extends ZodTypeAny = ZodTypeAny> {
|
|
73
71
|
readonly $kind: "command";
|
|
74
72
|
readonly actionName: string;
|
|
75
73
|
readonly input: z.output<TSchema>;
|
|
76
74
|
}
|
|
75
|
+
/** Ctx a handler body receives — the forge handler ctx with the validated `input` on it. */
|
|
76
|
+
export type ActionCtx<TSchema extends ZodTypeAny = ZodTypeAny> = HandlerContext & {
|
|
77
|
+
readonly input: z.output<TSchema>;
|
|
78
|
+
};
|
|
79
|
+
/** The handler body — one ctx arg (`{ input, request, send, ... }`). */
|
|
80
|
+
export type ActionHandlerFn<TSchema extends ZodTypeAny = ZodTypeAny> = (ctx: ActionCtx<TSchema>) => Promise<HandlerReturn> | HandlerReturn;
|
|
77
81
|
/**
|
|
78
|
-
* The non-callable action shape —
|
|
79
|
-
*
|
|
80
|
-
* `
|
|
81
|
-
*
|
|
82
|
-
* call signature); they're assignable wherever `ActionDefinition` is
|
|
83
|
-
* required because the call signature is purely additive.
|
|
82
|
+
* The non-callable action shape — the abstract reference type used in function
|
|
83
|
+
* signatures across the runtime. An action is a handler: it carries the kernel
|
|
84
|
+
* `config` + the hook surface (`run`/`use`) so `runtime.execute` dispatches it
|
|
85
|
+
* directly, plus the command-side metadata.
|
|
84
86
|
*
|
|
85
|
-
*
|
|
86
|
-
* a
|
|
87
|
-
*
|
|
88
|
-
* covariance over `TSchema` — `ActionDefinition<MySchema>` flows freely
|
|
89
|
-
* into `ActionDefinition<ZodTypeAny>` positions across the runtime.
|
|
87
|
+
* Concrete actions from `defineAction` are `CallableActionDefinition` (this + a
|
|
88
|
+
* call signature minting a `CommandMessage`); the call signature is additive so
|
|
89
|
+
* they're assignable wherever `ActionDefinition` is required.
|
|
90
90
|
*/
|
|
91
91
|
export interface ActionDefinition<TSchema extends ZodTypeAny = ZodTypeAny> {
|
|
92
92
|
/** Truthy when this definition was marked via `.public()` in a manifest. */
|
|
@@ -94,104 +94,72 @@ export interface ActionDefinition<TSchema extends ZodTypeAny = ZodTypeAny> {
|
|
|
94
94
|
readonly $kind: "action";
|
|
95
95
|
readonly name: string;
|
|
96
96
|
readonly description?: string;
|
|
97
|
-
|
|
97
|
+
/** Input schema (zod). */
|
|
98
|
+
readonly input: TSchema;
|
|
98
99
|
readonly retry?: RetryPolicy;
|
|
99
|
-
/**
|
|
100
|
-
* Optional policy tag(s) consumed by authz middleware (`@nwire/auth`).
|
|
101
|
-
* The framework treats this as an opaque string — interpretation is
|
|
102
|
-
* the authorizer's job. Examples: `'admin-only'`, `['student', 'self']`.
|
|
103
|
-
*/
|
|
100
|
+
/** Optional policy tag(s) consumed by authz middleware (`@nwire/auth`). */
|
|
104
101
|
readonly policy?: ActionPolicy;
|
|
105
|
-
/**
|
|
106
|
-
* Named persona who triggers this action. Studio uses it to build
|
|
107
|
-
* persona-grouped journey views ("show me Avi's full flow"). Free-form
|
|
108
|
-
* label — typically the persona's name with a short qualifier:
|
|
109
|
-
* `'Avi (9, beginner)'`, `'Dina (curriculum designer)'`.
|
|
110
|
-
*/
|
|
102
|
+
/** Named persona who triggers this action — Studio journey views. */
|
|
111
103
|
readonly persona?: string;
|
|
112
|
-
/**
|
|
113
|
-
* Journey step identifier this action belongs to. Lets Studio reconstruct
|
|
114
|
-
* the user journey across BCs. Free-form — by convention an id + label:
|
|
115
|
-
* `'J3-submit-exercise'`.
|
|
116
|
-
*/
|
|
104
|
+
/** Journey step identifier this action belongs to. */
|
|
117
105
|
readonly journeyStep?: string;
|
|
118
|
-
/**
|
|
119
|
-
* High-level capability name (UDOO-framework concept). Groups actions
|
|
120
|
-
* by what they enable. Example: `'submit-answer'`, `'review-flagged'`.
|
|
121
|
-
*/
|
|
106
|
+
/** High-level capability name (UDOO concept). */
|
|
122
107
|
readonly capability?: string;
|
|
123
108
|
/** Declared SLO target — Studio scores observed reality against this. */
|
|
124
109
|
readonly slo?: ActionSlo;
|
|
125
110
|
/** Free-form tags for filtering / grouping in Studio. */
|
|
126
111
|
readonly tags?: readonly string[];
|
|
127
112
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* (cross-module dispatch, public schema-only declarations), leave this
|
|
132
|
-
* undefined and register the handler separately via `manifest.handlers`.
|
|
133
|
-
*
|
|
134
|
-
* Typed as `HandlerDefinition<ZodTypeAny>` (not `HandlerDefinition<TSchema>`)
|
|
135
|
-
* to preserve ActionDefinition's covariance over TSchema — without this,
|
|
136
|
-
* `ActionDefinition<MySchema>` would not be assignable to
|
|
137
|
-
* `ActionDefinition<ZodTypeAny>` (the abstract action reference used by
|
|
138
|
-
* the runtime, `defineHandler`, ctx.request, etc.).
|
|
113
|
+
* Declared events this action emits — the *declared intent* the scanner
|
|
114
|
+
* reads into the static graph. The handler's runtime return is the source
|
|
115
|
+
* of truth.
|
|
139
116
|
*/
|
|
140
|
-
readonly
|
|
117
|
+
readonly emits?: readonly EventDefinition[];
|
|
141
118
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* runtime observers can detect drift (emitted-but-not-declared or
|
|
147
|
-
* declared-but-never-fired).
|
|
148
|
-
*
|
|
149
|
-
* Carries event names only (event refs are erased to `{ name }` to avoid
|
|
150
|
-
* coupling the action definition to its event modules at the type level).
|
|
119
|
+
* The kernel `@nwire/handler` config — `kind:"action"`, the adapted
|
|
120
|
+
* `(ctx)` handler fn, `input`, `emits`. Present iff an inline handler was
|
|
121
|
+
* declared. `actionsPlugin` reads `config.handler` to run the pipeline; the
|
|
122
|
+
* runtime reads it via the registry.
|
|
151
123
|
*/
|
|
152
|
-
readonly
|
|
153
|
-
/**
|
|
154
|
-
|
|
124
|
+
readonly config?: any;
|
|
125
|
+
/** Hook-chain run — delegates to the underlying handler. `runtime.execute` calls this. */
|
|
126
|
+
run?(ctx: unknown, opts?: {
|
|
127
|
+
readonly signal?: AbortSignal;
|
|
128
|
+
}): Promise<unknown>;
|
|
129
|
+
/** Attach a chain step to the underlying handler's hook. Returns the action. */
|
|
130
|
+
use?(fn: any, opts?: any): ActionDefinition<TSchema>;
|
|
131
|
+
/** Attach a listener to the underlying handler's hook. Returns the action. */
|
|
132
|
+
on?(fn: any, opts?: any): ActionDefinition<TSchema>;
|
|
133
|
+
/** Detach a previously attached chain/listener fn. Returns the action. */
|
|
134
|
+
off?(fn: any): ActionDefinition<TSchema>;
|
|
155
135
|
/** Return a callable action clone with `$public: true`. */
|
|
156
136
|
public: () => CallableActionDefinition<TSchema>;
|
|
157
137
|
}
|
|
158
138
|
/**
|
|
159
|
-
* The full action shape returned from `defineAction` — adds the call
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* submitAnswer.name // "submissions.submit-answer"
|
|
163
|
-
* submitAnswer({ studentId, ... }) // CommandMessage
|
|
164
|
-
* submitAnswer.public() // callable clone, $public: true
|
|
139
|
+
* The full action shape returned from `defineAction` — adds the call signature
|
|
140
|
+
* so consumers can mint a typed `CommandMessage` inline.
|
|
165
141
|
*/
|
|
166
142
|
export type CallableActionDefinition<TSchema extends ZodTypeAny = ZodTypeAny> = ActionDefinition<TSchema> & ((input: z.input<TSchema>) => CommandMessage<TSchema>);
|
|
167
143
|
export interface ActionMeta<TSchema extends ZodTypeAny = ZodTypeAny> {
|
|
168
144
|
readonly name: string;
|
|
169
145
|
readonly description?: string;
|
|
170
|
-
|
|
146
|
+
/** Input schema (zod). */
|
|
147
|
+
readonly input: TSchema;
|
|
171
148
|
readonly retry?: RetryPolicy;
|
|
172
149
|
readonly policy?: ActionPolicy;
|
|
173
|
-
/** See `ActionDefinition.persona`. */
|
|
174
150
|
readonly persona?: string;
|
|
175
|
-
/** See `ActionDefinition.journeyStep`. */
|
|
176
151
|
readonly journeyStep?: string;
|
|
177
|
-
/** See `ActionDefinition.capability`. */
|
|
178
152
|
readonly capability?: string;
|
|
179
|
-
/** See `ActionDefinition.slo`. */
|
|
180
153
|
readonly slo?: ActionSlo;
|
|
181
|
-
/** See `ActionDefinition.tags`. */
|
|
182
154
|
readonly tags?: readonly string[];
|
|
183
|
-
/** Declared events this action emits.
|
|
155
|
+
/** Declared events this action emits. */
|
|
184
156
|
readonly emits?: readonly EventDefinition[];
|
|
185
157
|
/**
|
|
186
|
-
* Optional inline handler
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* where contract and handler live together. For cross-module orchestration
|
|
190
|
-
* (handler wired separately, action referenced from another module),
|
|
191
|
-
* declare the action without `handler` and use `defineHandler(action, fn)`
|
|
192
|
-
* in the owning module.
|
|
158
|
+
* Optional inline handler — the common case where contract and handler live
|
|
159
|
+
* together. The body takes one `ctx` arg. For cross-module
|
|
160
|
+
* orchestration, omit it — a schema-only contract.
|
|
193
161
|
*/
|
|
194
|
-
readonly handler?:
|
|
162
|
+
readonly handler?: ActionHandlerFn<TSchema>;
|
|
195
163
|
}
|
|
196
164
|
export declare function defineAction<TSchema extends ZodTypeAny>(meta: ActionMeta<TSchema>): CallableActionDefinition<TSchema>;
|
|
197
165
|
export declare function defineAction<TSchema extends ZodTypeAny>(name: string, meta: Omit<ActionMeta<TSchema>, "name">): CallableActionDefinition<TSchema>;
|
|
@@ -200,8 +168,8 @@ export type ActionInput<A> = A extends ActionDefinition<infer S> ? z.output<S> :
|
|
|
200
168
|
export type ActionResult<A> = A extends ActionDefinition ? HandlerReturn : never;
|
|
201
169
|
/**
|
|
202
170
|
* Optional string-literal dispatch registry. Apps implement this on the
|
|
203
|
-
* container so `dispatch("orders.place-order", input)` typechecks when
|
|
204
|
-
*
|
|
171
|
+
* container so `dispatch("orders.place-order", input)` typechecks when a
|
|
172
|
+
* module augments the registry via declaration merging.
|
|
205
173
|
*/
|
|
206
174
|
export interface NwireActionRegistry {
|
|
207
175
|
readonly [actionName: string]: ActionDefinition;
|
|
@@ -214,10 +182,7 @@ export interface NwireActionRegistry {
|
|
|
214
182
|
export declare function isCommandMessage(x: unknown): x is CommandMessage;
|
|
215
183
|
/**
|
|
216
184
|
* Resolve either dispatch form into `(action, input)`. Centralised so every
|
|
217
|
-
* transport surface
|
|
218
|
-
* with one helper. When the caller passes a `CommandMessage`, the resolver
|
|
219
|
-
* looks up the action by name on the runtime; when they pass an
|
|
220
|
-
* `ActionDefinition` + input, it short-circuits.
|
|
185
|
+
* transport surface accepts both call shapes with one helper.
|
|
221
186
|
*/
|
|
222
187
|
export declare function resolveDispatch(actionOrCmd: ActionDefinition | CommandMessage, input: unknown, lookup: (name: string) => ActionDefinition | undefined): {
|
|
223
188
|
action: ActionDefinition;
|