@nwire/forge 0.11.1 → 0.12.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.
|
@@ -20,7 +20,7 @@ import { type Hook } from "@nwire/hooks";
|
|
|
20
20
|
import { type MessageEnvelope } from "@nwire/envelope";
|
|
21
21
|
import { type Runtime } from "@nwire/app";
|
|
22
22
|
import type { WorkflowDefinition, WorkflowInstance, WorkflowEffects } from "../primitives/define-workflow.js";
|
|
23
|
-
import type
|
|
23
|
+
import { type EventMessage } from "../messages/event-message.js";
|
|
24
24
|
import type { WorkflowFireHookCtx } from "../runtime/forge-types.js";
|
|
25
25
|
import type { WorkflowTimerStore } from "../stores/workflow-timer-store.js";
|
|
26
26
|
/**
|
|
@@ -20,6 +20,7 @@ import { randomUUID } from "node:crypto";
|
|
|
20
20
|
import { hook } from "@nwire/hooks";
|
|
21
21
|
import { loggerForEnvelope } from "@nwire/logger";
|
|
22
22
|
import { serializeError } from "@nwire/app";
|
|
23
|
+
import { eventFactory } from "../messages/event-message.js";
|
|
23
24
|
import { computeBackoff, parseDelay, sleep } from "../helpers/retry-helpers.js";
|
|
24
25
|
export class WorkflowChainRunner {
|
|
25
26
|
runtime;
|
|
@@ -88,6 +89,10 @@ export class WorkflowChainRunner {
|
|
|
88
89
|
const correlationKey = correlationKeyOverride ?? `${tenantPrefix}${userKey}`;
|
|
89
90
|
const fireCtx = {
|
|
90
91
|
...baseEffects,
|
|
92
|
+
envelope,
|
|
93
|
+
emit: (eventDef, payload) => baseEffects.publish(eventFactory(eventDef)(payload)),
|
|
94
|
+
resolve: (name) => this.runtime.resolve(name),
|
|
95
|
+
logger: log,
|
|
91
96
|
load: (key) => store.get(key),
|
|
92
97
|
save: (key, instance) => store.set(key, instance),
|
|
93
98
|
drop: (key) => store.delete(key),
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import type { z } from "zod";
|
|
20
20
|
import type { ZodTypeAny, SourceLocation } from "@nwire/messages";
|
|
21
21
|
import type { EventDefinition, EventPayload } from "@nwire/messages";
|
|
22
|
+
import type { MessageEnvelope } from "@nwire/envelope";
|
|
23
|
+
import type { Logger } from "@nwire/logger";
|
|
22
24
|
import type { ActionDefinition, ActionInput, ActionResult } from "./define-action.js";
|
|
23
25
|
import type { EventMessage } from "../messages/event-message.js";
|
|
24
26
|
import { type PublicMarker } from "../helpers/public-marker.js";
|
|
@@ -85,11 +87,13 @@ export type StateBody = () => Awaitable<Transition>;
|
|
|
85
87
|
* `on()` returns are ignored (there's no state machine to transition).
|
|
86
88
|
*/
|
|
87
89
|
export interface StatelessWorkflowContext extends WorkflowEffects {
|
|
88
|
-
on<E extends EventDefinition>(event: E, handler: (payload: EventPayload<E
|
|
90
|
+
on<E extends EventDefinition>(event: E, handler: (payload: EventPayload<E>, ctx: WorkflowReactionContext) => Awaitable<Transition>, opts?: WorkflowOnOptions): void;
|
|
89
91
|
/** Subscribe to the `complete` pseudo-event. Fires after each handler. */
|
|
90
92
|
on(event: CompleteEvent, handler: () => Awaitable<void>, opts?: WorkflowOnOptions): void;
|
|
91
93
|
/** Subscribe to a saga timer fire. */
|
|
92
|
-
on(event: TimerDefinition, handler: (payload
|
|
94
|
+
on(event: TimerDefinition, handler: (payload: unknown, ctx: WorkflowReactionContext) => Awaitable<Transition>, opts?: WorkflowOnOptions): void;
|
|
95
|
+
/** Canonical event verb — alias of `on`. Prefer `when`; `on` is retained for now. */
|
|
96
|
+
when: StatelessWorkflowContext["on"];
|
|
93
97
|
}
|
|
94
98
|
/**
|
|
95
99
|
* Stateful workflow context — adds `data`, `assign`, `states`, and the
|
|
@@ -174,6 +178,19 @@ export interface WorkflowDefinition extends PublicMarker {
|
|
|
174
178
|
* and run effects. Supplied by the kernel-side runtime.
|
|
175
179
|
*/
|
|
176
180
|
export interface FireContext extends WorkflowEffects {
|
|
181
|
+
/** The triggering event's envelope. Threaded onto the reaction ctx. */
|
|
182
|
+
readonly envelope: MessageEnvelope;
|
|
183
|
+
/** Publish an event by definition + payload (listener-parity `emit`). */
|
|
184
|
+
readonly emit: (event: EventDefinition & {
|
|
185
|
+
readonly name: string;
|
|
186
|
+
readonly schema: {
|
|
187
|
+
parse(input: unknown): unknown;
|
|
188
|
+
};
|
|
189
|
+
}, payload: unknown) => Promise<void>;
|
|
190
|
+
/** Resolve a dependency from the container. */
|
|
191
|
+
readonly resolve: <T = unknown>(name: string) => T;
|
|
192
|
+
/** The envelope-scoped logger. */
|
|
193
|
+
readonly logger: Logger;
|
|
177
194
|
/** Load or create the instance row for this correlation key. */
|
|
178
195
|
readonly load: (key: string) => WorkflowInstance | undefined;
|
|
179
196
|
readonly save: (key: string, instance: WorkflowInstance) => void;
|
|
@@ -199,6 +216,31 @@ export interface WorkflowInstance {
|
|
|
199
216
|
/** Names of events the current state has registered handlers for. */
|
|
200
217
|
scopedEvents: string[];
|
|
201
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Reaction context handed to a `when` handler as its second argument. The
|
|
221
|
+
* listener-subset (`envelope` · `send` · `emit` · `resolve` · `logger`) is
|
|
222
|
+
* identical to `ListenerContext`, so a `when(Event, (payload, ctx) => …)`
|
|
223
|
+
* block written for a listener runs unchanged here; the workflow adds
|
|
224
|
+
* `enqueue` · `publish` · `assign` · `schedule`.
|
|
225
|
+
*/
|
|
226
|
+
export interface WorkflowReactionContext {
|
|
227
|
+
readonly envelope: MessageEnvelope;
|
|
228
|
+
send<A extends ActionDefinition>(action: A, input: ActionInput<A>): Promise<ActionResult<A>>;
|
|
229
|
+
emit<E extends EventDefinition & {
|
|
230
|
+
readonly name: string;
|
|
231
|
+
readonly schema: {
|
|
232
|
+
parse(input: unknown): unknown;
|
|
233
|
+
};
|
|
234
|
+
}>(event: E, payload: EventPayload<E>): Promise<void>;
|
|
235
|
+
resolve<T = unknown>(name: string): T;
|
|
236
|
+
readonly logger: Logger;
|
|
237
|
+
enqueue<A extends ActionDefinition>(action: A, input: ActionInput<A>): Promise<void>;
|
|
238
|
+
publish(event: EventMessage): Promise<void>;
|
|
239
|
+
/** Stateful only — merge a patch into the instance data. No-op when stateless. */
|
|
240
|
+
assign(patch: Record<string, unknown>): Promise<void>;
|
|
241
|
+
/** Stateful only — enqueue a declared timer. No-op when stateless. */
|
|
242
|
+
schedule(timer: TimerDefinition, payload?: unknown): Promise<void>;
|
|
243
|
+
}
|
|
202
244
|
/**
|
|
203
245
|
* Stateful overload — when `options.data` is declared, the closure
|
|
204
246
|
* receives a `StatefulWorkflowContext` with `data`, `assign`, `states`,
|
|
@@ -81,6 +81,7 @@ closure, options) {
|
|
|
81
81
|
const probeCtx = stateful
|
|
82
82
|
? {
|
|
83
83
|
on: probeOn,
|
|
84
|
+
when: probeOn,
|
|
84
85
|
send: async () => undefined,
|
|
85
86
|
enqueue: async () => undefined,
|
|
86
87
|
publish: async () => undefined,
|
|
@@ -93,6 +94,7 @@ closure, options) {
|
|
|
93
94
|
}
|
|
94
95
|
: {
|
|
95
96
|
on: probeOn,
|
|
97
|
+
when: probeOn,
|
|
96
98
|
send: async () => undefined,
|
|
97
99
|
enqueue: async () => undefined,
|
|
98
100
|
publish: async () => undefined,
|
|
@@ -164,24 +166,43 @@ closure, options) {
|
|
|
164
166
|
async function fireStateless({ closure, event, fireCtx, completeMarker, }) {
|
|
165
167
|
const matched = [];
|
|
166
168
|
const completeHandlers = [];
|
|
169
|
+
const register = ((subscribedEvent, handler) => {
|
|
170
|
+
const eventName = subscribedEvent.name;
|
|
171
|
+
if (eventName === completeMarker.name) {
|
|
172
|
+
completeHandlers.push(handler);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (eventName === event.eventName) {
|
|
176
|
+
matched.push(handler);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
167
179
|
const ctx = {
|
|
168
|
-
on:
|
|
169
|
-
|
|
170
|
-
if (eventName === completeMarker.name) {
|
|
171
|
-
completeHandlers.push(handler);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (eventName === event.eventName) {
|
|
175
|
-
matched.push(handler);
|
|
176
|
-
}
|
|
177
|
-
}),
|
|
180
|
+
on: register,
|
|
181
|
+
when: register,
|
|
178
182
|
send: fireCtx.send,
|
|
179
183
|
enqueue: fireCtx.enqueue,
|
|
180
184
|
publish: fireCtx.publish,
|
|
181
185
|
};
|
|
182
186
|
closure(ctx);
|
|
187
|
+
// Reaction ctx — the listener-subset + workflow effects. Stateless, so
|
|
188
|
+
// assign/schedule are no-ops (no per-correlation instance or timers).
|
|
189
|
+
const reactionCtx = {
|
|
190
|
+
envelope: fireCtx.envelope,
|
|
191
|
+
send: fireCtx.send,
|
|
192
|
+
emit: fireCtx.emit,
|
|
193
|
+
resolve: fireCtx.resolve,
|
|
194
|
+
logger: fireCtx.logger,
|
|
195
|
+
enqueue: fireCtx.enqueue,
|
|
196
|
+
publish: fireCtx.publish,
|
|
197
|
+
assign: async () => {
|
|
198
|
+
fireCtx.logger.warn?.("ctx.assign() is a no-op in a stateless workflow — declare `data` to make it stateful, or move this reaction to a listener.");
|
|
199
|
+
},
|
|
200
|
+
schedule: async () => {
|
|
201
|
+
fireCtx.logger.warn?.("ctx.schedule() is a no-op in a stateless workflow — declare `data`/`states` to use timers.");
|
|
202
|
+
},
|
|
203
|
+
};
|
|
183
204
|
for (const h of matched) {
|
|
184
|
-
await h(event.payload);
|
|
205
|
+
await h(event.payload, reactionCtx);
|
|
185
206
|
}
|
|
186
207
|
// `complete` for a stateless workflow fires after each successful event.
|
|
187
208
|
if (matched.length > 0) {
|
|
@@ -248,6 +269,7 @@ async function fireStateful(args) {
|
|
|
248
269
|
};
|
|
249
270
|
const ctx = {
|
|
250
271
|
on: onImpl,
|
|
272
|
+
when: onImpl,
|
|
251
273
|
async send(action, input) {
|
|
252
274
|
if (scope.kind === "collect")
|
|
253
275
|
return undefined;
|
|
@@ -322,7 +344,25 @@ async function fireStateful(args) {
|
|
|
322
344
|
fireCtx.save(key, instance);
|
|
323
345
|
return;
|
|
324
346
|
}
|
|
325
|
-
|
|
347
|
+
// Reaction ctx — listener-subset + workflow effects. assign/schedule
|
|
348
|
+
// do the real per-instance work (handlers run in global scope here).
|
|
349
|
+
const reactionCtx = {
|
|
350
|
+
envelope: fireCtx.envelope,
|
|
351
|
+
send: fireCtx.send,
|
|
352
|
+
emit: fireCtx.emit,
|
|
353
|
+
resolve: fireCtx.resolve,
|
|
354
|
+
logger: fireCtx.logger,
|
|
355
|
+
enqueue: fireCtx.enqueue,
|
|
356
|
+
publish: fireCtx.publish,
|
|
357
|
+
async assign(patch) {
|
|
358
|
+
instance.data = { ...instance.data, ...patch };
|
|
359
|
+
fireCtx.save(key, instance);
|
|
360
|
+
},
|
|
361
|
+
async schedule(timer, payload) {
|
|
362
|
+
await fireCtx.scheduleTimer(timer.name, timer.delay, payload);
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
const next = await chosen.handler(event.payload, reactionCtx);
|
|
326
366
|
// Persist any assign() effects before the transition.
|
|
327
367
|
fireCtx.save(key, instance);
|
|
328
368
|
if (next && typeof next === "string" && stateSpecs[next]) {
|
|
@@ -27,7 +27,7 @@ import { InMemoryActorStore, createInitialInstance, ActorVersionConflictError, }
|
|
|
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
|
-
import { normalizeEventReturn } from "../messages/event-message.js";
|
|
30
|
+
import { normalizeEventReturn, eventFactory } from "../messages/event-message.js";
|
|
31
31
|
import { EVENT_PUBLISHING_PRIORITIES } from "../framework-events.js";
|
|
32
32
|
export class ForgeDispatcher {
|
|
33
33
|
runtime;
|
|
@@ -945,6 +945,14 @@ export class ForgeDispatcher {
|
|
|
945
945
|
const correlationKey = correlationKeyOverride ?? `${tenantPrefix}${userKey}`;
|
|
946
946
|
const fireCtx = {
|
|
947
947
|
...baseEffects,
|
|
948
|
+
envelope,
|
|
949
|
+
// Local publish through the forge chain — fans out to in-process
|
|
950
|
+
// reactions; the outbound stage drains it to a sink only if the
|
|
951
|
+
// event is .public() and a sink is installed. Built via the event
|
|
952
|
+
// factory so messageId / correlation / idempotency hold.
|
|
953
|
+
emit: (eventDef, payload) => self.publish([eventFactory(eventDef)(payload)], envelope),
|
|
954
|
+
resolve: (name) => handlerCtx.resolve(name),
|
|
955
|
+
logger: log,
|
|
948
956
|
load: (key) => store.get(key),
|
|
949
957
|
save: (key, instance) => store.set(key, instance),
|
|
950
958
|
drop: (key) => store.delete(key),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Nwire — the framework's core primitives. defineAction, defineEvent, defineHandler, defineActor, defineProjection, defineQuery, defineWorkflow, defineModule, defineApp, definePlugin, createApp. MessageEnvelope with correlation/causation. The runtime is the bus.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"actions",
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"emittery": "1.0.1",
|
|
35
35
|
"koa": "^2.16.4",
|
|
36
36
|
"zod": "^4.0.0",
|
|
37
|
-
"@nwire/
|
|
38
|
-
"@nwire/
|
|
39
|
-
"@nwire/
|
|
40
|
-
"@nwire/
|
|
41
|
-
"@nwire/
|
|
42
|
-
"@nwire/hooks": "0.
|
|
43
|
-
"@nwire/
|
|
44
|
-
"@nwire/
|
|
45
|
-
"@nwire/
|
|
46
|
-
"@nwire/
|
|
37
|
+
"@nwire/app": "0.12.0",
|
|
38
|
+
"@nwire/bus": "0.12.0",
|
|
39
|
+
"@nwire/container": "0.12.0",
|
|
40
|
+
"@nwire/dead-letter": "0.12.0",
|
|
41
|
+
"@nwire/envelope": "0.12.0",
|
|
42
|
+
"@nwire/hooks": "0.12.0",
|
|
43
|
+
"@nwire/messages": "0.12.0",
|
|
44
|
+
"@nwire/handler": "0.12.0",
|
|
45
|
+
"@nwire/wires": "0.12.0",
|
|
46
|
+
"@nwire/logger": "0.12.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/koa": "^2.15.0",
|