small-state 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.ja.md +549 -0
- package/README.md +553 -2
- package/dist/index.cjs +1742 -0
- package/dist/index.d.cts +389 -0
- package/dist/index.d.mts +389 -0
- package/dist/index.mjs +1738 -0
- package/package.json +45 -4
package/README.md
CHANGED
|
@@ -1,3 +1,554 @@
|
|
|
1
|
-
#
|
|
1
|
+
# small-state
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This README.md was automatically translated from the Japanese version and may contain inaccuracies. For accurate information, please refer to [the Japanese version](https://github.com/qyvvy/small-state/blob/dev/README.ja.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
A state machine library inspired by [XState](https://stately.ai/docs/xstate).
|
|
8
|
+
|
|
9
|
+
## Features & Advantages
|
|
10
|
+
|
|
11
|
+
- TypeScript support
|
|
12
|
+
- Low verbosity / Minimal boilerplate
|
|
13
|
+
- Clean syntax that is hard to get lost in
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## What is an FSM (Finite State Machine)?
|
|
17
|
+
|
|
18
|
+
A Finite State Machine is a computational model defined by a finite number of states and transitions between those states.
|
|
19
|
+
|
|
20
|
+
When the machine is in a specific state and an event is sent to it, it executes the specified logic for that event, updates its internal context/state, and optionally transitions to another state.<br>
|
|
21
|
+
When dealing with complex states and conditional branching, state machines demonstrate immense value by reducing programming bugs and improving code readability.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
First, define a state machine using `makeMachine`.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { makeMachine } from 'small-state'
|
|
30
|
+
|
|
31
|
+
const loginMachine = makeMachine(
|
|
32
|
+
{
|
|
33
|
+
name: 'login',
|
|
34
|
+
types: {
|
|
35
|
+
context: {} as { failedAttempts: number },
|
|
36
|
+
events: {} as { type: 'LOGIN_FAILED' } | { type: 'LOGIN_SUCCESS' } | { type: 'UNLOCK' },
|
|
37
|
+
},
|
|
38
|
+
initContext: () => ({ failedAttempts: 0 }),
|
|
39
|
+
},
|
|
40
|
+
({ on }) => ({
|
|
41
|
+
idle: [
|
|
42
|
+
on('LOGIN_FAILED', [
|
|
43
|
+
{
|
|
44
|
+
to: 'locked',
|
|
45
|
+
guard: ({ context }) => context.failedAttempts >= 1,
|
|
46
|
+
actions: ({ update, context }) => update({ failedAttempts: context.failedAttempts + 1 }),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
actions: ({ update, context }) => update({ failedAttempts: context.failedAttempts + 1 }),
|
|
50
|
+
},
|
|
51
|
+
]),
|
|
52
|
+
on('LOGIN_SUCCESS', { to: 'loggedIn' }),
|
|
53
|
+
],
|
|
54
|
+
locked: [
|
|
55
|
+
on('UNLOCK', { to: 'idle', actions: ({ update }) => update({ failedAttempts: 0 }) })
|
|
56
|
+
],
|
|
57
|
+
loggedIn: null,
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This state machine has three states: `idle`, `locked`, and `loggedIn`.<br>
|
|
63
|
+
When unspecified, the initial state defaults to the first defined state (in this case, `idle`).
|
|
64
|
+
|
|
65
|
+
To execute this machine, pass it to `createRuntime`.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createRuntime, type Runtime } from 'small-state'
|
|
69
|
+
|
|
70
|
+
const logRuntime = (idx: number, { runtimeStatus, stateValue, context }: Runtime<any>) =>
|
|
71
|
+
console.log(idx, { runtimeStatus, stateValue, context })
|
|
72
|
+
|
|
73
|
+
const loginRt = createRuntime(loginMachine)
|
|
74
|
+
logRuntime(0, loginRt)
|
|
75
|
+
/*
|
|
76
|
+
0 {
|
|
77
|
+
runtimeStatus: "notStarted",
|
|
78
|
+
stateValue: undefined,
|
|
79
|
+
context: {
|
|
80
|
+
failedAttempts: 0,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
*/
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A newly created runtime remains paused with `runtimeStatus: "notStarted"` until `.start()` is called.<br>In this state, `stateValue` is `undefined`.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
loginRt.start()
|
|
90
|
+
showStatus(1, loginRt)
|
|
91
|
+
/*
|
|
92
|
+
1 {
|
|
93
|
+
runtimeStatus: "active",
|
|
94
|
+
stateValue: "/idle",
|
|
95
|
+
context: {
|
|
96
|
+
failedAttempts: 0,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
*/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Calling `.start()` changes `runtimeStatus` to `"active"`. `stateValue` points to the current state. Since it is currently in the `"idle"` state, the path is `"/idle"`.<br>
|
|
103
|
+
Note that state values are represented by full paths, so they start with a leading `"/"`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
loginRt.send('LOGIN_FAILED')
|
|
107
|
+
showStatus(2, loginRt)
|
|
108
|
+
/*
|
|
109
|
+
2 {
|
|
110
|
+
runtimeStatus: "active",
|
|
111
|
+
stateValue: "/idle",
|
|
112
|
+
context: {
|
|
113
|
+
failedAttempts: 1,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
*/
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
When sending a `LOGIN_FAILED` event, the machine evaluates the `guard` functions in the `LOGIN_FAILED` definition array sequentially. It executes the `actions` of the first matching element.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
to: 'locked',
|
|
125
|
+
guard: ({ context }) => context.failedAttempts >= 2,
|
|
126
|
+
actions: ({ update, context }) => update({ failedAttempts: context.failedAttempts + 1 }),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
actions: ({ update, context }) => update({ failedAttempts: context.failedAttempts + 1 }),
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Since `failedAttempts` in `context` is 0, it fails the guard condition of the first element and matches the second element instead. (An element without a `guard` matches unconditionally.)<br>
|
|
135
|
+
`context` is updated, incrementing `failedAttempts` by +1 to 1.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
loginRt.send('LOGIN_FAILED')
|
|
139
|
+
showStatus(3, loginRt)
|
|
140
|
+
loginRt.send('LOGIN_FAILED')
|
|
141
|
+
showStatus(4, loginRt)
|
|
142
|
+
/*
|
|
143
|
+
3 {
|
|
144
|
+
runtimeStatus: "active",
|
|
145
|
+
stateValue: "/idle",
|
|
146
|
+
context: {
|
|
147
|
+
failedAttempts: 2,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
4 {
|
|
151
|
+
runtimeStatus: "active",
|
|
152
|
+
stateValue: "/locked",
|
|
153
|
+
context: {
|
|
154
|
+
failedAttempts: 3,
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
*/
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Sending `LOGIN_FAILED` two more times increases `failedAttempts` to 2 or more, fulfilling the condition `guard: ({ context }) => context.failedAttempts >= 2`. The `to` property represents the transition destination. If `to` is specified, the machine transitions to that state after executing `actions`. In this case, it transitions to the `"locked"` state, making `stateValue` equal to `"/locked"`.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
loginRt.send('LOGIN_FAILED')
|
|
164
|
+
showStatus(5, loginRt)
|
|
165
|
+
/*
|
|
166
|
+
5 {
|
|
167
|
+
runtimeStatus: "active",
|
|
168
|
+
stateValue: "/locked",
|
|
169
|
+
context: {
|
|
170
|
+
failedAttempts: 3,
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
*/
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The `locked` state has no event handler defined for `LOGIN_FAILED`, so sending this event produces no effect.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
loginRt.send('UNLOCK')
|
|
180
|
+
showStatus(6, loginRt)
|
|
181
|
+
/*
|
|
182
|
+
6 {
|
|
183
|
+
runtimeStatus: "active",
|
|
184
|
+
stateValue: "/idle",
|
|
185
|
+
context: {
|
|
186
|
+
failedAttempts: 0,
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
*/
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Triggering the `UNLOCK` event resets `failedAttempts` back to 0 and transitions back to `"idle"`.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
loginRt.send('LOGIN_SUCCESS')
|
|
196
|
+
showStatus(7, loginRt)
|
|
197
|
+
/*
|
|
198
|
+
7 {
|
|
199
|
+
runtimeStatus: "done",
|
|
200
|
+
stateValue: "/loggedIn",
|
|
201
|
+
context: {
|
|
202
|
+
failedAttempts: 0,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
*/
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
When receiving `LOGIN_SUCCESS` while in `idle`, it transitions to `"loggedIn"`.<br>
|
|
209
|
+
Since `loggedIn` is defined as `null`, it acts as a final state. The machine stops execution, and `runtimeStatus` becomes `"done"`.
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
## API
|
|
214
|
+
|
|
215
|
+
### Types
|
|
216
|
+
|
|
217
|
+
- Machine Definition
|
|
218
|
+
- `type AnyMachine = Machine<any, any, any, any, any, any, any, any, any>`
|
|
219
|
+
- Any machine instance.
|
|
220
|
+
- `type MContext = Record<string, any>`
|
|
221
|
+
- Context object representing state variables held inside the machine. Properties starting with `_` are internal to the machine and hidden from external visibility.
|
|
222
|
+
- `type SendableEvent = { type: string; [key: string]: any }`
|
|
223
|
+
- Events sent to the state machine from external callers via `runtime.send` or `sendTo`.
|
|
224
|
+
- `type EmittableEvent = { type: string; [key: string]: any }`
|
|
225
|
+
- Events emitted externally from inside the state machine via `emit`.
|
|
226
|
+
- `type ServiceFn<TInput = any> = (input: TInput, emit: Emit<EmittableEvent>, listen: Listen<SendableEvent>, signal: AbortSignal) => unknown`
|
|
227
|
+
- Function signature for service functions passed to `MachineConfig.services` or `invoke`.
|
|
228
|
+
- Events sent via `emit` or received via `listen` inside a service function are scoped to types defined specifically for that service function, rather than the parent machine's event type.
|
|
229
|
+
- `input: any` - Value passed via the `input` option of `invoke`.
|
|
230
|
+
- `emit: (event: EmittableEvent) => void`
|
|
231
|
+
- Function to communicate from child to parent. Event objects passed to this function can be subscribed to by the parent using the `on` helper.
|
|
232
|
+
- `listen: (listener: (event: SendableEvent) => void) => void`
|
|
233
|
+
- Registers a listener to subscribe to events sent from the parent via `sendTo`.
|
|
234
|
+
- `signal: AbortSignal`
|
|
235
|
+
- Signal object used internally by the function to check if it has been stopped. When `signal.aborted` is `true`, the function is considered stopped (e.g., due to exiting the state).
|
|
236
|
+
- `type Emit<EM extends EmittableEvent> = (event: EM) => void`
|
|
237
|
+
- By providing `Emit` with generic type parameters to the second argument of a service function wrapped in `defServiceFn`, the parent machine can recognize the emitted event types at type level.
|
|
238
|
+
- `type ListenFn<E extends SendableEvent> = (listener: (event: E) => void) => void`
|
|
239
|
+
- By providing `ListenFn` with generic type parameters to the third argument of a service function wrapped in `defServiceFn`, the service can accept specific `SendableEvent` types sent from the parent via `sendTo`.
|
|
240
|
+
- Runtime
|
|
241
|
+
- `type RuntimeStatus = 'notStarted' | 'active' | 'stopped' | 'done' | 'error'`
|
|
242
|
+
- Execution status of the runtime.
|
|
243
|
+
- `'notStarted'` - `.start()` has not been called yet.
|
|
244
|
+
- `'active'` - `.start()` has been called and the machine is running.
|
|
245
|
+
- `'stopped'` - `.stop()` was called and execution is halted.
|
|
246
|
+
- `'done'` - The machine reached a final state and finished execution.
|
|
247
|
+
- `'error'` - Execution stopped due to an error.
|
|
248
|
+
|
|
249
|
+
### Reserved Event Types
|
|
250
|
+
|
|
251
|
+
Event types surrounded by colons (e.g., `':fooBar:'`) are reserved for internal system use.
|
|
252
|
+
Beyond event types, any names bracketed by colons might also be reserved by the system in future releases.
|
|
253
|
+
The following event names are currently reserved:
|
|
254
|
+
|
|
255
|
+
- Inside standard states
|
|
256
|
+
- `':always:'` - Fires whenever a snapshot update occurs, such as immediately after transitioning to a state or after updating the context.
|
|
257
|
+
- `':childDone:'` - Fires when all child substates or parallel states reach a final state.
|
|
258
|
+
- Inside `invoke`
|
|
259
|
+
- `{type: ':done:', output: unknown}` - Fires when the invoked service completes successfully.
|
|
260
|
+
- `{type: ':error:', error: unknown}` - Fires when the invoked service terminates with an error.
|
|
261
|
+
|
|
262
|
+
### Utility Functions
|
|
263
|
+
|
|
264
|
+
- `defServiceFn(serviceFn): ServiceFn` - Defines a service function.
|
|
265
|
+
- Passing plain functions directly to `invoke` or defining them outside a machine configuration might cause type inference to break. Wrapping them with `defServiceFn` preserves type inference.
|
|
266
|
+
- `waitFor<RT extends Runtime<any>>(runtime, cond, options?): Promise<SnapshotFrom<RT>>` - Returns a Promise that resolves when the specified `runtime` satisfies `cond`.
|
|
267
|
+
|
|
268
|
+
### Primary Functions
|
|
269
|
+
|
|
270
|
+
- `makeMachine(config, states): Machine` - Creates a machine definition.
|
|
271
|
+
- `makeRuntime(machine, option?): Runtime` - Creates an executable runtime instance for a machine.
|
|
272
|
+
|
|
273
|
+
### `makeMachine(config: MachineConfig, defineStates: (tools)=>Record<string, StateDefinition>): Machine`
|
|
274
|
+
|
|
275
|
+
#### makeMachine > `MachineConfig`
|
|
276
|
+
|
|
277
|
+
A dictionary object containing basic machine settings and configuration properties:
|
|
278
|
+
|
|
279
|
+
- `types?: { context?: C; events?: E; emittable?: EM; input?: I; output?: O }`
|
|
280
|
+
- Specifies types for each entity using the pattern `{}` as `...`.
|
|
281
|
+
- `context: Record<string, any>` - Values readable and writable inside and outside the machine. Properties starting with `_` are private internal values and are not exposed externally.
|
|
282
|
+
- `events: SendableEvent` - Events that can be sent to the machine from the outside.
|
|
283
|
+
- `emittable: EmittableEvent` - Events that can be emitted outward from the machine.
|
|
284
|
+
- `input: any` - Type of input data supplied when initializing the machine.
|
|
285
|
+
- `output: any` - Type of return value output when the machine finishes execution.
|
|
286
|
+
- Type definitions use `{}` as `...` instead of standard type parameters to allow partial type specification while retaining type inference.
|
|
287
|
+
|
|
288
|
+
- `name?: string`
|
|
289
|
+
- Name of the machine. Used for logging or generating runtime IDs.
|
|
290
|
+
- `initialState?: string`
|
|
291
|
+
- Name of the initial state when starting the machine.
|
|
292
|
+
- `initContext?: (input: I) => C`
|
|
293
|
+
- Function returning initial context data when the machine starts. Highly recommended when state context is used.
|
|
294
|
+
- `output?: (args: CommonArgs) => O`
|
|
295
|
+
- Function executed when the machine reaches a `final` state, returning the machine's final output value. Accessible via `snapshot.output` on the runtime.
|
|
296
|
+
- When the machine is invoked as a child service, this value is passed as the `output` property of the parent machine's `{type: ':done:', output: O}` event.
|
|
297
|
+
- The event argument passed to this function is also of type `{type: ':done:', output: unknown}`. The `output` parameter contains the value produced by the final state options, or `undefined` if none was emitted.
|
|
298
|
+
- `guards?: Record<string, (args: CommonArgs, params: unknown)=>boolean>`
|
|
299
|
+
- Register guard functions used to evaluate transition eligibility. Registered guard functions can be referenced by string names.
|
|
300
|
+
- If a guard accepts parameters via its second argument `params`, pass an object formatted as `{[guardName: string]: ParamsValueOrFn}` (where the value is either a parameter value or a function returning it) instead of a simple string name when referencing the guard.
|
|
301
|
+
- `actions?: Record<string, (arg: {context, c, event, e, input, update, emit}, params: unknown)=>unknown>`
|
|
302
|
+
- Register side-effect action functions to execute during transitions. Registered actions can be referenced by string names.
|
|
303
|
+
- If an action accepts parameters via its second argument `params`, pass an object formatted as `{[actionName: string]: ParamsValueOrFn}` (where the value is either a parameter value or a function returning it) instead of a simple string name when calling the action.
|
|
304
|
+
- `services?: Record<string, AnyMachine | ServiceFn>`
|
|
305
|
+
- Dictionary mapping service names to machine definitions or service functions. Services can be started using `invoke`.
|
|
306
|
+
- Async functions are supported. Currently, running an async function as a service is the only way `small-state` manages async operations.
|
|
307
|
+
- A service function receives 4 arguments, and its return value is treated as `output`. See `ServiceFn` section.
|
|
308
|
+
- When defining a service function outside `makeMachine`, wrap it with `defServiceFn`.
|
|
309
|
+
|
|
310
|
+
#### makeMachine > `defineStates: (tools: {on, entryHook, exitHook, subStates, parallel, invoke})=> Record<string, StatesDefinition>`
|
|
311
|
+
|
|
312
|
+
A function accepting an object of helper utilities as parameters and returning state definitions.
|
|
313
|
+
The dictionary object returned key-maps state names to either definition arrays or `null`.
|
|
314
|
+
|
|
315
|
+
Setting a state's value to `null` designates it as a final state. Immediately upon entering a `null` state, if it is a substate of a parent state, that substate is considered complete. If it is a top-level state, the entire state machine completes execution.
|
|
316
|
+
|
|
317
|
+
Elements inside a state definition array can be configuration objects or return values from helper tools.
|
|
318
|
+
When providing a configuration object, the following properties can be set:
|
|
319
|
+
|
|
320
|
+
- `final?: boolean | (args: CommonArgs) => any`
|
|
321
|
+
- When set to `true`, the state is considered a final state. Useful when you need to specify other properties (such as `entryHook`) on a final state. If no extra hooks are needed, setting the state value to `null` directly instead of an array is equivalent.
|
|
322
|
+
- When passing a function, its return value is forwarded to the `output` property of the `':done:'` event.
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
The following helper functions are provided in `tools`:
|
|
326
|
+
|
|
327
|
+
- `on(eventType: string, exec: Exec|Exec[])`
|
|
328
|
+
- Defines action handling in response to received `SendableEvent`s when in this state. If an array of `Exec` objects is provided, only the action matching the first satisfied `guard` executes.
|
|
329
|
+
- `eventType: string`
|
|
330
|
+
- Defines actions triggered when this event is sent. Built-in special event types include `':always:'` and `':childDone:'`.
|
|
331
|
+
- `':always:'` - Fires continuously whenever an execution loop completes (e.g., right after entering a state or updating context). Note: Ensure a `guard` or a `to` transition is specified to prevent infinite execution loops.
|
|
332
|
+
- `':childDone:'` - Fires when child `subState`s or `parallel` states under this state reach a final state. Formatted as an event of type `{type: ':childDone:', output: unknown}`, where `output` contains values emitted by child final states.
|
|
333
|
+
- `Exec` - Configuration object containing the following properties:
|
|
334
|
+
- `guard?: OneOrMany<string | (args: CommonArgs)=>boolean>`
|
|
335
|
+
- Accepts a boolean function or guard name(s) registered in `MachineConfig.guards`. When providing an array, all guard conditions must evaluate to `true`.
|
|
336
|
+
- When `eventType` fires, `guard` conditions are evaluated in sequence. The first matching `Exec` executes.
|
|
337
|
+
- `actions?: OneOrMany<string | ActionFn>`
|
|
338
|
+
- Accepts action function(s) or action name(s) registered in `MachineConfig.actions`. Arrays of actions execute sequentially from top to bottom.
|
|
339
|
+
- Actions handle side-effects such as context updates or messaging external services.
|
|
340
|
+
- `to?: string` - Target state name for state transition. Transitions to the specified state after executing actions.
|
|
341
|
+
- Providing a simple state name resolves state targets relative to child states first, followed by sibling states.
|
|
342
|
+
- Paths starting with `./` (e.g., `'./foo'`) search strictly within child states.
|
|
343
|
+
- Paths starting with `../` (e.g., `'../foo'`) search strictly within sibling states.
|
|
344
|
+
- Absolute paths starting with `/` (e.g., `'/path/to/state'`) resolve from the root machine level. Absolute paths are required to target states outside child or sibling trees.
|
|
345
|
+
|
|
346
|
+
- `entryHook(...actions: ActionFn[])`
|
|
347
|
+
- Actions executed immediately upon entering the state. When multiple actions are passed via variadic arguments, they execute in parameter order. Runs before events configured via `on(':always:', ...)`.
|
|
348
|
+
- `exitHook(...actions: ActionFn[])`
|
|
349
|
+
- Actions executed right before leaving the state. Executes sequentially in parameter order when multiple actions are supplied.
|
|
350
|
+
- `subStates(stateDefinitions: Record<string, StateDefinition>, initialState?: string)`
|
|
351
|
+
- Can only be specified once per state and cannot be combined with `parallel` (doing so raises a definition error).
|
|
352
|
+
- Defines nested child substates. `stateDefinitions` follows the same structure as the return value of `makeMachine`'s state factory (dictionary mapping state names to arrays or `null`).
|
|
353
|
+
- When a substate reaches a final state, it triggers the parent state's `':childDone:'` event if one is defined.
|
|
354
|
+
- Entering a state with defined substates automatically activates its `initialState`. If `initialState` is omitted, it defaults to the first defined substate.
|
|
355
|
+
- `parallel(parallelDefinition: { [Region: string]: [Record<string, StateDefinition>] | [MetaItem[], Record<string, StateDefinition>] })`
|
|
356
|
+
- Can only be specified once per state and cannot be combined with `subStates` (doing so raises a definition error).
|
|
357
|
+
- Defines concurrent state regions that execute simultaneously. States defined inside contain regions (child states) and nested sub-tree states. All region child states remain active concurrently as long as the parent state is active.
|
|
358
|
+
- Accepts a dictionary mapping Region names to 1- or 2-element tuple arrays:
|
|
359
|
+
- A single-element tuple containing a state definition dictionary defines state definitions for that region (same format as `makeMachine` state definitions or `subStates`).
|
|
360
|
+
- A two-element tuple can supply an array of `MetaItem`s as its first element (allowing regional attributes such as `on`, `entryHook`, `exitHook`), followed by state definitions as its second element.
|
|
361
|
+
- When all regions reach final states, the parent state's `':childDone:'` event fires (if defined).
|
|
362
|
+
- If final states inside regions return values, the `output` object of `':childDone:'` will contain Region names as keys and returned final values as dictionary entries. Unreturned region final values are omitted.
|
|
363
|
+
- NOTE: The `parallel` API structure may change in future releases.
|
|
364
|
+
- `invoke(service, config?, ...events)`
|
|
365
|
+
- Spawns services (functions or machines) provided by `service` upon entering the state.
|
|
366
|
+
- Multiple `invoke` declarations can be attached to a single state.
|
|
367
|
+
- Built-in event hooks `':done:'` and `':error:'` are provided. Subscribing via `on(':done:', ...)` or `on(':error:', ...)` inside `...events` allows handling success or error completion.
|
|
368
|
+
- Invoked services are automatically canceled/stopped upon exiting the state. In this case, `':done:'` events do not fire.
|
|
369
|
+
- If multiple invoked services attempt to trigger parent transitions simultaneously, race conditions resolve by handling only the earliest processed event.
|
|
370
|
+
- Argument `service`
|
|
371
|
+
- Accepts direct machine references, inline functions, or service name strings registered in `MachineConfig.services`.
|
|
372
|
+
- Defining parameter-accepting functions as raw inline literals can break TypeScript inference; wrap inline service declarations with `defServiceFn` while explicitly typing parameters.
|
|
373
|
+
- Service functions receive 4 arguments (`input: InvocationInput, emit: Emit, listen: Listen, signal: AbortSignal`) and use their return values as output for `':done:'` events.
|
|
374
|
+
- Exception: If a service function returns a cleanup function, the `':done:'` event does not fire. The returned function is treated as a cleanup hook and executes when exiting the state.
|
|
375
|
+
- Events sent via `emit` or received via `listen` use types explicitly defined by the service function, not parent machine types.
|
|
376
|
+
- 4 parameters passed to service functions:
|
|
377
|
+
- `input: InvocationInput` - Value received from configuration `input`.
|
|
378
|
+
- `emit: (event: EmittableEvent) => void`
|
|
379
|
+
- Emit messages from child to parent. Emitted events can be handled using `...events` arguments in `invoke`.
|
|
380
|
+
- Allowed emitted event types are custom types defined by the service function itself.
|
|
381
|
+
- Wrapping functions with `defServiceFn` and annotating `emit` with `Emit<EmittableEvent>` allows parent machines to infer emitted event types.
|
|
382
|
+
- `listen: (listener: (event: SendableEvent) => void) => void`
|
|
383
|
+
- Registers event listeners to process messages sent from parent machines via `sendTo`.
|
|
384
|
+
- `signal: AbortSignal`
|
|
385
|
+
- Abort signal used to check if execution was stopped. `signal.aborted === true` indicates the service has been terminated.
|
|
386
|
+
- Argument `config` - Optional configuration object with the following properties:
|
|
387
|
+
- `input?: (args: CommonArgs) => InvocationInput`
|
|
388
|
+
- Returns values passed as the `input` argument to `service`. Can return any data type.
|
|
389
|
+
- `subKey?: string|number`
|
|
390
|
+
- Mandatory when spawning multiple concurrent instances of the same service name registered in `services`. Omitting `subKey` during duplicate instances throws an error.
|
|
391
|
+
- Named services track execution by key; `subKey` prevents collisions between identical service names.
|
|
392
|
+
- When set, services register under `${serviceName}:${subKey}` in `snapshot._children`.
|
|
393
|
+
- When using `sendTo`, target services must include `subKey`.
|
|
394
|
+
- Argument `...events`
|
|
395
|
+
- Use `on` functions to intercept internal events generated within invoked services. Supports custom events emitted via `emit` alongside standard built-in events.
|
|
396
|
+
- Standard built-in event definitions:
|
|
397
|
+
- `{type: ':done:', output: unknown}`
|
|
398
|
+
- Emitted upon successful completion of the service.
|
|
399
|
+
- `output` contains the awaited function return value or the machine's `output` function result.
|
|
400
|
+
- `{type: ':error:', error: unknown}`
|
|
401
|
+
- Emitted when service execution fails with an error.
|
|
402
|
+
|
|
403
|
+
#### makeMachine > Parameters Passed to Callbacks
|
|
404
|
+
|
|
405
|
+
Most callback functions receive an object matching `CommonArgs` as their first parameter:
|
|
406
|
+
|
|
407
|
+
- `CommonArgs`
|
|
408
|
+
- `context`
|
|
409
|
+
- Current context value of the machine (`null` if `initContext` was omitted).
|
|
410
|
+
- Immutable object; direct mutations are disallowed. Use `ActionFn`'s `update` function to modify context fields.
|
|
411
|
+
- `c` - Shorthand property for `context`. Provided for convenience.
|
|
412
|
+
- `event` - The event object that triggered the current action execution.
|
|
413
|
+
- `e` - Shorthand property for `event`. Provided for convenience.
|
|
414
|
+
- `input` - Static input data passed during machine runtime creation. Should remain immutable during lifecycle execution.
|
|
415
|
+
- While functions with side effects can technically be passed via `input`, side effects should ideally be dispatched via `emit`.
|
|
416
|
+
|
|
417
|
+
Callback parameters of this form are supplied to:
|
|
418
|
+
- `MachineConfig` functions (`output`, `guard`, `actions`, `guards` params, `invoke` input, `final`).
|
|
419
|
+
|
|
420
|
+
First-argument objects passed to `ActionFn` handlers in `entryHook`, `exitHook`, and `on` contain additional properties:
|
|
421
|
+
|
|
422
|
+
- `update: (updates: Partial<Context>) => void`
|
|
423
|
+
- Accepts partial context update objects to modify machine context state.
|
|
424
|
+
- Calling `update` does not immediately update `context` within the current execution scope (context context references within the same block retain older values due to immutability).
|
|
425
|
+
- Updated context values become available in subsequent action handlers.
|
|
426
|
+
- `emit: (event: EmittableEvent) => void`
|
|
427
|
+
- Emits event objects externally. Distinct from internal `SendableEvent`s received from external senders and handled by state `on` handlers.
|
|
428
|
+
- Define types on `MachineConfig.types.emittable`.
|
|
429
|
+
- Subscribed externally on runtimes via `runtime.onEmitted`.
|
|
430
|
+
- If emitted from invoked services, parent machines can subscribe using `on` handlers inside `invoke`.
|
|
431
|
+
- `sendTo: (service: string, subKey?: string|number, event: EventOfService) => void`
|
|
432
|
+
- Dispatches events to active invoked machine or service function targets.
|
|
433
|
+
- Target events must match acceptable send events for machines or registered `listen` handlers for service functions.
|
|
434
|
+
- Can only target services registered in `MachineConfig.services` (cannot target inline literal functions/machines).
|
|
435
|
+
- If `subKey` was specified on `invoke`, it must also be provided to `sendTo`.
|
|
436
|
+
- `check`
|
|
437
|
+
- `check: (type: string, params?: ParamsValue | ((args: CommonArgs)=>ParamValue)) => boolean`
|
|
438
|
+
- `check: (type: {[guardName: string]: ParamsValueOrFn}) => boolean`
|
|
439
|
+
- Evaluates current guard status dynamically when building conditional action chains alongside `enqueue` or `update`.
|
|
440
|
+
- Returns the current boolean outcome for the guard matching `type`.
|
|
441
|
+
- Guards accepting parameters take values via second-argument `params` or object parameter entries.
|
|
442
|
+
- `enqueue`
|
|
443
|
+
- `enqueue: (type: string, params?: ParamsValue | ((args: CommonArgs)=>ParamValue)) => void`
|
|
444
|
+
- `enqueue: (type: {[guardName: string]: ParamsValueOrFn}) => void`
|
|
445
|
+
- `enqueue: (fn: (args: {context, c, event, e, input, update, emit})=>void) => void`
|
|
446
|
+
- Used with `check` to build dynamic execution queues. Enqueued actions do not run immediately; they are scheduled to execute sequentially after exiting the active action function scope.
|
|
447
|
+
- Accepts registered action names, parameterized action configuration objects, or inline action functions.
|
|
448
|
+
- If `update` was called outside or in a preceding action, reference context inside enqueued callback parameters to read updated context states.
|
|
449
|
+
- Calling `enqueue` inside an enqueued action is currently forbidden and throws an error.
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
### `makeRuntime<M extends AnyMachine>(machine: M, options?: RuntimeOptions): Runtime`
|
|
454
|
+
|
|
455
|
+
Creates an executable runtime instance for a machine. Creating a runtime does not start the machine automatically; `.start()` must be called to begin execution.
|
|
456
|
+
|
|
457
|
+
- `RuntimeOptions` - Configuration object with properties:
|
|
458
|
+
- `id?: string` - Runtime ID string used for identification or logging. Defaults to combining machine name and numeric sequence counters.
|
|
459
|
+
- `input?: () => I`
|
|
460
|
+
- Function returning input data passed to machine `input`. Accessible across initializers (`initContext`) and execution callbacks.
|
|
461
|
+
- Intended for static reference data; mutating input state internally is discouraged.
|
|
462
|
+
|
|
463
|
+
Runtime instances expose the following members:
|
|
464
|
+
|
|
465
|
+
- `id: string` - Runtime ID.
|
|
466
|
+
- `machine: M` - Reference to the underlying machine definition.
|
|
467
|
+
- `getSnapshot(): Snapshot` - Returns a snapshot snapshot representing the runtime's current state.
|
|
468
|
+
- Reflects state at the instant of invocation; snapshot state may age over time.
|
|
469
|
+
- Returns an empty object prior to calling `.start()`.
|
|
470
|
+
- Snapshot objects contain:
|
|
471
|
+
- `runtimeId: string` - Unique identifier string for runtime validation.
|
|
472
|
+
- `runtimeStatus: RuntimeStatus` - Current status. See `RuntimeStatus` type.
|
|
473
|
+
- `states: string[]` - Full path strings of active state values. Returns arrays with length > 1 if parallel states are active; otherwise returns a single-element array.
|
|
474
|
+
- State paths prepend leading `/` separators and delimit nested state hierarchies using `/`.
|
|
475
|
+
- `stateValue: string | string[] | undefined` - Convenience helper for non-parallel machines. Returns the first string state path when `states.length === 1`, or `states` array otherwise.
|
|
476
|
+
- `context: PrivateKeyRemovedContext` - Public context data (properties starting with `_` are excluded).
|
|
477
|
+
- `c` - Convenience shorthand property for `context`.
|
|
478
|
+
- `output: O | undefined` - Contains final execution output values when `runtimeStatus === 'done'`; `undefined` otherwise.
|
|
479
|
+
- `error: unknown` - Contains error details if execution halted due to errors; `undefined` otherwise.
|
|
480
|
+
- `_children` - Debugging member. Keyed by service name mapping to running service instances. When `subKey` is present, keys map as `${serviceName}:${subKey}`.
|
|
481
|
+
- Direct shortcut getters exist on `runtime` instances for convenience:
|
|
482
|
+
- `states`, `stateValue`, `context`, `runtimeStatus`, `output`
|
|
483
|
+
- `subscribe(onSnapshot: ((current: Snapshot, prev: Snapshot)=>void)|undefined, onError?:(err:unknown)=>void): UnsubscribeFn`
|
|
484
|
+
- Registers snapshot listener callbacks fired whenever snapshot states update. The primary callback listener ignores runtime errors; error handling is delegated to `onError`.
|
|
485
|
+
- When integrating with state stores requiring unified update/error observation (such as React's `useSyncExternalStore`), pass handlers to both parameters:
|
|
486
|
+
```ts
|
|
487
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
488
|
+
const unsubscribe = rt.subscribe(onStoreChange, onStoreChange)
|
|
489
|
+
return unsubscribe
|
|
490
|
+
}, [rt]);
|
|
491
|
+
const snapshot = useSyncExternalStore(subscribe, () => rt.getSnapshot())
|
|
492
|
+
```
|
|
493
|
+
- If no error listener handler is registered, runtime errors are thrown as uncaught exceptions.
|
|
494
|
+
- Returns an `UnsubscribeFn` callback to unregister the listener.
|
|
495
|
+
- `onSnapshot` parameters:
|
|
496
|
+
- `current` - Latest updated snapshot instance.
|
|
497
|
+
- `prev` - Previous snapshot instance prior to update.
|
|
498
|
+
- NOTE: The `prev` argument may be deprecated in future releases.
|
|
499
|
+
- `UnsubscribeFn: () => void` - Cancels subscription.
|
|
500
|
+
- `start(): this` - Begins machine execution. Sets `runtimeStatus` to `'active'`, enters initial state structures, and enables `send` handling.
|
|
501
|
+
- `send(event: SendableEvent | string): this`
|
|
502
|
+
- Dispatches events into the runtime. For simple events without payload fields beyond `type`, string representations can be sent directly instead of event objects. Results are observable via `getSnapshot()` or `subscribe`.
|
|
503
|
+
- `stop(): this` - Halts active runtime instances. Stopped runtimes cannot be restarted. Sets `runtimeStatus` to `'stopped'`.
|
|
504
|
+
- `onEmitted(emittedEventType: string, handler: (emittedEvent: EmittableEvent) => void): UnsubscribeFn`
|
|
505
|
+
- Registers listeners for internal `EmittableEvent` events dispatched outward from within machines. Ideal for triggering application side-effects.
|
|
506
|
+
- Passing `'*'` to `emittedEventType` subscribes to all emitted event types.
|
|
507
|
+
- `UnsubscribeFn: () => void` - Cancels subscription.
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
## Differences from XState
|
|
512
|
+
|
|
513
|
+
| Feature / Behavior | XState | small-state |
|
|
514
|
+
| - | - | - |
|
|
515
|
+
| Path Delimiter | `.` | `/` |
|
|
516
|
+
| Context Updates | `assign` | `update` argument inside action handlers |
|
|
517
|
+
| Child to Parent Messaging | `sendParent` | `emit` |
|
|
518
|
+
| Parent to Child Messaging | actor id | service name (and optional `subKey`) |
|
|
519
|
+
| Peer Actor Messaging | `sendTo` | Not supported (use parent routing instead) |
|
|
520
|
+
| Child Emit to External | `emit` | Not supported (route via parent instead) |
|
|
521
|
+
| Eventless Transitions | `always` | `on(':always:', ...)` |
|
|
522
|
+
| Substate Done Hook | `onDone` | `on(':childDone:', ...)` |
|
|
523
|
+
| Invoke Done Hook | `onDone` | `on(':done:', ...)` |
|
|
524
|
+
| Invoke Error Hook | `onError` | `on(':error:', ...)` |
|
|
525
|
+
|
|
526
|
+
- If `initial` state is omitted, the first state in definition order becomes initial active state.
|
|
527
|
+
- When a state hierarchy is active, a leaf state is guaranteed to be active.
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
## Missing Features / Unimplemented Functionality
|
|
531
|
+
|
|
532
|
+
The following features are not supported:
|
|
533
|
+
|
|
534
|
+
- `delays`, `after`: Automatic state transitions after specified timeouts
|
|
535
|
+
- `syncSnapshot: boolean` child option: Automatically updating parent snapshot whenever child snapshot changes
|
|
536
|
+
- `spawn`, `spawnChild`
|
|
537
|
+
- `stopChild`
|
|
538
|
+
- `raise`
|
|
539
|
+
- `tags`
|
|
540
|
+
- `.matches()`
|
|
541
|
+
- State IDs (Workaround: Use absolute path state target string transitions)
|
|
542
|
+
- `reenter` (self-transitions)
|
|
543
|
+
- Machine root-level restrictions (the following cannot be declared directly at machine root):
|
|
544
|
+
- `invoke`, `entryHook`, `exitHook`, `parallel`
|
|
545
|
+
- Stateless machines
|
|
546
|
+
|
|
547
|
+
## TODO
|
|
548
|
+
|
|
549
|
+
- Replace ArkType parser with custom error parser to provide clear debugging feedback
|
|
550
|
+
- Refactor `parallel` interface structure
|
|
551
|
+
- Improve clarity and usability
|
|
552
|
+
- Support nested continuous parallel structures
|
|
553
|
+
- Improve error handling to catch runtime errors reliably and set status to `"error"`
|
|
554
|
+
- Include machine name strings in Runtime error messages
|