@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/dist/dev/main.js +5 -0
- package/dist/dev/main.js.map +11 -0
- package/dist/facade.d.ts.map +1 -1
- package/dist/fsm-service.d.ts +50 -17
- package/dist/fsm-service.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +5 -5
- package/dist/type.d.ts +63 -43
- package/dist/type.d.ts.map +1 -1
- package/package.json +14 -10
- package/src/facade.ts +2 -22
- package/src/fsm-service.ts +229 -124
- package/src/type.ts +75 -43
package/src/type.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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]:
|
|
27
|
+
/** An event can carry an optional, serializable payload. */
|
|
28
|
+
[key: string]: JsonValue;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Defines an assigner
|
|
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
|
-
* @
|
|
36
|
-
* @
|
|
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>> = (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
46
|
-
*
|
|
44
|
+
* Defines an effect — a **strictly synchronous**, fire-and-forget side-effect
|
|
45
|
+
* executed on state entry/exit.
|
|
47
46
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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>> = (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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>> = (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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>> = (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
102
|
-
readonly
|
|
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
|
-
/**
|
|
183
|
+
/** Synchronous side-effects executed upon entering this state. */
|
|
152
184
|
readonly entry?: SingleOrArray<Effect<TEvent, TContext>>;
|
|
153
|
-
/**
|
|
185
|
+
/** Synchronous side-effects executed upon exiting this state. */
|
|
154
186
|
readonly exit?: SingleOrArray<Effect<TEvent, TContext>>;
|
|
155
|
-
/**
|
|
156
|
-
readonly
|
|
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
|
}
|