effect-machine 0.11.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 (68) hide show
  1. package/README.md +128 -324
  2. package/dist/actor.d.ts +52 -31
  3. package/dist/actor.js +218 -283
  4. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  5. package/dist/cluster/adapters/in-memory.js +79 -0
  6. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  7. package/dist/cluster/entity-actor-ref.js +33 -0
  8. package/dist/cluster/entity-machine.d.ts +31 -49
  9. package/dist/cluster/entity-machine.js +178 -52
  10. package/dist/cluster/index.d.ts +5 -2
  11. package/dist/cluster/index.js +4 -1
  12. package/dist/cluster/persistence.d.ts +49 -0
  13. package/dist/cluster/persistence.js +18 -0
  14. package/dist/cluster/to-entity.d.ts +9 -3
  15. package/dist/cluster/to-entity.js +16 -4
  16. package/dist/errors.d.ts +25 -17
  17. package/dist/errors.js +10 -5
  18. package/dist/index.d.ts +6 -4
  19. package/dist/index.js +4 -3
  20. package/dist/internal/brands.d.ts +14 -1
  21. package/dist/internal/runtime.d.ts +142 -0
  22. package/dist/internal/runtime.js +357 -0
  23. package/dist/internal/transition.d.ts +10 -4
  24. package/dist/internal/transition.js +24 -12
  25. package/dist/internal/utils.d.ts +42 -6
  26. package/dist/internal/utils.js +27 -1
  27. package/dist/machine.d.ts +89 -55
  28. package/dist/machine.js +80 -68
  29. package/dist/schema.d.ts +35 -34
  30. package/dist/schema.js +33 -4
  31. package/dist/supervision.d.ts +97 -0
  32. package/dist/supervision.js +42 -0
  33. package/dist/testing.d.ts +17 -8
  34. package/dist/testing.js +22 -23
  35. package/package.json +7 -7
  36. package/v3/dist/actor.d.ts +54 -37
  37. package/v3/dist/actor.js +209 -277
  38. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  39. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  40. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  41. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  42. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  43. package/v3/dist/cluster/entity-machine.js +134 -50
  44. package/v3/dist/cluster/index.d.ts +5 -2
  45. package/v3/dist/cluster/index.js +4 -1
  46. package/v3/dist/cluster/persistence.d.ts +48 -0
  47. package/v3/dist/cluster/persistence.js +14 -0
  48. package/v3/dist/cluster/to-entity.d.ts +5 -2
  49. package/v3/dist/cluster/to-entity.js +12 -4
  50. package/v3/dist/errors.d.ts +18 -8
  51. package/v3/dist/errors.js +9 -4
  52. package/v3/dist/index.d.ts +6 -4
  53. package/v3/dist/index.js +3 -2
  54. package/v3/dist/internal/brands.d.ts +15 -1
  55. package/v3/dist/internal/runtime.d.ts +142 -0
  56. package/v3/dist/internal/runtime.js +335 -0
  57. package/v3/dist/internal/transition.d.ts +10 -4
  58. package/v3/dist/internal/transition.js +23 -11
  59. package/v3/dist/internal/utils.d.ts +42 -6
  60. package/v3/dist/internal/utils.js +27 -1
  61. package/v3/dist/machine.d.ts +35 -47
  62. package/v3/dist/machine.js +62 -64
  63. package/v3/dist/schema.d.ts +35 -34
  64. package/v3/dist/schema.js +29 -3
  65. package/v3/dist/supervision.d.ts +97 -0
  66. package/v3/dist/supervision.js +42 -0
  67. package/v3/dist/testing.d.ts +18 -9
  68. package/v3/dist/testing.js +21 -22
@@ -1,5 +1,4 @@
1
- import { INTERNAL_ENTER_EVENT, isEffect } from "./utils.js";
2
- import { BuiltMachine } from "../machine.js";
1
+ import { INTERNAL_ENTER_EVENT, isDeferReplyResult, isEffect, isReplyResult } from "./utils.js";
3
2
  import { Cause, Effect, Exit, Scope } from "effect";
4
3
  //#region src/internal/transition.ts
5
4
  /**
@@ -39,14 +38,22 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
39
38
  };
40
39
  const raw = transition.handler(handlerCtx);
41
40
  const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
42
- if (resolved !== null && typeof resolved === "object" && "state" in resolved && "reply" in resolved && !("_tag" in resolved)) return {
41
+ if (isReplyResult(resolved)) return {
43
42
  newState: resolved.state,
44
43
  hasReply: true,
44
+ deferReply: false,
45
45
  reply: resolved.reply
46
46
  };
47
+ if (isDeferReplyResult(resolved)) return {
48
+ newState: resolved.state,
49
+ hasReply: false,
50
+ deferReply: true,
51
+ reply: void 0
52
+ };
47
53
  return {
48
54
  newState: resolved,
49
55
  hasReply: false,
56
+ deferReply: false,
50
57
  reply: void 0
51
58
  };
52
59
  });
@@ -68,14 +75,16 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
68
75
  transitioned: false,
69
76
  reenter: false,
70
77
  hasReply: false,
78
+ deferReply: false,
71
79
  reply: void 0
72
80
  };
73
- const { newState, hasReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
81
+ const { newState, hasReply, deferReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
74
82
  return {
75
83
  newState,
76
84
  transitioned: true,
77
85
  reenter: transition.reenter === true,
78
86
  hasReply,
87
+ deferReply,
79
88
  reply
80
89
  };
81
90
  });
@@ -118,6 +127,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
118
127
  lifecycleRan: false,
119
128
  isFinal: false,
120
129
  hasReply: false,
130
+ deferReply: false,
121
131
  reply: void 0,
122
132
  postponed: false
123
133
  };
@@ -128,7 +138,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
128
138
  stateScopeRef.current = yield* Scope.make();
129
139
  if (hooks?.onTransition !== void 0) yield* hooks.onTransition(currentState, newState, event);
130
140
  if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
131
- yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError);
141
+ yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError, hooks?.onSpawnDefect);
132
142
  }
133
143
  return {
134
144
  newState,
@@ -137,6 +147,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
137
147
  lifecycleRan: runLifecycle,
138
148
  isFinal: machine.finalStates.has(newState._tag),
139
149
  hasReply: result.hasReply,
150
+ deferReply: result.deferReply,
140
151
  reply: result.reply,
141
152
  postponed: false
142
153
  };
@@ -146,7 +157,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
146
157
  *
147
158
  * @internal
148
159
  */
149
- const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError) {
160
+ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError, onSpawnDefect) {
150
161
  const spawnEffects = findSpawnEffects(machine, state._tag);
151
162
  const ctx = {
152
163
  actorId,
@@ -157,6 +168,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
157
168
  };
158
169
  const { effects: effectSlots } = machine._slots;
159
170
  const reportError = onError;
171
+ const defectSignal = onSpawnDefect;
160
172
  for (const spawnEffect of spawnEffects) {
161
173
  const effect = spawnEffect.handler({
162
174
  actorId,
@@ -167,13 +179,14 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
167
179
  system
168
180
  }).pipe(Effect.provideService(machine.Context, ctx), Effect.catchAllCause((cause) => {
169
181
  if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
170
- if (reportError === void 0) return Effect.failCause(cause).pipe(Effect.orDie);
171
- return reportError({
182
+ const report = reportError !== void 0 ? reportError({
172
183
  phase: "spawn",
173
184
  state,
174
185
  event,
175
186
  cause
176
- }).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
187
+ }) : Effect.void;
188
+ const signal = defectSignal !== void 0 ? defectSignal(cause) : Effect.void;
189
+ return report.pipe(Effect.zipRight(signal), Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
177
190
  }));
178
191
  yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
179
192
  }
@@ -246,11 +259,10 @@ const getIndex = (machine) => {
246
259
  * Find all transitions matching a state/event pair.
247
260
  * Returns empty array if no matches.
248
261
  *
249
- * Accepts both `Machine` and `BuiltMachine`.
250
262
  * O(1) lookup after first access (index is lazily built).
251
263
  */
252
264
  const findTransitions = (input, stateTag, eventTag) => {
253
- const index = getIndex(input instanceof BuiltMachine ? input._inner : input);
265
+ const index = getIndex(input);
254
266
  const specific = index.transitions.get(stateTag)?.get(eventTag) ?? [];
255
267
  if (specific.length > 0) return specific;
256
268
  return index.transitions.get("*")?.get(eventTag) ?? [];
@@ -23,15 +23,51 @@ type InstanceOf<C> = C extends ((...args: unknown[]) => infer R) ? R : never;
23
23
  type TaggedConstructor<T extends {
24
24
  readonly _tag: string;
25
25
  }> = (args: Omit<T, "_tag">) => T;
26
- /** Reply tuple returned from transition handlers for ask support */
27
- interface TransitionReply<State> {
26
+ declare const ReplyResultSymbol: unique symbol;
27
+ type ReplyResultSymbol = typeof ReplyResultSymbol;
28
+ /**
29
+ * Branded reply result from a transition handler.
30
+ * Created via `Machine.reply(state, value)`.
31
+ */
32
+ interface ReplyResult<State, Reply> {
33
+ readonly state: State;
34
+ readonly reply: Reply;
35
+ readonly [ReplyResultSymbol]: true;
36
+ }
37
+ /**
38
+ * Create a reply result for ask-bearing event handlers.
39
+ */
40
+ declare const makeReply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
41
+ /**
42
+ * Type guard for ReplyResult (symbol-based, replaces duck-typing).
43
+ */
44
+ declare const isReplyResult: (value: unknown) => value is ReplyResult<unknown, unknown>;
45
+ declare const DeferReplySymbol: unique symbol;
46
+ type DeferReplySymbol = typeof DeferReplySymbol;
47
+ /**
48
+ * Branded deferred reply result from a transition handler.
49
+ * Signals that the reply will be settled later by `self.reply()` in a spawn handler.
50
+ * Created via `Machine.deferReply(state)`.
51
+ */
52
+ interface DeferReplyResult<State> {
28
53
  readonly state: State;
29
- readonly reply: unknown;
54
+ readonly [DeferReplySymbol]: true;
30
55
  }
31
56
  /**
32
- * Transition handler result - either a new state, reply tuple, or Effect producing one
57
+ * Create a deferred reply result. Handler returns this to signal
58
+ * "spawn handler will call self.reply(value) later".
59
+ */
60
+ declare const makeDeferReply: <State>(state: State) => DeferReplyResult<State>;
61
+ /**
62
+ * Type guard for DeferReplyResult.
63
+ */
64
+ declare const isDeferReplyResult: (value: unknown) => value is DeferReplyResult<unknown>;
65
+ /**
66
+ * Transition handler result.
67
+ * - When Reply is `never`: handler returns plain State (no reply allowed)
68
+ * - When Reply is concrete: handler must return ReplyResult via Machine.reply()
33
69
  */
34
- type TransitionResult<State, R> = State | TransitionReply<State> | Effect.Effect<State | TransitionReply<State>, never, R>;
70
+ type TransitionResult<State, R, Reply = never> = [Reply] extends [never] ? State | Effect.Effect<State, never, R> : ReplyResult<State, Reply> | DeferReplyResult<State> | Effect.Effect<ReplyResult<State, Reply> | DeferReplyResult<State>, never, R>;
35
71
  /**
36
72
  * Internal event tags used for lifecycle effect contexts.
37
73
  * Prefixed with $ to distinguish from user events.
@@ -62,4 +98,4 @@ declare const isEffect: (value: unknown) => value is Effect.Effect<unknown, unkn
62
98
  */
63
99
  declare const stubSystem: ActorSystem;
64
100
  //#endregion
65
- export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionReply, TransitionResult, getTag, isEffect, stubSystem };
101
+ export { ArgsOf, DeferReplyResult, DeferReplySymbol, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, ReplyResult, ReplyResultSymbol, TagOf, TaggedConstructor, TransitionResult, getTag, isDeferReplyResult, isEffect, isReplyResult, makeDeferReply, makeReply, stubSystem };
@@ -1,5 +1,31 @@
1
1
  import { Effect, Stream } from "effect";
2
2
  //#region src/internal/utils.ts
3
+ const ReplyResultSymbol = Symbol.for("effect-machine/ReplyResult");
4
+ /**
5
+ * Create a reply result for ask-bearing event handlers.
6
+ */
7
+ const makeReply = (state, reply) => ({
8
+ state,
9
+ reply,
10
+ [ReplyResultSymbol]: true
11
+ });
12
+ /**
13
+ * Type guard for ReplyResult (symbol-based, replaces duck-typing).
14
+ */
15
+ const isReplyResult = (value) => value !== null && typeof value === "object" && ReplyResultSymbol in value;
16
+ const DeferReplySymbol = Symbol.for("effect-machine/DeferReply");
17
+ /**
18
+ * Create a deferred reply result. Handler returns this to signal
19
+ * "spawn handler will call self.reply(value) later".
20
+ */
21
+ const makeDeferReply = (state) => ({
22
+ state,
23
+ [DeferReplySymbol]: true
24
+ });
25
+ /**
26
+ * Type guard for DeferReplyResult.
27
+ */
28
+ const isDeferReplyResult = (value) => value !== null && typeof value === "object" && DeferReplySymbol in value;
3
29
  /**
4
30
  * Internal event tags used for lifecycle effect contexts.
5
31
  * Prefixed with $ to distinguish from user events.
@@ -42,4 +68,4 @@ const stubSystem = {
42
68
  subscribe: () => () => {}
43
69
  };
44
70
  //#endregion
45
- export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect, stubSystem };
71
+ export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isDeferReplyResult, isEffect, isReplyResult, makeDeferReply, makeReply, stubSystem };
@@ -1,15 +1,16 @@
1
- import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
2
- import { TransitionResult } from "./internal/utils.js";
3
- import { BrandedEvent, BrandedState, TaggedOrConstructor } from "./internal/brands.js";
1
+ import { Supervision } from "./supervision.js";
2
+ import { ReplyResult, TransitionResult } from "./internal/utils.js";
3
+ import { BrandedEvent, BrandedState, ExtractReply, TaggedOrConstructor } from "./internal/brands.js";
4
4
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
5
5
  import { DuplicateActorError } from "./errors.js";
6
+ import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
6
7
  import { findTransitions } from "./internal/transition.js";
7
8
  import { ActorRef, ActorSystem } from "./actor.js";
8
9
  import { Cause, Context, Duration, Effect, Schema, Scope } from "effect";
9
10
 
10
11
  //#region src/machine.d.ts
11
12
  declare namespace machine_d_exports {
12
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, spawn };
13
+ export { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, materializeMachine, replay, reply, spawn };
13
14
  }
14
15
  /**
15
16
  * Self reference for sending events back to the machine
@@ -22,7 +23,7 @@ interface MachineRef<Event> {
22
23
  readonly _tag: string;
23
24
  }, E2 extends {
24
25
  readonly _tag: string;
25
- }, R2>(id: string, machine: BuiltMachine<S2, E2, R2>) => Effect.Effect<ActorRef<S2, E2>, DuplicateActorError, R2>;
26
+ }, R2>(id: string, machine: Machine<S2, E2, R2, any, any, any, any>) => Effect.Effect<ActorRef<S2, E2>, DuplicateActorError, R2>;
26
27
  }
27
28
  /**
28
29
  * Handler context passed to transition handlers
@@ -45,9 +46,11 @@ interface StateHandlerContext<State, Event, ED extends EffectsDef> {
45
46
  readonly system: ActorSystem;
46
47
  }
47
48
  /**
48
- * Transition handler function
49
+ * Transition handler function.
50
+ * When Reply is concrete (event has a reply schema), handler must return Machine.reply().
51
+ * When Reply is never, handler returns plain state.
49
52
  */
50
- type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R> = (ctx: HandlerContext<S, E, GD, ED>) => TransitionResult<NewState, R>;
53
+ type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R, Reply = never> = (ctx: HandlerContext<S, E, GD, ED>) => TransitionResult<NewState, R, Reply>;
51
54
  /**
52
55
  * State effect handler function
53
56
  */
@@ -91,9 +94,6 @@ interface TimeoutConfig<State, Event> {
91
94
  /** Event to send when the timer fires. Static or derived from current state. */
92
95
  readonly event: Event | ((state: State) => Event);
93
96
  }
94
- type IsAny<T> = 0 extends 1 & T ? true : false;
95
- type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
96
- type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
97
97
  interface MakeConfig<SD extends Record<string, Schema.Struct.Fields>, ED extends Record<string, Schema.Struct.Fields>, S extends BrandedState, E extends BrandedEvent, GD extends GuardsDef, EFD extends EffectsDef> {
98
98
  readonly state: MachineStateSchema<SD> & {
99
99
  Type: S;
@@ -113,22 +113,14 @@ type HasEffectKeys<EFD extends EffectsDef> = [keyof EFD] extends [never] ? false
113
113
  type SlotContext<State, Event> = MachineContext<State, Event, MachineRef<Event>>;
114
114
  /** Combined handlers for build() - guards and effects only */
115
115
  type ProvideHandlers<State, Event, GD extends GuardsDef, EFD extends EffectsDef, R> = (HasGuardKeys<GD> extends true ? GuardHandlers<GD, SlotContext<State, Event>, R> : object) & (HasEffectKeys<EFD> extends true ? EffectHandlers<EFD, SlotContext<State, Event>, R> : object);
116
- /** Whether the machine has any guard or effect slots */
117
- type HasSlots<GD extends GuardsDef, EFD extends EffectsDef> = HasGuardKeys<GD> extends true ? true : HasEffectKeys<EFD>;
118
116
  /**
119
- * A finalized machine ready for spawning.
117
+ * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
118
+ * If no handlers provided and machine has no slots, returns the machine as-is.
119
+ * Validates that all required slots are provided and no extra slots are given.
120
120
  *
121
- * Created by calling `.build()` on a `Machine`. This is the only type
122
- * accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
123
- * Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
121
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
124
122
  */
125
- declare class BuiltMachine<State, Event, R = never> {
126
- /** @internal */
127
- readonly _inner: Machine<State, Event, R, any, any, any, any>;
128
- /** @internal */
129
- constructor(machine: Machine<State, Event, R, any, any, any, any>);
130
- get initial(): State;
131
- }
123
+ declare const materializeMachine: <S, E, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, any, any, GD, EFD>, handlers?: Record<string, any>) => Machine<S, E, never, any, any, GD, EFD>;
132
124
  /**
133
125
  * Machine definition with fluent builder API.
134
126
  *
@@ -171,6 +163,8 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
171
163
  };
172
164
  readonly stateSchema?: Schema.Schema<State, unknown, never>;
173
165
  readonly eventSchema?: Schema.Schema<Event, unknown, never>;
166
+ /** @internal */
167
+ readonly _replySchemas: ReadonlyMap<string, Schema.Schema.Any>;
174
168
  /**
175
169
  * Context tag for accessing machine state/event/self in slot handlers.
176
170
  * Uses shared module-level tag for all machines.
@@ -186,24 +180,25 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
186
180
  }>;
187
181
  get guardsSchema(): GuardsSchema<GD> | undefined;
188
182
  get effectsSchema(): EffectsSchema<EFD> | undefined;
183
+ get replySchemas(): ReadonlyMap<string, Schema.Schema.Any>;
189
184
  /** @internal */
190
185
  constructor(initial: State, stateSchema?: Schema.Schema<State, unknown, never>, eventSchema?: Schema.Schema<Event, unknown, never>, guardsSchema?: GuardsSchema<GD>, effectsSchema?: EffectsSchema<EFD>);
191
186
  from<NS extends VariantsUnion<_SD> & BrandedState, R1>(state: TaggedOrConstructor<NS>, build: (scope: TransitionScope<State, Event, R, _SD, _ED, GD, EFD, NS>) => R1): Machine<State, Event, R, _SD, _ED, GD, EFD>;
192
187
  from<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, R1>(states: NS, build: (scope: TransitionScope<State, Event, R, _SD, _ED, GD, EFD, NS[number] extends TaggedOrConstructor<infer S extends VariantsUnion<_SD> & BrandedState> ? S : never>) => R1): Machine<State, Event, R, _SD, _ED, GD, EFD>;
193
188
  /** @internal */
194
- scopeTransition<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: ReadonlyArray<TaggedOrConstructor<NS>>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never>, reenter: boolean): Machine<State, Event, R, _SD, _ED, GD, EFD>;
189
+ scopeTransition<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: ReadonlyArray<TaggedOrConstructor<NS>>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>, reenter: boolean): Machine<State, Event, R, _SD, _ED, GD, EFD>;
195
190
  /** Register transition for a single state */
196
- on<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
191
+ on<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
197
192
  /** Register transition for multiple states (handler receives union of state types) */
198
- on<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
193
+ on<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
199
194
  /**
200
195
  * Like `on()`, but forces onEnter/spawn to run even when transitioning to the same state tag.
201
196
  * Use this to restart timers, re-run spawned effects, or reset state-scoped effects.
202
197
  */
203
198
  /** Single state */
204
- reenter<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
199
+ reenter<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
205
200
  /** Multiple states */
206
- reenter<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
201
+ reenter<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
207
202
  /**
208
203
  * Register a wildcard transition that fires from any state when no specific transition matches.
209
204
  * Specific `.on()` transitions always take priority over `.onAny()`.
@@ -301,50 +296,43 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
301
296
  */
302
297
  postpone<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, events: TaggedOrConstructor<VariantsUnion<_ED> & BrandedEvent> | ReadonlyArray<TaggedOrConstructor<VariantsUnion<_ED> & BrandedEvent>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
303
298
  final<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
304
- /**
305
- * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
306
- *
307
- * - Machines with slots: pass implementations as the first argument.
308
- * - Machines without slots: call with no arguments.
309
- */
310
- build<R2 = never>(...args: HasSlots<GD, EFD> extends true ? [handlers: ProvideHandlers<State, Event, GD, EFD, R2>] : [handlers?: ProvideHandlers<State, Event, GD, EFD, R2>]): BuiltMachine<State, Event, R | NormalizeR<R2>>;
311
299
  static make<SD extends Record<string, Schema.Struct.Fields>, ED extends Record<string, Schema.Struct.Fields>, S extends BrandedState, E extends BrandedEvent, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(config: MakeConfig<SD, ED, S, E, GD, EFD>): Machine<S, E, never, SD, ED, GD, EFD>;
312
300
  }
313
301
  declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema.Struct.Fields>, _ED extends Record<string, Schema.Struct.Fields>, GD extends GuardsDef, EFD extends EffectsDef, SelectedState extends VariantsUnion<_SD> & BrandedState> {
314
302
  private readonly machine;
315
303
  private readonly states;
316
304
  constructor(machine: Machine<State, Event, R, _SD, _ED, GD, EFD>, states: ReadonlyArray<TaggedOrConstructor<SelectedState>>);
317
- on<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
318
- reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
305
+ on<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never, ExtractReply<NE>>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
306
+ reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never, ExtractReply<NE>>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
319
307
  }
320
308
  declare const make: typeof Machine.make;
309
+ type AnyMachine<S, E, R> = Machine<S, E, R, any, any, any, any>;
321
310
  /**
322
- * Spawn an actor from a built machine.
311
+ * Spawn an actor from a machine.
323
312
  *
324
313
  * Options:
325
314
  * - `id` — custom actor ID (default: random)
326
315
  * - `hydrate` — restore from a previously-saved state snapshot.
327
- * The actor starts in the hydrated state and re-runs spawn effects
328
- * for that state (timers, scoped resources, etc.). Transition history
329
- * is not replayed — only the current state's entry effects run.
330
- *
331
- * Persistence is composed in userland by observing `actor.changes`
332
- * and saving snapshots to your own storage.
316
+ * - `slots` slot handler implementations for slotful machines.
333
317
  */
334
318
  declare const spawn: <S extends {
335
319
  readonly _tag: string;
336
320
  }, E extends {
337
321
  readonly _tag: string;
338
- }, R>(machine: BuiltMachine<S, E, R>, idOrOptions?: string | {
322
+ }, R>(machine: AnyMachine<S, E, R>, options?: string | {
339
323
  id?: string;
340
324
  hydrate?: S;
325
+ slots?: Record<string, any>;
326
+ supervision?: Supervision.Policy;
341
327
  }) => Effect.Effect<ActorRef<S, E>, never, R>;
342
328
  declare const replay: <S extends {
343
329
  readonly _tag: string;
344
330
  }, E extends {
345
331
  readonly _tag: string;
346
- }, R>(machine: BuiltMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
332
+ }, R>(machine: AnyMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
347
333
  from?: S;
334
+ slots?: Record<string, any>;
348
335
  }) => Effect.Effect<S, never, R>;
336
+ declare const reply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
349
337
  //#endregion
350
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, replay, spawn };
338
+ export { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, type ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, materializeMachine, replay, reply, spawn };
@@ -1,19 +1,20 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { Inspector } from "./inspection.js";
3
- import { getTag, stubSystem } from "./internal/utils.js";
2
+ import { getTag, makeReply, stubSystem } from "./internal/utils.js";
4
3
  import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
4
+ import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
5
5
  import { emitWithTimestamp } from "./internal/inspection.js";
6
+ import { Inspector } from "./inspection.js";
6
7
  import { MachineContextTag } from "./slot.js";
7
- import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
8
8
  import { createActor } from "./actor.js";
9
9
  import { Cause, Effect, Exit, Option, Scope } from "effect";
10
10
  //#region src/machine.ts
11
11
  var machine_exports = /* @__PURE__ */ __exportAll({
12
- BuiltMachine: () => BuiltMachine,
13
12
  Machine: () => Machine,
14
13
  findTransitions: () => findTransitions,
15
14
  make: () => make,
15
+ materializeMachine: () => materializeMachine,
16
16
  replay: () => replay,
17
+ reply: () => reply,
17
18
  spawn: () => spawn
18
19
  });
19
20
  const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Inspector).pipe(Effect.option), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
@@ -26,22 +27,49 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Insp
26
27
  timestamp
27
28
  })));
28
29
  /**
29
- * A finalized machine ready for spawning.
30
+ * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
31
+ * If no handlers provided and machine has no slots, returns the machine as-is.
32
+ * Validates that all required slots are provided and no extra slots are given.
30
33
  *
31
- * Created by calling `.build()` on a `Machine`. This is the only type
32
- * accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
33
- * Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
34
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
34
35
  */
35
- var BuiltMachine = class {
36
- /** @internal */
37
- _inner;
38
- /** @internal */
39
- constructor(machine) {
40
- this._inner = machine;
41
- }
42
- get initial() {
43
- return this._inner.initial;
36
+ const materializeMachine = (machine, handlers) => {
37
+ if (handlers === void 0) {
38
+ const hasGuards = machine._guardsSchema !== void 0 && Object.keys(machine._guardsSchema.definitions).length > 0;
39
+ const hasEffects = machine._effectsSchema !== void 0 && Object.keys(machine._effectsSchema.definitions).length > 0;
40
+ if (hasGuards || hasEffects) {
41
+ const missing = [];
42
+ if (machine._guardsSchema !== void 0) missing.push(...Object.keys(machine._guardsSchema.definitions));
43
+ if (machine._effectsSchema !== void 0) missing.push(...Object.keys(machine._effectsSchema.definitions));
44
+ throw new ProvisionValidationError({
45
+ missing,
46
+ extra: []
47
+ });
48
+ }
49
+ return machine;
44
50
  }
51
+ const requiredSlots = /* @__PURE__ */ new Set();
52
+ if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) requiredSlots.add(name);
53
+ if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) requiredSlots.add(name);
54
+ const providedSlots = new Set(Object.keys(handlers));
55
+ const missing = [];
56
+ const extra = [];
57
+ for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
58
+ for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
59
+ if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
60
+ missing,
61
+ extra
62
+ });
63
+ const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._guardsSchema, machine._effectsSchema);
64
+ result._transitions = [...machine._transitions];
65
+ result._finalStates = new Set(machine._finalStates);
66
+ result._spawnEffects = [...machine._spawnEffects];
67
+ result._backgroundEffects = [...machine._backgroundEffects];
68
+ result._postponeRules = [...machine._postponeRules];
69
+ result._replySchemas = machine._replySchemas;
70
+ if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) result._guardHandlers.set(name, handlers[name]);
71
+ if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) result._effectHandlers.set(name, handlers[name]);
72
+ return result;
45
73
  };
46
74
  /**
47
75
  * Machine definition with fluent builder API.
@@ -69,6 +97,7 @@ var Machine = class Machine {
69
97
  /** @internal */ _slots;
70
98
  stateSchema;
71
99
  eventSchema;
100
+ /** @internal */ _replySchemas;
72
101
  /**
73
102
  * Context tag for accessing machine state/event/self in slot handlers.
74
103
  * Uses shared module-level tag for all machines.
@@ -95,6 +124,9 @@ var Machine = class Machine {
95
124
  get effectsSchema() {
96
125
  return this._effectsSchema;
97
126
  }
127
+ get replySchemas() {
128
+ return this._replySchemas;
129
+ }
98
130
  /** @internal */
99
131
  constructor(initial, stateSchema, eventSchema, guardsSchema, effectsSchema) {
100
132
  this.initial = initial;
@@ -109,6 +141,7 @@ var Machine = class Machine {
109
141
  this._effectHandlers = /* @__PURE__ */ new Map();
110
142
  this.stateSchema = stateSchema;
111
143
  this.eventSchema = eventSchema;
144
+ this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
112
145
  this._slots = {
113
146
  guards: this._guardsSchema !== void 0 ? this._guardsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
114
147
  const handler = this._guardHandlers.get(name);
@@ -344,40 +377,6 @@ var Machine = class Machine {
344
377
  this._finalStates.add(stateTag);
345
378
  return this;
346
379
  }
347
- /**
348
- * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
349
- *
350
- * - Machines with slots: pass implementations as the first argument.
351
- * - Machines without slots: call with no arguments.
352
- */
353
- build(...args) {
354
- const handlers = args[0];
355
- if (handlers !== void 0) {
356
- const requiredSlots = /* @__PURE__ */ new Set();
357
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) requiredSlots.add(name);
358
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) requiredSlots.add(name);
359
- const providedSlots = new Set(Object.keys(handlers));
360
- const missing = [];
361
- const extra = [];
362
- for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
363
- for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
364
- if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
365
- missing,
366
- extra
367
- });
368
- const result = new Machine(this.initial, this.stateSchema, this.eventSchema, this._guardsSchema, this._effectsSchema);
369
- result._transitions = [...this._transitions];
370
- result._finalStates = new Set(this._finalStates);
371
- result._spawnEffects = [...this._spawnEffects];
372
- result._backgroundEffects = [...this._backgroundEffects];
373
- result._postponeRules = [...this._postponeRules];
374
- const anyHandlers = handlers;
375
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
376
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
377
- return new BuiltMachine(result);
378
- }
379
- return new BuiltMachine(this);
380
- }
381
380
  static make(config) {
382
381
  return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
383
382
  }
@@ -398,27 +397,25 @@ var TransitionScope = class {
398
397
  };
399
398
  const make = Machine.make;
400
399
  /**
401
- * Spawn an actor from a built machine.
400
+ * Spawn an actor from a machine.
402
401
  *
403
402
  * Options:
404
403
  * - `id` — custom actor ID (default: random)
405
404
  * - `hydrate` — restore from a previously-saved state snapshot.
406
- * The actor starts in the hydrated state and re-runs spawn effects
407
- * for that state (timers, scoped resources, etc.). Transition history
408
- * is not replayed — only the current state's entry effects run.
409
- *
410
- * Persistence is composed in userland by observing `actor.changes`
411
- * and saving snapshots to your own storage.
405
+ * - `slots` slot handler implementations for slotful machines.
412
406
  */
413
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
414
- const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
415
- const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner, { initialState: opts?.hydrate });
407
+ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, options) {
408
+ const opts = typeof options === "string" ? { id: options } : options;
409
+ const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
410
+ initialState: opts?.hydrate,
411
+ supervision: opts?.supervision
412
+ });
416
413
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
417
414
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
418
415
  return actor;
419
416
  });
420
- const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
421
- const machine = built._inner;
417
+ const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
418
+ const machine = materializeMachine(input, options?.slots);
422
419
  let state = options?.from ?? machine.initial;
423
420
  const hasPostponeRules = machine.postponeRules.length > 0;
424
421
  const postponed = [];
@@ -460,5 +457,6 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
460
457
  }
461
458
  return state;
462
459
  });
460
+ const reply = makeReply;
463
461
  //#endregion
464
- export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, spawn };
462
+ export { Machine, findTransitions, machine_exports, make, materializeMachine, replay, reply, spawn };