@uuxxx/fsm 1.2.3 → 1.3.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/README.md +238 -193
- package/dist/index.d.ts +80 -6
- package/dist/index.js +2 -2
- package/package.json +20 -40
- package/dist/Plugin-DGULTFg-.d.ts +0 -38
- package/dist/history-plugin.d.ts +0 -10
- package/dist/history-plugin.js +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/@uuxxx%2Ffsm)
|
|
4
4
|
|
|
5
|
-
A lightweight, type-safe finite state machine library for
|
|
5
|
+
A lightweight, type-safe finite state machine library for TypeScript with plugin support, lifecycle hooks, and full type inference.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Full type inference** — transition methods, states, and plugin APIs are auto-generated from config
|
|
10
|
+
- **Multiple transition types** — static, dynamic, async, wildcard (`*`), and multi-source
|
|
11
|
+
- **Lifecycle hooks** — `onBeforeTransition` (with veto) and `onAfterTransition`
|
|
12
|
+
- **Plugin system** — extend your FSM with custom APIs
|
|
13
|
+
- **Custom error handling** — provide an `onError` callback or let errors throw
|
|
14
|
+
- **Zero dependencies** aside from `@uuxxx/utils`
|
|
6
15
|
|
|
7
16
|
## Installation
|
|
8
17
|
|
|
@@ -21,70 +30,66 @@ import { makeFsm } from '@uuxxx/fsm';
|
|
|
21
30
|
|
|
22
31
|
type State = 'idle' | 'loading' | 'success' | 'error';
|
|
23
32
|
|
|
24
|
-
const STATES: State[] = ['idle', 'loading', 'success', 'error']
|
|
25
|
-
|
|
26
33
|
const fsm = makeFsm({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
init: 'idle',
|
|
35
|
+
states: ['idle', 'loading', 'success', 'error'] as State[],
|
|
36
|
+
transitions: {
|
|
37
|
+
start: {
|
|
38
|
+
from: 'idle',
|
|
39
|
+
to: 'loading',
|
|
40
|
+
},
|
|
41
|
+
succeed: {
|
|
42
|
+
from: 'loading',
|
|
43
|
+
to: 'success',
|
|
44
|
+
},
|
|
45
|
+
fail: {
|
|
46
|
+
from: 'loading',
|
|
47
|
+
to: 'error',
|
|
48
|
+
},
|
|
49
|
+
reset: {
|
|
50
|
+
from: ['success', 'error'],
|
|
51
|
+
to: 'idle',
|
|
52
|
+
},
|
|
53
|
+
goto: {
|
|
54
|
+
from: '*',
|
|
55
|
+
to: (state: State) => state,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
51
58
|
});
|
|
52
59
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
fsm.
|
|
58
|
-
console.log(fsm.state()); // 'loading'
|
|
59
|
-
|
|
60
|
-
fsm.succeed();
|
|
61
|
-
console.log(fsm.state()); // 'success'
|
|
62
|
-
|
|
63
|
-
fsm.reset()
|
|
64
|
-
console.log(fsm.state()) // 'idle'
|
|
65
|
-
|
|
66
|
-
fsm.goto('error')
|
|
67
|
-
console.log(fsm.state()) // 'error'
|
|
60
|
+
fsm.state(); // 'idle'
|
|
61
|
+
fsm.start(); // 'loading'
|
|
62
|
+
fsm.succeed(); // 'success'
|
|
63
|
+
fsm.reset(); // 'idle'
|
|
64
|
+
fsm.goto('error'); // 'error'
|
|
68
65
|
```
|
|
69
66
|
|
|
67
|
+
Each transition key becomes a method on the FSM instance with the correct type signature inferred from config.
|
|
68
|
+
|
|
70
69
|
## API Reference
|
|
71
70
|
|
|
72
71
|
### `makeFsm(config)`
|
|
73
72
|
|
|
74
73
|
Creates a new finite state machine instance.
|
|
75
74
|
|
|
76
|
-
####
|
|
75
|
+
#### Config
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
| Property | Type | Required | Description |
|
|
78
|
+
| ------------- | --------------------------------------------- | -------- | ----------------------------------------------------------------------------- |
|
|
79
|
+
| `init` | `TState` | Yes | Initial state |
|
|
80
|
+
| `states` | `TState[]` | Yes | All valid states |
|
|
81
|
+
| `transitions` | `Record<string, Transition<TState>>` | Yes | Transition definitions (keys become methods) |
|
|
82
|
+
| `methods` | `{ onBeforeTransition?, onAfterTransition? }` | No | Lifecycle hooks |
|
|
83
|
+
| `plugins` | `Plugin[]` | No | Array of plugins |
|
|
84
|
+
| `onError` | `(msg: string) => void` | No | Custom error handler. By default, errors throw `Error` with a `[FSM]:` prefix |
|
|
84
85
|
|
|
85
86
|
#### Returns
|
|
86
87
|
|
|
87
|
-
An FSM instance
|
|
88
|
+
An FSM instance combining:
|
|
89
|
+
|
|
90
|
+
- **State methods** — `state()`, `allStates()`
|
|
91
|
+
- **Transition methods** — one per key in `transitions`
|
|
92
|
+
- **Plugin APIs** — one namespace per plugin
|
|
88
93
|
|
|
89
94
|
### State Methods
|
|
90
95
|
|
|
@@ -92,17 +97,9 @@ An FSM instance with transition methods, state methods, and plugin APIs.
|
|
|
92
97
|
|
|
93
98
|
Returns the current state.
|
|
94
99
|
|
|
95
|
-
```typescript
|
|
96
|
-
const currentState = fsm.state();
|
|
97
|
-
```
|
|
98
|
-
|
|
99
100
|
#### `fsm.allStates()`
|
|
100
101
|
|
|
101
|
-
Returns an array of all
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
const allStates = fsm.allStates();
|
|
105
|
-
```
|
|
102
|
+
Returns an array of all valid states.
|
|
106
103
|
|
|
107
104
|
### Transitions
|
|
108
105
|
|
|
@@ -110,204 +107,252 @@ Transitions are defined as objects with `from` and `to` properties:
|
|
|
110
107
|
|
|
111
108
|
```typescript
|
|
112
109
|
type Transition<TState> = {
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
from: '*' | TState | TState[];
|
|
111
|
+
to: TState | ((...args: any[]) => TState | Promise<TState>);
|
|
115
112
|
};
|
|
116
113
|
```
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
#### `from` — source state(s)
|
|
116
|
+
|
|
117
|
+
| Form | Example | Description |
|
|
118
|
+
| --------------- | ---------------------- | --------------------- |
|
|
119
|
+
| Single state | `'idle'` | Only from this state |
|
|
120
|
+
| Multiple states | `['loading', 'error']` | From any listed state |
|
|
121
|
+
| Wildcard | `'*'` | From any state |
|
|
122
|
+
|
|
123
|
+
#### `to` — target state
|
|
124
|
+
|
|
125
|
+
| Form | Example | Description |
|
|
126
|
+
| ------- | --------------------------------- | -------------------------------- |
|
|
127
|
+
| Static | `'loading'` | Always transitions to this state |
|
|
128
|
+
| Dynamic | `(id: string) => \`user\_${id}\`` | Compute target from arguments |
|
|
129
|
+
| Async | `async () => await fetchState()` | Returns `Promise<TState>` |
|
|
130
|
+
|
|
131
|
+
#### Transition behavior
|
|
132
|
+
|
|
133
|
+
- **Circular transitions are skipped** — if `from === to`, the transition is silently canceled with a warning.
|
|
134
|
+
- **Concurrent async transitions are blocked** — starting a new transition while an async one is pending triggers an error.
|
|
135
|
+
- **Invalid target states** — transitioning to a state not in `states` triggers an error.
|
|
136
|
+
- **Forbidden transitions** — calling a transition from a state not matching `from` triggers an error.
|
|
137
|
+
- **Return value** — every transition method returns the new state (or `Promise<TState>` for async transitions).
|
|
126
138
|
|
|
127
139
|
#### Examples
|
|
128
140
|
|
|
129
141
|
```typescript
|
|
130
142
|
const transitions = {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
143
|
+
// Static transition
|
|
144
|
+
start: {
|
|
145
|
+
from: 'idle',
|
|
146
|
+
to: 'loading',
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// Multiple source states
|
|
150
|
+
reset: {
|
|
151
|
+
from: ['success', 'error'],
|
|
152
|
+
to: 'idle',
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// Wildcard (from any state)
|
|
156
|
+
goto: {
|
|
157
|
+
from: '*',
|
|
158
|
+
to: (target: State) => target,
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// Async transition
|
|
162
|
+
fetch: {
|
|
163
|
+
from: 'idle',
|
|
164
|
+
to: async () => {
|
|
165
|
+
const result = await fetchData();
|
|
166
|
+
return result.ok ? 'success' : 'error';
|
|
167
|
+
},
|
|
168
|
+
},
|
|
157
169
|
};
|
|
158
170
|
```
|
|
159
171
|
|
|
172
|
+
### Error Handling
|
|
173
|
+
|
|
174
|
+
By default, the FSM throws on errors (forbidden transitions, invalid states, concurrent transitions). You can provide a custom `onError` handler to change this behavior:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const fsm = makeFsm({
|
|
178
|
+
init: 'idle',
|
|
179
|
+
states: ['idle', 'loading'],
|
|
180
|
+
transitions: {
|
|
181
|
+
start: { from: 'idle', to: 'loading' },
|
|
182
|
+
stop: { from: 'loading', to: 'idle' },
|
|
183
|
+
},
|
|
184
|
+
onError: (msg) => {
|
|
185
|
+
console.warn(msg); // Handle gracefully instead of throwing
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Won't throw — calls onError instead
|
|
190
|
+
fsm.stop(); // "idle" → "idle" via "stop" is forbidden (from doesn't match)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
When `onError` is provided, the FSM state remains unchanged after an error.
|
|
194
|
+
|
|
160
195
|
### Lifecycle Methods
|
|
161
196
|
|
|
162
|
-
Lifecycle methods
|
|
197
|
+
Lifecycle methods hook into the transition process:
|
|
163
198
|
|
|
164
199
|
```typescript
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
};
|
|
200
|
+
const fsm = makeFsm({
|
|
201
|
+
// ...
|
|
202
|
+
methods: {
|
|
203
|
+
onBeforeTransition: (event) => {
|
|
204
|
+
console.log(`${event.from} → ${event.to} via ${event.transition}`);
|
|
205
|
+
return false; // Return false to cancel the transition
|
|
206
|
+
},
|
|
207
|
+
onAfterTransition: (event) => {
|
|
208
|
+
console.log('Transition complete:', event.transition);
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
178
212
|
```
|
|
179
213
|
|
|
180
|
-
####
|
|
214
|
+
#### Lifecycle event object
|
|
181
215
|
|
|
182
|
-
|
|
216
|
+
| Property | Type | Description |
|
|
217
|
+
| ------------ | -------------------- | --------------------------------------- |
|
|
218
|
+
| `transition` | `string` | Name of the transition (the config key) |
|
|
219
|
+
| `from` | `TState` | State before the transition |
|
|
220
|
+
| `to` | `TState` | Target state |
|
|
221
|
+
| `args` | `any[] \| undefined` | Arguments passed to dynamic transitions |
|
|
183
222
|
|
|
184
|
-
|
|
185
|
-
- `event`: Object with `transition`, `from`, `to`, and optional `args`
|
|
223
|
+
#### `onBeforeTransition(event)`
|
|
186
224
|
|
|
187
|
-
|
|
225
|
+
Called before a transition. Return `false` to veto (cancel) the transition.
|
|
188
226
|
|
|
189
|
-
|
|
227
|
+
#### `onAfterTransition(event)`
|
|
190
228
|
|
|
191
|
-
|
|
192
|
-
- `event`: Object with `transition`, `from`, `to`, and optional `args`
|
|
229
|
+
Called after a successful transition. The FSM state is already updated at this point.
|
|
193
230
|
|
|
194
231
|
## Plugins
|
|
195
232
|
|
|
196
|
-
Plugins extend the FSM with additional
|
|
233
|
+
Plugins extend the FSM with additional methods, grouped under a namespace.
|
|
197
234
|
|
|
198
235
|
### Plugin API
|
|
199
236
|
|
|
200
|
-
|
|
237
|
+
Each plugin receives an `api` object with:
|
|
201
238
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
239
|
+
| Method | Description |
|
|
240
|
+
| ---------------------------------- | ----------------------------------------------------------------- |
|
|
241
|
+
| `api.state()` | Get current state |
|
|
242
|
+
| `api.allStates()` | Get all valid states |
|
|
243
|
+
| `api.init(callback)` | Run callback when FSM is created (receives initial state) |
|
|
244
|
+
| `api.onBeforeTransition(callback)` | Register before-transition listener. Returns unsubscribe function |
|
|
245
|
+
| `api.onAfterTransition(callback)` | Register after-transition listener. Returns unsubscribe function |
|
|
246
|
+
| `api.onError(callback)` | Register error listener. Returns unsubscribe function |
|
|
207
247
|
|
|
208
248
|
### Creating a Plugin
|
|
209
249
|
|
|
210
250
|
```typescript
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return api.state();
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
};
|
|
251
|
+
import type { FsmLabel, FsmPlugin, FsmTransition } from '@uuxxx/fsm';
|
|
252
|
+
|
|
253
|
+
export const myPlugin = <TState extends FsmLabel, TTransitions extends Record<string, FsmTransition<TState>>>() =>
|
|
254
|
+
((api) => {
|
|
255
|
+
let count = 0;
|
|
256
|
+
|
|
257
|
+
api.onAfterTransition(() => {
|
|
258
|
+
count++;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
name: 'counter' as const,
|
|
263
|
+
api: {
|
|
264
|
+
getCount: () => count,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}) satisfies FsmPlugin<TState, TTransitions>;
|
|
233
268
|
```
|
|
234
269
|
|
|
235
270
|
### Using Plugins
|
|
236
271
|
|
|
237
272
|
```typescript
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const fsm = makeFsm(config);
|
|
273
|
+
const fsm = makeFsm({
|
|
274
|
+
// ...
|
|
275
|
+
plugins: [myPlugin()],
|
|
276
|
+
});
|
|
244
277
|
|
|
245
|
-
|
|
246
|
-
|
|
278
|
+
fsm.start();
|
|
279
|
+
fsm.counter.getCount(); // 1
|
|
247
280
|
```
|
|
248
281
|
|
|
282
|
+
Plugin names must be unique — registering two plugins with the same name triggers an error.
|
|
283
|
+
|
|
249
284
|
## Built-in Plugins
|
|
250
285
|
|
|
251
286
|
### History Plugin
|
|
252
287
|
|
|
253
|
-
Tracks state history
|
|
288
|
+
Tracks state history with pointer-based navigation.
|
|
254
289
|
|
|
255
290
|
```typescript
|
|
256
|
-
import { makeFsm
|
|
257
|
-
|
|
258
|
-
const config = {
|
|
259
|
-
// ... config
|
|
260
|
-
plugins: [historyPlugin()],
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const fsm = makeFsm(config);
|
|
291
|
+
import { makeFsm } from '@uuxxx/fsm';
|
|
292
|
+
import { fsmHistoryPlugin } from '@uuxxx/fsm/history-plugin';
|
|
264
293
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
294
|
+
const fsm = makeFsm({
|
|
295
|
+
init: 'a',
|
|
296
|
+
states: ['a', 'b', 'c'],
|
|
297
|
+
transitions: {
|
|
298
|
+
goto: { from: '*', to: (s: 'a' | 'b' | 'c') => s },
|
|
299
|
+
},
|
|
300
|
+
plugins: [fsmHistoryPlugin()],
|
|
301
|
+
});
|
|
268
302
|
|
|
269
|
-
|
|
270
|
-
|
|
303
|
+
fsm.goto('b');
|
|
304
|
+
fsm.goto('c');
|
|
305
|
+
fsm.history.get(); // ['a', 'b', 'c']
|
|
271
306
|
|
|
272
|
-
fsm.history.back(1); //
|
|
273
|
-
fsm.history.
|
|
307
|
+
fsm.history.back(1); // returns 'b'
|
|
308
|
+
fsm.history.back(1); // returns 'a'
|
|
309
|
+
fsm.history.forward(2); // returns 'c'
|
|
274
310
|
```
|
|
275
311
|
|
|
276
|
-
#### History API
|
|
312
|
+
#### History API
|
|
313
|
+
|
|
314
|
+
| Method | Returns | Description |
|
|
315
|
+
| ---------------------------- | ---------- | ---------------------------------------------------------------------------------- |
|
|
316
|
+
| `fsm.history.get()` | `TState[]` | Full history array |
|
|
317
|
+
| `fsm.history.back(steps)` | `TState` | Move pointer back by `steps`, returns the state at that position. Clamps to start |
|
|
318
|
+
| `fsm.history.forward(steps)` | `TState` | Move pointer forward by `steps`, returns the state at that position. Clamps to end |
|
|
277
319
|
|
|
278
|
-
|
|
279
|
-
- `fsm.history.back(steps?)`: Go back N steps (default: 1)
|
|
280
|
-
- `fsm.history.forward(steps?)`: Go forward N steps (default: 1)
|
|
320
|
+
> **Note:** `back()` and `forward()` move the internal history pointer and return the state at that position. They do **not** trigger a state transition on the FSM — use transition methods if you need to change the actual FSM state.
|
|
281
321
|
|
|
282
|
-
|
|
322
|
+
When a transition occurs, any forward history after the current pointer is discarded (like browser navigation).
|
|
283
323
|
|
|
284
|
-
|
|
324
|
+
## Exported Types
|
|
285
325
|
|
|
286
|
-
|
|
287
|
-
- Pending async transition when starting a new sync transition
|
|
288
|
-
- Invalid target state
|
|
289
|
-
- Duplicate plugin names
|
|
326
|
+
The library exports the following types for use in plugins and generic code:
|
|
290
327
|
|
|
291
328
|
```typescript
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
329
|
+
import type {
|
|
330
|
+
FsmConfig, // Config<TState, TTransitions, TPlugins>
|
|
331
|
+
FsmTransition, // Transition<TState>
|
|
332
|
+
FsmPlugin, // Plugin<TState, TTransitions>
|
|
333
|
+
FsmLabel, // string (state label type)
|
|
334
|
+
} from '@uuxxx/fsm';
|
|
297
335
|
```
|
|
298
336
|
|
|
299
337
|
## TypeScript Support
|
|
300
338
|
|
|
301
|
-
The library is
|
|
339
|
+
The library is built with TypeScript-first design. All types are inferred from config — no manual type annotations needed:
|
|
302
340
|
|
|
303
341
|
```typescript
|
|
304
342
|
const fsm = makeFsm({
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
343
|
+
init: 'idle',
|
|
344
|
+
states: ['idle', 'running', 'stopped'],
|
|
345
|
+
transitions: {
|
|
346
|
+
start: { from: 'idle', to: 'running' },
|
|
347
|
+
stop: { from: 'running', to: 'stopped' },
|
|
348
|
+
},
|
|
311
349
|
});
|
|
312
|
-
|
|
350
|
+
|
|
351
|
+
fsm.start(); // ✓ typed — only callable from 'idle'
|
|
352
|
+
fsm.stop(); // ✓ typed — only callable from 'running'
|
|
353
|
+
fsm.state(); // ✓ returns 'idle' | 'running' | 'stopped'
|
|
313
354
|
```
|
|
355
|
+
|
|
356
|
+
## License
|
|
357
|
+
|
|
358
|
+
[MIT](./LICENSE)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,89 @@
|
|
|
1
|
-
|
|
1
|
+
type AnyFn = (...args: any[]) => any; //#endregion
|
|
2
|
+
type Rec<T = unknown> = Record<string, T>; //#endregion
|
|
3
|
+
type KeyOf<T extends Rec> = keyof T; //#endregion
|
|
4
|
+
type Noop = () => void; //#endregion
|
|
5
|
+
type Key = string | number | symbol; //#endregion
|
|
6
|
+
type Vdx<T> = T | void; //#endregion
|
|
7
|
+
type EmptyArray = []; //#endregion
|
|
8
|
+
type Entries<T extends Rec> = { [K in KeyOf<T>]: [K, T[K]] }[KeyOf<T>]; //#endregion
|
|
9
|
+
type Label = string;
|
|
10
|
+
/**
|
|
11
|
+
* Defines a single state transition.
|
|
12
|
+
*
|
|
13
|
+
* - `from` — source state(s): a single state, an array of states, or `'*'` for any state.
|
|
14
|
+
* - `to` — target: a static state, a function returning a state, or an async function returning `Promise<TState>`.
|
|
15
|
+
*/
|
|
16
|
+
type Transition<TState extends Label> = {
|
|
17
|
+
from: '*' | TState | TState[];
|
|
18
|
+
to: TState | ((...args: any[]) => TState | Promise<TState>);
|
|
19
|
+
};
|
|
20
|
+
/** Event object passed to lifecycle hooks during a transition. */
|
|
21
|
+
type Lifecycle<TState extends Label, TEntry extends [Key, Transition<Label>]> = {
|
|
22
|
+
args?: Parameters<Extract<TEntry[1]['to'], AnyFn>>;
|
|
23
|
+
transition: TEntry[0];
|
|
24
|
+
from: TState;
|
|
25
|
+
to: TState;
|
|
26
|
+
};
|
|
27
|
+
type LifecycleMethod<TState extends Label, TTransitions extends Rec<Transition<TState>>> = (lifecycle: Lifecycle<TState, Entries<TTransitions>>) => void;
|
|
28
|
+
type CancelableLifecycleMethod<TState extends Label, TTransitions extends Rec<Transition<TState>>> = (lifecycle: Lifecycle<TState, Entries<TTransitions>>) => Vdx<boolean>;
|
|
29
|
+
type LifecycleMethods<TState extends Label, TTransitions extends Rec<Transition<TState>>> = {
|
|
30
|
+
onBeforeTransition?: CancelableLifecycleMethod<TState, TTransitions>;
|
|
31
|
+
onAfterTransition?: LifecycleMethod<TState, TTransitions>;
|
|
32
|
+
};
|
|
33
|
+
type StateMethods<TState extends Label> = {
|
|
34
|
+
state: () => TState;
|
|
35
|
+
allStates: () => TState[];
|
|
36
|
+
};
|
|
37
|
+
/** API object passed to each plugin during registration. Provides state access, lifecycle hooks, and error listeners. */
|
|
38
|
+
type ApiForPlugin<TState extends Label, TTransitions extends Rec<Transition<TState>>> = {
|
|
39
|
+
init: (listener: (state: TState) => void) => void;
|
|
40
|
+
onError: (listener: (msg: string) => void) => Noop;
|
|
41
|
+
} & StateMethods<TState> & { [K in KeyOf<LifecycleMethods<TState, TTransitions>>]-?: (listener: LifecycleMethods<TState, TTransitions>[K]) => Noop };
|
|
42
|
+
type PluginApi = {
|
|
43
|
+
name: string;
|
|
44
|
+
api: Rec<AnyFn>;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A plugin is a function that receives the {@link ApiForPlugin} and returns
|
|
48
|
+
* `{ name, api }` — the name becomes a namespace on the FSM instance,
|
|
49
|
+
* and `api` methods are accessible under that namespace.
|
|
50
|
+
*/
|
|
51
|
+
type Plugin<TState extends Label = Label, TTransitions extends Rec<Transition<TState>> = Rec<Transition<TState>>> = (api: ApiForPlugin<TState, TTransitions>) => PluginApi;
|
|
52
|
+
/** Configuration object for {@link makeFsm}. */
|
|
2
53
|
type Config<TState extends Label, TTransitions extends Rec<Transition<TState>>, TPlugins extends Array<Plugin<TState, TTransitions>> = EmptyArray> = {
|
|
3
|
-
init: TState;
|
|
4
|
-
states: TState[];
|
|
5
|
-
transitions: TTransitions;
|
|
6
|
-
methods?: LifecycleMethods<TState, TTransitions>;
|
|
7
|
-
plugins?: TPlugins;
|
|
54
|
+
/** Initial state of the FSM. */init: TState; /** All valid states. The FSM will reject transitions to states not in this list. */
|
|
55
|
+
states: TState[]; /** Transition definitions. Each key becomes a method on the FSM instance. */
|
|
56
|
+
transitions: TTransitions; /** Optional lifecycle hooks (`onBeforeTransition`, `onAfterTransition`). */
|
|
57
|
+
methods?: LifecycleMethods<TState, TTransitions>; /** Optional plugins to extend the FSM with additional APIs. */
|
|
58
|
+
plugins?: TPlugins; /** Custom error handler. By default, errors throw with a `[FSM]:` prefix. */
|
|
59
|
+
onError?: (msg: string) => void;
|
|
8
60
|
};
|
|
9
61
|
type TransitionMethods<TTransitions extends Rec<Transition<Label>>> = { [K in KeyOf<TTransitions>]: TTransitions[K]['to'] extends Label ? () => TTransitions[K]['to'] : TTransitions[K]['to'] };
|
|
10
62
|
type PluginsMethods<TState extends Label, TTransitions extends Rec<Transition<TState>>, TPlugins extends Array<Plugin<TState, TTransitions>>> = { [K in ReturnType<TPlugins[number]>['name']]: Extract<ReturnType<TPlugins[number]>, {
|
|
11
63
|
name: K;
|
|
12
64
|
}>['api'] };
|
|
13
65
|
type Methods<TState extends Label, TTransitions extends Rec<Transition<TState>>, TPlugins extends Array<Plugin<TState, TTransitions>>> = TransitionMethods<TTransitions> & StateMethods<TState> & PluginsMethods<TState, TTransitions, TPlugins>;
|
|
66
|
+
/**
|
|
67
|
+
* Creates a finite state machine instance from the given configuration.
|
|
68
|
+
*
|
|
69
|
+
* Returns an object combining state methods (`state()`, `allStates()`),
|
|
70
|
+
* auto-generated transition methods (one per key in `config.transitions`),
|
|
71
|
+
* and plugin APIs (one namespace per plugin).
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const fsm = makeFsm({
|
|
76
|
+
* init: 'idle',
|
|
77
|
+
* states: ['idle', 'loading', 'done'],
|
|
78
|
+
* transitions: {
|
|
79
|
+
* start: { from: 'idle', to: 'loading' },
|
|
80
|
+
* finish: { from: 'loading', to: 'done' },
|
|
81
|
+
* },
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* fsm.start(); // 'loading'
|
|
85
|
+
* fsm.finish(); // 'done'
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
14
88
|
declare const makeFsm: <TState extends Label, TTransitions extends Rec<Transition<TState>>, TPlugins extends Array<Plugin<TState, TTransitions>>>(config: Config<TState, TTransitions, TPlugins>) => Methods<TState, TTransitions, TPlugins>;
|
|
15
89
|
export { type Config as FsmConfig, type Label as FsmLabel, type Plugin as FsmPlugin, type Transition as FsmTransition, makeFsm };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
let t={nlx:t=>null===t,ulx:t=>void 0===t,nil:e=>t.nlx(e)||t.ulx(e),not:{nlx:t=>null!==t,ulx:t=>void 0!==t,nil:e=>t.not.nlx(e)&&t.not.ulx(e)},array:t=>Array.isArray(t),string:t=>"string"==typeof t,function:t=>"function"==typeof t,promise:t=>t instanceof Promise,boolean:t=>"boolean"==typeof t,false:t=>!1===t,true:t=>!0===t};function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),r.push.apply(r,n)}return r}function n(t){for(var n=1;n<arguments.length;n++){var i=null!=arguments[n]?arguments[n]:{};n%2?r(Object(i),!0).forEach(function(r){!function(t,r,n){var i;(i=function(t,r){if("object"!=e(t)||!t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var i=n.call(t,r||"default");if("object"!=e(i))return i;throw TypeError("@@toPrimitive must return a primitive value.")}return("string"===r?String:Number)(t)}(r,"string"),(r="symbol"==e(i)?i:i+"")in t)?Object.defineProperty(t,r,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[r]=n}(t,r,i[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):r(Object(i)).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(i,e))})}return t}let i=e=>{var r,i,o;let
|
|
1
|
+
let t={nlx:t=>null===t,ulx:t=>void 0===t,nil:e=>t.nlx(e)||t.ulx(e),not:{nlx:t=>null!==t,ulx:t=>void 0!==t,nil:e=>t.not.nlx(e)&&t.not.ulx(e)},array:t=>Array.isArray(t),string:t=>"string"==typeof t,function:t=>"function"==typeof t,promise:t=>t instanceof Promise,boolean:t=>"boolean"==typeof t,false:t=>!1===t,true:t=>!0===t};function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),r.push.apply(r,n)}return r}function n(t){for(var n=1;n<arguments.length;n++){var i=null!=arguments[n]?arguments[n]:{};n%2?r(Object(i),!0).forEach(function(r){!function(t,r,n){var i;(i=function(t,r){if("object"!=e(t)||!t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var i=n.call(t,r||"default");if("object"!=e(i))return i;throw TypeError("@@toPrimitive must return a primitive value.")}return("string"===r?String:Number)(t)}(r,"string"),(r="symbol"==e(i)?i:i+"")in t)?Object.defineProperty(t,r,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[r]=n}(t,r,i[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):r(Object(i)).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(i,e))})}return t}let i=e=>{var r,i,o,l;let s,a,u,f,c,m,b=e.init,p=e.states.includes(e.init)?[...e.states]:[...e.states,e.init],y=(s=new Map,{listen(t,e){var r;return s.has(t)||s.set(t,[]),null==(r=s.get(t))||r.push(e),()=>{this.unlisten(t,e)}},unlisten(t,e){let r=s.get(t);if(!r)return;let n=r.filter(t=>t!==e);n.length?s.set(t,n):s.delete(t)},emit(e,...r){var n,i;return null!=(n=null==(i=s.get(e))?void 0:i.map(t=>t(...r)).filter(t.not.ulx))?n:[]},unlistenAll(t){s.delete(t)}});Object.entries(null!=(r=e.methods)?r:{}).forEach(([t,e])=>{y.listen(t,e)}),y.listen("onAfterTransition",({to:t})=>{b=t}),y.listen("error",null!=(i=e.onError)?i:t=>{throw Error(`[FSM]: ${t}`)});let g={state:()=>b,allStates:()=>p},d=(o=e.transitions,a=((e,{state:r,allStates:n})=>{let i,o={},l={register(s,a){let u=r=>{n().includes(r.to)?r.to===r.from?e.emit("warn",`
|
|
2
2
|
Transition: "${s}" is canceled because it's circular.
|
|
3
3
|
Current state is ${r.from}. Transition target state is ${r.to}
|
|
4
|
-
`):e.emit("onBeforeTransition",r).filter(t.boolean).every(t.true)&&e.emit("onAfterTransition",r):e.emit("error",`Transition: "${s}" can't be executed. It has invalid "to": "${r.to}"`)};return o[s]=(...n)=>{if(t.array(a.from)?!a.from.includes(r()):"*"!==a.from&&a.from!==r())return e.emit("error",`Transition: "${s}" is forbidden`),r();if(i)return e.emit("error",`Transition: "${s}" can't be made. Has pending transtion: "${i}"`),r();if(!t.function(a.to))return u({transition:s,from:r(),to:a.to}),r();let o=a.to(...n);return t.promise(o)?(i=s,o.then(t=>(i=void 0,u({transition:s,from:r(),to:t,args:n}),r()))):(u({transition:s,from:r(),to:o,args:n}),r())},l},make:()=>o};return l})(
|
|
4
|
+
`):e.emit("onBeforeTransition",r).filter(t.boolean).every(t.true)&&e.emit("onAfterTransition",r):e.emit("error",`Transition: "${s}" can't be executed. It has invalid "to": "${r.to}"`)};return o[s]=(...n)=>{if(t.array(a.from)?!a.from.includes(r()):"*"!==a.from&&a.from!==r())return e.emit("error",`Transition: "${s}" is forbidden`),r();if(i)return e.emit("error",`Transition: "${s}" can't be made. Has pending transtion: "${i}"`),r();if(!t.function(a.to))return u({transition:s,from:r(),to:a.to}),r();let o=a.to(...n);return t.promise(o)?(i=s,o.then(t=>(i=void 0,u({transition:s,from:r(),to:t,args:n}),r()))):(u({transition:s,from:r(),to:o,args:n}),r())},l},make:()=>o};return l})(y,g),Object.entries(o).forEach(([t,e])=>a.register(t,e)),a.make()),v=(l=e.plugins,u={},f=n({init(t){y.listen("init",t)},onError:t=>y.listen("error",t),onBeforeTransition:t=>y.listen("onBeforeTransition",t),onAfterTransition:t=>y.listen("onAfterTransition",t)},g),m=c={register(t){let{name:e,api:r}=t(f);return e in u&&y.emit("error",`There are at least two plugins with the same name: "${e}"`),u[e]=r,c},make:()=>u},(null!=l?l:[]).forEach(m.register),m.make());return y.emit("init",b),n(n(n({},g),v),d)};export{i as makeFsm};
|
package/package.json
CHANGED
|
@@ -1,68 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uuxxx/fsm",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"description": "Javascript library for creating finite state machine",
|
|
7
|
-
"homepage": "https://github.com/uuxxx/fsm/blob/main/README.md",
|
|
8
|
-
"type": "module",
|
|
9
|
-
"publishConfig": {
|
|
10
|
-
"access": "public"
|
|
11
|
-
},
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/uuxxx/fsm.git"
|
|
15
|
-
},
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Lightweight, type-safe finite state machine for TypeScript with plugin support and lifecycle hooks",
|
|
16
5
|
"keywords": [
|
|
17
6
|
"finite state machine",
|
|
18
|
-
"fsm"
|
|
7
|
+
"fsm",
|
|
8
|
+
"lifecycle",
|
|
9
|
+
"plugins",
|
|
10
|
+
"state machine",
|
|
11
|
+
"type-safe",
|
|
12
|
+
"typescript"
|
|
19
13
|
],
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
"homepage": "https://github.com/uuxxx/fsm/blob/main/packages/core/README.md",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Artem Tryapichnikov <golysheeet@gmail.com>",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/uuxxx/fsm.git",
|
|
20
|
+
"directory": "packages/core"
|
|
23
21
|
},
|
|
24
22
|
"files": [
|
|
25
23
|
"dist",
|
|
26
24
|
"README.md",
|
|
27
25
|
"LICENSE"
|
|
28
26
|
],
|
|
27
|
+
"type": "module",
|
|
29
28
|
"exports": {
|
|
30
29
|
".": {
|
|
31
30
|
"types": "./dist/index.d.ts",
|
|
32
31
|
"default": "./dist/index.js"
|
|
33
|
-
},
|
|
34
|
-
"./history-plugin": {
|
|
35
|
-
"types": "./dist/history-plugin.d.ts",
|
|
36
|
-
"default": "./dist/history-plugin.js"
|
|
37
32
|
}
|
|
38
33
|
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
39
37
|
"devDependencies": {
|
|
40
|
-
"@babel/core": "^7.28.4",
|
|
41
|
-
"@babel/preset-env": "^7.28.3",
|
|
42
|
-
"@babel/preset-typescript": "^7.27.1",
|
|
43
|
-
"@changesets/cli": "^2.29.7",
|
|
44
38
|
"@swc/core": "^1.13.5",
|
|
45
|
-
"eslint": "^9.37.0",
|
|
46
|
-
"eslint-config-xo-typescript": "^9.0.0",
|
|
47
|
-
"lefthook": "^2.0.2",
|
|
48
39
|
"rolldown": "^1.0.0-beta.41",
|
|
49
40
|
"rolldown-plugin-dts": "^0.22.1",
|
|
50
41
|
"rollup-plugin-swc3": "^0.12.1",
|
|
51
|
-
"
|
|
52
|
-
"tslib": "^2.8.1",
|
|
53
|
-
"typescript": "^5.9.3",
|
|
54
|
-
"vitest": "^4.0.18"
|
|
55
|
-
},
|
|
56
|
-
"dependencies": {
|
|
57
|
-
"@uuxxx/utils": "^0.0.3"
|
|
42
|
+
"tslib": "^2.8.1"
|
|
58
43
|
},
|
|
59
44
|
"scripts": {
|
|
60
45
|
"build": "rolldown -c rolldown.config.ts",
|
|
61
|
-
"changeset:version": "changeset version && git add --all && git commit -m 'chore: bump'",
|
|
62
|
-
"changeset:publish": "changeset publish",
|
|
63
|
-
"test": "vitest run",
|
|
64
|
-
"lint": "eslint",
|
|
65
|
-
"lint:fix": "eslint --fix",
|
|
66
46
|
"check:types": "tsc --noEmit"
|
|
67
47
|
}
|
|
68
48
|
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
type AnyFn = (...args: any[]) => any; //#endregion
|
|
2
|
-
type Rec<T = unknown> = Record<string, T>; //#endregion
|
|
3
|
-
type KeyOf<T extends Rec> = keyof T; //#endregion
|
|
4
|
-
type Noop = () => void; //#endregion
|
|
5
|
-
type Key = string | number | symbol; //#endregion
|
|
6
|
-
type Vdx<T> = T | void; //#endregion
|
|
7
|
-
type EmptyArray = []; //#endregion
|
|
8
|
-
type Entries<T extends Rec> = { [K in KeyOf<T>]: [K, T[K]] }[KeyOf<T>]; //#endregion
|
|
9
|
-
type Label = string;
|
|
10
|
-
type Transition<TState extends Label> = {
|
|
11
|
-
from: '*' | TState | TState[];
|
|
12
|
-
to: TState | ((...args: any[]) => TState | Promise<TState>);
|
|
13
|
-
};
|
|
14
|
-
type Lifecycle<TState extends Label, TEntry extends [Key, Transition<Label>]> = {
|
|
15
|
-
args?: Parameters<Extract<TEntry[1]['to'], AnyFn>>;
|
|
16
|
-
transition: TEntry[0];
|
|
17
|
-
from: TState;
|
|
18
|
-
to: TState;
|
|
19
|
-
};
|
|
20
|
-
type LifecycleMethod<TState extends Label, TTransitions extends Rec<Transition<TState>>> = (lifecycle: Lifecycle<TState, Entries<TTransitions>>) => void;
|
|
21
|
-
type CancelableLifecycleMethod<TState extends Label, TTransitions extends Rec<Transition<TState>>> = (lifecycle: Lifecycle<TState, Entries<TTransitions>>) => Vdx<boolean>;
|
|
22
|
-
type LifecycleMethods<TState extends Label, TTransitions extends Rec<Transition<TState>>> = {
|
|
23
|
-
onBeforeTransition?: CancelableLifecycleMethod<TState, TTransitions>;
|
|
24
|
-
onAfterTransition?: LifecycleMethod<TState, TTransitions>;
|
|
25
|
-
};
|
|
26
|
-
type StateMethods<TState extends Label> = {
|
|
27
|
-
state: () => TState;
|
|
28
|
-
allStates: () => TState[];
|
|
29
|
-
};
|
|
30
|
-
type ApiForPlugin<TState extends Label, TTransitions extends Rec<Transition<TState>>> = {
|
|
31
|
-
init: (listener: (state: TState) => void) => void;
|
|
32
|
-
} & StateMethods<TState> & { [K in KeyOf<LifecycleMethods<TState, TTransitions>>]-?: (listener: LifecycleMethods<TState, TTransitions>[K]) => Noop };
|
|
33
|
-
type PluginApi = {
|
|
34
|
-
name: string;
|
|
35
|
-
api: Rec<AnyFn>;
|
|
36
|
-
};
|
|
37
|
-
type Plugin<TState extends Label = Label, TTransitions extends Rec<Transition<TState>> = Rec<Transition<TState>>> = (api: ApiForPlugin<TState, TTransitions>) => PluginApi;
|
|
38
|
-
export { Transition as a, KeyOf as c, LifecycleMethods as i, Rec as l, Plugin as n, Label as o, StateMethods as r, EmptyArray as s, ApiForPlugin as t };
|
package/dist/history-plugin.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { a as Transition, l as Rec, o as Label, t as ApiForPlugin } from "./Plugin-DGULTFg-.js";
|
|
2
|
-
declare const historyPlugin: <TState extends Label, TTransitions extends Rec<Transition<TState>>>() => (api: ApiForPlugin<TState, TTransitions>) => {
|
|
3
|
-
name: "history";
|
|
4
|
-
api: {
|
|
5
|
-
get(): TState[];
|
|
6
|
-
back(steps: number): TState;
|
|
7
|
-
forward(steps: number): TState;
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
export { historyPlugin as fsmHistoryPlugin };
|
package/dist/history-plugin.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
let t=()=>t=>{let i=[],n=0;return t.init(t=>i.push(t)),t.onAfterTransition(({to:t})=>{n++,i.splice(n,i.length-n,t)}),{name:"history",api:{get:()=>i,back:t=>i[n=Math.max(0,n-t)],forward:t=>(n=Math.min(i.length-1,n+t),i[n])}}};export{t as fsmHistoryPlugin};
|