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
@@ -18,7 +18,7 @@ import { Cause, Effect, Exit, Scope } from "effect";
18
18
  *
19
19
  * Used by:
20
20
  * - executeTransition (actor event loop, testing)
21
- * - persistent-actor replay (restore, replayTo)
21
+ * - Machine.replay (event sourcing restore)
22
22
  *
23
23
  * @internal
24
24
  */
@@ -37,8 +37,18 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
37
37
  guards,
38
38
  effects
39
39
  };
40
- const result = transition.handler(handlerCtx);
41
- return isEffect(result) ? yield* result.pipe(Effect.provideService(machine.Context, ctx)) : result;
40
+ const raw = transition.handler(handlerCtx);
41
+ 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 {
43
+ newState: resolved.state,
44
+ hasReply: true,
45
+ reply: resolved.reply
46
+ };
47
+ return {
48
+ newState: resolved,
49
+ hasReply: false,
50
+ reply: void 0
51
+ };
42
52
  });
43
53
  /**
44
54
  * Execute a transition for a given state and event.
@@ -56,15 +66,28 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
56
66
  if (transition === void 0) return {
57
67
  newState: currentState,
58
68
  transitioned: false,
59
- reenter: false
69
+ reenter: false,
70
+ hasReply: false,
71
+ reply: void 0
60
72
  };
73
+ const { newState, hasReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
61
74
  return {
62
- newState: yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId),
75
+ newState,
63
76
  transitioned: true,
64
- reenter: transition.reenter === true
77
+ reenter: transition.reenter === true,
78
+ hasReply,
79
+ reply
65
80
  };
66
81
  });
67
82
  /**
83
+ * Check if an event should be postponed in the current state.
84
+ * @internal
85
+ */
86
+ const shouldPostpone = (machine, stateTag, eventTag) => {
87
+ for (const rule of machine.postponeRules) if (rule.stateTag === stateTag && rule.eventTag === eventTag) return true;
88
+ return false;
89
+ };
90
+ /**
68
91
  * Process a single event through the machine.
69
92
  *
70
93
  * Handles:
@@ -93,7 +116,10 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
93
116
  previousState: currentState,
94
117
  transitioned: false,
95
118
  lifecycleRan: false,
96
- isFinal: false
119
+ isFinal: false,
120
+ hasReply: false,
121
+ reply: void 0,
122
+ postponed: false
97
123
  };
98
124
  const newState = result.newState;
99
125
  const runLifecycle = newState._tag !== currentState._tag || result.reenter;
@@ -109,7 +135,10 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
109
135
  previousState: currentState,
110
136
  transitioned: true,
111
137
  lifecycleRan: runLifecycle,
112
- isFinal: machine.finalStates.has(newState._tag)
138
+ isFinal: machine.finalStates.has(newState._tag),
139
+ hasReply: result.hasReply,
140
+ reply: result.reply,
141
+ postponed: false
113
142
  };
114
143
  });
115
144
  /**
@@ -236,4 +265,4 @@ const findSpawnEffects = (machine, stateTag) => {
236
265
  return getIndex(machine).spawn.get(stateTag) ?? [];
237
266
  };
238
267
  //#endregion
239
- export { executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler };
268
+ export { executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
@@ -26,7 +26,12 @@ type TaggedConstructor<T extends {
26
26
  /**
27
27
  * Transition handler result - either a new state or Effect producing one
28
28
  */
29
- type TransitionResult<State, R> = State | Effect.Effect<State, never, R>;
29
+ /** Reply tuple returned from transition handlers for ask support */
30
+ interface TransitionReply<State> {
31
+ readonly state: State;
32
+ readonly reply: unknown;
33
+ }
34
+ type TransitionResult<State, R> = State | TransitionReply<State> | Effect.Effect<State | TransitionReply<State>, never, R>;
30
35
  /**
31
36
  * Internal event tags used for lifecycle effect contexts.
32
37
  * Prefixed with $ to distinguish from user events.
@@ -57,4 +62,4 @@ declare const isEffect: (value: unknown) => value is Effect.Effect<unknown, unkn
57
62
  */
58
63
  declare const stubSystem: ActorSystem;
59
64
  //#endregion
60
- export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionResult, getTag, isEffect, stubSystem };
65
+ export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionReply, TransitionResult, getTag, isEffect, stubSystem };
@@ -37,17 +37,13 @@ const isEffect = Effect.isEffect;
37
37
  */
38
38
  const stubSystem = {
39
39
  spawn: () => Effect.die("spawn not supported in stub system"),
40
- restore: () => Effect.die("restore not supported in stub system"),
41
40
  get: () => Effect.die("get not supported in stub system"),
42
41
  stop: () => Effect.die("stop not supported in stub system"),
43
42
  events: Stream.empty,
44
43
  get actors() {
45
44
  return /* @__PURE__ */ new Map();
46
45
  },
47
- subscribe: () => () => {},
48
- listPersisted: () => Effect.die("listPersisted not supported in stub system"),
49
- restoreMany: () => Effect.die("restoreMany not supported in stub system"),
50
- restoreAll: () => Effect.die("restoreAll not supported in stub system")
46
+ subscribe: () => () => {}
51
47
  };
52
48
  //#endregion
53
49
  export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect, stubSystem };
package/dist/machine.d.ts CHANGED
@@ -2,21 +2,22 @@ import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers,
2
2
  import { TransitionResult } from "./internal/utils.js";
3
3
  import { BrandedEvent, BrandedState, TaggedOrConstructor } from "./internal/brands.js";
4
4
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
5
- import { PersistenceConfig, PersistentMachine } from "./persistence/persistent-machine.js";
6
5
  import { DuplicateActorError } from "./errors.js";
7
6
  import { findTransitions } from "./internal/transition.js";
8
7
  import { ActorRef, ActorSystem } from "./actor.js";
9
- import { Cause, Effect, Schedule, Schema, Scope, ServiceMap } from "effect";
8
+ import { Cause, Duration, Effect, Schema, Scope, ServiceMap } from "effect";
10
9
 
11
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, TaskOptions, 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 {
@@ -73,19 +74,23 @@ interface SpawnEffect<State, Event, ED extends EffectsDef, R> {
73
74
  interface BackgroundEffect<State, Event, ED extends EffectsDef, R> {
74
75
  readonly handler: StateEffectHandler<State, Event, ED, R>;
75
76
  }
76
- /** Options for `persist` */
77
- interface PersistOptions {
78
- readonly snapshotSchedule: Schedule.Schedule<unknown, {
79
- readonly _tag: string;
80
- }>;
81
- readonly journalEvents: boolean;
82
- readonly machineType?: string;
83
- }
84
77
  interface TaskOptions<State, Event, ED extends EffectsDef, A, E1, ES, EF> {
85
78
  readonly onSuccess: (value: A, ctx: StateHandlerContext<State, Event, ED>) => ES;
86
79
  readonly onFailure?: (cause: Cause.Cause<E1>, ctx: StateHandlerContext<State, Event, ED>) => EF;
87
80
  readonly name?: string;
88
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.Input | ((state: State) => Duration.Input);
91
+ /** Event to send when the timer fires. Static or derived from current state. */
92
+ readonly event: Event | ((state: State) => Event);
93
+ }
89
94
  type IsAny<T> = 0 extends 1 & T ? true : false;
90
95
  type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
91
96
  type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
@@ -123,11 +128,6 @@ declare class BuiltMachine<State, Event, R = never> {
123
128
  /** @internal */
124
129
  constructor(machine: Machine<State, Event, R, any, any, any, any>);
125
130
  get initial(): State;
126
- persist(config: PersistOptions): PersistentMachine<State & {
127
- readonly _tag: string;
128
- }, Event & {
129
- readonly _tag: string;
130
- }, R>;
131
131
  }
132
132
  /**
133
133
  * Machine definition with fluent builder API.
@@ -152,6 +152,11 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
152
152
  /** @internal */
153
153
  readonly _finalStates: Set<string>;
154
154
  /** @internal */
155
+ readonly _postponeRules: Array<{
156
+ readonly stateTag: string;
157
+ readonly eventTag: string;
158
+ }>;
159
+ /** @internal */
155
160
  readonly _guardsSchema?: GuardsSchema<GD>;
156
161
  /** @internal */
157
162
  readonly _effectsSchema?: EffectsSchema<EFD>;
@@ -175,6 +180,10 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
175
180
  get spawnEffects(): ReadonlyArray<SpawnEffect<State, Event, EFD, R>>;
176
181
  get backgroundEffects(): ReadonlyArray<BackgroundEffect<State, Event, EFD, R>>;
177
182
  get finalStates(): ReadonlySet<string>;
183
+ get postponeRules(): ReadonlyArray<{
184
+ readonly stateTag: string;
185
+ readonly eventTag: string;
186
+ }>;
178
187
  get guardsSchema(): GuardsSchema<GD> | undefined;
179
188
  get effectsSchema(): EffectsSchema<EFD> | undefined;
180
189
  /** @internal */
@@ -230,6 +239,28 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
230
239
  * Interrupts do not emit failure events.
231
240
  */
232
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>;
233
264
  /**
234
265
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
235
266
  * Use effect slots defined via `Slot.Effects` for the actual work.
@@ -251,6 +282,24 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
251
282
  * ```
252
283
  */
253
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>;
254
303
  final<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
255
304
  /**
256
305
  * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
@@ -259,12 +308,6 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
259
308
  * - Machines without slots: call with no arguments.
260
309
  */
261
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>>;
262
- /** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
263
- persist(config: PersistOptions): PersistentMachine<State & {
264
- readonly _tag: string;
265
- }, Event & {
266
- readonly _tag: string;
267
- }, R>;
268
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>;
269
312
  }
270
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> {
@@ -275,17 +318,33 @@ declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema
275
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>;
276
319
  }
277
320
  declare const make: typeof Machine.make;
278
- declare const spawn: {
279
- <S extends {
280
- readonly _tag: string;
281
- }, E extends {
282
- readonly _tag: string;
283
- }, R>(machine: BuiltMachine<S, E, R>): Effect.Effect<ActorRef<S, E>, never, R>;
284
- <S extends {
285
- readonly _tag: string;
286
- }, E extends {
287
- readonly _tag: string;
288
- }, R>(machine: BuiltMachine<S, E, R>, id: string): Effect.Effect<ActorRef<S, E>, never, R>;
289
- };
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>;
290
349
  //#endregion
291
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, type PersistenceConfig, type PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, 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 };
package/dist/machine.js CHANGED
@@ -1,11 +1,10 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
2
  import { Inspector } from "./inspection.js";
3
- import { getTag } from "./internal/utils.js";
3
+ import { getTag, stubSystem } from "./internal/utils.js";
4
4
  import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
5
- import { persist } from "./persistence/persistent-machine.js";
6
5
  import { emitWithTimestamp } from "./internal/inspection.js";
7
6
  import { MachineContextTag } from "./slot.js";
8
- import { findTransitions, invalidateIndex } from "./internal/transition.js";
7
+ import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
9
8
  import { createActor } from "./actor.js";
10
9
  import { Cause, Effect, Exit, Option, Scope } from "effect";
11
10
  //#region src/machine.ts
@@ -14,6 +13,7 @@ var machine_exports = /* @__PURE__ */ __exportAll({
14
13
  Machine: () => Machine,
15
14
  findTransitions: () => findTransitions,
16
15
  make: () => make,
16
+ replay: () => replay,
17
17
  spawn: () => spawn
18
18
  });
19
19
  const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspector), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
@@ -42,9 +42,6 @@ var BuiltMachine = class {
42
42
  get initial() {
43
43
  return this._inner.initial;
44
44
  }
45
- persist(config) {
46
- return this._inner.persist(config);
47
- }
48
45
  };
49
46
  /**
50
47
  * Machine definition with fluent builder API.
@@ -64,6 +61,7 @@ var Machine = class Machine {
64
61
  /** @internal */ _spawnEffects;
65
62
  /** @internal */ _backgroundEffects;
66
63
  /** @internal */ _finalStates;
64
+ /** @internal */ _postponeRules;
67
65
  /** @internal */ _guardsSchema;
68
66
  /** @internal */ _effectsSchema;
69
67
  /** @internal */ _guardHandlers;
@@ -88,6 +86,9 @@ var Machine = class Machine {
88
86
  get finalStates() {
89
87
  return this._finalStates;
90
88
  }
89
+ get postponeRules() {
90
+ return this._postponeRules;
91
+ }
91
92
  get guardsSchema() {
92
93
  return this._guardsSchema;
93
94
  }
@@ -101,6 +102,7 @@ var Machine = class Machine {
101
102
  this._spawnEffects = [];
102
103
  this._backgroundEffects = [];
103
104
  this._finalStates = /* @__PURE__ */ new Set();
105
+ this._postponeRules = [];
104
106
  this._guardsSchema = guardsSchema;
105
107
  this._effectsSchema = effectsSchema;
106
108
  this._guardHandlers = /* @__PURE__ */ new Map();
@@ -259,6 +261,36 @@ var Machine = class Machine {
259
261
  return this.spawn(state, handler);
260
262
  }
261
263
  /**
264
+ * State timeout — gen_statem's `state_timeout`.
265
+ *
266
+ * Entering the state starts a timer. Leaving cancels it (via state scope).
267
+ * `.reenter()` restarts the timer with fresh state values.
268
+ * Compiles to `.task()` internally — preserves `@machine.task` inspection events.
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * machine
273
+ * .timeout(State.Loading, {
274
+ * duration: Duration.seconds(30),
275
+ * event: Event.Timeout,
276
+ * })
277
+ * // Dynamic duration from state
278
+ * .timeout(State.Retrying, {
279
+ * duration: (state) => Duration.seconds(state.backoff),
280
+ * event: Event.GiveUp,
281
+ * })
282
+ * ```
283
+ */
284
+ timeout(state, config) {
285
+ const stateTag = getTag(state);
286
+ const resolveDuration = typeof config.duration === "function" ? config.duration : () => config.duration;
287
+ const resolveEvent = typeof config.event === "function" ? config.event : () => config.event;
288
+ return this.task(state, (ctx) => Effect.sleep(resolveDuration(ctx.state)), {
289
+ onSuccess: (_, ctx) => resolveEvent(ctx.state),
290
+ name: `$timeout:${stateTag}`
291
+ });
292
+ }
293
+ /**
262
294
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
263
295
  * Use effect slots defined via `Slot.Effects` for the actual work.
264
296
  *
@@ -282,6 +314,35 @@ var Machine = class Machine {
282
314
  this._backgroundEffects.push({ handler });
283
315
  return this;
284
316
  }
317
+ /**
318
+ * Postpone events — gen_statem's event postpone.
319
+ *
320
+ * When a matching event arrives in the given state, it is buffered instead of
321
+ * processed. After the next state transition (tag change), all buffered events
322
+ * are drained through the loop in FIFO order.
323
+ *
324
+ * Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
325
+ * with `ActorStoppedError` on stop/interrupt/final-state.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * machine
330
+ * .postpone(State.Connecting, Event.Data) // single event
331
+ * .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
332
+ * ```
333
+ */
334
+ postpone(state, events) {
335
+ const stateTag = getTag(state);
336
+ const eventList = Array.isArray(events) ? events : [events];
337
+ for (const ev of eventList) {
338
+ const eventTag = getTag(ev);
339
+ this._postponeRules.push({
340
+ stateTag,
341
+ eventTag
342
+ });
343
+ }
344
+ return this;
345
+ }
285
346
  final(state) {
286
347
  const stateTag = getTag(state);
287
348
  this._finalStates.add(stateTag);
@@ -313,6 +374,7 @@ var Machine = class Machine {
313
374
  result._finalStates = new Set(this._finalStates);
314
375
  result._spawnEffects = [...this._spawnEffects];
315
376
  result._backgroundEffects = [...this._backgroundEffects];
377
+ result._postponeRules = [...this._postponeRules];
316
378
  const anyHandlers = handlers;
317
379
  if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
318
380
  if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
@@ -320,10 +382,6 @@ var Machine = class Machine {
320
382
  }
321
383
  return new BuiltMachine(this);
322
384
  }
323
- /** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
324
- persist(config) {
325
- return persist(config)(this);
326
- }
327
385
  static make(config) {
328
386
  return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
329
387
  }
@@ -343,11 +401,68 @@ var TransitionScope = class {
343
401
  }
344
402
  };
345
403
  const make = Machine.make;
346
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, id) {
347
- const actor = yield* createActor(id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner);
404
+ /**
405
+ * Spawn an actor from a built machine.
406
+ *
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.
413
+ *
414
+ * Persistence is composed in userland by observing `actor.changes`
415
+ * and saving snapshots to your own storage.
416
+ */
417
+ const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
418
+ 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 });
348
420
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
349
421
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
350
422
  return actor;
351
423
  });
424
+ const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
425
+ const machine = built._inner;
426
+ let state = options?.from ?? machine.initial;
427
+ const hasPostponeRules = machine.postponeRules.length > 0;
428
+ const postponed = [];
429
+ const dummySend = Effect.fn("effect-machine.replay.send")((_event) => Effect.void);
430
+ const self = {
431
+ send: dummySend,
432
+ cast: dummySend,
433
+ spawn: () => Effect.die("spawn not supported in replay")
434
+ };
435
+ for (const event of events) {
436
+ if (machine.finalStates.has(state._tag)) break;
437
+ if (hasPostponeRules && shouldPostpone(machine, state._tag, event._tag)) {
438
+ postponed.push(event);
439
+ continue;
440
+ }
441
+ const transition = resolveTransition(machine, state, event);
442
+ if (transition !== void 0) {
443
+ const result = yield* runTransitionHandler(machine, transition, state, event, self, stubSystem, "replay");
444
+ const previousTag = state._tag;
445
+ state = result.newState;
446
+ if ((state._tag !== previousTag || transition.reenter === true) && postponed.length > 0) {
447
+ let drainTag = previousTag;
448
+ while (state._tag !== drainTag && postponed.length > 0) {
449
+ if (machine.finalStates.has(state._tag)) break;
450
+ drainTag = state._tag;
451
+ const drained = postponed.splice(0);
452
+ for (const postponedEvent of drained) {
453
+ if (machine.finalStates.has(state._tag)) break;
454
+ if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
455
+ postponed.push(postponedEvent);
456
+ continue;
457
+ }
458
+ const pTransition = resolveTransition(machine, state, postponedEvent);
459
+ if (pTransition !== void 0) state = (yield* runTransitionHandler(machine, pTransition, state, postponedEvent, self, stubSystem, "replay")).newState;
460
+ }
461
+ }
462
+ }
463
+ }
464
+ }
465
+ return state;
466
+ });
352
467
  //#endregion
353
- export { BuiltMachine, Machine, findTransitions, machine_exports, make, spawn };
468
+ export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, spawn };
package/dist/testing.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { stubSystem } from "./internal/utils.js";
2
2
  import { AssertionError } from "./errors.js";
3
3
  import { BuiltMachine } from "./machine.js";
4
- import { executeTransition } from "./internal/transition.js";
4
+ import { executeTransition, shouldPostpone } from "./internal/transition.js";
5
5
  import { Effect, SubscriptionRef } from "effect";
6
6
  //#region src/testing.ts
7
7
  /**
@@ -26,18 +26,44 @@ import { Effect, SubscriptionRef } from "effect";
26
26
  */
27
27
  const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
28
28
  const machine = input instanceof BuiltMachine ? input._inner : input;
29
+ const dummySend = Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void);
29
30
  const dummySelf = {
30
- send: Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void),
31
+ send: dummySend,
32
+ cast: dummySend,
31
33
  spawn: () => Effect.die("spawn not supported in simulation")
32
34
  };
33
35
  let currentState = machine.initial;
34
36
  const states = [currentState];
37
+ const hasPostponeRules = machine.postponeRules.length > 0;
38
+ const postponed = [];
35
39
  for (const event of events) {
40
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
41
+ postponed.push(event);
42
+ continue;
43
+ }
36
44
  const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "simulation");
37
45
  if (!result.transitioned) continue;
46
+ const prevTag = currentState._tag;
38
47
  currentState = result.newState;
39
48
  states.push(currentState);
40
49
  if (machine.finalStates.has(currentState._tag)) break;
50
+ let drainTag = prevTag;
51
+ while (currentState._tag !== drainTag && postponed.length > 0) {
52
+ drainTag = currentState._tag;
53
+ const drained = postponed.splice(0);
54
+ for (const postponedEvent of drained) {
55
+ if (shouldPostpone(machine, currentState._tag, postponedEvent._tag)) {
56
+ postponed.push(postponedEvent);
57
+ continue;
58
+ }
59
+ const drainResult = yield* executeTransition(machine, currentState, postponedEvent, dummySelf, stubSystem, "simulation");
60
+ if (drainResult.transitioned) {
61
+ currentState = drainResult.newState;
62
+ states.push(currentState);
63
+ if (machine.finalStates.has(currentState._tag)) break;
64
+ }
65
+ }
66
+ }
41
67
  }
42
68
  return {
43
69
  states,
@@ -113,20 +139,48 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
113
139
  */
114
140
  const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
115
141
  const machine = input instanceof BuiltMachine ? input._inner : input;
142
+ const dummySend = Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void);
116
143
  const dummySelf = {
117
- send: Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void),
144
+ send: dummySend,
145
+ cast: dummySend,
118
146
  spawn: () => Effect.die("spawn not supported in test harness")
119
147
  };
120
148
  const stateRef = yield* SubscriptionRef.make(machine.initial);
149
+ const hasPostponeRules = machine.postponeRules.length > 0;
150
+ const postponed = [];
121
151
  return {
122
152
  state: stateRef,
123
153
  send: Effect.fn("effect-machine.testHarness.send")(function* (event) {
124
154
  const currentState = yield* SubscriptionRef.get(stateRef);
155
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
156
+ postponed.push(event);
157
+ return currentState;
158
+ }
125
159
  const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "test-harness");
126
160
  if (!result.transitioned) return currentState;
161
+ const prevTag = currentState._tag;
127
162
  const newState = result.newState;
128
163
  yield* SubscriptionRef.set(stateRef, newState);
129
164
  if (options?.onTransition !== void 0) options.onTransition(currentState, event, newState);
165
+ let drainTag = prevTag;
166
+ let currentTag = newState._tag;
167
+ while (currentTag !== drainTag && postponed.length > 0) {
168
+ drainTag = currentTag;
169
+ const drained = postponed.splice(0);
170
+ for (const postponedEvent of drained) {
171
+ const state = yield* SubscriptionRef.get(stateRef);
172
+ if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
173
+ postponed.push(postponedEvent);
174
+ continue;
175
+ }
176
+ const drainResult = yield* executeTransition(machine, state, postponedEvent, dummySelf, stubSystem, "test-harness");
177
+ if (drainResult.transitioned) {
178
+ yield* SubscriptionRef.set(stateRef, drainResult.newState);
179
+ currentTag = drainResult.newState._tag;
180
+ if (options?.onTransition !== void 0) options.onTransition(state, postponedEvent, drainResult.newState);
181
+ }
182
+ }
183
+ }
130
184
  return newState;
131
185
  }),
132
186
  getState: SubscriptionRef.get(stateRef)