@alwatr/fsm 9.32.0 → 9.33.1

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.
package/src/type.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type {Awaitable, SingleOrArray} from '@alwatr/type-helper';
1
+ import type {JsonValue, SingleOrArray} from '@alwatr/type-helper';
2
2
  import type {SignalConfig} from '@alwatr/signal';
3
3
 
4
4
  /**
@@ -24,66 +24,90 @@ export type MachineState<TState extends string, TContext extends Record<string,
24
24
  export interface MachineEvent<TEventType extends string = string> {
25
25
  /** The unique type of the event. */
26
26
  readonly type: TEventType;
27
- /** An event can carry an optional payload. */
28
- [key: string]: unknown;
27
+ /** An event can carry an optional, serializable payload. */
28
+ [key: string]: JsonValue;
29
29
  }
30
30
 
31
31
  /**
32
- * Defines an assigner (synchronous action) that updates the context during transitions.
33
- * It returns the complete new context object or void/undefined if no changes are made.
32
+ * Defines an assigner a **pure, synchronous context reducer** applied during transitions.
34
33
  *
35
- * @template TContext The type of the machine's context.
36
- * @template TEvent The type of the event that triggered this assigner.
37
- * @returns The complete next context object or void.
34
+ * @param event The event that triggered the transition. Readonly to prevent mutations.
35
+ * @param context The current context before the transition. Mutable for convenience, but treat it as immutable — return a new context object instead of mutating it.
36
+ * @returns The complete next context object, or void.
38
37
  */
39
- export type Assigner<TEvent extends MachineEvent, TContext extends Record<string, unknown>> = (params: {
40
- readonly event: Readonly<TEvent>;
41
- readonly context: TContext;
42
- }) => TContext | void;
38
+ export type Assigner<TEvent extends MachineEvent, TContext extends Record<string, unknown>> = (
39
+ event: Readonly<TEvent>,
40
+ context: TContext,
41
+ ) => TContext | void;
43
42
 
44
43
  /**
45
- * Defines an effect (fire-and-forget side-effect action) executed on state entry/exit.
46
- * It can interact with the outside world, but does not return new events to trigger transitions.
44
+ * Defines an effect — a **strictly synchronous**, fire-and-forget side-effect
45
+ * executed on state entry/exit.
47
46
  *
48
- * @template TContext The type of the machine's context.
49
- * @template TEvent The type of the event that triggered this effect.
50
- * @returns void or a Promise<void>.
47
+ * ## Why synchronous-only? (Architectural decision)
48
+ *
49
+ * The FSM core is a deterministic, Run-to-Completion (RTC) step function:
50
+ * `(state, event) -> (state', effects)`. Allowing async effects inside the core
51
+ * creates ordering ambiguity — the continuation of an async effect may run against
52
+ * a state/context that no longer exists. This mirrors the design of SCXML actions,
53
+ * XState actions, and Erlang's gen_statem.
54
+ *
55
+ * **Any asynchronous work belongs in an {@link Actor}**, which has a proper
56
+ * lifecycle (spawn on entry, cleanup on exit) and communicates results back to
57
+ * the machine via `dispatch`, keeping the core deterministic.
58
+ *
59
+ * @param event The event that triggered the effect. Readonly to prevent mutations.
60
+ * @param context The current context of the machine. Readonly to prevent mutations.
51
61
  */
52
- export type Effect<TEvent extends MachineEvent, TContext extends Record<string, unknown>> = (params: {
53
- readonly event: Readonly<TEvent>;
54
- readonly context: Readonly<TContext>;
55
- }) => Awaitable<void>;
62
+ export type Effect<TEvent extends MachineEvent, TContext extends Record<string, unknown>> = (
63
+ event: Readonly<TEvent>,
64
+ context: Readonly<TContext>,
65
+ ) => void;
56
66
 
57
67
  /**
58
68
  * Defines a conditional guard function for a transition.
59
69
  * The transition is only taken if this function returns true.
60
70
  *
61
- * @template TContext The type of the machine's context.
62
- * @template TEvent The type of the event.
71
+ * Guards MUST be pure and synchronous. A guard that throws is treated as `false`
72
+ * (logged, transition branch skipped) so a single faulty predicate cannot brick
73
+ * the machine.
74
+ *
75
+ * @param event The event that triggered the transition. Readonly to prevent mutations.
76
+ * @param context The current context of the machine. Readonly to prevent mutations.
63
77
  * @returns `true` if the transition should be taken, `false` otherwise.
64
78
  */
65
- export type Guard<TEvent extends MachineEvent, TContext extends Record<string, unknown>> = (params: {
66
- readonly event: Readonly<TEvent>;
67
- readonly context: Readonly<TContext>;
68
- }) => boolean;
79
+ export type Guard<TEvent extends MachineEvent, TContext extends Record<string, unknown>> = (
80
+ event: Readonly<TEvent>,
81
+ context: Readonly<TContext>,
82
+ ) => boolean;
69
83
 
70
84
  /**
71
- * Defines an actor (asynchronous lifecycle process) invoked on state entry.
72
- * It starts an operation and can send events back to the parent FSM via `dispatch`.
73
- * It can return a cleanup function to be called when exiting the state or destroying the machine.
85
+ * Defines an actor — an **asynchronous lifecycle process** spawned on state entry.
86
+ *
87
+ * This is the ONLY sanctioned home for async work in the machine (network requests,
88
+ * polling intervals, websocket listeners, timers). An actor:
89
+ *
90
+ * 1. Is spawned when the machine enters the state.
91
+ * 2. Receives `dispatch` to asynchronously send events back to the parent FSM.
92
+ * 3. May return a synchronous cleanup function, executed automatically (in LIFO
93
+ * order) when the machine exits the state or is destroyed.
74
94
  *
75
95
  * @template TEvent The union type of all events in the machine.
76
96
  * @template TContext The type of the machine's context.
77
97
  */
78
- export type Actor<TEvent extends MachineEvent, TContext extends Record<string, unknown>> = (params: {
79
- readonly event: Readonly<TEvent>;
80
- readonly context: Readonly<TContext>;
81
- readonly dispatch: (event: TEvent) => void;
82
- }) => (() => void) | void;
98
+ export type Actor<TEvent extends MachineEvent, TContext extends Record<string, unknown>> = (
99
+ context: Readonly<TContext>,
100
+ dispatch: (event: TEvent) => void,
101
+ ) => VoidFunction | void;
83
102
 
84
103
  /**
85
104
  * Defines a transition for a given state and event. It specifies the target state,
86
- * actions, and an optional guard.
105
+ * assigners, and an optional guard.
106
+ *
107
+ * - With `target`: an **external** transition — exit effects run, actors are cleaned
108
+ * up, then entry effects run and actors are re-spawned (even on self-transitions).
109
+ * - Without `target`: an **internal** transition — only assigners run; entry/exit
110
+ * effects and actors are untouched.
87
111
  *
88
112
  * @template TState The type of the state.
89
113
  * @template TEvent The type of the event.
@@ -98,8 +122,8 @@ export interface Transition<
98
122
  readonly target?: TState;
99
123
  /** A guard function that must return true for the transition to occur. */
100
124
  readonly guard?: Guard<TEvent, TContext>;
101
- /** An array of assigners to execute. These update context synchronously. */
102
- readonly assigners?: SingleOrArray<Assigner<TEvent, TContext>>;
125
+ /** A single assigner or an ordered chain of assigners. Applied atomically. */
126
+ readonly assigner?: SingleOrArray<Assigner<TEvent, TContext>>;
103
127
  }
104
128
 
105
129
  /**
@@ -123,6 +147,14 @@ export interface FsmPersistenceConfig {
123
147
  * The declarative configuration object for creating a state machine.
124
148
  * This object defines the entire behavior of the machine.
125
149
  *
150
+ * ## Persistence requirement
151
+ *
152
+ * When `persistent` is enabled, EVERY state — including terminal states with no
153
+ * transitions — MUST be declared in `states` (e.g. `success: {}`). The engine uses
154
+ * the presence of a state's config entry to validate rehydrated state names from
155
+ * storage; an undeclared state is treated as removed/renamed and the machine is
156
+ * reset to `initial`.
157
+ *
126
158
  * @template TState The union type of all possible states.
127
159
  * @template TEvent The union type of all possible events.
128
160
  * @template TContext The type of the machine's context.
@@ -135,7 +167,7 @@ export interface StateMachineConfig<
135
167
  /** The initial finite state value. */
136
168
  readonly initial: TState;
137
169
 
138
- /** The initial context (extended state) of the machine. */
170
+ /** The initial context (extended state) of the machine. Must be serializable. */
139
171
  readonly context: TContext;
140
172
 
141
173
  /** If provided, the FSM's state will be persisted in localStorage. */
@@ -148,12 +180,12 @@ export interface StateMachineConfig<
148
180
  readonly on?: {
149
181
  readonly [E in TEvent['type']]?: SingleOrArray<Transition<TState, Extract<TEvent, {type: E}>, TContext>>;
150
182
  };
151
- /** An array of side-effect effects to execute upon entering this state. */
183
+ /** Synchronous side-effects executed upon entering this state. */
152
184
  readonly entry?: SingleOrArray<Effect<TEvent, TContext>>;
153
- /** An array of side-effect effects to execute upon exiting this state. */
185
+ /** Synchronous side-effects executed upon exiting this state. */
154
186
  readonly exit?: SingleOrArray<Effect<TEvent, TContext>>;
155
- /** An array of actors to spawn upon entering this state, cleaned up when leaving. */
156
- readonly actors?: SingleOrArray<Actor<TEvent, TContext>>;
187
+ /** Async lifecycle actors spawned upon entering this state, cleaned up (LIFO) when leaving. */
188
+ readonly actor?: SingleOrArray<Actor<TEvent, TContext>>;
157
189
  };
158
190
  };
159
191
  }