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
package/dist/machine.d.ts CHANGED
@@ -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 { DeferReplyResult, ReplyResult, TransitionResult } from "./internal/utils.js";
2
+ import { BrandedEvent, BrandedState, ExtractReply, TaggedOrConstructor } from "./internal/brands.js";
4
3
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
5
4
  import { DuplicateActorError } from "./errors.js";
5
+ import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
6
+ import { Supervision } from "./supervision.js";
6
7
  import { findTransitions } from "./internal/transition.js";
7
8
  import { ActorRef, ActorSystem } from "./actor.js";
8
9
  import { Cause, Duration, Effect, Schema, Scope, ServiceMap } 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, DeferReplyResult, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, deferReply, findTransitions, make, materializeMachine, replay, reply, spawn };
13
14
  }
14
15
  /**
15
16
  * Self reference for sending events back to the machine
@@ -22,7 +23,13 @@ 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>;
27
+ /**
28
+ * Settle a deferred reply from a spawn handler.
29
+ * Only usable when the transition handler returned `Machine.deferReply(state)`.
30
+ * Returns true if a pending reply was settled, false if none was pending.
31
+ */
32
+ readonly reply: (value: unknown) => Effect.Effect<boolean>;
26
33
  }
27
34
  /**
28
35
  * Handler context passed to transition handlers
@@ -45,9 +52,11 @@ interface StateHandlerContext<State, Event, ED extends EffectsDef> {
45
52
  readonly system: ActorSystem;
46
53
  }
47
54
  /**
48
- * Transition handler function
55
+ * Transition handler function.
56
+ * When Reply is concrete (event has a reply schema), handler must return Machine.reply().
57
+ * When Reply is never, handler returns plain state.
49
58
  */
50
- type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R> = (ctx: HandlerContext<S, E, GD, ED>) => TransitionResult<NewState, R>;
59
+ 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
60
  /**
52
61
  * State effect handler function
53
62
  */
@@ -91,9 +100,6 @@ interface TimeoutConfig<State, Event> {
91
100
  /** Event to send when the timer fires. Static or derived from current state. */
92
101
  readonly event: Event | ((state: State) => Event);
93
102
  }
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
103
  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
104
  readonly state: MachineStateSchema<SD> & {
99
105
  Type: S;
@@ -113,22 +119,14 @@ type HasEffectKeys<EFD extends EffectsDef> = [keyof EFD] extends [never] ? false
113
119
  type SlotContext<State, Event> = MachineContext<State, Event, MachineRef<Event>>;
114
120
  /** Combined handlers for build() - guards and effects only */
115
121
  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
122
  /**
119
- * A finalized machine ready for spawning.
123
+ * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
124
+ * If no handlers provided and machine has no slots, returns the machine as-is.
125
+ * Validates that all required slots are provided and no extra slots are given.
120
126
  *
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`.
127
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
124
128
  */
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
- }
129
+ 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
130
  /**
133
131
  * Machine definition with fluent builder API.
134
132
  *
@@ -171,6 +169,8 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
171
169
  };
172
170
  readonly stateSchema?: Schema.Schema<State>;
173
171
  readonly eventSchema?: Schema.Schema<Event>;
172
+ /** @internal */
173
+ readonly _replySchemas: ReadonlyMap<string, Schema.Decoder<unknown>>;
174
174
  /**
175
175
  * Context tag for accessing machine state/event/self in slot handlers.
176
176
  * Uses shared module-level tag for all machines.
@@ -186,24 +186,25 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
186
186
  }>;
187
187
  get guardsSchema(): GuardsSchema<GD> | undefined;
188
188
  get effectsSchema(): EffectsSchema<EFD> | undefined;
189
+ get replySchemas(): ReadonlyMap<string, Schema.Decoder<unknown>>;
189
190
  /** @internal */
190
191
  constructor(initial: State, stateSchema?: Schema.Schema<State>, eventSchema?: Schema.Schema<Event>, guardsSchema?: GuardsSchema<GD>, effectsSchema?: EffectsSchema<EFD>);
191
192
  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
193
  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
194
  /** @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>;
195
+ 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
196
  /** 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>;
197
+ 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
198
  /** 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>;
199
+ 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
200
  /**
200
201
  * Like `on()`, but forces onEnter/spawn to run even when transitioning to the same state tag.
201
202
  * Use this to restart timers, re-run spawned effects, or reset state-scoped effects.
202
203
  */
203
204
  /** 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>;
205
+ 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
206
  /** 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>;
207
+ 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
208
  /**
208
209
  * Register a wildcard transition that fires from any state when no specific transition matches.
209
210
  * Specific `.on()` transitions always take priority over `.onAny()`.
@@ -301,50 +302,83 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
301
302
  */
302
303
  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
304
  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
305
  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
306
  }
313
307
  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
308
  private readonly machine;
315
309
  private readonly states;
316
310
  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>;
311
+ 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>;
312
+ 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
313
  }
320
314
  declare const make: typeof Machine.make;
321
315
  /**
322
- * Spawn an actor from a built machine.
316
+ * Spawn an actor directly without ActorSystem ceremony.
317
+ * Accepts a `Machine` directly. For slotful machines, pass `{ slots }` in options.
318
+ *
319
+ * **Single actor, no registry.** Caller manages lifetime via `actor.stop`.
320
+ * If a `Scope` exists in context, cleanup attaches automatically on scope close.
321
+ *
322
+ * For registry, lookup by ID, persistence, or multi-actor coordination,
323
+ * use `ActorSystemService` / `system.spawn` instead.
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * // Fire-and-forget — caller manages lifetime
328
+ * const actor = yield* Machine.spawn(machine.build());
329
+ * yield* actor.send(Event.Start);
330
+ * yield* actor.awaitFinal;
331
+ * yield* actor.stop;
332
+ *
333
+ * // Scope-aware — auto-cleans up on scope close
334
+ * yield* Effect.scoped(Effect.gen(function* () {
335
+ * const actor = yield* Machine.spawn(machine.build());
336
+ * yield* actor.send(Event.Start);
337
+ * // actor.stop called automatically when scope closes
338
+ * }));
339
+ * ```
340
+ */
341
+ type AnyMachine<S, E, R> = Machine<S, E, R, any, any, any, any>;
342
+ /**
343
+ * Spawn an actor from a machine.
344
+ *
345
+ * For machines with slots, pass implementations via `{ slots: { ... } }`.
323
346
  *
324
- * Options:
325
- * - `id` — custom actor ID (default: random)
326
- * - `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.
347
+ * @example
348
+ * ```ts
349
+ * // No slots
350
+ * const actor = yield* Machine.spawn(machine);
330
351
  *
331
- * Persistence is composed in userland by observing `actor.changes`
332
- * and saving snapshots to your own storage.
352
+ * // With slots
353
+ * const actor = yield* Machine.spawn(machine, {
354
+ * slots: { canRetry: ({ max }, { state }) => state.attempts < max },
355
+ * });
356
+ *
357
+ * // With hydration
358
+ * const actor = yield* Machine.spawn(machine, { hydrate: savedState });
359
+ * ```
333
360
  */
334
361
  declare const spawn: <S extends {
335
362
  readonly _tag: string;
336
363
  }, E extends {
337
364
  readonly _tag: string;
338
- }, R>(machine: BuiltMachine<S, E, R>, idOrOptions?: string | {
365
+ }, R>(machine: AnyMachine<S, E, R>, options?: string | {
339
366
  id?: string;
340
367
  hydrate?: S;
368
+ slots?: Record<string, any>;
369
+ supervision?: Supervision.Policy;
341
370
  }) => Effect.Effect<ActorRef<S, E>, never, R>;
342
- declare const replay: <S extends {
343
- readonly _tag: string;
344
- }, E extends {
345
- readonly _tag: string;
346
- }, R>(machine: BuiltMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
347
- from?: S;
348
- }) => Effect.Effect<S, never, R>;
371
+ declare const replay: {
372
+ <S extends {
373
+ readonly _tag: string;
374
+ }, E extends {
375
+ readonly _tag: string;
376
+ }, R>(machine: AnyMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
377
+ from?: S;
378
+ slots?: Record<string, any>;
379
+ }): Effect.Effect<S, never, R>;
380
+ };
381
+ declare const reply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
382
+ declare const deferReply: <State>(state: State) => DeferReplyResult<State>;
349
383
  //#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 };
384
+ export { BackgroundEffect, type DeferReplyResult, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, type ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, deferReply, findTransitions, machine_d_exports, make, materializeMachine, replay, reply, spawn };
package/dist/machine.js CHANGED
@@ -1,19 +1,21 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
2
  import { Inspector } from "./inspection.js";
3
- import { getTag, stubSystem } from "./internal/utils.js";
4
- import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
5
- import { emitWithTimestamp } from "./internal/inspection.js";
6
- import { MachineContextTag } from "./slot.js";
3
+ import { getTag, makeDeferReply, makeReply, stubSystem } from "./internal/utils.js";
7
4
  import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
5
+ import { emitWithTimestamp } from "./internal/inspection.js";
6
+ import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
8
7
  import { createActor } from "./actor.js";
9
- import { Cause, Effect, Exit, Option, Scope } from "effect";
8
+ import { MachineContextTag } from "./slot.js";
9
+ import { Cause, Effect, Exit, Option, Random, Scope } from "effect";
10
10
  //#region src/machine.ts
11
11
  var machine_exports = /* @__PURE__ */ __exportAll({
12
- BuiltMachine: () => BuiltMachine,
13
12
  Machine: () => Machine,
13
+ deferReply: () => deferReply,
14
14
  findTransitions: () => findTransitions,
15
15
  make: () => make,
16
+ materializeMachine: () => materializeMachine,
16
17
  replay: () => replay,
18
+ reply: () => reply,
17
19
  spawn: () => spawn
18
20
  });
19
21
  const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspector), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
@@ -26,22 +28,49 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspec
26
28
  timestamp
27
29
  })));
28
30
  /**
29
- * A finalized machine ready for spawning.
31
+ * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
32
+ * If no handlers provided and machine has no slots, returns the machine as-is.
33
+ * Validates that all required slots are provided and no extra slots are given.
30
34
  *
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`.
35
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
34
36
  */
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;
37
+ const materializeMachine = (machine, handlers) => {
38
+ if (handlers === void 0) {
39
+ const hasGuards = machine._guardsSchema !== void 0 && Object.keys(machine._guardsSchema.definitions).length > 0;
40
+ const hasEffects = machine._effectsSchema !== void 0 && Object.keys(machine._effectsSchema.definitions).length > 0;
41
+ if (hasGuards || hasEffects) {
42
+ const missing = [];
43
+ if (machine._guardsSchema !== void 0) missing.push(...Object.keys(machine._guardsSchema.definitions));
44
+ if (machine._effectsSchema !== void 0) missing.push(...Object.keys(machine._effectsSchema.definitions));
45
+ throw new ProvisionValidationError({
46
+ missing,
47
+ extra: []
48
+ });
49
+ }
50
+ return machine;
44
51
  }
52
+ const requiredSlots = /* @__PURE__ */ new Set();
53
+ if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) requiredSlots.add(name);
54
+ if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) requiredSlots.add(name);
55
+ const providedSlots = new Set(Object.keys(handlers));
56
+ const missing = [];
57
+ const extra = [];
58
+ for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
59
+ for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
60
+ if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
61
+ missing,
62
+ extra
63
+ });
64
+ const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._guardsSchema, machine._effectsSchema);
65
+ result._transitions = [...machine._transitions];
66
+ result._finalStates = new Set(machine._finalStates);
67
+ result._spawnEffects = [...machine._spawnEffects];
68
+ result._backgroundEffects = [...machine._backgroundEffects];
69
+ result._postponeRules = [...machine._postponeRules];
70
+ result._replySchemas = machine._replySchemas;
71
+ if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) result._guardHandlers.set(name, handlers[name]);
72
+ if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) result._effectHandlers.set(name, handlers[name]);
73
+ return result;
45
74
  };
46
75
  /**
47
76
  * Machine definition with fluent builder API.
@@ -69,6 +98,7 @@ var Machine = class Machine {
69
98
  /** @internal */ _slots;
70
99
  stateSchema;
71
100
  eventSchema;
101
+ /** @internal */ _replySchemas;
72
102
  /**
73
103
  * Context tag for accessing machine state/event/self in slot handlers.
74
104
  * Uses shared module-level tag for all machines.
@@ -95,6 +125,9 @@ var Machine = class Machine {
95
125
  get effectsSchema() {
96
126
  return this._effectsSchema;
97
127
  }
128
+ get replySchemas() {
129
+ return this._replySchemas;
130
+ }
98
131
  /** @internal */
99
132
  constructor(initial, stateSchema, eventSchema, guardsSchema, effectsSchema) {
100
133
  this.initial = initial;
@@ -105,6 +138,7 @@ var Machine = class Machine {
105
138
  this._postponeRules = [];
106
139
  this._guardsSchema = guardsSchema;
107
140
  this._effectsSchema = effectsSchema;
141
+ this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
108
142
  this._guardHandlers = /* @__PURE__ */ new Map();
109
143
  this._effectHandlers = /* @__PURE__ */ new Map();
110
144
  this.stateSchema = stateSchema;
@@ -348,40 +382,6 @@ var Machine = class Machine {
348
382
  this._finalStates.add(stateTag);
349
383
  return this;
350
384
  }
351
- /**
352
- * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
353
- *
354
- * - Machines with slots: pass implementations as the first argument.
355
- * - Machines without slots: call with no arguments.
356
- */
357
- build(...args) {
358
- const handlers = args[0];
359
- if (handlers !== void 0) {
360
- const requiredSlots = /* @__PURE__ */ new Set();
361
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) requiredSlots.add(name);
362
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) requiredSlots.add(name);
363
- const providedSlots = new Set(Object.keys(handlers));
364
- const missing = [];
365
- const extra = [];
366
- for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
367
- for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
368
- if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
369
- missing,
370
- extra
371
- });
372
- const result = new Machine(this.initial, this.stateSchema, this.eventSchema, this._guardsSchema, this._effectsSchema);
373
- result._transitions = [...this._transitions];
374
- result._finalStates = new Set(this._finalStates);
375
- result._spawnEffects = [...this._spawnEffects];
376
- result._backgroundEffects = [...this._backgroundEffects];
377
- result._postponeRules = [...this._postponeRules];
378
- const anyHandlers = handlers;
379
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
380
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
381
- return new BuiltMachine(result);
382
- }
383
- return new BuiltMachine(this);
384
- }
385
385
  static make(config) {
386
386
  return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
387
387
  }
@@ -402,27 +402,36 @@ var TransitionScope = class {
402
402
  };
403
403
  const make = Machine.make;
404
404
  /**
405
- * Spawn an actor from a built machine.
405
+ * Spawn an actor from a machine.
406
+ *
407
+ * For machines with slots, pass implementations via `{ slots: { ... } }`.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * // No slots
412
+ * const actor = yield* Machine.spawn(machine);
406
413
  *
407
- * Options:
408
- * - `id` custom actor ID (default: random)
409
- * - `hydrate` restore from a previously-saved state snapshot.
410
- * The actor starts in the hydrated state and re-runs spawn effects
411
- * for that state (timers, scoped resources, etc.). Transition history
412
- * is not replayed — only the current state's entry effects run.
414
+ * // With slots
415
+ * const actor = yield* Machine.spawn(machine, {
416
+ * slots: { canRetry: ({ max }, { state }) => state.attempts < max },
417
+ * });
413
418
  *
414
- * Persistence is composed in userland by observing `actor.changes`
415
- * and saving snapshots to your own storage.
419
+ * // With hydration
420
+ * const actor = yield* Machine.spawn(machine, { hydrate: savedState });
421
+ * ```
416
422
  */
417
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
423
+ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
418
424
  const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
419
- const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner, { initialState: opts?.hydrate });
425
+ const actor = yield* createActor(opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
426
+ initialState: opts?.hydrate,
427
+ supervision: opts?.supervision
428
+ });
420
429
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
421
430
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
422
431
  return actor;
423
432
  });
424
- const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
425
- const machine = built._inner;
433
+ const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
434
+ const machine = materializeMachine(input, options?.slots);
426
435
  let state = options?.from ?? machine.initial;
427
436
  const hasPostponeRules = machine.postponeRules.length > 0;
428
437
  const postponed = [];
@@ -430,7 +439,8 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
430
439
  const self = {
431
440
  send: dummySend,
432
441
  cast: dummySend,
433
- spawn: () => Effect.die("spawn not supported in replay")
442
+ spawn: () => Effect.die("spawn not supported in replay"),
443
+ reply: () => Effect.succeed(false)
434
444
  };
435
445
  for (const event of events) {
436
446
  if (machine.finalStates.has(state._tag)) break;
@@ -464,5 +474,7 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
464
474
  }
465
475
  return state;
466
476
  });
477
+ const reply = makeReply;
478
+ const deferReply = makeDeferReply;
467
479
  //#endregion
468
- export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, spawn };
480
+ export { Machine, deferReply, findTransitions, machine_exports, make, materializeMachine, replay, reply, spawn };
package/dist/schema.d.ts CHANGED
@@ -1,7 +1,17 @@
1
- import { FullEventBrand, FullStateBrand } from "./internal/brands.js";
1
+ import { FullEventBrand, FullStateBrand, ReplyTypeBrand } from "./internal/brands.js";
2
2
  import { Schema } from "effect";
3
3
 
4
4
  //#region src/schema.d.ts
5
+ declare const ReplySchemaSymbol: unique symbol;
6
+ type ReplySchemaSymbol = typeof ReplySchemaSymbol;
7
+ /**
8
+ * Fields annotated with a reply schema.
9
+ * Structurally identical to Schema.Struct.Fields at runtime,
10
+ * but carries the reply schema type at compile time.
11
+ */
12
+ type ReplyFields<F extends Schema.Struct.Fields, RS extends Schema.Schema<unknown>> = F & {
13
+ readonly [ReplySchemaSymbol]: RS;
14
+ };
5
15
  /**
6
16
  * Extract the TypeScript type from a TaggedStruct schema
7
17
  */
@@ -12,20 +22,23 @@ type TaggedStructType<Tag extends string, Fields extends Schema.Struct.Fields> =
12
22
  type VariantSchemas<D extends Record<string, Schema.Struct.Fields>> = { readonly [K in keyof D & string]: Schema.TaggedStruct<K, D[K]> };
13
23
  /**
14
24
  * Build union type from variant schemas.
15
- * Used for constraining fluent method type params.
25
+ * Reply-bearing variants carry ReplyTypeBrand<R> for ask() inference.
16
26
  */
17
- type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> }[keyof D & string];
27
+ type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> & (D[K] extends {
28
+ readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
29
+ } ? ReplyTypeBrand<R> : unknown) }[keyof D & string];
18
30
  /**
19
- * Check if fields are empty (no required properties)
31
+ * Check if fields are empty (no required string properties).
32
+ * Symbol keys (like ReplySchemaSymbol) are metadata, not payload fields.
20
33
  */
21
- type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends never ? true : false;
34
+ type IsEmptyFields<Fields extends Schema.Struct.Fields> = string & keyof Fields extends never ? true : false;
22
35
  /**
23
- * Constructor functions for each variant.
24
- * Empty structs: plain values with `_tag`: `State.Idle`
25
- * Non-empty structs require args: `State.Loading({ url })`
26
- *
27
- * Each variant also has a `derive` method for constructing from a source object.
36
+ * Resolve the reply brand for a variant's fields.
37
+ * If fields carry ReplySchemaSymbol, adds ReplyTypeBrand<R>.
28
38
  */
39
+ type VariantReplyBrand<Fields extends Schema.Struct.Fields> = Fields extends {
40
+ readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
41
+ } ? ReplyTypeBrand<R> : unknown;
29
42
  /**
30
43
  * Constructor functions for each variant.
31
44
  * Empty structs: plain values with `_tag`: `State.Idle`
@@ -33,10 +46,11 @@ type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends n
33
46
  *
34
47
  * Each variant also has a `derive` method for constructing from a source object.
35
48
  * The source type uses `object` to accept branded state types without index signature issues.
49
+ * Reply-bearing variants carry ReplyTypeBrand<R> for ask() type inference.
36
50
  */
37
- type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & {
51
+ type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]> & {
38
52
  readonly derive: (source: object) => TaggedStructType<K, D[K]> & Brand;
39
- } : ((args: Schema.Struct.Type<D[K]>) => TaggedStructType<K, D[K]> & Brand) & {
53
+ } : ((args: Schema.Struct.Type<D[K]>) => TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]>) & {
40
54
  readonly derive: (source: object, partial?: Partial<Schema.Struct.Type<D[K]>>) => TaggedStructType<K, D[K]> & Brand;
41
55
  readonly _tag: K;
42
56
  } };
@@ -67,6 +81,11 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
67
81
  <R>(cases: MatchCases<D, R>): (value: VariantsUnion<D> & Brand) => R;
68
82
  <R>(value: VariantsUnion<D> & Brand, cases: MatchCases<D, R>): R;
69
83
  };
84
+ /**
85
+ * Reply schemas per variant tag. Only populated for event schemas
86
+ * with variants defined via `Event.reply()`.
87
+ */
88
+ readonly _replySchemas: ReadonlyMap<string, Schema.Decoder<unknown>>;
70
89
  }
71
90
  /**
72
91
  * Schema-first state definition that provides:
@@ -116,26 +135,8 @@ type MachineEventSchema<D extends Record<string, Schema.Struct.Fields>> = Schema
116
135
  * ```
117
136
  */
118
137
  declare const State: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineStateSchema<D>;
119
- /**
120
- * Create a schema-first Event definition.
121
- *
122
- * The schema's definition type D creates a unique brand, preventing
123
- * accidental use of constructors from different event schemas
124
- * (unless they have identical definitions).
125
- *
126
- * @example
127
- * ```ts
128
- * const OrderEvent = MachineSchema.Event({
129
- * Ship: { trackingId: Schema.String },
130
- * Cancel: {},
131
- * })
132
- *
133
- * type OrderEvent = typeof OrderEvent.Type
134
- *
135
- * // Construct
136
- * const e = OrderEvent.Ship({ trackingId: "abc" })
137
- * ```
138
- */
139
- declare const Event: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>;
138
+ declare const Event: (<const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>) & {
139
+ reply: <F extends Schema.Struct.Fields, RS extends Schema.Schema<unknown>>(fields: F, replySchema: RS) => ReplyFields<F, RS>;
140
+ };
140
141
  //#endregion
141
- export { Event, MachineEventSchema, MachineStateSchema, State, VariantsUnion };
142
+ export { Event, MachineEventSchema, MachineStateSchema, ReplyFields, ReplySchemaSymbol, State, VariantsUnion };