effect-machine 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +118 -55
  2. package/dist/actor.d.ts +77 -179
  3. package/dist/actor.js +161 -113
  4. package/dist/cluster/entity-machine.js +5 -3
  5. package/dist/errors.d.ts +12 -1
  6. package/dist/errors.js +8 -1
  7. package/dist/index.d.ts +4 -8
  8. package/dist/index.js +2 -7
  9. package/dist/internal/transition.d.ts +27 -3
  10. package/dist/internal/transition.js +38 -9
  11. package/dist/internal/utils.d.ts +7 -2
  12. package/dist/internal/utils.js +1 -5
  13. package/dist/machine.d.ts +94 -35
  14. package/dist/machine.js +128 -13
  15. package/dist/testing.js +57 -3
  16. package/package.json +10 -9
  17. package/v3/dist/actor.d.ts +210 -0
  18. package/{dist-v3 → v3/dist}/actor.js +198 -117
  19. package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -1
  20. package/{dist-v3 → v3/dist}/cluster/entity-machine.js +8 -6
  21. package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
  22. package/{dist-v3 → v3/dist}/cluster/to-entity.js +1 -1
  23. package/v3/dist/errors.d.ts +76 -0
  24. package/{dist-v3 → v3/dist}/errors.js +9 -2
  25. package/v3/dist/index.d.ts +9 -0
  26. package/v3/dist/index.js +8 -0
  27. package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
  28. package/v3/dist/inspection.js +156 -0
  29. package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
  30. package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
  31. package/v3/dist/internal/inspection.js +20 -0
  32. package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
  33. package/{dist-v3 → v3/dist}/internal/transition.js +47 -15
  34. package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
  35. package/{dist-v3 → v3/dist}/internal/utils.js +2 -6
  36. package/{dist-v3 → v3/dist}/machine.d.ts +113 -40
  37. package/{dist-v3 → v3/dist}/machine.js +191 -15
  38. package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
  39. package/{dist-v3 → v3/dist}/schema.js +5 -2
  40. package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
  41. package/{dist-v3 → v3/dist}/slot.js +1 -1
  42. package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
  43. package/{dist-v3 → v3/dist}/testing.js +60 -6
  44. package/dist/persistence/adapter.d.ts +0 -135
  45. package/dist/persistence/adapter.js +0 -25
  46. package/dist/persistence/adapters/in-memory.d.ts +0 -32
  47. package/dist/persistence/adapters/in-memory.js +0 -174
  48. package/dist/persistence/index.d.ts +0 -5
  49. package/dist/persistence/index.js +0 -5
  50. package/dist/persistence/persistent-actor.d.ts +0 -50
  51. package/dist/persistence/persistent-actor.js +0 -368
  52. package/dist/persistence/persistent-machine.d.ts +0 -105
  53. package/dist/persistence/persistent-machine.js +0 -22
  54. package/dist-v3/actor.d.ts +0 -291
  55. package/dist-v3/errors.d.ts +0 -27
  56. package/dist-v3/index.d.ts +0 -12
  57. package/dist-v3/index.js +0 -13
  58. package/dist-v3/inspection.js +0 -48
  59. package/dist-v3/internal/inspection.js +0 -13
  60. package/dist-v3/persistence/adapter.d.ts +0 -125
  61. package/dist-v3/persistence/adapter.js +0 -25
  62. package/dist-v3/persistence/adapters/in-memory.d.ts +0 -32
  63. package/dist-v3/persistence/adapters/in-memory.js +0 -174
  64. package/dist-v3/persistence/index.d.ts +0 -5
  65. package/dist-v3/persistence/index.js +0 -5
  66. package/dist-v3/persistence/persistent-actor.d.ts +0 -49
  67. package/dist-v3/persistence/persistent-actor.js +0 -365
  68. package/dist-v3/persistence/persistent-machine.d.ts +0 -105
  69. package/dist-v3/persistence/persistent-machine.js +0 -22
  70. /package/{dist-v3 → v3/dist}/_virtual/_rolldown/runtime.js +0 -0
  71. /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
  72. /package/{dist-v3 → v3/dist}/cluster/index.js +0 -0
  73. /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
@@ -1,22 +1,23 @@
1
+ import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
1
2
  import { TransitionResult } from "./internal/utils.js";
2
3
  import { BrandedEvent, BrandedState, TaggedOrConstructor } from "./internal/brands.js";
3
4
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
4
- import { PersistenceConfig, PersistentMachine } from "./persistence/persistent-machine.js";
5
5
  import { DuplicateActorError } from "./errors.js";
6
- import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
7
6
  import { findTransitions } from "./internal/transition.js";
8
7
  import { ActorRef, ActorSystem } from "./actor.js";
9
- import { Cause, Context, Effect, Schedule, Schema, Scope } from "effect";
8
+ import { Cause, Context, Duration, Effect, Schema, Scope } from "effect";
10
9
 
11
- //#region src-v3/machine.d.ts
10
+ //#region src/machine.d.ts
12
11
  declare namespace machine_d_exports {
13
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, PersistenceConfig, PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, Transition, TransitionHandler, findTransitions, make, spawn };
12
+ export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, spawn };
14
13
  }
15
14
  /**
16
15
  * Self reference for sending events back to the machine
17
16
  */
18
17
  interface MachineRef<Event> {
19
18
  readonly send: (event: Event) => Effect.Effect<void>;
19
+ /** Fire-and-forget alias for send (OTP gen_server:cast). */
20
+ readonly cast: (event: Event) => Effect.Effect<void>;
20
21
  readonly spawn: <S2 extends {
21
22
  readonly _tag: string;
22
23
  }, E2 extends {
@@ -36,6 +37,7 @@ interface HandlerContext<State, Event, GD extends GuardsDef, ED extends EffectsD
36
37
  * Handler context passed to state effect handlers (onEnter, spawn, background)
37
38
  */
38
39
  interface StateHandlerContext<State, Event, ED extends EffectsDef> {
40
+ readonly actorId: string;
39
41
  readonly state: State;
40
42
  readonly event: Event;
41
43
  readonly self: MachineRef<Event>;
@@ -72,13 +74,22 @@ interface SpawnEffect<State, Event, ED extends EffectsDef, R> {
72
74
  interface BackgroundEffect<State, Event, ED extends EffectsDef, R> {
73
75
  readonly handler: StateEffectHandler<State, Event, ED, R>;
74
76
  }
75
- /** Options for `persist` */
76
- interface PersistOptions {
77
- readonly snapshotSchedule: Schedule.Schedule<unknown, {
78
- readonly _tag: string;
79
- }>;
80
- readonly journalEvents: boolean;
81
- readonly machineType?: string;
77
+ interface TaskOptions<State, Event, ED extends EffectsDef, A, E1, ES, EF> {
78
+ readonly onSuccess: (value: A, ctx: StateHandlerContext<State, Event, ED>) => ES;
79
+ readonly onFailure?: (cause: Cause.Cause<E1>, ctx: StateHandlerContext<State, Event, ED>) => EF;
80
+ readonly name?: string;
81
+ }
82
+ /**
83
+ * Configuration for `.timeout()` — gen_statem-style state timeouts.
84
+ *
85
+ * Entering the state starts a timer. Leaving cancels it.
86
+ * `.reenter()` restarts the timer with fresh state values.
87
+ */
88
+ interface TimeoutConfig<State, Event> {
89
+ /** Duration before firing. Static or derived from current state. */
90
+ readonly duration: Duration.DurationInput | ((state: State) => Duration.DurationInput);
91
+ /** Event to send when the timer fires. Static or derived from current state. */
92
+ readonly event: Event | ((state: State) => Event);
82
93
  }
83
94
  type IsAny<T> = 0 extends 1 & T ? true : false;
84
95
  type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
@@ -117,11 +128,6 @@ declare class BuiltMachine<State, Event, R = never> {
117
128
  /** @internal */
118
129
  constructor(machine: Machine<State, Event, R, any, any, any, any>);
119
130
  get initial(): State;
120
- persist(config: PersistOptions): PersistentMachine<State & {
121
- readonly _tag: string;
122
- }, Event & {
123
- readonly _tag: string;
124
- }, R>;
125
131
  }
126
132
  /**
127
133
  * Machine definition with fluent builder API.
@@ -146,6 +152,11 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
146
152
  /** @internal */
147
153
  readonly _finalStates: Set<string>;
148
154
  /** @internal */
155
+ readonly _postponeRules: Array<{
156
+ readonly stateTag: string;
157
+ readonly eventTag: string;
158
+ }>;
159
+ /** @internal */
149
160
  readonly _guardsSchema?: GuardsSchema<GD>;
150
161
  /** @internal */
151
162
  readonly _effectsSchema?: EffectsSchema<EFD>;
@@ -169,10 +180,18 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
169
180
  get spawnEffects(): ReadonlyArray<SpawnEffect<State, Event, EFD, R>>;
170
181
  get backgroundEffects(): ReadonlyArray<BackgroundEffect<State, Event, EFD, R>>;
171
182
  get finalStates(): ReadonlySet<string>;
183
+ get postponeRules(): ReadonlyArray<{
184
+ readonly stateTag: string;
185
+ readonly eventTag: string;
186
+ }>;
172
187
  get guardsSchema(): GuardsSchema<GD> | undefined;
173
188
  get effectsSchema(): EffectsSchema<EFD> | undefined;
174
189
  /** @internal */
175
190
  constructor(initial: State, stateSchema?: Schema.Schema<State, unknown, never>, eventSchema?: Schema.Schema<Event, unknown, never>, guardsSchema?: GuardsSchema<GD>, effectsSchema?: EffectsSchema<EFD>);
191
+ 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
+ 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
+ /** @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>;
176
195
  /** Register transition for a single state */
177
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>;
178
197
  /** Register transition for multiple states (handler receives union of state types) */
@@ -219,10 +238,29 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
219
238
  * State-scoped task that runs on entry and sends success/failure events.
220
239
  * Interrupts do not emit failure events.
221
240
  */
222
- task<NS extends VariantsUnion<_SD> & BrandedState, A, E1, ES extends VariantsUnion<_ED> & BrandedEvent, EF extends VariantsUnion<_ED> & BrandedEvent>(state: TaggedOrConstructor<NS>, run: (ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => Effect.Effect<A, E1, Scope.Scope>, options: {
223
- readonly onSuccess: (value: A, ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => ES;
224
- readonly onFailure?: (cause: Cause.Cause<E1>, ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => EF;
225
- }): Machine<State, Event, R, _SD, _ED, GD, EFD>;
241
+ task<NS extends VariantsUnion<_SD> & BrandedState, A, E1, ES extends VariantsUnion<_ED> & BrandedEvent, EF extends VariantsUnion<_ED> & BrandedEvent>(state: TaggedOrConstructor<NS>, run: (ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => Effect.Effect<A, E1, Scope.Scope>, options: TaskOptions<NS, VariantsUnion<_ED> & BrandedEvent, EFD, A, E1, ES, EF>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
242
+ /**
243
+ * State timeout gen_statem's `state_timeout`.
244
+ *
245
+ * Entering the state starts a timer. Leaving cancels it (via state scope).
246
+ * `.reenter()` restarts the timer with fresh state values.
247
+ * Compiles to `.task()` internally — preserves `@machine.task` inspection events.
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * machine
252
+ * .timeout(State.Loading, {
253
+ * duration: Duration.seconds(30),
254
+ * event: Event.Timeout,
255
+ * })
256
+ * // Dynamic duration from state
257
+ * .timeout(State.Retrying, {
258
+ * duration: (state) => Duration.seconds(state.backoff),
259
+ * event: Event.GiveUp,
260
+ * })
261
+ * ```
262
+ */
263
+ timeout<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, config: TimeoutConfig<NS, VariantsUnion<_ED> & BrandedEvent>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
226
264
  /**
227
265
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
228
266
  * Use effect slots defined via `Slot.Effects` for the actual work.
@@ -244,6 +282,24 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
244
282
  * ```
245
283
  */
246
284
  background(handler: StateEffectHandler<State, Event, EFD, Scope.Scope>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
285
+ /**
286
+ * Postpone events — gen_statem's event postpone.
287
+ *
288
+ * When a matching event arrives in the given state, it is buffered instead of
289
+ * processed. After the next state transition (tag change), all buffered events
290
+ * are drained through the loop in FIFO order.
291
+ *
292
+ * Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
293
+ * with `ActorStoppedError` on stop/interrupt/final-state.
294
+ *
295
+ * @example
296
+ * ```ts
297
+ * machine
298
+ * .postpone(State.Connecting, Event.Data) // single event
299
+ * .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
300
+ * ```
301
+ */
302
+ 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>;
247
303
  final<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
248
304
  /**
249
305
  * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
@@ -252,26 +308,43 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
252
308
  * - Machines without slots: call with no arguments.
253
309
  */
254
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>>;
255
- /** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
256
- persist(config: PersistOptions): PersistentMachine<State & {
257
- readonly _tag: string;
258
- }, Event & {
259
- readonly _tag: string;
260
- }, R>;
261
311
  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>;
262
312
  }
313
+ 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
+ private readonly machine;
315
+ private readonly states;
316
+ 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>;
319
+ }
263
320
  declare const make: typeof Machine.make;
264
- declare const spawn: {
265
- <S extends {
266
- readonly _tag: string;
267
- }, E extends {
268
- readonly _tag: string;
269
- }, R>(machine: BuiltMachine<S, E, R>): Effect.Effect<ActorRef<S, E>, never, R>;
270
- <S extends {
271
- readonly _tag: string;
272
- }, E extends {
273
- readonly _tag: string;
274
- }, R>(machine: BuiltMachine<S, E, R>, id: string): Effect.Effect<ActorRef<S, E>, never, R>;
275
- };
321
+ /**
322
+ * Spawn an actor from a built machine.
323
+ *
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.
330
+ *
331
+ * Persistence is composed in userland by observing `actor.changes`
332
+ * and saving snapshots to your own storage.
333
+ */
334
+ declare const spawn: <S extends {
335
+ readonly _tag: string;
336
+ }, E extends {
337
+ readonly _tag: string;
338
+ }, R>(machine: BuiltMachine<S, E, R>, idOrOptions?: string | {
339
+ id?: string;
340
+ hydrate?: S;
341
+ }) => 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>;
276
349
  //#endregion
277
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, type PersistenceConfig, type PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, Transition, TransitionHandler, findTransitions, machine_d_exports, make, spawn };
350
+ export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, replay, spawn };
@@ -1,19 +1,30 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { getTag } from "./internal/utils.js";
2
+ import { Inspector } from "./inspection.js";
3
+ import { getTag, stubSystem } from "./internal/utils.js";
3
4
  import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
4
- import { persist } from "./persistence/persistent-machine.js";
5
+ import { emitWithTimestamp } from "./internal/inspection.js";
5
6
  import { MachineContextTag } from "./slot.js";
6
- import { findTransitions, invalidateIndex } from "./internal/transition.js";
7
+ import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
7
8
  import { createActor } from "./actor.js";
8
9
  import { Cause, Effect, Exit, Option, Scope } from "effect";
9
- //#region src-v3/machine.ts
10
+ //#region src/machine.ts
10
11
  var machine_exports = /* @__PURE__ */ __exportAll({
11
12
  BuiltMachine: () => BuiltMachine,
12
13
  Machine: () => Machine,
13
14
  findTransitions: () => findTransitions,
14
15
  make: () => make,
16
+ replay: () => replay,
15
17
  spawn: () => spawn
16
18
  });
19
+ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Inspector).pipe(Effect.option), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
20
+ type: "@machine.task",
21
+ actorId: input.actorId,
22
+ state: input.state,
23
+ taskName: input.taskName,
24
+ phase: input.phase,
25
+ error: input.error,
26
+ timestamp
27
+ })));
17
28
  /**
18
29
  * A finalized machine ready for spawning.
19
30
  *
@@ -31,9 +42,6 @@ var BuiltMachine = class {
31
42
  get initial() {
32
43
  return this._inner.initial;
33
44
  }
34
- persist(config) {
35
- return this._inner.persist(config);
36
- }
37
45
  };
38
46
  /**
39
47
  * Machine definition with fluent builder API.
@@ -53,6 +61,7 @@ var Machine = class Machine {
53
61
  /** @internal */ _spawnEffects;
54
62
  /** @internal */ _backgroundEffects;
55
63
  /** @internal */ _finalStates;
64
+ /** @internal */ _postponeRules;
56
65
  /** @internal */ _guardsSchema;
57
66
  /** @internal */ _effectsSchema;
58
67
  /** @internal */ _guardHandlers;
@@ -77,6 +86,9 @@ var Machine = class Machine {
77
86
  get finalStates() {
78
87
  return this._finalStates;
79
88
  }
89
+ get postponeRules() {
90
+ return this._postponeRules;
91
+ }
80
92
  get guardsSchema() {
81
93
  return this._guardsSchema;
82
94
  }
@@ -90,6 +102,7 @@ var Machine = class Machine {
90
102
  this._spawnEffects = [];
91
103
  this._backgroundEffects = [];
92
104
  this._finalStates = /* @__PURE__ */ new Set();
105
+ this._postponeRules = [];
93
106
  this._guardsSchema = guardsSchema;
94
107
  this._effectsSchema = effectsSchema;
95
108
  this._guardHandlers = /* @__PURE__ */ new Map();
@@ -116,6 +129,15 @@ var Machine = class Machine {
116
129
  })) : {}
117
130
  };
118
131
  }
132
+ from(stateOrStates, build) {
133
+ build(new TransitionScope(this, Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates]));
134
+ return this;
135
+ }
136
+ /** @internal */
137
+ scopeTransition(states, event, handler, reenter) {
138
+ for (const state of states) this.addTransition(state, event, handler, reenter);
139
+ return this;
140
+ }
119
141
  on(stateOrStates, event, handler) {
120
142
  const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
121
143
  for (const s of states) this.addTransition(s, event, handler, false);
@@ -190,14 +212,41 @@ var Machine = class Machine {
190
212
  */
191
213
  task(state, run, options) {
192
214
  const handler = Effect.fn("effect-machine.task")(function* (ctx) {
215
+ yield* emitTaskInspection({
216
+ actorId: ctx.actorId,
217
+ state: ctx.state,
218
+ taskName: options.name,
219
+ phase: "start"
220
+ });
193
221
  const exit = yield* Effect.exit(run(ctx));
194
222
  if (Exit.isSuccess(exit)) {
223
+ yield* emitTaskInspection({
224
+ actorId: ctx.actorId,
225
+ state: ctx.state,
226
+ taskName: options.name,
227
+ phase: "success"
228
+ });
195
229
  yield* ctx.self.send(options.onSuccess(exit.value, ctx));
196
230
  yield* Effect.yieldNow();
197
231
  return;
198
232
  }
199
233
  const cause = exit.cause;
200
- if (Cause.isInterruptedOnly(cause)) return;
234
+ if (Cause.isInterruptedOnly(cause)) {
235
+ yield* emitTaskInspection({
236
+ actorId: ctx.actorId,
237
+ state: ctx.state,
238
+ taskName: options.name,
239
+ phase: "interrupt"
240
+ });
241
+ return;
242
+ }
243
+ yield* emitTaskInspection({
244
+ actorId: ctx.actorId,
245
+ state: ctx.state,
246
+ taskName: options.name,
247
+ phase: "failure",
248
+ error: Cause.pretty(cause)
249
+ });
201
250
  if (options.onFailure !== void 0) {
202
251
  yield* ctx.self.send(options.onFailure(cause, ctx));
203
252
  yield* Effect.yieldNow();
@@ -208,6 +257,36 @@ var Machine = class Machine {
208
257
  return this.spawn(state, handler);
209
258
  }
210
259
  /**
260
+ * State timeout — gen_statem's `state_timeout`.
261
+ *
262
+ * Entering the state starts a timer. Leaving cancels it (via state scope).
263
+ * `.reenter()` restarts the timer with fresh state values.
264
+ * Compiles to `.task()` internally — preserves `@machine.task` inspection events.
265
+ *
266
+ * @example
267
+ * ```ts
268
+ * machine
269
+ * .timeout(State.Loading, {
270
+ * duration: Duration.seconds(30),
271
+ * event: Event.Timeout,
272
+ * })
273
+ * // Dynamic duration from state
274
+ * .timeout(State.Retrying, {
275
+ * duration: (state) => Duration.seconds(state.backoff),
276
+ * event: Event.GiveUp,
277
+ * })
278
+ * ```
279
+ */
280
+ timeout(state, config) {
281
+ const stateTag = getTag(state);
282
+ const resolveDuration = typeof config.duration === "function" ? config.duration : () => config.duration;
283
+ const resolveEvent = typeof config.event === "function" ? config.event : () => config.event;
284
+ return this.task(state, (ctx) => Effect.sleep(resolveDuration(ctx.state)), {
285
+ onSuccess: (_, ctx) => resolveEvent(ctx.state),
286
+ name: `$timeout:${stateTag}`
287
+ });
288
+ }
289
+ /**
211
290
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
212
291
  * Use effect slots defined via `Slot.Effects` for the actual work.
213
292
  *
@@ -231,6 +310,35 @@ var Machine = class Machine {
231
310
  this._backgroundEffects.push({ handler });
232
311
  return this;
233
312
  }
313
+ /**
314
+ * Postpone events — gen_statem's event postpone.
315
+ *
316
+ * When a matching event arrives in the given state, it is buffered instead of
317
+ * processed. After the next state transition (tag change), all buffered events
318
+ * are drained through the loop in FIFO order.
319
+ *
320
+ * Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
321
+ * with `ActorStoppedError` on stop/interrupt/final-state.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * machine
326
+ * .postpone(State.Connecting, Event.Data) // single event
327
+ * .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
328
+ * ```
329
+ */
330
+ postpone(state, events) {
331
+ const stateTag = getTag(state);
332
+ const eventList = Array.isArray(events) ? events : [events];
333
+ for (const ev of eventList) {
334
+ const eventTag = getTag(ev);
335
+ this._postponeRules.push({
336
+ stateTag,
337
+ eventTag
338
+ });
339
+ }
340
+ return this;
341
+ }
234
342
  final(state) {
235
343
  const stateTag = getTag(state);
236
344
  this._finalStates.add(stateTag);
@@ -262,6 +370,7 @@ var Machine = class Machine {
262
370
  result._finalStates = new Set(this._finalStates);
263
371
  result._spawnEffects = [...this._spawnEffects];
264
372
  result._backgroundEffects = [...this._backgroundEffects];
373
+ result._postponeRules = [...this._postponeRules];
265
374
  const anyHandlers = handlers;
266
375
  if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
267
376
  if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
@@ -269,20 +378,87 @@ var Machine = class Machine {
269
378
  }
270
379
  return new BuiltMachine(this);
271
380
  }
272
- /** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
273
- persist(config) {
274
- return persist(config)(this);
275
- }
276
381
  static make(config) {
277
382
  return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
278
383
  }
279
384
  };
385
+ var TransitionScope = class {
386
+ constructor(machine, states) {
387
+ this.machine = machine;
388
+ this.states = states;
389
+ }
390
+ on(event, handler) {
391
+ this.machine.scopeTransition(this.states, event, handler, false);
392
+ return this;
393
+ }
394
+ reenter(event, handler) {
395
+ this.machine.scopeTransition(this.states, event, handler, true);
396
+ return this;
397
+ }
398
+ };
280
399
  const make = Machine.make;
281
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, id) {
282
- const actor = yield* createActor(id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner);
400
+ /**
401
+ * Spawn an actor from a built machine.
402
+ *
403
+ * Options:
404
+ * - `id` — custom actor ID (default: random)
405
+ * - `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.
412
+ */
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 });
283
416
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
284
417
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
285
418
  return actor;
286
419
  });
420
+ const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
421
+ const machine = built._inner;
422
+ let state = options?.from ?? machine.initial;
423
+ const hasPostponeRules = machine.postponeRules.length > 0;
424
+ const postponed = [];
425
+ const dummySend = Effect.fn("effect-machine.replay.send")((_event) => Effect.void);
426
+ const self = {
427
+ send: dummySend,
428
+ cast: dummySend,
429
+ spawn: () => Effect.die("spawn not supported in replay")
430
+ };
431
+ for (const event of events) {
432
+ if (machine.finalStates.has(state._tag)) break;
433
+ if (hasPostponeRules && shouldPostpone(machine, state._tag, event._tag)) {
434
+ postponed.push(event);
435
+ continue;
436
+ }
437
+ const transition = resolveTransition(machine, state, event);
438
+ if (transition !== void 0) {
439
+ const result = yield* runTransitionHandler(machine, transition, state, event, self, stubSystem, "replay");
440
+ const previousTag = state._tag;
441
+ state = result.newState;
442
+ if ((state._tag !== previousTag || transition.reenter === true) && postponed.length > 0) {
443
+ let drainTag = previousTag;
444
+ while (state._tag !== drainTag && postponed.length > 0) {
445
+ if (machine.finalStates.has(state._tag)) break;
446
+ drainTag = state._tag;
447
+ const drained = postponed.splice(0);
448
+ for (const postponedEvent of drained) {
449
+ if (machine.finalStates.has(state._tag)) break;
450
+ if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
451
+ postponed.push(postponedEvent);
452
+ continue;
453
+ }
454
+ const pTransition = resolveTransition(machine, state, postponedEvent);
455
+ if (pTransition !== void 0) state = (yield* runTransitionHandler(machine, pTransition, state, postponedEvent, self, stubSystem, "replay")).newState;
456
+ }
457
+ }
458
+ }
459
+ }
460
+ }
461
+ return state;
462
+ });
287
463
  //#endregion
288
- export { BuiltMachine, Machine, findTransitions, machine_exports, make, spawn };
464
+ export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, spawn };
@@ -1,7 +1,7 @@
1
1
  import { FullEventBrand, FullStateBrand } from "./internal/brands.js";
2
2
  import { Schema } from "effect";
3
3
 
4
- //#region src-v3/schema.d.ts
4
+ //#region src/schema.d.ts
5
5
  /**
6
6
  * Extract the TypeScript type from a TaggedStruct schema
7
7
  */
@@ -1,6 +1,6 @@
1
1
  import { InvalidSchemaError, MissingMatchHandlerError } from "./errors.js";
2
2
  import { Schema } from "effect";
3
- //#region src-v3/schema.ts
3
+ //#region src/schema.ts
4
4
  /**
5
5
  * Schema-first State/Event definitions for effect-machine.
6
6
  *
@@ -58,7 +58,10 @@ const buildMachineSchema = (definition) => {
58
58
  constructor.derive = (source, partial) => {
59
59
  const result = { _tag: tag };
60
60
  for (const key of fieldNames) if (key in source) result[key] = source[key];
61
- if (partial !== void 0) for (const [key, value] of Object.entries(partial)) result[key] = value;
61
+ if (partial !== void 0) for (const [key, value] of Object.entries(partial)) {
62
+ if (key === "_tag") continue;
63
+ result[key] = value;
64
+ }
62
65
  return result;
63
66
  };
64
67
  constructors[tag] = constructor;
@@ -1,7 +1,7 @@
1
1
  import { ActorSystem } from "./actor.js";
2
- import { Effect, Schema } from "effect";
2
+ import { Context, Effect, Schema } from "effect";
3
3
 
4
- //#region src-v3/slot.d.ts
4
+ //#region src/slot.d.ts
5
5
  /** Schema fields definition (like Schema.Struct.Fields) */
6
6
  type Fields = Record<string, Schema.Schema.All>;
7
7
  /** Extract the encoded type from schema fields (used for parameters) */
@@ -43,6 +43,7 @@ type EffectSlots<D extends EffectsDef> = { readonly [K in keyof D & string]: Eff
43
43
  * Shared across all machines via MachineContextTag.
44
44
  */
45
45
  interface MachineContext<State, Event, Self> {
46
+ readonly actorId: string;
46
47
  readonly state: State;
47
48
  readonly event: Event;
48
49
  readonly self: Self;
@@ -53,7 +54,7 @@ interface MachineContext<State, Event, Self> {
53
54
  * Single module-level tag instead of per-machine allocation.
54
55
  * @internal
55
56
  */
56
- declare const MachineContextTag: any;
57
+ declare const MachineContextTag: Context.Tag<MachineContext<any, any, any>, MachineContext<any, any, any>>;
57
58
  /**
58
59
  * Guard handler implementation.
59
60
  * Receives params and context, returns Effect<boolean>.
@@ -1,5 +1,5 @@
1
1
  import { Context } from "effect";
2
- //#region src-v3/slot.ts
2
+ //#region src/slot.ts
3
3
  /**
4
4
  * Slot module - schema-based, parameterized guards and effects.
5
5
  *
@@ -1,9 +1,9 @@
1
+ import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
1
2
  import { AssertionError } from "./errors.js";
2
- import { EffectsDef, GuardsDef } from "./slot.js";
3
- import { BuiltMachine, Machine } from "./machine.js";
3
+ import { BuiltMachine, Machine, MachineRef } from "./machine.js";
4
4
  import { Effect, SubscriptionRef } from "effect";
5
5
 
6
- //#region src-v3/testing.d.ts
6
+ //#region src/testing.d.ts
7
7
  /** Accept either Machine or BuiltMachine for testing utilities. */
8
8
  type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> = Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>;
9
9
  /**
@@ -40,7 +40,7 @@ declare const simulate: <S extends {
40
40
  }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[]) => Effect.Effect<{
41
41
  states: S[];
42
42
  finalState: S;
43
- }, never, Exclude<R, unknown>>;
43
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
44
44
  /**
45
45
  * Assert that a machine can reach a specific state given a sequence of events
46
46
  */
@@ -48,7 +48,7 @@ declare const assertReaches: <S extends {
48
48
  readonly _tag: string;
49
49
  }, E extends {
50
50
  readonly _tag: string;
51
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string) => Effect.Effect<any, unknown, unknown>;
51
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string) => Effect.Effect<S, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
52
52
  /**
53
53
  * Assert that a machine follows a specific path of state tags
54
54
  *
@@ -65,7 +65,10 @@ declare const assertPath: <S extends {
65
65
  readonly _tag: string;
66
66
  }, E extends {
67
67
  readonly _tag: string;
68
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<any, unknown, unknown>;
68
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<{
69
+ states: S[];
70
+ finalState: S;
71
+ }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
69
72
  /**
70
73
  * Assert that a machine never reaches a specific state given a sequence of events
71
74
  *
@@ -83,7 +86,10 @@ declare const assertNeverReaches: <S extends {
83
86
  readonly _tag: string;
84
87
  }, E extends {
85
88
  readonly _tag: string;
86
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string) => Effect.Effect<any, unknown, unknown>;
89
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string) => Effect.Effect<{
90
+ states: S[];
91
+ finalState: S;
92
+ }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
87
93
  /**
88
94
  * Create a controllable test harness for a machine
89
95
  */
@@ -129,7 +135,7 @@ declare const createTestHarness: <S extends {
129
135
  readonly _tag: string;
130
136
  }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, options?: TestHarnessOptions<S, E> | undefined) => Effect.Effect<{
131
137
  state: SubscriptionRef.SubscriptionRef<S>;
132
- send: (event: E) => Effect.Effect<S, never, never>;
138
+ send: (event: E) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
133
139
  getState: Effect.Effect<S, never, never>;
134
140
  }, never, never>;
135
141
  //#endregion