effect-machine 0.9.0 → 0.10.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 (62) hide show
  1. package/README.md +76 -16
  2. package/dist/actor.d.ts +55 -89
  3. package/dist/actor.js +135 -30
  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 +3 -3
  8. package/dist/index.js +2 -2
  9. package/dist/internal/transition.d.ts +26 -2
  10. package/dist/internal/transition.js +37 -8
  11. package/dist/internal/utils.d.ts +7 -2
  12. package/dist/machine.d.ts +66 -3
  13. package/dist/machine.js +65 -0
  14. package/dist/persistence/persistent-actor.js +52 -16
  15. package/dist/testing.js +57 -3
  16. package/package.json +9 -8
  17. package/{dist-v3 → v3/dist}/actor.d.ts +65 -78
  18. package/{dist-v3 → v3/dist}/actor.js +173 -35
  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 +13 -0
  26. package/v3/dist/index.js +13 -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 +1 -1
  36. package/{dist-v3 → v3/dist}/machine.d.ts +86 -9
  37. package/{dist-v3 → v3/dist}/machine.js +128 -2
  38. package/{dist-v3 → v3/dist}/persistence/adapter.d.ts +18 -5
  39. package/{dist-v3 → v3/dist}/persistence/adapter.js +1 -1
  40. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.d.ts +1 -1
  41. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.js +1 -1
  42. package/{dist-v3 → v3/dist}/persistence/persistent-actor.d.ts +7 -6
  43. package/{dist-v3 → v3/dist}/persistence/persistent-actor.js +58 -19
  44. package/{dist-v3 → v3/dist}/persistence/persistent-machine.d.ts +1 -1
  45. package/{dist-v3 → v3/dist}/persistence/persistent-machine.js +1 -1
  46. package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
  47. package/{dist-v3 → v3/dist}/schema.js +5 -2
  48. package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
  49. package/{dist-v3 → v3/dist}/slot.js +1 -1
  50. package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
  51. package/{dist-v3 → v3/dist}/testing.js +60 -6
  52. package/dist-v3/errors.d.ts +0 -27
  53. package/dist-v3/index.d.ts +0 -12
  54. package/dist-v3/index.js +0 -13
  55. package/dist-v3/inspection.js +0 -48
  56. package/dist-v3/internal/inspection.js +0 -13
  57. /package/{dist-v3 → v3/dist}/_virtual/_rolldown/runtime.js +0 -0
  58. /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
  59. /package/{dist-v3 → v3/dist}/cluster/index.js +0 -0
  60. /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
  61. /package/{dist-v3 → v3/dist}/persistence/index.d.ts +0 -0
  62. /package/{dist-v3 → v3/dist}/persistence/index.js +0 -0
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { EffectHandlers, EffectSlot, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlot, GuardSlots, GuardsDef, GuardsSchema, MachineContext, Slot } from "./slot.js";
2
2
  import { Event, MachineEventSchema, MachineStateSchema, State } from "./schema.js";
3
3
  import { PersistenceConfig, PersistentMachine, isPersistentMachine } from "./persistence/persistent-machine.js";
4
- import { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
4
+ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
5
5
  import { ProcessEventResult } from "./internal/transition.js";
6
6
  import { PersistentActorRef, createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
7
7
  import { ActorMetadata, PersistedEvent, PersistenceAdapter, PersistenceAdapterTag, PersistenceError, RestoreFailure, RestoreResult, Snapshot, VersionConflictError } from "./persistence/adapter.js";
8
8
  import { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter } from "./persistence/adapters/in-memory.js";
9
9
  import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, ProvideHandlers, SpawnEffect, StateHandlerContext, TaskOptions, Transition, machine_d_exports } from "./machine.js";
10
- import { ActorRef, ActorSystem, Default, SystemEvent, SystemEventListener } from "./actor.js";
10
+ import { ActorRef, ActorRefSync, ActorSystem, Default, SystemEvent, SystemEventListener } from "./actor.js";
11
11
  import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
12
12
  import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
13
- export { type ActorMetadata, type ActorRef, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, InMemoryPersistenceAdapter, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, type PersistOptions, type PersistedEvent, type PersistenceAdapter, PersistenceAdapterTag, type PersistenceConfig, PersistenceError, type PersistentActorRef, type PersistentMachine, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type RestoreFailure, type RestoreResult, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type Snapshot, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, makeInspectorEffect, restorePersistentActor, simulate, tracingInspector };
13
+ export { type ActorMetadata, type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, InMemoryPersistenceAdapter, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, type PersistOptions, type PersistedEvent, type PersistenceAdapter, PersistenceAdapterTag, type PersistenceConfig, PersistenceError, type PersistentActorRef, type PersistentMachine, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type RestoreFailure, type RestoreResult, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type Snapshot, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, makeInspectorEffect, restorePersistentActor, simulate, tracingInspector };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
2
- import { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
2
+ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
3
3
  import { isPersistentMachine } from "./persistence/persistent-machine.js";
4
4
  import { Slot } from "./slot.js";
5
5
  import { machine_exports } from "./machine.js";
@@ -10,4 +10,4 @@ import { Event, State } from "./schema.js";
10
10
  import { assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
11
11
  import { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter } from "./persistence/adapters/in-memory.js";
12
12
  import "./persistence/index.js";
13
- export { Default as ActorSystemDefault, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, InMemoryPersistenceAdapter, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, MissingSchemaError, PersistenceAdapterTag, PersistenceError, ProvisionValidationError, Slot, SlotProvisionError, State, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, makeInspectorEffect, restorePersistentActor, simulate, tracingInspector };
13
+ export { ActorStoppedError, Default as ActorSystemDefault, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, InMemoryPersistenceAdapter, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceAdapterTag, PersistenceError, ProvisionValidationError, Slot, SlotProvisionError, State, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, makeInspectorEffect, restorePersistentActor, simulate, tracingInspector };
@@ -29,7 +29,11 @@ declare const runTransitionHandler: <S extends {
29
29
  readonly _tag: string;
30
30
  }, E extends {
31
31
  readonly _tag: string;
32
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
32
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
33
+ newState: S;
34
+ hasReply: boolean;
35
+ reply: unknown;
36
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
33
37
  /**
34
38
  * Execute a transition for a given state and event.
35
39
  * Handles transition resolution, handler invocation, and guard/effect slot creation.
@@ -49,6 +53,8 @@ declare const executeTransition: <S extends {
49
53
  newState: S;
50
54
  transitioned: boolean;
51
55
  reenter: boolean;
56
+ hasReply: boolean;
57
+ reply: unknown;
52
58
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
53
59
  /**
54
60
  * Optional hooks for event processing inspection/tracing.
@@ -84,7 +90,22 @@ interface ProcessEventResult<S> {
84
90
  readonly lifecycleRan: boolean;
85
91
  /** Whether new state is final */
86
92
  readonly isFinal: boolean;
93
+ /** Whether the handler provided a reply (structural, not value-based) */
94
+ readonly hasReply: boolean;
95
+ /** Domain reply value from handler (used by ask). Only meaningful when hasReply is true. */
96
+ readonly reply?: unknown;
97
+ /** Whether the event was postponed (buffered for retry after next state change) */
98
+ readonly postponed: boolean;
87
99
  }
100
+ /**
101
+ * Check if an event should be postponed in the current state.
102
+ * @internal
103
+ */
104
+ declare const shouldPostpone: <S extends {
105
+ readonly _tag: string;
106
+ }, E extends {
107
+ readonly _tag: string;
108
+ }, R>(machine: Machine<S, E, R, any, any, any, any>, stateTag: string, eventTag: string) => boolean;
88
109
  /**
89
110
  * Process a single event through the machine.
90
111
  *
@@ -109,6 +130,9 @@ declare const processEventCore: <S extends {
109
130
  transitioned: boolean;
110
131
  lifecycleRan: boolean;
111
132
  isFinal: boolean;
133
+ hasReply: boolean;
134
+ reply: unknown;
135
+ postponed: boolean;
112
136
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
113
137
  /**
114
138
  * Run spawn effects for a state (forked into state scope, auto-cancelled on state exit).
@@ -157,4 +181,4 @@ declare const findSpawnEffects: <S extends {
157
181
  readonly _tag: string;
158
182
  }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, GD, EFD>, stateTag: string) => ReadonlyArray<SpawnEffect<S, E, EFD, R>>;
159
183
  //#endregion
160
- export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler };
184
+ export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
@@ -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 };
package/dist/machine.d.ts CHANGED
@@ -6,17 +6,19 @@ import { PersistenceConfig, PersistentMachine } from "./persistence/persistent-m
6
6
  import { DuplicateActorError } from "./errors.js";
7
7
  import { findTransitions } from "./internal/transition.js";
8
8
  import { ActorRef, ActorSystem } from "./actor.js";
9
- import { Cause, Effect, Schedule, Schema, Scope, ServiceMap } from "effect";
9
+ import { Cause, Duration, Effect, Schedule, Schema, Scope, ServiceMap } from "effect";
10
10
 
11
11
  //#region src/machine.d.ts
12
12
  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 };
13
+ export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, PersistenceConfig, PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, spawn };
14
14
  }
15
15
  /**
16
16
  * Self reference for sending events back to the machine
17
17
  */
18
18
  interface MachineRef<Event> {
19
19
  readonly send: (event: Event) => Effect.Effect<void>;
20
+ /** Fire-and-forget alias for send (OTP gen_server:cast). */
21
+ readonly cast: (event: Event) => Effect.Effect<void>;
20
22
  readonly spawn: <S2 extends {
21
23
  readonly _tag: string;
22
24
  }, E2 extends {
@@ -86,6 +88,18 @@ interface TaskOptions<State, Event, ED extends EffectsDef, A, E1, ES, EF> {
86
88
  readonly onFailure?: (cause: Cause.Cause<E1>, ctx: StateHandlerContext<State, Event, ED>) => EF;
87
89
  readonly name?: string;
88
90
  }
91
+ /**
92
+ * Configuration for `.timeout()` — gen_statem-style state timeouts.
93
+ *
94
+ * Entering the state starts a timer. Leaving cancels it.
95
+ * `.reenter()` restarts the timer with fresh state values.
96
+ */
97
+ interface TimeoutConfig<State, Event> {
98
+ /** Duration before firing. Static or derived from current state. */
99
+ readonly duration: Duration.Input | ((state: State) => Duration.Input);
100
+ /** Event to send when the timer fires. Static or derived from current state. */
101
+ readonly event: Event | ((state: State) => Event);
102
+ }
89
103
  type IsAny<T> = 0 extends 1 & T ? true : false;
90
104
  type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
91
105
  type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
@@ -152,6 +166,11 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
152
166
  /** @internal */
153
167
  readonly _finalStates: Set<string>;
154
168
  /** @internal */
169
+ readonly _postponeRules: Array<{
170
+ readonly stateTag: string;
171
+ readonly eventTag: string;
172
+ }>;
173
+ /** @internal */
155
174
  readonly _guardsSchema?: GuardsSchema<GD>;
156
175
  /** @internal */
157
176
  readonly _effectsSchema?: EffectsSchema<EFD>;
@@ -175,6 +194,10 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
175
194
  get spawnEffects(): ReadonlyArray<SpawnEffect<State, Event, EFD, R>>;
176
195
  get backgroundEffects(): ReadonlyArray<BackgroundEffect<State, Event, EFD, R>>;
177
196
  get finalStates(): ReadonlySet<string>;
197
+ get postponeRules(): ReadonlyArray<{
198
+ readonly stateTag: string;
199
+ readonly eventTag: string;
200
+ }>;
178
201
  get guardsSchema(): GuardsSchema<GD> | undefined;
179
202
  get effectsSchema(): EffectsSchema<EFD> | undefined;
180
203
  /** @internal */
@@ -230,6 +253,28 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
230
253
  * Interrupts do not emit failure events.
231
254
  */
232
255
  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>;
256
+ /**
257
+ * State timeout — gen_statem's `state_timeout`.
258
+ *
259
+ * Entering the state starts a timer. Leaving cancels it (via state scope).
260
+ * `.reenter()` restarts the timer with fresh state values.
261
+ * Compiles to `.task()` internally — preserves `@machine.task` inspection events.
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * machine
266
+ * .timeout(State.Loading, {
267
+ * duration: Duration.seconds(30),
268
+ * event: Event.Timeout,
269
+ * })
270
+ * // Dynamic duration from state
271
+ * .timeout(State.Retrying, {
272
+ * duration: (state) => Duration.seconds(state.backoff),
273
+ * event: Event.GiveUp,
274
+ * })
275
+ * ```
276
+ */
277
+ timeout<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, config: TimeoutConfig<NS, VariantsUnion<_ED> & BrandedEvent>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
233
278
  /**
234
279
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
235
280
  * Use effect slots defined via `Slot.Effects` for the actual work.
@@ -251,6 +296,24 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
251
296
  * ```
252
297
  */
253
298
  background(handler: StateEffectHandler<State, Event, EFD, Scope.Scope>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
299
+ /**
300
+ * Postpone events — gen_statem's event postpone.
301
+ *
302
+ * When a matching event arrives in the given state, it is buffered instead of
303
+ * processed. After the next state transition (tag change), all buffered events
304
+ * are drained through the loop in FIFO order.
305
+ *
306
+ * Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
307
+ * with `ActorStoppedError` on stop/interrupt/final-state.
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * machine
312
+ * .postpone(State.Connecting, Event.Data) // single event
313
+ * .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
314
+ * ```
315
+ */
316
+ 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
317
  final<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
255
318
  /**
256
319
  * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
@@ -288,4 +351,4 @@ declare const spawn: {
288
351
  }, R>(machine: BuiltMachine<S, E, R>, id: string): Effect.Effect<ActorRef<S, E>, never, R>;
289
352
  };
290
353
  //#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 };
354
+ export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, type PersistenceConfig, type PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, spawn };
package/dist/machine.js CHANGED
@@ -64,6 +64,7 @@ var Machine = class Machine {
64
64
  /** @internal */ _spawnEffects;
65
65
  /** @internal */ _backgroundEffects;
66
66
  /** @internal */ _finalStates;
67
+ /** @internal */ _postponeRules;
67
68
  /** @internal */ _guardsSchema;
68
69
  /** @internal */ _effectsSchema;
69
70
  /** @internal */ _guardHandlers;
@@ -88,6 +89,9 @@ var Machine = class Machine {
88
89
  get finalStates() {
89
90
  return this._finalStates;
90
91
  }
92
+ get postponeRules() {
93
+ return this._postponeRules;
94
+ }
91
95
  get guardsSchema() {
92
96
  return this._guardsSchema;
93
97
  }
@@ -101,6 +105,7 @@ var Machine = class Machine {
101
105
  this._spawnEffects = [];
102
106
  this._backgroundEffects = [];
103
107
  this._finalStates = /* @__PURE__ */ new Set();
108
+ this._postponeRules = [];
104
109
  this._guardsSchema = guardsSchema;
105
110
  this._effectsSchema = effectsSchema;
106
111
  this._guardHandlers = /* @__PURE__ */ new Map();
@@ -259,6 +264,36 @@ var Machine = class Machine {
259
264
  return this.spawn(state, handler);
260
265
  }
261
266
  /**
267
+ * State timeout — gen_statem's `state_timeout`.
268
+ *
269
+ * Entering the state starts a timer. Leaving cancels it (via state scope).
270
+ * `.reenter()` restarts the timer with fresh state values.
271
+ * Compiles to `.task()` internally — preserves `@machine.task` inspection events.
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * machine
276
+ * .timeout(State.Loading, {
277
+ * duration: Duration.seconds(30),
278
+ * event: Event.Timeout,
279
+ * })
280
+ * // Dynamic duration from state
281
+ * .timeout(State.Retrying, {
282
+ * duration: (state) => Duration.seconds(state.backoff),
283
+ * event: Event.GiveUp,
284
+ * })
285
+ * ```
286
+ */
287
+ timeout(state, config) {
288
+ const stateTag = getTag(state);
289
+ const resolveDuration = typeof config.duration === "function" ? config.duration : () => config.duration;
290
+ const resolveEvent = typeof config.event === "function" ? config.event : () => config.event;
291
+ return this.task(state, (ctx) => Effect.sleep(resolveDuration(ctx.state)), {
292
+ onSuccess: (_, ctx) => resolveEvent(ctx.state),
293
+ name: `$timeout:${stateTag}`
294
+ });
295
+ }
296
+ /**
262
297
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
263
298
  * Use effect slots defined via `Slot.Effects` for the actual work.
264
299
  *
@@ -282,6 +317,35 @@ var Machine = class Machine {
282
317
  this._backgroundEffects.push({ handler });
283
318
  return this;
284
319
  }
320
+ /**
321
+ * Postpone events — gen_statem's event postpone.
322
+ *
323
+ * When a matching event arrives in the given state, it is buffered instead of
324
+ * processed. After the next state transition (tag change), all buffered events
325
+ * are drained through the loop in FIFO order.
326
+ *
327
+ * Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
328
+ * with `ActorStoppedError` on stop/interrupt/final-state.
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * machine
333
+ * .postpone(State.Connecting, Event.Data) // single event
334
+ * .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
335
+ * ```
336
+ */
337
+ postpone(state, events) {
338
+ const stateTag = getTag(state);
339
+ const eventList = Array.isArray(events) ? events : [events];
340
+ for (const ev of eventList) {
341
+ const eventTag = getTag(ev);
342
+ this._postponeRules.push({
343
+ stateTag,
344
+ eventTag
345
+ });
346
+ }
347
+ return this;
348
+ }
285
349
  final(state) {
286
350
  const stateTag = getTag(state);
287
351
  this._finalStates.add(stateTag);
@@ -313,6 +377,7 @@ var Machine = class Machine {
313
377
  result._finalStates = new Set(this._finalStates);
314
378
  result._spawnEffects = [...this._spawnEffects];
315
379
  result._backgroundEffects = [...this._backgroundEffects];
380
+ result._postponeRules = [...this._postponeRules];
316
381
  const anyHandlers = handlers;
317
382
  if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
318
383
  if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
@@ -1,9 +1,10 @@
1
1
  import { Inspector } from "../inspection.js";
2
2
  import { INTERNAL_INIT_EVENT, stubSystem } from "../internal/utils.js";
3
+ import { NoReplyError } from "../errors.js";
3
4
  import { emitWithTimestamp } from "../internal/inspection.js";
4
- import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler } from "../internal/transition.js";
5
+ import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone } from "../internal/transition.js";
5
6
  import { PersistenceAdapterTag } from "./adapter.js";
6
- import { ActorSystem, buildActorRefCore, notifyListeners } from "../actor.js";
7
+ import { ActorSystem, buildActorRefCore, notifyListeners, settlePendingReplies } from "../actor.js";
7
8
  import { Cause, Clock, Deferred, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
8
9
  //#region src/persistence/persistent-actor.ts
9
10
  /** Get current time in milliseconds using Effect Clock */
@@ -19,7 +20,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
19
20
  for (const persistedEvent of events) {
20
21
  if (stopVersion !== void 0 && persistedEvent.version > stopVersion) break;
21
22
  const transition = resolveTransition(machine, state, persistedEvent.event);
22
- if (transition !== void 0) state = yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem, "restore");
23
+ if (transition !== void 0) state = (yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem, "restore")).newState;
23
24
  version = persistedEvent.version;
24
25
  }
25
26
  return {
@@ -30,7 +31,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
30
31
  /**
31
32
  * Build PersistentActorRef with all methods
32
33
  */
33
- const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap) => {
34
+ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap, pendingReplies) => {
34
35
  const { machine, persistence } = persistentMachine;
35
36
  const typedMachine = machine;
36
37
  const persist = Effect.gen(function* () {
@@ -44,8 +45,10 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
44
45
  const version = Ref.get(versionRef).pipe(Effect.withSpan("effect-machine.persistentActor.version"));
45
46
  const replayTo = Effect.fn("effect-machine.persistentActor.replayTo")(function* (targetVersion) {
46
47
  if (targetVersion <= (yield* Ref.get(versionRef))) {
48
+ const dummySend = Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void);
47
49
  const dummySelf = {
48
- send: Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void),
50
+ send: dummySend,
51
+ cast: dummySend,
49
52
  spawn: () => Effect.die("spawn not supported in replay")
50
53
  };
51
54
  const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
@@ -70,7 +73,7 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
70
73
  }
71
74
  });
72
75
  return {
73
- ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap),
76
+ ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies),
74
77
  persist,
75
78
  version,
76
79
  replayTo
@@ -92,11 +95,16 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
92
95
  const eventQueue = yield* Queue.unbounded();
93
96
  const stoppedRef = yield* Ref.make(false);
94
97
  const childrenMap = /* @__PURE__ */ new Map();
98
+ const selfSend = Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
99
+ if (yield* Ref.get(stoppedRef)) return;
100
+ yield* Queue.offer(eventQueue, {
101
+ _tag: "send",
102
+ event
103
+ });
104
+ });
95
105
  const self = {
96
- send: Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
97
- if (yield* Ref.get(stoppedRef)) return;
98
- yield* Queue.offer(eventQueue, { event });
99
- }),
106
+ send: selfSend,
107
+ cast: selfSend,
100
108
  spawn: (childId, childMachine) => Effect.gen(function* () {
101
109
  const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
102
110
  childrenMap.set(childId, child);
@@ -177,9 +185,10 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
177
185
  finalState: resolvedInitial,
178
186
  timestamp
179
187
  }));
180
- return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
188
+ return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, /* @__PURE__ */ new Set());
181
189
  }
182
- const loopFiber = yield* Effect.forkDetach(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system));
190
+ const pendingReplies = /* @__PURE__ */ new Set();
191
+ const loopFiber = yield* Effect.forkDetach(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies));
183
192
  return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
184
193
  const finalState = yield* SubscriptionRef.get(stateRef);
185
194
  yield* emitWithTimestamp(inspector, (timestamp) => ({
@@ -190,16 +199,17 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
190
199
  }));
191
200
  yield* Ref.set(stoppedRef, true);
192
201
  yield* Fiber.interrupt(loopFiber);
202
+ yield* settlePendingReplies(pendingReplies, id);
193
203
  yield* Scope.close(stateScopeRef.current, Exit.void);
194
204
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
195
205
  yield* Fiber.interrupt(snapshotFiber);
196
206
  yield* Fiber.interrupt(persistenceFiber);
197
- }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
207
+ }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, pendingReplies);
198
208
  });
199
209
  /**
200
210
  * Main event loop for persistent actor
201
211
  */
202
- const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system) {
212
+ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies) {
203
213
  const { machine, persistence } = persistentMachine;
204
214
  const typedMachine = machine;
205
215
  const hooks = inspector === void 0 ? void 0 : {
@@ -228,10 +238,28 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
228
238
  timestamp
229
239
  }))
230
240
  };
241
+ const postponed = [];
242
+ const pendingDrain = [];
243
+ const hasPostponeRules = machine.postponeRules.length > 0;
231
244
  while (true) {
232
- const { event, reply } = yield* Queue.take(eventQueue);
245
+ const queued = pendingDrain.length > 0 ? pendingDrain.shift() : yield* Queue.take(eventQueue);
246
+ const event = queued.event;
233
247
  const currentState = yield* SubscriptionRef.get(stateRef);
234
248
  const currentVersion = yield* Ref.get(versionRef);
249
+ if (hasPostponeRules && shouldPostpone(typedMachine, currentState._tag, event._tag)) {
250
+ postponed.push(queued);
251
+ if (queued._tag === "call") yield* Deferred.succeed(queued.reply, {
252
+ newState: currentState,
253
+ previousState: currentState,
254
+ transitioned: false,
255
+ lifecycleRan: false,
256
+ isFinal: false,
257
+ hasReply: false,
258
+ reply: void 0,
259
+ postponed: true
260
+ });
261
+ continue;
262
+ }
235
263
  yield* emitWithTimestamp(inspector, (timestamp) => ({
236
264
  type: "@machine.event",
237
265
  actorId: id,
@@ -240,7 +268,12 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
240
268
  timestamp
241
269
  }));
242
270
  const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, id, hooks);
243
- if (reply !== void 0) yield* Deferred.succeed(reply, result);
271
+ if (queued._tag === "call") yield* Deferred.succeed(queued.reply, result);
272
+ else if (queued._tag === "ask") if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
273
+ else yield* Deferred.fail(queued.reply, new NoReplyError({
274
+ actorId: id,
275
+ eventTag: event._tag
276
+ }));
244
277
  if (!result.transitioned) continue;
245
278
  const newVersion = currentVersion + 1;
246
279
  yield* Ref.set(versionRef, newVersion);
@@ -268,12 +301,15 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
268
301
  timestamp
269
302
  }));
270
303
  yield* Ref.set(stoppedRef, true);
304
+ postponed.length = 0;
305
+ yield* settlePendingReplies(pendingReplies, id);
271
306
  yield* Scope.close(stateScopeRef.current, Exit.void);
272
307
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
273
308
  yield* Fiber.interrupt(snapshotFiber);
274
309
  yield* Fiber.interrupt(persistenceFiber);
275
310
  return;
276
311
  }
312
+ if (result.lifecycleRan && postponed.length > 0) pendingDrain.push(...postponed.splice(0));
277
313
  }
278
314
  });
279
315
  /**