@nwire/forge 0.10.0 → 0.11.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/dist/framework-events.d.ts +44 -2
- package/dist/framework-events.js +10 -1
- package/dist/helpers/cli-runner.d.ts +1 -1
- package/dist/helpers/cli-runner.js +5 -4
- package/dist/index.d.ts +25 -13
- package/dist/index.js +25 -13
- package/dist/plugins/actions-chain.d.ts +48 -0
- package/dist/plugins/actions-chain.js +251 -0
- package/dist/plugins/actions-plugin.d.ts +40 -0
- package/dist/plugins/actions-plugin.js +76 -0
- package/dist/plugins/actors-chain.d.ts +52 -0
- package/dist/plugins/actors-chain.js +204 -0
- package/dist/plugins/actors-plugin.d.ts +36 -0
- package/dist/plugins/actors-plugin.js +62 -0
- package/dist/plugins/dlq-plugin.d.ts +29 -0
- package/dist/plugins/dlq-plugin.js +37 -0
- package/dist/plugins/idempotency-plugin.d.ts +28 -0
- package/dist/plugins/idempotency-plugin.js +57 -0
- package/dist/plugins/projections-chain.d.ts +34 -0
- package/dist/plugins/projections-chain.js +86 -0
- package/dist/plugins/projections-plugin.d.ts +36 -0
- package/dist/plugins/projections-plugin.js +63 -0
- package/dist/plugins/queries-chain.d.ts +33 -0
- package/dist/plugins/queries-chain.js +77 -0
- package/dist/plugins/queries-plugin.d.ts +41 -0
- package/dist/plugins/queries-plugin.js +74 -0
- package/dist/plugins/workflows-chain.d.ts +51 -0
- package/dist/plugins/workflows-chain.js +203 -0
- package/dist/plugins/workflows-plugin.d.ts +47 -0
- package/dist/plugins/workflows-plugin.js +81 -0
- package/dist/runtime/create-forge-app.d.ts +11 -11
- package/dist/runtime/create-forge-app.js +28 -32
- package/dist/runtime/forge-dispatcher.d.ts +24 -0
- package/dist/runtime/forge-dispatcher.js +76 -22
- package/dist/runtime/forge-plugin.d.ts +57 -42
- package/dist/runtime/forge-plugin.js +72 -59
- package/dist/runtime/with-forge.d.ts +26 -0
- package/dist/runtime/with-forge.js +30 -0
- package/dist/stores/idempotency-store.d.ts +15 -0
- package/dist/stores/idempotency-store.js +7 -0
- package/package.json +11 -11
|
@@ -14,8 +14,26 @@
|
|
|
14
14
|
* domain-specific slots.
|
|
15
15
|
*/
|
|
16
16
|
import type { Hook } from "@nwire/hooks";
|
|
17
|
+
import type { MessageEnvelope } from "@nwire/envelope";
|
|
17
18
|
import type { ActionDefinition } from "./primitives/define-action.js";
|
|
18
19
|
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
|
+
}
|
|
19
37
|
declare module "@nwire/app" {
|
|
20
38
|
interface FrameworkHooks {
|
|
21
39
|
/** Fired before a dispatch reaches the handler. Veto by skipping next(). */
|
|
@@ -48,8 +66,32 @@ declare module "@nwire/app" {
|
|
|
48
66
|
readonly eventName: string;
|
|
49
67
|
readonly payload: unknown;
|
|
50
68
|
}>;
|
|
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>;
|
|
51
85
|
}
|
|
52
86
|
}
|
|
53
|
-
/** Forge-specific framework-hook slot names —
|
|
54
|
-
export declare const forgeFrameworkSlots: readonly ["ActionDispatching", "ActionCompleted", "ActionFailed", "EventRecording", "EventRecorded"];
|
|
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. */
|
|
90
|
+
export declare const EVENT_PUBLISHING_PRIORITIES: {
|
|
91
|
+
readonly idempotency: 1000;
|
|
92
|
+
readonly actors: 800;
|
|
93
|
+
readonly projections: 600;
|
|
94
|
+
readonly workflows: 400;
|
|
95
|
+
readonly bus: 200;
|
|
96
|
+
};
|
|
55
97
|
export type ForgeFrameworkSlot = (typeof forgeFrameworkSlots)[number];
|
package/dist/framework-events.js
CHANGED
|
@@ -13,11 +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 —
|
|
16
|
+
/** Forge-specific framework-hook slot names — materialised by `createForgePlugin`. */
|
|
17
17
|
export const forgeFrameworkSlots = [
|
|
18
18
|
"ActionDispatching",
|
|
19
19
|
"ActionCompleted",
|
|
20
20
|
"ActionFailed",
|
|
21
21
|
"EventRecording",
|
|
22
22
|
"EventRecorded",
|
|
23
|
+
"EventPublishing",
|
|
23
24
|
];
|
|
25
|
+
/** Priority slots forge's own chain participants attach at. */
|
|
26
|
+
export const EVENT_PUBLISHING_PRIORITIES = {
|
|
27
|
+
idempotency: 1000,
|
|
28
|
+
actors: 800,
|
|
29
|
+
projections: 600,
|
|
30
|
+
workflows: 400,
|
|
31
|
+
bus: 200,
|
|
32
|
+
};
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* Wire-mode invariance: same domain handlers run in HTTP, queue, and CLI.
|
|
34
34
|
* The CLI doesn't know about HTTP routes; it dispatches via the runtime.
|
|
35
35
|
*/
|
|
36
|
-
import type {
|
|
36
|
+
import type { App } from "@nwire/app";
|
|
37
37
|
export interface RunCliOptions {
|
|
38
38
|
/**
|
|
39
39
|
* Override stdout/stderr — useful for tests. Defaults to console.log /
|
|
@@ -34,6 +34,7 @@
|
|
|
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
38
|
export async function runCli(app, argv, options = {}) {
|
|
38
39
|
const stdout = options.stdout ?? ((line) => console.log(line));
|
|
39
40
|
const stderr = options.stderr ?? ((line) => console.error(line));
|
|
@@ -43,14 +44,14 @@ export async function runCli(app, argv, options = {}) {
|
|
|
43
44
|
}
|
|
44
45
|
const targetName = argv[0];
|
|
45
46
|
const rest = argv.slice(1);
|
|
46
|
-
const dispatcher = app
|
|
47
|
+
const dispatcher = forgeDispatcher(app);
|
|
47
48
|
const action = dispatcher.findActionByName(targetName);
|
|
48
49
|
if (action) {
|
|
49
50
|
const { tenant, parsed } = parseFlags(rest);
|
|
50
51
|
try {
|
|
51
52
|
const validated = action.schema.parse(parsed);
|
|
52
53
|
const envelope = tenant ? seedEnvelope({ tenant }) : undefined;
|
|
53
|
-
await
|
|
54
|
+
await dispatcher.dispatch(action, validated, envelope);
|
|
54
55
|
stdout("OK");
|
|
55
56
|
return 0;
|
|
56
57
|
}
|
|
@@ -63,7 +64,7 @@ export async function runCli(app, argv, options = {}) {
|
|
|
63
64
|
if (dispatcher.listQueries().includes(targetName)) {
|
|
64
65
|
const { tenant, parsed } = parseFlags(rest);
|
|
65
66
|
try {
|
|
66
|
-
const result = await
|
|
67
|
+
const result = await dispatcher.query(targetName, parsed, tenant);
|
|
67
68
|
stdout(JSON.stringify(result, null, 2));
|
|
68
69
|
return 0;
|
|
69
70
|
}
|
|
@@ -76,7 +77,7 @@ export async function runCli(app, argv, options = {}) {
|
|
|
76
77
|
return 1;
|
|
77
78
|
}
|
|
78
79
|
function printHelp(app, stdout) {
|
|
79
|
-
const dispatcher = app
|
|
80
|
+
const dispatcher = forgeDispatcher(app);
|
|
80
81
|
stdout("nwire — action CLI runner\n");
|
|
81
82
|
stdout("Usage: amit <action-or-query-name> [--field value ...] [--tenant <id>]\n");
|
|
82
83
|
stdout("Actions:");
|
package/dist/index.d.ts
CHANGED
|
@@ -2,21 +2,33 @@
|
|
|
2
2
|
* `@nwire/forge` — CQRS engine delivered as an app plugin.
|
|
3
3
|
*
|
|
4
4
|
* import { createApp } from "@nwire/app";
|
|
5
|
-
* import {
|
|
5
|
+
* import { createForgePlugin, forgeDispatcher } from "@nwire/forge";
|
|
6
6
|
*
|
|
7
|
-
* const app = createApp({
|
|
7
|
+
* const app = createApp({
|
|
8
|
+
* appName: "orders",
|
|
9
|
+
* plugins: [createForgePlugin({ handlers, actors, projections, … })],
|
|
10
|
+
* });
|
|
11
|
+
* await app.start();
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* import { createForgeApp } from "@nwire/forge";
|
|
13
|
-
*
|
|
14
|
-
* const app = createForgeApp({ name: "orders", handlers, actors, … });
|
|
13
|
+
* const dispatcher = forgeDispatcher(app);
|
|
14
|
+
* await dispatcher.dispatch(placeOrder, { … });
|
|
15
15
|
*/
|
|
16
|
-
export { forgePlugin, createForgePlugin, FORGE_DISPATCHER_BINDING, } from "./runtime/forge-plugin.js";
|
|
16
|
+
export { forgePlugin, createForgePlugin, forgeDispatcher, type ForgePluginOptions, FORGE_DISPATCHER_BINDING, } from "./runtime/forge-plugin.js";
|
|
17
|
+
export { withForge } from "./runtime/with-forge.js";
|
|
18
|
+
export { actorsPlugin, FORGE_ACTOR_CHAIN_BINDING, FORGE_ACTOR_STORE_BINDING, type ActorsPluginOptions, } from "./plugins/actors-plugin.js";
|
|
19
|
+
export { ActorChainRunner, type ActorTransitionListener } from "./plugins/actors-chain.js";
|
|
20
|
+
export { projectionsPlugin, FORGE_PROJECTION_CHAIN_BINDING, FORGE_PROJECTION_STORE_BINDING, type ProjectionsPluginOptions, } from "./plugins/projections-plugin.js";
|
|
21
|
+
export { ProjectionChainRunner } from "./plugins/projections-chain.js";
|
|
22
|
+
export { workflowsPlugin, FORGE_WORKFLOW_CHAIN_BINDING, FORGE_WORKFLOW_TIMER_STORE_BINDING, type WorkflowsPluginOptions, } from "./plugins/workflows-plugin.js";
|
|
23
|
+
export { WorkflowChainRunner, type WorkflowEffectsFactory } from "./plugins/workflows-chain.js";
|
|
24
|
+
export { queriesPlugin, FORGE_QUERY_RUNNER_BINDING } from "./plugins/queries-plugin.js";
|
|
25
|
+
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";
|
|
28
|
+
export { idempotencyPlugin, FORGE_IDEMPOTENCY_STORE_BINDING, type IdempotencyPluginOptions, } from "./plugins/idempotency-plugin.js";
|
|
29
|
+
export { dlqPlugin, type DlqPluginOptions } from "./plugins/dlq-plugin.js";
|
|
17
30
|
export { ForgeDispatcher, type ForgeDispatcherOptions } from "./runtime/forge-dispatcher.js";
|
|
18
31
|
export type { ForgeTelemetry, DispatchOptions, ActionBeforeHookCtx, ActionAfterHookCtx, ActorTransitionHookCtx, WorkflowFireHookCtx, } from "./runtime/forge-types.js";
|
|
19
|
-
export { createForgeApp, type ForgeApp, type ForgeAppOptions, } from "./runtime/create-forge-app.js";
|
|
20
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";
|
|
21
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";
|
|
22
34
|
export { defineSchema, isSchemaDefinition, type SchemaDefinition, type SchemaOptions, type SchemaStateSpec, type SchemaStorageHints, } from "./primitives/define-schema.js";
|
|
@@ -26,7 +38,7 @@ export { eventFactory, isEventMessage, normalizeEventReturn, type EventMessage,
|
|
|
26
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";
|
|
27
39
|
export { defineProjection, type ProjectionDefinition, type ProjectionOptions, type ProjectionReducer, } from "./primitives/define-projection.js";
|
|
28
40
|
export { defineQuery, type QueryDefinition, type QueryOptions } from "./primitives/define-query.js";
|
|
29
|
-
export { defineUpcaster, applyUpcasters, type UpcasterDefinition } from "./primitives/define-upcaster.js";
|
|
41
|
+
export { defineUpcaster, applyUpcasters, type UpcasterDefinition, } from "./primitives/define-upcaster.js";
|
|
30
42
|
export { defineExternalCall, type ExternalCallDefinition, type ExternalCallMeta, type ExternalCallTarget, type ExternalCallSlo, type ExternalCallRetry, type ExternalCallExecutor, } from "./primitives/define-external-call.js";
|
|
31
43
|
export { defineInboundWebhook, type InboundWebhookDefinition, type InboundWebhookMeta, type WebhookSignatureVerifier, type WebhookDedupe, } from "./primitives/define-inbound-webhook.js";
|
|
32
44
|
export { defineOutbox, type OutboxDefinition, type OutboxMeta } from "./primitives/define-outbox.js";
|
|
@@ -39,11 +51,11 @@ export { InMemoryIdempotencyStore, type IdempotencyStore } from "./stores/idempo
|
|
|
39
51
|
export { InMemoryWorkflowTimerStore, timerEventName, type WorkflowTimer, type WorkflowTimerStore, } from "./stores/workflow-timer-store.js";
|
|
40
52
|
export { parseDelay } from "./helpers/retry-helpers.js";
|
|
41
53
|
export { isPublic, type PublicMarker } from "./helpers/public-marker.js";
|
|
42
|
-
export { attachDevLogger, formatTelemetry, type DevLoggerOptions } from "./observability/dev-logger.js";
|
|
54
|
+
export { attachDevLogger, formatTelemetry, type DevLoggerOptions, } from "./observability/dev-logger.js";
|
|
43
55
|
export { runCli, type RunCliOptions } from "./helpers/cli-runner.js";
|
|
44
56
|
export { NoopLogger, ConsoleLogger, loggerForEnvelope, type Logger } from "@nwire/logger";
|
|
45
57
|
export { InMemoryDeadLetterSink, buildDeadLetterEntry, type DeadLetterSink, type DeadLetterEntry, } from "@nwire/dead-letter";
|
|
46
58
|
export { defineResource, isResourceDefinition, type ResourceDefinition, type DefineResourceOptions, } from "@nwire/handler";
|
|
47
59
|
export { response, isResponseSpec, isResponseInstance, ok, created, accepted, noContent, notModified, gone, type ResponseSpec, type ListResponseSpec, type ResponseInstance, type ResponseKind, } from "./helpers/response.js";
|
|
48
|
-
import "./framework-events";
|
|
60
|
+
import "./framework-events.js";
|
|
49
61
|
export type { FrameworkHooks, PluginKind } from "@nwire/app";
|
package/dist/index.js
CHANGED
|
@@ -2,21 +2,33 @@
|
|
|
2
2
|
* `@nwire/forge` — CQRS engine delivered as an app plugin.
|
|
3
3
|
*
|
|
4
4
|
* import { createApp } from "@nwire/app";
|
|
5
|
-
* import {
|
|
5
|
+
* import { createForgePlugin, forgeDispatcher } from "@nwire/forge";
|
|
6
6
|
*
|
|
7
|
-
* const app = createApp({
|
|
7
|
+
* const app = createApp({
|
|
8
|
+
* appName: "orders",
|
|
9
|
+
* plugins: [createForgePlugin({ handlers, actors, projections, … })],
|
|
10
|
+
* });
|
|
11
|
+
* await app.start();
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* import { createForgeApp } from "@nwire/forge";
|
|
13
|
-
*
|
|
14
|
-
* const app = createForgeApp({ name: "orders", handlers, actors, … });
|
|
13
|
+
* const dispatcher = forgeDispatcher(app);
|
|
14
|
+
* await dispatcher.dispatch(placeOrder, { … });
|
|
15
15
|
*/
|
|
16
16
|
// ─── Plugin + composition ──────────────────────────────────────────
|
|
17
|
-
export { forgePlugin, createForgePlugin, FORGE_DISPATCHER_BINDING, } from "./runtime/forge-plugin.js";
|
|
17
|
+
export { forgePlugin, createForgePlugin, forgeDispatcher, FORGE_DISPATCHER_BINDING, } from "./runtime/forge-plugin.js";
|
|
18
|
+
export { withForge } from "./runtime/with-forge.js";
|
|
19
|
+
export { actorsPlugin, FORGE_ACTOR_CHAIN_BINDING, FORGE_ACTOR_STORE_BINDING, } from "./plugins/actors-plugin.js";
|
|
20
|
+
export { ActorChainRunner } from "./plugins/actors-chain.js";
|
|
21
|
+
export { projectionsPlugin, FORGE_PROJECTION_CHAIN_BINDING, FORGE_PROJECTION_STORE_BINDING, } from "./plugins/projections-plugin.js";
|
|
22
|
+
export { ProjectionChainRunner } from "./plugins/projections-chain.js";
|
|
23
|
+
export { workflowsPlugin, FORGE_WORKFLOW_CHAIN_BINDING, FORGE_WORKFLOW_TIMER_STORE_BINDING, } from "./plugins/workflows-plugin.js";
|
|
24
|
+
export { WorkflowChainRunner } from "./plugins/workflows-chain.js";
|
|
25
|
+
export { queriesPlugin, FORGE_QUERY_RUNNER_BINDING } from "./plugins/queries-plugin.js";
|
|
26
|
+
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 { ActionRunner } from "./plugins/actions-chain.js";
|
|
29
|
+
export { idempotencyPlugin, FORGE_IDEMPOTENCY_STORE_BINDING, } from "./plugins/idempotency-plugin.js";
|
|
30
|
+
export { dlqPlugin } from "./plugins/dlq-plugin.js";
|
|
18
31
|
export { ForgeDispatcher } from "./runtime/forge-dispatcher.js";
|
|
19
|
-
export { createForgeApp, } from "./runtime/create-forge-app.js";
|
|
20
32
|
// ─── Domain primitives ─────────────────────────────────────────────
|
|
21
33
|
export { defineAction, isCommandMessage, resolveDispatch, } from "./primitives/define-action.js";
|
|
22
34
|
export { defineActor, eventKey, } from "./primitives/define-actor.js";
|
|
@@ -27,7 +39,7 @@ export { eventFactory, isEventMessage, normalizeEventReturn, } from "./messages/
|
|
|
27
39
|
export { defineWorkflow, COMPLETE_EVENT_NAME, } from "./primitives/define-workflow.js";
|
|
28
40
|
export { defineProjection, } from "./primitives/define-projection.js";
|
|
29
41
|
export { defineQuery } from "./primitives/define-query.js";
|
|
30
|
-
export { defineUpcaster, applyUpcasters } from "./primitives/define-upcaster.js";
|
|
42
|
+
export { defineUpcaster, applyUpcasters, } from "./primitives/define-upcaster.js";
|
|
31
43
|
export { defineExternalCall, } from "./primitives/define-external-call.js";
|
|
32
44
|
export { defineInboundWebhook, } from "./primitives/define-inbound-webhook.js";
|
|
33
45
|
export { defineOutbox } from "./primitives/define-outbox.js";
|
|
@@ -42,7 +54,7 @@ export { InMemoryWorkflowTimerStore, timerEventName, } from "./stores/workflow-t
|
|
|
42
54
|
// ─── Helpers ───────────────────────────────────────────────────────
|
|
43
55
|
export { parseDelay } from "./helpers/retry-helpers.js";
|
|
44
56
|
export { isPublic } from "./helpers/public-marker.js";
|
|
45
|
-
export { attachDevLogger, formatTelemetry } from "./observability/dev-logger.js";
|
|
57
|
+
export { attachDevLogger, formatTelemetry, } from "./observability/dev-logger.js";
|
|
46
58
|
export { runCli } from "./helpers/cli-runner.js";
|
|
47
59
|
// ─── Re-exports from neighbours ────────────────────────────────────
|
|
48
60
|
export { NoopLogger, ConsoleLogger, loggerForEnvelope } from "@nwire/logger";
|
|
@@ -51,4 +63,4 @@ export { defineResource, isResourceDefinition, } from "@nwire/handler";
|
|
|
51
63
|
export { response, isResponseSpec, isResponseInstance, ok, created, accepted, noContent, notModified, gone, } from "./helpers/response.js";
|
|
52
64
|
// Module augmentation side effect — adds Action* / Event* slots to
|
|
53
65
|
// FrameworkHooks on @nwire/app's runtime.
|
|
54
|
-
import "./framework-events";
|
|
66
|
+
import "./framework-events.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action runner — handler registry + dispatch loop.
|
|
3
|
+
*
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
import { type Runtime } from "@nwire/app";
|
|
15
|
+
import { type MessageEnvelope } from "@nwire/envelope";
|
|
16
|
+
import { type Hook } from "@nwire/hooks";
|
|
17
|
+
import type { Container } from "@nwire/container";
|
|
18
|
+
import { type DeadLetterSink } from "@nwire/dead-letter";
|
|
19
|
+
import type { ActionDefinition, ActionInput, ActionResult } from "../primitives/define-action.js";
|
|
20
|
+
import type { HandlerContext, HandlerDefinition } from "../primitives/define-handler.js";
|
|
21
|
+
import type { ActionBeforeHookCtx, ActionAfterHookCtx, DispatchOptions } from "../runtime/forge-types.js";
|
|
22
|
+
import { type EventMessage } from "../messages/event-message.js";
|
|
23
|
+
/** Publish callback — injected so the runner doesn't know about the chain hook directly. */
|
|
24
|
+
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
|
+
export declare class ActionRunner {
|
|
28
|
+
private readonly runtime;
|
|
29
|
+
private readonly container;
|
|
30
|
+
private readonly deadLetterSink;
|
|
31
|
+
private readonly publishEvents;
|
|
32
|
+
readonly handlers: Map<string, HandlerDefinition<any>>;
|
|
33
|
+
readonly beforeHooks: Map<string, Hook<ActionBeforeHookCtx>>;
|
|
34
|
+
readonly afterHooks: Map<string, Hook<ActionAfterHookCtx>>;
|
|
35
|
+
readonly augmenters: CtxAugmenter[];
|
|
36
|
+
constructor(runtime: Runtime, container: Container, deadLetterSink: DeadLetterSink, publishEvents: PublishEventsFn);
|
|
37
|
+
register(handler: HandlerDefinition<any>): void;
|
|
38
|
+
/** Action handler names, in registration order. */
|
|
39
|
+
listHandlers(): readonly string[];
|
|
40
|
+
findActionByName(name: string): ActionDefinition<any> | undefined;
|
|
41
|
+
ensureBeforeHook(name: string): Hook<ActionBeforeHookCtx>;
|
|
42
|
+
ensureAfterHook(name: string): Hook<ActionAfterHookCtx>;
|
|
43
|
+
/** Plugins can hang extra ctx members on each handler invocation. */
|
|
44
|
+
registerCtxAugmenter(fn: CtxAugmenter): void;
|
|
45
|
+
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;
|
|
48
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action runner — handler registry + dispatch loop.
|
|
3
|
+
*
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
import { serializeError } from "@nwire/app";
|
|
15
|
+
import { deriveEnvelope, seedEnvelope } from "@nwire/envelope";
|
|
16
|
+
import { loggerForEnvelope } from "@nwire/logger";
|
|
17
|
+
import { isValidated, markValidated } from "@nwire/messages";
|
|
18
|
+
import { hook } from "@nwire/hooks";
|
|
19
|
+
import { buildDeadLetterEntry } from "@nwire/dead-letter";
|
|
20
|
+
import { normalizeEventReturn } from "../messages/event-message.js";
|
|
21
|
+
import { computeBackoff, sleep } from "../helpers/retry-helpers.js";
|
|
22
|
+
export class ActionRunner {
|
|
23
|
+
runtime;
|
|
24
|
+
container;
|
|
25
|
+
deadLetterSink;
|
|
26
|
+
publishEvents;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
handlers = new Map();
|
|
29
|
+
beforeHooks = new Map();
|
|
30
|
+
afterHooks = new Map();
|
|
31
|
+
augmenters = [];
|
|
32
|
+
constructor(runtime, container, deadLetterSink, publishEvents) {
|
|
33
|
+
this.runtime = runtime;
|
|
34
|
+
this.container = container;
|
|
35
|
+
this.deadLetterSink = deadLetterSink;
|
|
36
|
+
this.publishEvents = publishEvents;
|
|
37
|
+
}
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
register(handler) {
|
|
40
|
+
if (this.handlers.has(handler.action.name)) {
|
|
41
|
+
throw new Error(`actionsPlugin: handler for action "${handler.action.name}" already registered.`);
|
|
42
|
+
}
|
|
43
|
+
this.handlers.set(handler.action.name, handler);
|
|
44
|
+
this.ensureBeforeHook(handler.action.name);
|
|
45
|
+
this.ensureAfterHook(handler.action.name);
|
|
46
|
+
}
|
|
47
|
+
/** Action handler names, in registration order. */
|
|
48
|
+
listHandlers() {
|
|
49
|
+
return [...this.handlers.keys()];
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
findActionByName(name) {
|
|
53
|
+
return this.handlers.get(name)?.action;
|
|
54
|
+
}
|
|
55
|
+
ensureBeforeHook(name) {
|
|
56
|
+
let h = this.beforeHooks.get(name);
|
|
57
|
+
if (h)
|
|
58
|
+
return h;
|
|
59
|
+
h = hook(`action.before:${name}`);
|
|
60
|
+
this.beforeHooks.set(name, h);
|
|
61
|
+
return h;
|
|
62
|
+
}
|
|
63
|
+
ensureAfterHook(name) {
|
|
64
|
+
let h = this.afterHooks.get(name);
|
|
65
|
+
if (h)
|
|
66
|
+
return h;
|
|
67
|
+
h = hook(`action.after:${name}`);
|
|
68
|
+
this.afterHooks.set(name, h);
|
|
69
|
+
return h;
|
|
70
|
+
}
|
|
71
|
+
/** Plugins can hang extra ctx members on each handler invocation. */
|
|
72
|
+
registerCtxAugmenter(fn) {
|
|
73
|
+
this.augmenters.push(fn);
|
|
74
|
+
}
|
|
75
|
+
async dispatch(action, input, parentEnvelope, opts) {
|
|
76
|
+
const handler = this.handlers.get(action.name);
|
|
77
|
+
if (!handler) {
|
|
78
|
+
throw new Error(`actionsPlugin: no handler registered for action "${action.name}".`);
|
|
79
|
+
}
|
|
80
|
+
const appName = this.runtime.appName;
|
|
81
|
+
const envelope = parentEnvelope ? deriveEnvelope(parentEnvelope) : seedEnvelope({});
|
|
82
|
+
const validated = isValidated(input) ? input : markValidated(action.schema.parse(input));
|
|
83
|
+
const log = loggerForEnvelope(this.runtime.logger, envelope);
|
|
84
|
+
const signal = opts?.signal ?? new AbortController().signal;
|
|
85
|
+
const ctx = this.buildHandlerContext(envelope, signal);
|
|
86
|
+
this.runtime.pushTelemetry({
|
|
87
|
+
kind: "action.dispatched",
|
|
88
|
+
action: action.name,
|
|
89
|
+
input: validated,
|
|
90
|
+
envelope,
|
|
91
|
+
appName,
|
|
92
|
+
ts: new Date().toISOString(),
|
|
93
|
+
});
|
|
94
|
+
const startedAt = performance.now();
|
|
95
|
+
const dispatchResult = await this.runtime.hooks.ActionDispatching.runDetailed({
|
|
96
|
+
action,
|
|
97
|
+
input: validated,
|
|
98
|
+
ctx,
|
|
99
|
+
});
|
|
100
|
+
if (dispatchResult.outcome === "failed")
|
|
101
|
+
throw dispatchResult.error;
|
|
102
|
+
if (dispatchResult.outcome !== "completed")
|
|
103
|
+
return undefined;
|
|
104
|
+
const beforeHook = this.beforeHooks.get(action.name);
|
|
105
|
+
if (beforeHook) {
|
|
106
|
+
const beforeCtx = { action, input: validated, ctx };
|
|
107
|
+
await beforeHook.run(beforeCtx);
|
|
108
|
+
if (beforeCtx.vetoed)
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const retry = action.retry;
|
|
112
|
+
const maxAttempts = 1 + (retry?.max ?? 0);
|
|
113
|
+
let attempt = 0;
|
|
114
|
+
let lastError;
|
|
115
|
+
while (attempt < maxAttempts) {
|
|
116
|
+
attempt++;
|
|
117
|
+
if (attempt > 1 && signal.aborted) {
|
|
118
|
+
throw lastError;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
if (attempt > 1) {
|
|
122
|
+
const delay = computeBackoff(retry, attempt - 1);
|
|
123
|
+
if (delay > 0)
|
|
124
|
+
await sleep(delay);
|
|
125
|
+
}
|
|
126
|
+
const rawResult = await handler.handler(validated, ctx);
|
|
127
|
+
const events = normalizeEventReturn(rawResult ?? null);
|
|
128
|
+
if (events.length > 0) {
|
|
129
|
+
await this.publishEvents(events, envelope);
|
|
130
|
+
}
|
|
131
|
+
const durationMs = performance.now() - startedAt;
|
|
132
|
+
this.runtime.pushTelemetry({
|
|
133
|
+
kind: "action.completed",
|
|
134
|
+
action: action.name,
|
|
135
|
+
durationMs,
|
|
136
|
+
emittedEvents: events.map((e) => e.eventName),
|
|
137
|
+
envelope,
|
|
138
|
+
appName,
|
|
139
|
+
ts: new Date().toISOString(),
|
|
140
|
+
});
|
|
141
|
+
const afterHook = this.afterHooks.get(action.name);
|
|
142
|
+
if (afterHook) {
|
|
143
|
+
try {
|
|
144
|
+
await afterHook.run({
|
|
145
|
+
action,
|
|
146
|
+
input: validated,
|
|
147
|
+
result: rawResult ?? undefined,
|
|
148
|
+
durationMs,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
log.error(`action.after hook threw`, {
|
|
153
|
+
action: action.name,
|
|
154
|
+
error: err?.message,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
void this.runtime.hooks.ActionCompleted.run({
|
|
159
|
+
action,
|
|
160
|
+
input: validated,
|
|
161
|
+
result: rawResult ?? undefined,
|
|
162
|
+
durationMs,
|
|
163
|
+
});
|
|
164
|
+
return rawResult;
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
lastError = err;
|
|
168
|
+
this.runtime.pushTelemetry({
|
|
169
|
+
kind: "action.failed",
|
|
170
|
+
action: action.name,
|
|
171
|
+
attempt,
|
|
172
|
+
maxAttempts,
|
|
173
|
+
willRetry: attempt < maxAttempts,
|
|
174
|
+
error: serializeError(err),
|
|
175
|
+
envelope,
|
|
176
|
+
appName,
|
|
177
|
+
ts: new Date().toISOString(),
|
|
178
|
+
});
|
|
179
|
+
if (attempt >= maxAttempts) {
|
|
180
|
+
void this.runtime.hooks.ActionFailed.run({
|
|
181
|
+
action,
|
|
182
|
+
input: validated,
|
|
183
|
+
error: err,
|
|
184
|
+
durationMs: performance.now() - startedAt,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const entry = buildDeadLetterEntry(action.name, validated, envelope, attempt, lastError);
|
|
190
|
+
await this.deadLetterSink.record(entry);
|
|
191
|
+
log.error(`dead-lettered after ${attempt} attempts`, {
|
|
192
|
+
action: action.name,
|
|
193
|
+
error: entry.lastError.message,
|
|
194
|
+
});
|
|
195
|
+
this.runtime.pushTelemetry({
|
|
196
|
+
kind: "dlq.recorded",
|
|
197
|
+
action: action.name,
|
|
198
|
+
attempts: attempt,
|
|
199
|
+
error: serializeError(lastError),
|
|
200
|
+
envelope,
|
|
201
|
+
appName,
|
|
202
|
+
ts: new Date().toISOString(),
|
|
203
|
+
});
|
|
204
|
+
throw lastError;
|
|
205
|
+
}
|
|
206
|
+
/** Build the handler ctx for one dispatch. */
|
|
207
|
+
buildHandlerContext(envelope, signal) {
|
|
208
|
+
const self = this;
|
|
209
|
+
const container = this.container;
|
|
210
|
+
const ctx = {
|
|
211
|
+
container,
|
|
212
|
+
envelope,
|
|
213
|
+
logger: loggerForEnvelope(this.runtime.logger, envelope),
|
|
214
|
+
signal,
|
|
215
|
+
resolve(name) {
|
|
216
|
+
return container.resolve(name);
|
|
217
|
+
},
|
|
218
|
+
get requestId() {
|
|
219
|
+
return envelope.messageId;
|
|
220
|
+
},
|
|
221
|
+
async request(action, input) {
|
|
222
|
+
return self.dispatch(action, input, envelope, { signal });
|
|
223
|
+
},
|
|
224
|
+
async send(action, input) {
|
|
225
|
+
await self.dispatch(action, input, envelope, { signal });
|
|
226
|
+
},
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
+
async query(queryDef, input) {
|
|
229
|
+
// Resolve the standalone QueryRunner when present; otherwise fall
|
|
230
|
+
// through to the bundled forge dispatcher.
|
|
231
|
+
if (container.has("forge.queryRunner")) {
|
|
232
|
+
const runner = container.resolve("forge.queryRunner");
|
|
233
|
+
return runner.run(queryDef.name, input, envelope.tenant ?? "");
|
|
234
|
+
}
|
|
235
|
+
const dispatcher = container.resolve("forge.dispatcher");
|
|
236
|
+
return dispatcher.query(queryDef.name, input, envelope.tenant ?? "");
|
|
237
|
+
},
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
239
|
+
async use() {
|
|
240
|
+
throw new Error("actionsPlugin: ctx.use requires the bundled forge dispatcher. Standalone ctx.use is not yet implemented.");
|
|
241
|
+
},
|
|
242
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
243
|
+
async externalCall() {
|
|
244
|
+
throw new Error("actionsPlugin: ctx.externalCall requires the bundled forge dispatcher.");
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
for (const augment of this.augmenters)
|
|
248
|
+
augment(ctx, envelope);
|
|
249
|
+
return ctx;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `actionsPlugin` — standalone forge action concern.
|
|
3
|
+
*
|
|
4
|
+
* import { createApp } from "@nwire/app";
|
|
5
|
+
* import {
|
|
6
|
+
* actionsPlugin,
|
|
7
|
+
* idempotencyPlugin,
|
|
8
|
+
* forgePlugin,
|
|
9
|
+
* } from "@nwire/forge";
|
|
10
|
+
*
|
|
11
|
+
* const app = createApp({
|
|
12
|
+
* appName: "orders",
|
|
13
|
+
* plugins: [
|
|
14
|
+
* forgePlugin,
|
|
15
|
+
* actionsPlugin([placeOrderHandler, sendReceiptHandler]),
|
|
16
|
+
* ],
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* Owns:
|
|
20
|
+
* - the action handler registry through `ActionRunner`
|
|
21
|
+
* - the `forge.deadLetterSink` container binding (overridable)
|
|
22
|
+
* - the `forge.actionRunner` container binding so other plugins can
|
|
23
|
+
* resolve it (queue-worker, http adapter, please CLI, etc.)
|
|
24
|
+
* - the per-action retry loop, DLQ recording, and action.dispatched /
|
|
25
|
+
* action.completed / action.failed / dlq.recorded telemetry
|
|
26
|
+
*
|
|
27
|
+
* Handlers returning events feed the `EventPublishing` chain so the
|
|
28
|
+
* actors / projections / workflows / bus steps all run in order. The
|
|
29
|
+
* standalone plugin is equivalent to forgePlugin's bundled handler path.
|
|
30
|
+
*/
|
|
31
|
+
import type { PluginDefinition } from "@nwire/app";
|
|
32
|
+
import { type DeadLetterSink } from "@nwire/dead-letter";
|
|
33
|
+
import type { HandlerDefinition } from "../primitives/define-handler.js";
|
|
34
|
+
export declare const FORGE_ACTION_RUNNER_BINDING: "forge.actionRunner";
|
|
35
|
+
export declare const FORGE_DEAD_LETTER_SINK_BINDING: "forge.deadLetterSink";
|
|
36
|
+
export interface ActionsPluginOptions {
|
|
37
|
+
/** Override the dead-letter sink. Defaults to `InMemoryDeadLetterSink`. */
|
|
38
|
+
readonly deadLetterSink?: DeadLetterSink;
|
|
39
|
+
}
|
|
40
|
+
export declare function actionsPlugin(handlers: readonly HandlerDefinition<any>[], opts?: ActionsPluginOptions): PluginDefinition;
|