@nwire/forge 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +100 -83
  2. package/dist/framework-events.d.ts +8 -37
  3. package/dist/framework-events.js +7 -3
  4. package/dist/helpers/cli-runner.js +21 -10
  5. package/dist/index.d.ts +8 -7
  6. package/dist/index.js +7 -6
  7. package/dist/plugins/actions-chain.d.ts +39 -22
  8. package/dist/plugins/actions-chain.js +117 -78
  9. package/dist/plugins/actions-plugin.d.ts +26 -23
  10. package/dist/plugins/actions-plugin.js +122 -44
  11. package/dist/plugins/actors-chain.d.ts +9 -2
  12. package/dist/plugins/actors-chain.js +62 -2
  13. package/dist/plugins/actors-plugin.d.ts +1 -1
  14. package/dist/plugins/actors-plugin.js +24 -14
  15. package/dist/plugins/external-calls-plugin.d.ts +28 -0
  16. package/dist/plugins/external-calls-plugin.js +136 -0
  17. package/dist/plugins/idempotency-plugin.d.ts +15 -1
  18. package/dist/plugins/idempotency-plugin.js +56 -11
  19. package/dist/plugins/projections-chain.d.ts +2 -2
  20. package/dist/plugins/projections-chain.js +2 -2
  21. package/dist/plugins/projections-plugin.d.ts +1 -1
  22. package/dist/plugins/projections-plugin.js +4 -13
  23. package/dist/plugins/queries-chain.d.ts +4 -3
  24. package/dist/plugins/queries-chain.js +8 -5
  25. package/dist/plugins/queries-plugin.d.ts +15 -29
  26. package/dist/plugins/queries-plugin.js +36 -49
  27. package/dist/plugins/workflows-chain.d.ts +9 -2
  28. package/dist/plugins/workflows-chain.js +19 -1
  29. package/dist/plugins/workflows-plugin.d.ts +1 -1
  30. package/dist/plugins/workflows-plugin.js +12 -20
  31. package/dist/primitives/define-action.d.ts +80 -115
  32. package/dist/primitives/define-action.js +111 -56
  33. package/dist/primitives/define-actor.d.ts +103 -214
  34. package/dist/primitives/define-actor.js +157 -216
  35. package/dist/primitives/define-handler.d.ts +42 -112
  36. package/dist/primitives/define-handler.js +14 -45
  37. package/dist/primitives/define-projection.d.ts +23 -28
  38. package/dist/primitives/define-projection.js +29 -32
  39. package/dist/primitives/define-query.d.ts +52 -42
  40. package/dist/primitives/define-query.js +65 -28
  41. package/dist/primitives/define-workflow.d.ts +8 -11
  42. package/dist/primitives/define-workflow.js +14 -8
  43. package/dist/runtime/forge-dispatcher.d.ts +30 -12
  44. package/dist/runtime/forge-dispatcher.js +199 -237
  45. package/dist/runtime/forge-plugin.d.ts +8 -0
  46. package/dist/runtime/forge-plugin.js +113 -17
  47. package/dist/runtime/forge-plugins.d.ts +55 -0
  48. package/dist/runtime/forge-plugins.js +57 -0
  49. package/dist/runtime/forge-types.d.ts +8 -2
  50. package/dist/runtime/with-forge.d.ts +8 -11
  51. package/dist/runtime/with-forge.js +9 -11
  52. package/dist/stores/idempotency-store.d.ts +1 -1
  53. package/package.json +12 -12
@@ -1,64 +1,85 @@
1
1
  /**
2
- * `defineAction` — declares a typed message the system can handle. Actions
3
- * are the framework's center of gravity: named intent, typed input, no
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 submitAnswer = defineAction({
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 carries a `name` (its routing key) and a `schema` (input type).
14
- * It is BOTH a meta record (`.name`, `.schema`, …) AND a callable factory:
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
- * execute(submitAnswer({ studentId, attemptId, choice })) // dispatch
17
- * defineHandler(submitAnswer, fn) // bind handler
18
- * ctx.send(submitAnswer, input) // also valid
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
- * Calling the action mints a typed `CommandMessage` with input validated
21
- * against the zod schema — the dispatch site can't pass bad data. Mirrors
22
- * the `defineEvent → EventMessage` pattern (Phase 41.9).
24
+ * execute(submitAnswer({ ... })) // dispatch via minted CommandMessage
25
+ * ctx.request(submitAnswer, input) // dispatch by reference
26
+ * submitAnswer.name // "submissions.submit-answer"
23
27
  *
24
- * Inline shorthand: `defineAction({ ..., handler })` synthesizes a handler
25
- * alongside the contract. For cross-module orchestration where the handler
26
- * lives elsewhere, leave `handler` undefined and wire via `defineHandler`.
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
- import { captureSourceLocation } from "@nwire/messages";
29
- import { defineHandler, } from "./define-handler.js";
33
+ import { defineHandler } from "@nwire/handler";
30
34
  export function defineAction(nameOrMeta, maybeMeta) {
31
- const $source = captureSourceLocation();
32
35
  const meta = typeof nameOrMeta === "string"
33
36
  ? { ...(maybeMeta ?? {}), name: nameOrMeta }
34
37
  : nameOrMeta;
35
- return buildAction(meta, $source, false);
38
+ return buildAction(meta, false);
36
39
  }
37
40
  /**
38
- * Internal — build the callable action with all meta props attached.
39
- * Separated so `.public()` can re-build a clone with `$public: true` while
40
- * sharing one construction path (avoids drift between fresh-defined and
41
- * cloned actions).
41
+ * Internal — build the callable action with the kernel handler bridged on.
42
+ * Separated so `.public()` re-builds a clone sharing one construction path.
42
43
  */
43
- function buildAction(meta, $source, isPublic) {
44
+ function buildAction(meta, isPublic) {
45
+ const schema = meta.input;
46
+ if (!schema) {
47
+ throw new Error(`defineAction("${meta.name}"): an \`input\` (zod) schema is required.`);
48
+ }
44
49
  const factory = (input) => ({
45
50
  $kind: "command",
46
51
  actionName: meta.name,
47
- input: meta.schema.parse(input),
52
+ input: schema.parse(input),
48
53
  });
49
- // Inline handler delegate to `defineHandler` so there is a single
50
- // construction path for HandlerDefinition.
51
- let handler;
52
- if (meta.handler) {
53
- // Will be patched onto `factory` below; build the handler def first so it
54
- // references the action contract (factory itself, post-property-attach).
55
- handler = undefined; // placeholder, set after props attach
56
- }
54
+ // The action IS a handler: build it via the one definer, adapting the forge
55
+ // `(input, ctx)` body to the kernel single-`ctx` shape. Studio meta rides in
56
+ // `config.meta`; the command pipeline (retry/emits) reads forge-shaped fields
57
+ // off the action object directly.
58
+ const kernel = meta.handler
59
+ ? defineHandler.kind("action")(meta.name, {
60
+ input: schema,
61
+ // The body is `(ctx) => …` with `input` on ctx — same shape the kernel
62
+ // handler passes, so it's the handler directly (no adapter).
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ handler: meta.handler,
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ emits: meta.emits,
67
+ description: meta.description,
68
+ meta: {
69
+ persona: meta.persona,
70
+ journeyStep: meta.journeyStep,
71
+ capability: meta.capability,
72
+ slo: meta.slo,
73
+ tags: meta.tags,
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ },
76
+ })
77
+ : undefined;
57
78
  Object.defineProperties(factory, {
58
79
  $kind: { value: "action", enumerable: true },
59
80
  name: { value: meta.name, enumerable: true, configurable: true },
60
81
  description: { value: meta.description, enumerable: true },
61
- schema: { value: meta.schema, enumerable: true },
82
+ input: { value: schema, enumerable: true },
62
83
  retry: { value: meta.retry, enumerable: true },
63
84
  policy: { value: meta.policy, enumerable: true },
64
85
  persona: { value: meta.persona, enumerable: true },
@@ -68,8 +89,55 @@ function buildAction(meta, $source, isPublic) {
68
89
  tags: { value: meta.tags, enumerable: true },
69
90
  emits: { value: meta.emits, enumerable: true },
70
91
  toString: { value: () => meta.name, enumerable: false },
71
- $source: { value: $source, enumerable: false, configurable: true },
72
92
  });
93
+ if (kernel) {
94
+ Object.defineProperties(factory, {
95
+ // The kernel config — `runtime.execute` + `actionsPlugin` read it.
96
+ config: { value: kernel.config, enumerable: true, configurable: true },
97
+ run: {
98
+ value: (ctx, opts) =>
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ kernel.run(ctx, opts),
101
+ enumerable: false,
102
+ configurable: true,
103
+ },
104
+ use: {
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ value: (fn, opts) => {
107
+ kernel.use(fn, opts);
108
+ return factory;
109
+ },
110
+ enumerable: false,
111
+ },
112
+ on: {
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ value: (fn, opts) => {
115
+ kernel.on(fn, opts);
116
+ return factory;
117
+ },
118
+ enumerable: false,
119
+ },
120
+ off: {
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ value: (fn) => {
123
+ kernel.off(fn);
124
+ return factory;
125
+ },
126
+ enumerable: false,
127
+ },
128
+ runDetailed: {
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ value: (ctx, opts) => kernel.runDetailed(ctx, opts),
131
+ enumerable: false,
132
+ },
133
+ tap: {
134
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
135
+ value: (observer) => kernel.tap(observer),
136
+ enumerable: false,
137
+ },
138
+ stepCounts: { value: () => kernel.stepCounts(), enumerable: false },
139
+ });
140
+ }
73
141
  if (isPublic) {
74
142
  Object.defineProperty(factory, "$public", {
75
143
  value: true,
@@ -77,19 +145,9 @@ function buildAction(meta, $source, isPublic) {
77
145
  configurable: true,
78
146
  });
79
147
  }
80
- // Now that `factory` carries all action metadata, defineHandler can bind
81
- // to it as a proper ActionDefinition reference.
82
- if (meta.handler) {
83
- handler = defineHandler(factory, meta.handler);
84
- Object.defineProperty(factory, "handler", {
85
- value: handler,
86
- enumerable: true,
87
- configurable: true,
88
- });
89
- }
90
148
  Object.defineProperty(factory, "public", {
91
149
  value: function publicMarker() {
92
- return buildAction(meta, $source, true);
150
+ return buildAction(meta, true);
93
151
  },
94
152
  enumerable: false,
95
153
  configurable: true,
@@ -109,10 +167,7 @@ export function isCommandMessage(x) {
109
167
  }
110
168
  /**
111
169
  * Resolve either dispatch form into `(action, input)`. Centralised so every
112
- * transport surface (execute, send, ctx.request) accepts both call shapes
113
- * with one helper. When the caller passes a `CommandMessage`, the resolver
114
- * looks up the action by name on the runtime; when they pass an
115
- * `ActionDefinition` + input, it short-circuits.
170
+ * transport surface accepts both call shapes with one helper.
116
171
  */
117
172
  export function resolveDispatch(actionOrCmd, input, lookup) {
118
173
  if (isCommandMessage(actionOrCmd)) {
@@ -120,7 +175,7 @@ export function resolveDispatch(actionOrCmd, input, lookup) {
120
175
  if (!action) {
121
176
  throw new Error(`dispatch: action "${actionOrCmd.actionName}" is not registered. ` +
122
177
  `If you used the callable form (\`execute(myAction(input))\`), make ` +
123
- `sure the action is included in a module's \`actions\` array.`);
178
+ `sure the action is included in the app's \`handlers\`.`);
124
179
  }
125
180
  return { action, input: actionOrCmd.input };
126
181
  }
@@ -1,195 +1,143 @@
1
1
  /**
2
2
  * `defineActor` — the primitive for entities that carry state, transitions,
3
- * and timers. The actor is data + state machine in one declaration.
3
+ * and timers. An actor is a thing with identity that guards an invariant: one
4
+ * instance per id, it owns its state, and it's the only thing that changes it.
4
5
  *
5
- * export const Submission = defineActor('submission', {
6
- * schema: SubmissionSchema,
7
- * key: 'submissionId',
8
- * initial: 'submitted',
9
- * states: {
10
- * submitted: {
11
- * on: {
12
- * [AnswerFlaggedEvent.name]: {
13
- * target: 'under-review',
14
- * assign: (ctx, event) => ({ confidence: event.confidence })
15
- * },
16
- * [AnswerAutoGradedEvent.name]: {
17
- * target: 'graded',
18
- * assign: (ctx, event) => ({
19
- * verdict: event.verdict,
20
- * confidence: event.confidence,
21
- * gradedAt: event.gradedAt
22
- * })
23
- * }
24
- * }
25
- * },
26
- * 'under-review': {
27
- * on: {
28
- * [SubmissionManuallyGradedEvent.name]: {
29
- * target: 'graded',
30
- * assign: (ctx, event) => ({ verdict: event.verdict })
31
- * }
32
- * },
33
- * after: {
34
- * '3d': sendReviewReminder
35
- * }
36
- * },
37
- * graded: { final: true }
38
- * }
39
- * })
6
+ * Same builder shape as `defineWorkflow` — a closure with the one event verb
7
+ * `when` — plus identity + durability. State, key, and lifecycle come from a
8
+ * `defineSchema`; the closure declares the transitions:
40
9
  *
41
- * See:
42
- * - `__docs__/framework/foundation-v3-spec.md` design rationale
43
- * - `ADR-006-actors-vs-reactions.md` when to use defineActor vs `when`
44
- * - `ADR-007-declarative-state-via-assign.md` why `assign` returns partials
10
+ * export const Submission = defineActor("submission",
11
+ * ({ data, id, validate, recordThat, states, when, after }) => {
12
+ * const { submitted, underReview, graded } = states; // callable state bodies
13
+ * const flag = (reason: string) => { validate(data, [isSubmitted]); recordThat(Flagged({ id, reason })); };
14
+ * submitted(() => {
15
+ * when(AnswerWasSubmitted, (e, { assign }) => { assign({ ...e }); }); // stay
16
+ * when(SubmissionWasAutoGraded, (e, { assign }) => { assign({ ...e }); return graded; });
17
+ * when(SubmissionWasFlagged, () => underReview);
18
+ * });
19
+ * when(SubmissionDeleted, () => deleted); // top-level = always-listener
20
+ * underReview(() => after("review-reminder", "3d", sendReviewReminder));
21
+ * return { flag }; // methods: a plain object from the closure
22
+ * },
23
+ * { schema: SubmissionData, stuckThresholds, slas },
24
+ * );
25
+ *
26
+ * A `when` at the closure top is active in every state; a `when` inside a state
27
+ * body fires only in that state; the state-scoped one wins. A `when` returns a
28
+ * state to transition (nothing to stay); `ctx.assign(patch)` folds the actor's
29
+ * data. Identical to a workflow reaction — the difference is identity (keyed by
30
+ * the schema's `key`) and durability (an actor always persists).
31
+ *
32
+ * Default lifecycle: an actor always has a terminal. If the schema declares no
33
+ * `final` state, `deleted` (final) is injected — a free soft-delete terminal.
34
+ *
35
+ * `assign` vs `methods`: `assign(patch)` is a pure data fold during a
36
+ * transition. `methods` (in the options) are pure invariant-enforcers called
37
+ * from a handler via `ctx.actor(Actor, id)` — they receive state, return an
38
+ * event (or throw), and never do I/O.
45
39
  */
46
40
  import type { z } from "zod";
47
- import type { ZodTypeAny, SourceLocation } from "@nwire/messages";
48
- import type { EventDefinition } from "@nwire/messages";
49
- import { type SchemaDefinition } from "./define-schema.js";
41
+ import type { ZodTypeAny, EventDefinition } from "@nwire/messages";
42
+ import type { SchemaDefinition } from "./define-schema.js";
50
43
  /**
51
44
  * Reaction to one event in one state.
52
- *
53
- * - `target` — name of the state to transition to (omit to stay in this state).
54
- * - `assign` — pure function returning a partial to merge into ctx.
55
- * MUST be synchronous, MUST NOT mutate ctx, MUST NOT do I/O.
45
+ * - `target` — name of the state to transition to (omit to stay).
46
+ * - `assign` — pure function returning a partial to merge into the data.
56
47
  */
57
48
  export interface ActorReaction<TCtx, TEvent> {
58
49
  readonly target?: string;
59
50
  readonly assign?: (ctx: Readonly<TCtx>, event: TEvent) => Partial<TCtx>;
60
51
  }
61
- /**
62
- * Spec for one timer in a state's `after` block. `'3d'` = key, value is the
63
- * action to send when the timer fires (or an `{ delay, action }` form when the
64
- * timer name and the delay differ).
65
- */
52
+ /** Spec for one timer in a state's `after` block. */
66
53
  export interface ActorTimerSpec {
67
54
  readonly delay: string;
68
55
  /** The action name to dispatch when the timer fires. */
69
56
  readonly action: string;
70
- /**
71
- * Optional: a function that builds the action's input from the actor's
72
- * current ctx + key. Defaults to `{ <key>: actorKey }`.
73
- */
57
+ /** Optional: build the action's input from ctx + key. Defaults to `{ <key>: actorKey }`. */
74
58
  readonly buildInput?: (ctx: unknown, key: string) => unknown;
75
59
  }
76
- /**
77
- * Configuration for one state of an actor.
78
- */
60
+ /** Configuration for one state of an actor (the runtime-facing shape). */
79
61
  export interface ActorStateConfig<TCtx> {
80
62
  /** Reactions to events while in this state. Keyed by event name. */
81
63
  readonly on?: Readonly<Record<string, ActorReaction<TCtx, unknown>>>;
82
- /**
83
- * Timers scheduled on entry to this state, cancelled on exit.
84
- * Keyed by timer name. Value is either a delay string + action name,
85
- * or just an action name with the timer name used as the delay key.
86
- */
64
+ /** Timers scheduled on entry, cancelled on exit. Keyed by timer name. */
87
65
  readonly after?: Readonly<Record<string, string | ActorTimerSpec>>;
88
66
  /** Mark this state as the lifecycle end. */
89
67
  readonly final?: boolean;
90
68
  }
91
69
  /**
92
- * A pure invariant-enforcer + event-minter method. Methods take the actor's
93
- * current state (read-only) plus method args, and either return an event
94
- * (or events), throw on invariant violation, or return a read-only value
95
- * (for queries like `isGradable()`). Methods MUST NOT do I/O or read other
96
- * actors — that lives in the handler.
97
- *
98
- * methods: {
99
- * flagForReview(state, reason: string): EventMessage {
100
- * if (state.status !== 'pending') throw new Error('only pending')
101
- * return FlaggedForReviewEvent({ reason, flaggedAt: ... })
102
- * },
103
- * isGradable(state): boolean {
104
- * return state.status === 'pending' && state.flags === 0
105
- * }
106
- * }
70
+ * A pure invariant-enforcer + event-minter method. Takes the actor's current
71
+ * state (read-only) plus args; returns event(s), throws on invariant violation,
72
+ * or returns a read value. MUST NOT do I/O or read other actors.
107
73
  */
108
74
  export type ActorMethod<TCtx> = (state: TCtx, ...args: any[]) => any;
109
- /**
110
- * `defineActor` options. `TSchema` typically a `z.ZodObject` describing the
111
- * actor's data shape — every field of the schema is part of the actor's `ctx`.
112
- */
113
- /**
114
- * Per-state "stuck" thresholds (milliseconds). When an actor instance has
115
- * been in this state for >= the threshold, Studio surfaces it in the
116
- * stuck-state inbox. Use this to declare "if a submission is in
117
- * under-review for >48h, something needs Dina's attention."
118
- */
75
+ /** Per-state "stuck" thresholds (ms) — Studio surfaces instances exceeding them. */
119
76
  export type ActorStuckThresholds = Readonly<Record<string, number>>;
120
- /**
121
- * Per-state SLA declaration. `maxDurationMs` is the hard limit; once
122
- * crossed, Studio raises an alert. `escalateTo` is a free-form audience tag
123
- * (consumed by alerting integrations).
124
- */
77
+ /** Per-state SLA declaration with escalation hint. */
125
78
  export interface ActorSlaConfig {
126
79
  readonly maxDurationMs: number;
127
80
  readonly escalateTo?: string;
128
81
  }
129
82
  export type ActorSlas = Readonly<Record<string, ActorSlaConfig>>;
130
- export interface ActorOptions<TSchema extends ZodTypeAny, TMethods extends Readonly<Record<string, ActorMethod<z.output<TSchema>>>> = Readonly<Record<string, ActorMethod<z.output<TSchema>>>>> {
131
- readonly schema: TSchema;
132
- /** Field path in event payloads used to look up the actor instance. */
133
- readonly key: string;
134
- /** Initial state name. The first event for an unknown key starts here. */
135
- readonly initial: string;
136
- /** State machine configuration. */
137
- readonly states: Readonly<Record<string, ActorStateConfig<z.output<TSchema>>>>;
138
- /**
139
- * Optional pure methods callable from a handler via `ctx.use(Actor, id)`.
140
- * Each method receives the current state pre-bound; it returns event(s)
141
- * (which the handler returns to the runtime), throws on invariant
142
- * violation, or returns a non-event read value.
143
- */
144
- readonly methods?: TMethods;
145
- /**
146
- * Studio-aware: stuck-state thresholds in ms. Studio surfaces actor
147
- * instances exceeding the threshold. Example for Submission actor:
148
- * `{ 'under-review': 3 * 24 * 60 * 60 * 1000 }` (3 days).
149
- */
150
- readonly stuckThresholds?: ActorStuckThresholds;
151
- /**
152
- * Studio-aware: per-state hard SLAs with escalation hints.
153
- * `{ 'under-review': { maxDurationMs: 7 * 24 * 60 * 60 * 1000, escalateTo: 'curriculum-lead' } }`.
154
- */
155
- readonly slas?: ActorSlas;
156
- }
157
83
  /**
158
- * A typed state reference produced by `states.<name>` inside the closure
159
- * form. Returning a StateRef from a transition tells the runtime which state
160
- * to transition into.
84
+ * A callable state body `states.<name>`. Call it with a body to scope the
85
+ * `when`s inside to that state (`submitted(() => when(…))`); return it from a
86
+ * reaction to transition there (`return graded`). Same idea as the workflow
87
+ * state callable.
161
88
  */
162
89
  export interface StateRef {
90
+ (body: () => void): void;
163
91
  readonly $kind: "state-ref";
164
92
  readonly name: string;
165
93
  }
166
- /**
167
- * Closure-form context what the actor body receives when called as
168
- * `defineActor(schema, ({data, id, validate, recordThat, states, when, methods}) => ...)`.
169
- * At define time `data`/`id` are placeholders; at each `use()` call the
170
- * closure is re-invoked with the live instance state + key.
171
- */
172
- export interface ActorClosureContext<TState> {
173
- readonly data: TState;
94
+ /** Action reference (or bare name) used as a timer's target. */
95
+ export type ActorTimerAction = string | {
96
+ readonly name: string;
97
+ };
98
+ /** What the actor builder closure receives the workflow ctx, minus effects. */
99
+ export interface ActorBuilderContext<TState> {
100
+ /**
101
+ * Live view of the instance's current data — a proxy that reads the data the
102
+ * reaction is folding over. Read it to compute the next value (e.g.
103
+ * `assign({ count: (data.count ?? 0) + 1 })`), same as a workflow's `data`.
104
+ */
105
+ readonly data: Readonly<TState>;
106
+ /** The instance id (the schema `key`'s value). Live inside a method call. */
174
107
  readonly id: string;
108
+ /**
109
+ * Enforce invariants from a method: each predicate returns `true` or a
110
+ * failure message; any failure throws an `InvariantError`. `state` defaults
111
+ * to the live instance data. No-op during the define-time pass.
112
+ */
175
113
  readonly validate: <TIn>(input: TIn, predicates: ReadonlyArray<(input: TIn, state?: TState) => true | string>, state?: TState) => void;
114
+ /** Mint a domain event from a method; collected + published after the call. */
176
115
  readonly recordThat: (event: {
177
116
  eventName: string;
178
117
  payload: unknown;
179
118
  }) => void;
119
+ /** Callable state bodies, sourced from the schema (+ injected terminal). */
180
120
  readonly states: Record<string, StateRef>;
181
- readonly when: <E extends {
121
+ /** The one event verb. Top-level = always-active; inside a state body = scoped. */
122
+ when<E extends {
182
123
  name: string;
183
124
  }>(event: E, fn: (payload: any, ctx: {
184
125
  assign: (delta: Partial<TState>) => void;
185
- }) => StateRef | void) => void;
186
- readonly methods: <T extends Record<string, (...args: never[]) => unknown>>(obj: T) => T;
126
+ }) => StateRef | void): void;
127
+ /** Schedule a state-entry timer (cancelled on exit). Scoped to the current state body. */
128
+ after(name: string, delay: string, action: ActorTimerAction): void;
129
+ }
130
+ /** Options for `defineActor(name, body, options)`. */
131
+ export interface ActorOptions<TFields extends Readonly<Record<string, ZodTypeAny>>> {
132
+ /** Data shape + key + lifecycle states (from `defineSchema`). */
133
+ readonly schema: SchemaDefinition<TFields>;
134
+ readonly stuckThresholds?: ActorStuckThresholds;
135
+ readonly slas?: ActorSlas;
187
136
  }
188
137
  /**
189
138
  * `ActorDefinition` is the data the runtime registers and dispatches against.
190
- * `defineActor` returns this; consumers should not construct it manually.
191
139
  */
192
- export interface ActorDefinition<TSchema extends ZodTypeAny = ZodTypeAny, TMethods extends Readonly<Record<string, ActorMethod<z.output<TSchema>>>> = Readonly<Record<string, ActorMethod<z.output<TSchema>>>>> {
140
+ export interface ActorDefinition<TSchema extends ZodTypeAny = ZodTypeAny, TMethods extends Readonly<Record<string, (...args: any[]) => unknown>> = Readonly<Record<string, (...args: any[]) => unknown>>> {
193
141
  readonly $kind: "actor";
194
142
  readonly name: string;
195
143
  readonly schema: TSchema;
@@ -199,105 +147,46 @@ export interface ActorDefinition<TSchema extends ZodTypeAny = ZodTypeAny, TMetho
199
147
  readonly methods?: TMethods;
200
148
  readonly stuckThresholds?: ActorStuckThresholds;
201
149
  readonly slas?: ActorSlas;
202
- /**
203
- * Quick-lookup index: every event name this actor reacts to in any state,
204
- * with the list of (stateName, reaction) entries that handle it. Built at
205
- * definition time so dispatch is O(1) per event.
206
- */
150
+ /** event name → [(state, reaction)] handling it. Built at definition time. */
207
151
  readonly eventIndex: ReadonlyMap<string, ReadonlyArray<{
208
152
  state: string;
209
153
  reaction: ActorReaction<z.output<TSchema>, unknown>;
210
154
  }>>;
211
155
  /**
212
- * Closure-form binder. When set, `runtime.use(Actor, id)` calls this with
213
- * the live `state` + `id` to produce the bound methods. Methods record
214
- * events via `recordThat`, which the framework collects after the method
215
- * returns. Methods may also return a value to the caller.
216
- *
217
- * `null` for classic / schema-bound-object actors.
156
+ * Per-use binder. `ctx.actor(Actor, id)` calls it with the live `state` + `id`
157
+ * to re-invoke the closure and produce methods bound over that instance;
158
+ * methods record events via `recordThat`, collected from `recorded` after each
159
+ * call. (Methods come from the closure's returned object, not options.)
218
160
  */
219
161
  readonly closureBinder?: (state: z.output<TSchema>, id: string) => {
220
162
  methods: Record<string, (...args: unknown[]) => unknown>;
221
- /** Events emitted by the most recent method call. Cleared each call. */
222
163
  recorded: ReadonlyArray<{
223
164
  eventName: string;
224
165
  payload: unknown;
225
166
  }>;
226
167
  };
227
- /** Where the actor was declared. Studio uses this for IDE-open links. */
228
- readonly $source?: SourceLocation;
229
168
  }
230
169
  /**
231
- * The shape returned by `ctx.use(Actor, id)`: the loaded `state` plus every
232
- * method from the actor definition, pre-bound to that state.
170
+ * The shape returned by `ctx.actor(Actor, id)`: the loaded `state` plus every
171
+ * method, pre-bound to that state.
233
172
  */
234
173
  export type ActorInstanceView<TActor> = TActor extends ActorDefinition<infer TSchema, infer TMethods> ? {
235
174
  readonly state: Readonly<z.output<TSchema>>;
236
175
  readonly key: string;
237
176
  readonly stateName: string;
238
177
  } & {
239
- readonly [K in keyof TMethods]: TMethods[K] extends (state: z.output<TSchema>, ...args: infer A) => infer R ? (...args: A) => R : never;
178
+ readonly [K in keyof TMethods]: TMethods[K] extends (...args: infer A) => infer R ? (...args: A) => R : never;
240
179
  } : never;
241
180
  /**
242
- * Schema-bound form: the actor borrows `key`, `initial`, and the set of valid
243
- * state names from a `SchemaDefinition`. Per-state `on`/`after` transitions are
244
- * still declared here, but only for states declared in the schema. States the
245
- * schema marks `final: true` cannot have `on:` reactions.
181
+ * Define an actor one builder closure + a `defineSchema` for the shape.
246
182
  *
247
- * const Submission = defineActor({
248
- * schema: SubmissionData, // SchemaDefinition
249
- * states: {
250
- * submitted: { on: { ... } },
251
- * "under-review": { on: { ... }, after: { ... } },
252
- * // graded is final in the schema omit it
253
- * },
254
- * methods: { ... },
255
- * });
256
- */
257
- export interface ActorOptionsBound<TFields extends Readonly<Record<string, ZodTypeAny>>, TMethods extends Readonly<Record<string, ActorMethod<z.output<z.ZodObject<TFields>>>>> = Readonly<Record<string, ActorMethod<z.output<z.ZodObject<TFields>>>>>> {
258
- /** Optional — defaults to `schema.name`. */
259
- readonly name?: string;
260
- readonly schema: SchemaDefinition<TFields>;
261
- /** Per-state transition tables. Only states declared in the schema are valid. */
262
- readonly states?: Readonly<Record<string, ActorStateConfig<z.output<z.ZodObject<TFields>>>>>;
263
- readonly methods?: TMethods;
264
- readonly stuckThresholds?: ActorStuckThresholds;
265
- readonly slas?: ActorSlas;
266
- }
267
- /**
268
- * Closure form options — what `defineActor(schema, fn, options?)` accepts
269
- * for stuckThresholds + slas + name override. The transitions + methods
270
- * live in the closure body.
271
- */
272
- export interface ActorOptionsClosure {
273
- readonly name?: string;
274
- readonly stuckThresholds?: ActorStuckThresholds;
275
- readonly slas?: ActorSlas;
276
- }
277
- export declare function defineActor<TSchema extends ZodTypeAny, TMethods extends Readonly<Record<string, ActorMethod<z.output<TSchema>>>> = Readonly<Record<string, ActorMethod<z.output<TSchema>>>>>(name: string, options: ActorOptions<TSchema, TMethods>): ActorDefinition<TSchema, TMethods>;
278
- export declare function defineActor<TFields extends Readonly<Record<string, ZodTypeAny>>, TMethods extends Readonly<Record<string, ActorMethod<z.output<z.ZodObject<TFields>>>>> = Readonly<Record<string, ActorMethod<z.output<z.ZodObject<TFields>>>>>>(options: ActorOptionsBound<TFields, TMethods>): ActorDefinition<z.ZodObject<TFields>, TMethods>;
279
- /**
280
- * Closure form:
281
- *
282
- * const Station = defineActor(StationData,
283
- * ({ data, id, validate, recordThat, states, when, methods }) => {
284
- * const { active, decommissioned } = states
285
- * const create = (input) => { validate(...); recordThat(Created({...})); }
286
- * when(Created, (e, { assign }) => { assign({...}) })
287
- * when(Deleted, (e, { assign }) => { assign({...}); return decommissioned })
288
- * return methods({ create })
289
- * },
290
- * { stuckThresholds: { ... } },
291
- * )
292
- *
293
- * The closure runs once at define time to register transitions + capture
294
- * method names; the runtime re-runs it per `use(Actor, id)` call to produce
295
- * bound methods over the live state.
296
- */
297
- export declare function defineActor<TFields extends Readonly<Record<string, ZodTypeAny>>>(schema: SchemaDefinition<TFields>, body: (ctx: ActorClosureContext<z.output<z.ZodObject<TFields>>>) => Record<string, (...args: never[]) => unknown> | void, options?: ActorOptionsClosure): ActorDefinition<z.ZodObject<TFields>, any>;
298
- /**
299
- * Helper: turn an `EventDefinition` reference into a string suitable for the
300
- * `on` map key. Equivalent to `Event.name` but reads as `eventKey(Event)` at
301
- * call sites where the import is shorter.
302
- */
183
+ * The closure runs once at define time (stub ctx) to collect transitions
184
+ * (`when`) and timers (`after`), with state bodies scoping them; the runtime
185
+ * folds events through `actors-chain` (locking, OCC, persistence, the
186
+ * transition hook). Methods are defined in the closure and returned as a plain
187
+ * object `return { create, update }` and bound over the live instance per
188
+ * `ctx.actor(Actor, id)` (no `methods()` wrapper, no static methods option).
189
+ */
190
+ export declare function defineActor<TFields extends Readonly<Record<string, ZodTypeAny>>, TMethods extends Readonly<Record<string, (...args: any[]) => unknown>> = Record<string, never>>(name: string, body: (ctx: ActorBuilderContext<z.output<z.ZodObject<TFields>>>) => TMethods, options: ActorOptions<TFields>): ActorDefinition<z.ZodObject<TFields>, TMethods>;
191
+ /** Helper: `Event.name` as a string. Reads as `eventKey(Event)` at call sites. */
303
192
  export declare function eventKey(event: EventDefinition): string;