@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workflowsPlugin` — standalone forge workflow concern.
|
|
3
|
+
*
|
|
4
|
+
* import { createApp } from "@nwire/app";
|
|
5
|
+
* import { workflowsPlugin, forgePlugin } from "@nwire/forge";
|
|
6
|
+
*
|
|
7
|
+
* const app = createApp({
|
|
8
|
+
* appName: "orders",
|
|
9
|
+
* plugins: [
|
|
10
|
+
* forgePlugin,
|
|
11
|
+
* workflowsPlugin([autoCharge, sendReceipt]),
|
|
12
|
+
* ],
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* Owns:
|
|
16
|
+
* - the `forge.workflowTimerStore` container binding
|
|
17
|
+
* - the workflow registry (private to the plugin's `WorkflowChainRunner`)
|
|
18
|
+
* - the `forge.publish.workflows` step on the EventPublishing chain
|
|
19
|
+
* (priority 400 — between projections and bus delivery)
|
|
20
|
+
* - the `forge.workflowChain` container binding so other plugins can
|
|
21
|
+
* resolve the runner (the timer scheduler fires through it)
|
|
22
|
+
*
|
|
23
|
+
* Workflow effects (`send` / `enqueue` / `publish`) are resolved through
|
|
24
|
+
* the forge dispatcher bound on the container. `forgePlugin` (or a
|
|
25
|
+
* future `actionsPlugin`) must be installed alongside this plugin.
|
|
26
|
+
*
|
|
27
|
+
* If `forgePlugin`'s `options.workflows` is also passed, both installs
|
|
28
|
+
* fire every event — install ONE path, not both. A diagnostic warning
|
|
29
|
+
* fires at `AppReady` when both attachments are present.
|
|
30
|
+
*/
|
|
31
|
+
import type { PluginDefinition } from "@nwire/app";
|
|
32
|
+
import type { EventBus } from "@nwire/bus";
|
|
33
|
+
import { type WorkflowTimerStore } from "../stores/workflow-timer-store.js";
|
|
34
|
+
import type { WorkflowDefinition } from "../primitives/define-workflow.js";
|
|
35
|
+
export declare const FORGE_WORKFLOW_CHAIN_BINDING: "forge.workflowChain";
|
|
36
|
+
export declare const FORGE_WORKFLOW_TIMER_STORE_BINDING: "forge.workflowTimerStore";
|
|
37
|
+
export interface WorkflowsPluginOptions {
|
|
38
|
+
/** Override the timer store. Defaults to `InMemoryWorkflowTimerStore`. */
|
|
39
|
+
readonly workflowTimerStore?: WorkflowTimerStore;
|
|
40
|
+
/**
|
|
41
|
+
* Optional bus reference. Not used directly by workflows, but plugins
|
|
42
|
+
* that wrap the timer scheduler may need it; surfaced here so the
|
|
43
|
+
* standalone plugin doesn't force them through a re-resolve.
|
|
44
|
+
*/
|
|
45
|
+
readonly bus?: EventBus;
|
|
46
|
+
}
|
|
47
|
+
export declare function workflowsPlugin(workflows: readonly WorkflowDefinition[], opts?: WorkflowsPluginOptions): PluginDefinition;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workflowsPlugin` — standalone forge workflow concern.
|
|
3
|
+
*
|
|
4
|
+
* import { createApp } from "@nwire/app";
|
|
5
|
+
* import { workflowsPlugin, forgePlugin } from "@nwire/forge";
|
|
6
|
+
*
|
|
7
|
+
* const app = createApp({
|
|
8
|
+
* appName: "orders",
|
|
9
|
+
* plugins: [
|
|
10
|
+
* forgePlugin,
|
|
11
|
+
* workflowsPlugin([autoCharge, sendReceipt]),
|
|
12
|
+
* ],
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* Owns:
|
|
16
|
+
* - the `forge.workflowTimerStore` container binding
|
|
17
|
+
* - the workflow registry (private to the plugin's `WorkflowChainRunner`)
|
|
18
|
+
* - the `forge.publish.workflows` step on the EventPublishing chain
|
|
19
|
+
* (priority 400 — between projections and bus delivery)
|
|
20
|
+
* - the `forge.workflowChain` container binding so other plugins can
|
|
21
|
+
* resolve the runner (the timer scheduler fires through it)
|
|
22
|
+
*
|
|
23
|
+
* Workflow effects (`send` / `enqueue` / `publish`) are resolved through
|
|
24
|
+
* the forge dispatcher bound on the container. `forgePlugin` (or a
|
|
25
|
+
* future `actionsPlugin`) must be installed alongside this plugin.
|
|
26
|
+
*
|
|
27
|
+
* If `forgePlugin`'s `options.workflows` is also passed, both installs
|
|
28
|
+
* fire every event — install ONE path, not both. A diagnostic warning
|
|
29
|
+
* fires at `AppReady` when both attachments are present.
|
|
30
|
+
*/
|
|
31
|
+
import { InMemoryWorkflowTimerStore, } from "../stores/workflow-timer-store.js";
|
|
32
|
+
import { FORGE_DISPATCHER_BINDING } from "../runtime/forge-plugin.js";
|
|
33
|
+
import { EVENT_PUBLISHING_PRIORITIES } from "../framework-events.js";
|
|
34
|
+
import { WorkflowChainRunner } from "./workflows-chain.js";
|
|
35
|
+
export const FORGE_WORKFLOW_CHAIN_BINDING = "forge.workflowChain";
|
|
36
|
+
export const FORGE_WORKFLOW_TIMER_STORE_BINDING = "forge.workflowTimerStore";
|
|
37
|
+
export function workflowsPlugin(workflows, opts = {}) {
|
|
38
|
+
return {
|
|
39
|
+
name: "forge.workflows",
|
|
40
|
+
register({ bind }) {
|
|
41
|
+
const store = opts.workflowTimerStore ?? new InMemoryWorkflowTimerStore();
|
|
42
|
+
bind(FORGE_WORKFLOW_TIMER_STORE_BINDING, () => store);
|
|
43
|
+
},
|
|
44
|
+
setup({ runtime, container, on }) {
|
|
45
|
+
const timerStore = container.resolve(FORGE_WORKFLOW_TIMER_STORE_BINDING);
|
|
46
|
+
const chain = new WorkflowChainRunner(runtime, timerStore, (envelope) => {
|
|
47
|
+
// Effects resolve the dispatcher lazily so the binding exists by
|
|
48
|
+
// the time a workflow actually fires.
|
|
49
|
+
const dispatcher = container.resolve(FORGE_DISPATCHER_BINDING);
|
|
50
|
+
const effects = {
|
|
51
|
+
async send(action, input) {
|
|
52
|
+
return dispatcher.dispatch(action, input, envelope);
|
|
53
|
+
},
|
|
54
|
+
async enqueue(action, input) {
|
|
55
|
+
void dispatcher.dispatch(action, input, envelope);
|
|
56
|
+
},
|
|
57
|
+
async publish(eventMsg) {
|
|
58
|
+
await dispatcher.publish([eventMsg], envelope);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
return effects;
|
|
62
|
+
});
|
|
63
|
+
for (const workflow of workflows)
|
|
64
|
+
chain.register(workflow);
|
|
65
|
+
container.register(FORGE_WORKFLOW_CHAIN_BINDING, chain);
|
|
66
|
+
runtime.hooks.EventPublishing.use(async (payload, next) => {
|
|
67
|
+
await chain.apply(payload.event, payload.envelope);
|
|
68
|
+
await next();
|
|
69
|
+
}, { 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
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `createForgeApp` —
|
|
2
|
+
* `createForgeApp` — composition entry point for forge-shaped apps.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A thin wrapper over `createApp({appName, plugins})` that installs forge,
|
|
5
|
+
* registers direct domain primitives (actors / projections / workflows /
|
|
6
|
+
* etc.) during boot, and surfaces forge's dispatch verbs (`dispatch`,
|
|
7
|
+
* `publish`, `query`, …) as methods on the returned handle. Each verb
|
|
8
|
+
* resolves the `ForgeDispatcher` bound by `forgePlugin` and delegates.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* plugin's boot() runs).
|
|
10
|
+
* The App is the bounded context. Pass actors / projections / queries /
|
|
11
|
+
* workflows / external calls / handlers directly on opts; they register
|
|
12
|
+
* against the dispatcher during `AppBooting`, after every plugin's setup
|
|
13
|
+
* ran (so the dispatcher is bound) but before plugin boot queues fire.
|
|
14
14
|
*/
|
|
15
|
-
import { type Container } from "@nwire/container/awilix";
|
|
16
15
|
import { type PluginDefinition, type Runtime } from "@nwire/app";
|
|
16
|
+
import type { Container } from "@nwire/container";
|
|
17
17
|
import type { Logger } from "@nwire/logger";
|
|
18
18
|
import type { MessageEnvelope } from "@nwire/envelope";
|
|
19
19
|
import type { z } from "zod";
|
|
@@ -1,39 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `createForgeApp` —
|
|
2
|
+
* `createForgeApp` — composition entry point for forge-shaped apps.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A thin wrapper over `createApp({appName, plugins})` that installs forge,
|
|
5
|
+
* registers direct domain primitives (actors / projections / workflows /
|
|
6
|
+
* etc.) during boot, and surfaces forge's dispatch verbs (`dispatch`,
|
|
7
|
+
* `publish`, `query`, …) as methods on the returned handle. Each verb
|
|
8
|
+
* resolves the `ForgeDispatcher` bound by `forgePlugin` and delegates.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* plugin's boot() runs).
|
|
10
|
+
* The App is the bounded context. Pass actors / projections / queries /
|
|
11
|
+
* workflows / external calls / handlers directly on opts; they register
|
|
12
|
+
* against the dispatcher during `AppBooting`, after every plugin's setup
|
|
13
|
+
* ran (so the dispatcher is bound) but before plugin boot queues fire.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import { createRuntime } from "@nwire/app";
|
|
15
|
+
import { createApp } from "@nwire/app";
|
|
17
16
|
import { forgePlugin as defaultForgePlugin, FORGE_DISPATCHER_BINDING, } from "./forge-plugin.js";
|
|
18
17
|
export function createForgeApp(options) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// Forge plugin first; user plugins after. Users can pass an explicit
|
|
19
|
+
// `createForgePlugin(opts)` in their list to control stores.
|
|
20
|
+
const plugins = ensureForgePlugin(options.plugins ?? []);
|
|
21
|
+
const base = createApp({
|
|
23
22
|
appName: options.name,
|
|
23
|
+
container: options.container,
|
|
24
|
+
logger: options.logger,
|
|
25
|
+
plugins,
|
|
24
26
|
});
|
|
25
|
-
// Forge plugin first; user plugins after. The user can also pass an
|
|
26
|
-
// explicit `createForgePlugin(opts)` in their list to control stores.
|
|
27
|
-
const plugins = ensureForgePlugin(options.plugins ?? []);
|
|
28
|
-
for (const p of plugins) {
|
|
29
|
-
runtime.registerPlugin(p);
|
|
30
|
-
}
|
|
31
27
|
// Direct registrations land on the dispatcher during AppBooting —
|
|
32
28
|
// after every plugin's setup ran (so forge's dispatcher is bound)
|
|
33
29
|
// but before plugin boot queues fire (so domain routes are wired
|
|
34
|
-
// when downstream
|
|
35
|
-
runtime.hooks.AppBooting.use(async (_, next) => {
|
|
36
|
-
const d = container.resolve(FORGE_DISPATCHER_BINDING);
|
|
30
|
+
// when downstream plugin boot work might reach for them).
|
|
31
|
+
base.runtime.hooks.AppBooting.use(async (_, next) => {
|
|
32
|
+
const d = base.container.resolve(FORGE_DISPATCHER_BINDING);
|
|
37
33
|
for (const h of options.handlers ?? []) {
|
|
38
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
35
|
d.registerActionHandler(h);
|
|
@@ -50,16 +46,16 @@ export function createForgeApp(options) {
|
|
|
50
46
|
d.registerExternalCall(call);
|
|
51
47
|
await next();
|
|
52
48
|
}, { name: "forge.app.register-domain", priority: 1_000_000 });
|
|
53
|
-
const dispatcher = () => container.resolve(FORGE_DISPATCHER_BINDING);
|
|
49
|
+
const dispatcher = () => base.container.resolve(FORGE_DISPATCHER_BINDING);
|
|
54
50
|
const app = {
|
|
55
51
|
$nwireApp: true,
|
|
56
52
|
appName: options.name,
|
|
57
|
-
runtime,
|
|
58
|
-
container,
|
|
59
|
-
start: () =>
|
|
60
|
-
stop: (reason) =>
|
|
61
|
-
boot: () =>
|
|
62
|
-
shutdown: () =>
|
|
53
|
+
runtime: base.runtime,
|
|
54
|
+
container: base.container,
|
|
55
|
+
start: () => base.start(),
|
|
56
|
+
stop: (reason) => base.stop(reason),
|
|
57
|
+
boot: () => base.start(),
|
|
58
|
+
shutdown: () => base.stop(),
|
|
63
59
|
dispatcher,
|
|
64
60
|
dispatch: (action, input, parentEnvelope, opts) => dispatcher().dispatch(action, input, parentEnvelope, opts),
|
|
65
61
|
publish: (events, parentEnvelope) => dispatcher().publish(events, parentEnvelope),
|
|
@@ -133,7 +133,31 @@ export declare class ForgeDispatcher {
|
|
|
133
133
|
fireDueTimers(now?: number): Promise<number>;
|
|
134
134
|
fireDueWorkflowTimers(now?: Date): Promise<number>;
|
|
135
135
|
dispatch<A extends ActionDefinition>(action: A, input: ActionInput<A>, parentEnvelope?: MessageEnvelope, opts?: DispatchOptions): Promise<ActionResult<A>>;
|
|
136
|
+
/**
|
|
137
|
+
* Publish a batch of events. Each event flows through the
|
|
138
|
+
* `EventPublishing` hook chain — idempotency → actors → projections →
|
|
139
|
+
* workflows → bus — at structurally enforced priorities. The chain
|
|
140
|
+
* participants are attached once at plugin setup via
|
|
141
|
+
* {@link attachPublishChain}.
|
|
142
|
+
*
|
|
143
|
+
* Telemetry (`event.deduped` / `event.published`) is pushed here based
|
|
144
|
+
* on whether the idempotency step short-circuited the chain.
|
|
145
|
+
*/
|
|
136
146
|
publish(events: readonly EventMessage[], parentEnvelope: MessageEnvelope): Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* Attach forge's atomic publish chain to the `EventPublishing` hook.
|
|
149
|
+
* Priority slots are defined in `EVENT_PUBLISHING_PRIORITIES`:
|
|
150
|
+
*
|
|
151
|
+
* 1000 — idempotency gate. Short-circuits on duplicate.
|
|
152
|
+
* 800 — actor state transitions.
|
|
153
|
+
* 600 — projection folds.
|
|
154
|
+
* 400 — workflow correlation + fire.
|
|
155
|
+
* 200 — cross-process bus delivery (public events only).
|
|
156
|
+
*
|
|
157
|
+
* Called once at plugin setup. Idempotent — re-attaching is a no-op
|
|
158
|
+
* because the hook engine guards against duplicate step names.
|
|
159
|
+
*/
|
|
160
|
+
attachPublishChain(): void;
|
|
137
161
|
applyExternalEvent(eventName: string, payload: unknown, envelope: MessageEnvelope): Promise<void>;
|
|
138
162
|
private foldProjections;
|
|
139
163
|
private applyToActors;
|
|
@@ -21,13 +21,14 @@ import { hook } from "@nwire/hooks";
|
|
|
21
21
|
import { isValidated, markValidated } from "@nwire/messages";
|
|
22
22
|
import { seedEnvelope, deriveEnvelope } from "@nwire/envelope";
|
|
23
23
|
import { loggerForEnvelope } from "@nwire/logger";
|
|
24
|
-
import { buildDeadLetterEntry, InMemoryDeadLetterSink } from "@nwire/dead-letter";
|
|
24
|
+
import { buildDeadLetterEntry, InMemoryDeadLetterSink, } from "@nwire/dead-letter";
|
|
25
25
|
import { sleep, computeBackoff, parseDelay } from "../helpers/retry-helpers.js";
|
|
26
26
|
import { InMemoryActorStore, createInitialInstance, ActorVersionConflictError, } from "../stores/actor-store.js";
|
|
27
27
|
import { InMemoryProjectionStore } from "../stores/projection-store.js";
|
|
28
28
|
import { InMemoryIdempotencyStore } from "../stores/idempotency-store.js";
|
|
29
29
|
import { InMemoryWorkflowTimerStore, timerEventName, } from "../stores/workflow-timer-store.js";
|
|
30
30
|
import { normalizeEventReturn } from "../messages/event-message.js";
|
|
31
|
+
import { EVENT_PUBLISHING_PRIORITIES } from "../framework-events.js";
|
|
31
32
|
export class ForgeDispatcher {
|
|
32
33
|
runtime;
|
|
33
34
|
// ─── Domain registries ──────────────────────────────────────────────
|
|
@@ -575,13 +576,30 @@ export class ForgeDispatcher {
|
|
|
575
576
|
return hctx.result;
|
|
576
577
|
}
|
|
577
578
|
// ─── Publish + applyExternalEvent ──────────────────────────────────
|
|
579
|
+
/**
|
|
580
|
+
* Publish a batch of events. Each event flows through the
|
|
581
|
+
* `EventPublishing` hook chain — idempotency → actors → projections →
|
|
582
|
+
* workflows → bus — at structurally enforced priorities. The chain
|
|
583
|
+
* participants are attached once at plugin setup via
|
|
584
|
+
* {@link attachPublishChain}.
|
|
585
|
+
*
|
|
586
|
+
* Telemetry (`event.deduped` / `event.published`) is pushed here based
|
|
587
|
+
* on whether the idempotency step short-circuited the chain.
|
|
588
|
+
*/
|
|
578
589
|
async publish(events, parentEnvelope) {
|
|
579
590
|
const appName = this.runtime.appName;
|
|
580
591
|
for (let i = 0; i < events.length; i++) {
|
|
581
592
|
const event = events[i];
|
|
582
593
|
const dedupKey = events.length === 1 ? parentEnvelope.messageId : `${parentEnvelope.messageId}#${i}`;
|
|
583
594
|
const childEnvelope = deriveEnvelope(parentEnvelope);
|
|
584
|
-
|
|
595
|
+
const payload = {
|
|
596
|
+
event,
|
|
597
|
+
envelope: childEnvelope,
|
|
598
|
+
dedupKey,
|
|
599
|
+
deduped: false,
|
|
600
|
+
};
|
|
601
|
+
await this.runtime.hooks.EventPublishing.run(payload);
|
|
602
|
+
if (payload.deduped) {
|
|
585
603
|
this.runtime.pushTelemetry({
|
|
586
604
|
kind: "event.deduped",
|
|
587
605
|
event,
|
|
@@ -590,37 +608,74 @@ export class ForgeDispatcher {
|
|
|
590
608
|
appName,
|
|
591
609
|
ts: new Date().toISOString(),
|
|
592
610
|
});
|
|
593
|
-
continue;
|
|
594
611
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
this.runtime.pushTelemetry({
|
|
600
|
-
kind: "event.published",
|
|
601
|
-
event,
|
|
602
|
-
envelope: childEnvelope,
|
|
603
|
-
source: "in-process",
|
|
604
|
-
appName,
|
|
605
|
-
ts: new Date().toISOString(),
|
|
606
|
-
});
|
|
607
|
-
if (this.publishToBus && this.bus && this.publicEventNames.has(event.eventName)) {
|
|
608
|
-
await this.bus.publish({
|
|
609
|
-
eventName: event.eventName,
|
|
610
|
-
payload: event.payload,
|
|
612
|
+
else {
|
|
613
|
+
this.runtime.pushTelemetry({
|
|
614
|
+
kind: "event.published",
|
|
615
|
+
event,
|
|
611
616
|
envelope: childEnvelope,
|
|
612
|
-
|
|
617
|
+
source: "in-process",
|
|
618
|
+
appName,
|
|
619
|
+
ts: new Date().toISOString(),
|
|
613
620
|
});
|
|
614
621
|
}
|
|
615
622
|
}
|
|
616
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* Attach forge's atomic publish chain to the `EventPublishing` hook.
|
|
626
|
+
* Priority slots are defined in `EVENT_PUBLISHING_PRIORITIES`:
|
|
627
|
+
*
|
|
628
|
+
* 1000 — idempotency gate. Short-circuits on duplicate.
|
|
629
|
+
* 800 — actor state transitions.
|
|
630
|
+
* 600 — projection folds.
|
|
631
|
+
* 400 — workflow correlation + fire.
|
|
632
|
+
* 200 — cross-process bus delivery (public events only).
|
|
633
|
+
*
|
|
634
|
+
* Called once at plugin setup. Idempotent — re-attaching is a no-op
|
|
635
|
+
* because the hook engine guards against duplicate step names.
|
|
636
|
+
*/
|
|
637
|
+
attachPublishChain() {
|
|
638
|
+
const hook = this.runtime.hooks.EventPublishing;
|
|
639
|
+
hook.use(async (payload, next) => {
|
|
640
|
+
const isNew = await this.idempotencyStore.recordIfNew(payload.dedupKey);
|
|
641
|
+
if (!isNew) {
|
|
642
|
+
payload.deduped = true;
|
|
643
|
+
return; // veto — downstream steps skipped
|
|
644
|
+
}
|
|
645
|
+
await next();
|
|
646
|
+
}, { name: "forge.publish.idempotency", priority: EVENT_PUBLISHING_PRIORITIES.idempotency });
|
|
647
|
+
hook.use(async (payload, next) => {
|
|
648
|
+
await this.applyToActors(payload.event, payload.envelope);
|
|
649
|
+
await next();
|
|
650
|
+
}, { name: "forge.publish.actors", priority: EVENT_PUBLISHING_PRIORITIES.actors });
|
|
651
|
+
hook.use(async (payload, next) => {
|
|
652
|
+
await this.foldProjections(payload.event, payload.envelope);
|
|
653
|
+
await next();
|
|
654
|
+
}, { name: "forge.publish.projections", priority: EVENT_PUBLISHING_PRIORITIES.projections });
|
|
655
|
+
hook.use(async (payload, next) => {
|
|
656
|
+
await this.runWorkflows(payload.event, payload.envelope);
|
|
657
|
+
await next();
|
|
658
|
+
}, { name: "forge.publish.workflows", priority: EVENT_PUBLISHING_PRIORITIES.workflows });
|
|
659
|
+
hook.use(async (payload, next) => {
|
|
660
|
+
if (this.publishToBus && this.bus && this.publicEventNames.has(payload.event.eventName)) {
|
|
661
|
+
await this.bus.publish({
|
|
662
|
+
eventName: payload.event.eventName,
|
|
663
|
+
payload: payload.event.payload,
|
|
664
|
+
envelope: payload.envelope,
|
|
665
|
+
origin: this.runtime.appName,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
await next();
|
|
669
|
+
}, { name: "forge.publish.bus", priority: EVENT_PUBLISHING_PRIORITIES.bus });
|
|
670
|
+
}
|
|
617
671
|
async applyExternalEvent(eventName, payload, envelope) {
|
|
618
672
|
if (!this.externalEventNames.has(eventName)) {
|
|
619
673
|
throw new Error(`Runtime.applyExternalEvent: "${eventName}" was not declared in any module's needs.externalEvents.`);
|
|
620
674
|
}
|
|
621
675
|
const appName = this.runtime.appName;
|
|
622
676
|
const event = { eventName, payload };
|
|
623
|
-
|
|
677
|
+
const isNew = await this.idempotencyStore.recordIfNew(envelope.messageId);
|
|
678
|
+
if (!isNew) {
|
|
624
679
|
this.runtime.pushTelemetry({
|
|
625
680
|
kind: "event.deduped",
|
|
626
681
|
event,
|
|
@@ -631,7 +686,6 @@ export class ForgeDispatcher {
|
|
|
631
686
|
});
|
|
632
687
|
return;
|
|
633
688
|
}
|
|
634
|
-
await this.idempotencyStore.record(envelope.messageId);
|
|
635
689
|
await this.applyToActors(event, envelope);
|
|
636
690
|
await this.foldProjections(event, envelope);
|
|
637
691
|
await this.runWorkflows(event, envelope);
|
|
@@ -1,59 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Forge plugin — installs the dispatcher, materialises forge's framework
|
|
3
|
+
* hooks, pins the action handler step on the dispatch chain, and registers
|
|
4
|
+
* any domain primitives passed in options.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
6
|
+
* import { createApp } from "@nwire/app";
|
|
7
|
+
* import { createForgePlugin, FORGE_DISPATCHER_BINDING } from "@nwire/forge";
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
9
|
+
* const app = createApp({
|
|
10
|
+
* appName: "orders",
|
|
11
|
+
* plugins: [createForgePlugin({
|
|
12
|
+
* actors: [Order],
|
|
13
|
+
* projections: [OrdersDashboard],
|
|
14
|
+
* handlers: [placeOrder, sendReceipt],
|
|
15
|
+
* })],
|
|
16
|
+
* });
|
|
8
17
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
18
|
+
* await app.start();
|
|
19
|
+
* const dispatcher = app.container.resolve(FORGE_DISPATCHER_BINDING);
|
|
11
20
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* there to call `dispatch / publish / fireDueTimers / …`.
|
|
17
|
-
* 3. Materialise the Action* / Event* framework-hook slots forge's
|
|
18
|
-
* module augmentation declares — kernel pre-instantiates only
|
|
19
|
-
* App* / Plugin* / Wire* slots; forge's domain slots come in via
|
|
20
|
-
* the plugin so a non-forge app's runtime stays uncluttered.
|
|
21
|
-
* 4. Pin the action handler chain step on `runtime.dispatchHook$` at
|
|
22
|
-
* priority `-Infinity` so user-registered `runtime.use()` middleware
|
|
23
|
-
* stays strictly OUTSIDE the handler invocation.
|
|
21
|
+
* Handlers and direct registrations land on the dispatcher during the
|
|
22
|
+
* `AppBooting` chain — after every plugin's setup ran (so the dispatcher
|
|
23
|
+
* is bound) but before plugin boot queues fire (so downstream plugins'
|
|
24
|
+
* boot work can reach domain routes already wired).
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* their own `bind({ dispose })` registrations.
|
|
26
|
+
* `forgePlugin` is the default-options shortcut for tests, demos, and
|
|
27
|
+
* single-process apps. For custom stores or a cross-service bus, use
|
|
28
|
+
* `createForgePlugin(opts)`.
|
|
29
29
|
*/
|
|
30
30
|
import type { PluginDefinition } from "@nwire/app";
|
|
31
|
-
import { type ForgeDispatcherOptions } from "./forge-dispatcher.js";
|
|
32
|
-
|
|
31
|
+
import { ForgeDispatcher, type ForgeDispatcherOptions } from "./forge-dispatcher.js";
|
|
32
|
+
import type { ActorDefinition } from "../primitives/define-actor.js";
|
|
33
|
+
import type { ProjectionDefinition } from "../primitives/define-projection.js";
|
|
34
|
+
import type { QueryDefinition } from "../primitives/define-query.js";
|
|
35
|
+
import type { WorkflowDefinition } from "../primitives/define-workflow.js";
|
|
36
|
+
import type { ExternalCallDefinition } from "../primitives/define-external-call.js";
|
|
37
|
+
import type { HandlerDefinition as ForgeHandlerDef } from "../primitives/define-handler.js";
|
|
38
|
+
/** Container binding the forge dispatcher is registered under. */
|
|
33
39
|
export declare const FORGE_DISPATCHER_BINDING: "forge.dispatcher";
|
|
34
|
-
/** Capability bindings
|
|
40
|
+
/** Capability bindings consumed by handler / resolver ctx via `ctx.resolve(...)`. */
|
|
35
41
|
export declare const FORGE_EXECUTE_BINDING: "execute";
|
|
36
42
|
export declare const FORGE_SEND_BINDING: "send";
|
|
37
43
|
export declare const FORGE_USE_PROJECTION_BINDING: "useProjection";
|
|
38
44
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*/
|
|
43
|
-
export declare const forgePlugin: PluginDefinition;
|
|
44
|
-
/**
|
|
45
|
-
* Configurable forge plugin. Pass:
|
|
46
|
-
* - durable stores (actorStore, projectionStore, deadLetterSink, …)
|
|
47
|
-
* - a cross-service EventBus to bridge in-process events out
|
|
48
|
-
* - domain registrations (actors, workflows, projections, queries,
|
|
49
|
-
* externalCalls) — picked up at AppBooting and forwarded to the
|
|
50
|
-
* dispatcher so consumers don't need to wire them manually.
|
|
45
|
+
* Options accepted by `createForgePlugin`. Includes the dispatcher's own
|
|
46
|
+
* options (stores, bus) and the domain primitives that register against
|
|
47
|
+
* the dispatcher during the boot phase.
|
|
51
48
|
*/
|
|
52
49
|
export interface ForgePluginOptions extends ForgeDispatcherOptions {
|
|
53
|
-
readonly
|
|
54
|
-
readonly
|
|
55
|
-
readonly projections?: readonly any[];
|
|
56
|
-
readonly queries?: readonly any[];
|
|
57
|
-
readonly
|
|
50
|
+
readonly handlers?: readonly ForgeHandlerDef<any>[];
|
|
51
|
+
readonly actors?: readonly ActorDefinition[];
|
|
52
|
+
readonly projections?: readonly ProjectionDefinition<any>[];
|
|
53
|
+
readonly queries?: readonly QueryDefinition<any, any, any>[];
|
|
54
|
+
readonly workflows?: readonly WorkflowDefinition[];
|
|
55
|
+
readonly externalCalls?: readonly ExternalCallDefinition<any, any>[];
|
|
58
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Construct a forge plugin with custom stores, bus, and/or domain
|
|
59
|
+
* primitives. The plugin's setup is synchronous; domain registration
|
|
60
|
+
* happens during the App's `AppBooting` chain.
|
|
61
|
+
*/
|
|
59
62
|
export declare function createForgePlugin(options?: ForgePluginOptions): PluginDefinition;
|
|
63
|
+
/** Default forge plugin — in-memory stores, no bus, no domain options. */
|
|
64
|
+
export declare const forgePlugin: PluginDefinition;
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the forge dispatcher from an app's container. Convenience for
|
|
67
|
+
* test code and tooling that needs direct dispatcher access; handler code
|
|
68
|
+
* uses `ctx.execute` / `ctx.send` / `ctx.publish` instead.
|
|
69
|
+
*/
|
|
70
|
+
export declare function forgeDispatcher(app: {
|
|
71
|
+
container: {
|
|
72
|
+
resolve<T>(name: string): T;
|
|
73
|
+
};
|
|
74
|
+
}): ForgeDispatcher;
|