@uuxxx/fsm 1.2.4 → 1.3.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/README.md +180 -107
- package/dist/index.d.ts +79 -6
- package/package.json +12 -35
- package/dist/Plugin-Dx6AAZEC.d.ts +0 -39
- 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,11 +30,9 @@ 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
34
|
init: 'idle',
|
|
28
|
-
states:
|
|
35
|
+
states: ['idle', 'loading', 'success', 'error'],
|
|
29
36
|
transitions: {
|
|
30
37
|
start: {
|
|
31
38
|
from: 'idle',
|
|
@@ -50,41 +57,39 @@ const fsm = makeFsm({
|
|
|
50
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
|
|
|
@@ -115,21 +112,36 @@ type Transition<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
|
-
|
|
143
|
+
// Static transition
|
|
144
|
+
start: {
|
|
133
145
|
from: 'idle',
|
|
134
146
|
to: 'loading',
|
|
135
147
|
},
|
|
@@ -143,84 +155,113 @@ const transitions = {
|
|
|
143
155
|
// Wildcard (from any state)
|
|
144
156
|
goto: {
|
|
145
157
|
from: '*',
|
|
146
|
-
to: (
|
|
158
|
+
to: (target: State) => target,
|
|
147
159
|
},
|
|
148
160
|
|
|
149
161
|
// Async transition
|
|
150
|
-
|
|
162
|
+
fetch: {
|
|
151
163
|
from: 'idle',
|
|
152
164
|
to: async () => {
|
|
153
165
|
const result = await fetchData();
|
|
154
|
-
return result.
|
|
166
|
+
return result.ok ? 'success' : 'error';
|
|
155
167
|
},
|
|
156
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
|
-
// ...
|
|
200
|
+
const fsm = makeFsm({
|
|
201
|
+
// ...
|
|
167
202
|
methods: {
|
|
168
203
|
onBeforeTransition: (event) => {
|
|
169
|
-
console.log(
|
|
170
|
-
// Return false to cancel the transition
|
|
171
|
-
return true;
|
|
204
|
+
console.log(`${event.from} → ${event.to} via ${event.transition}`);
|
|
205
|
+
return false; // Return false to cancel the transition
|
|
172
206
|
},
|
|
173
207
|
onAfterTransition: (event) => {
|
|
174
|
-
console.log('Transition
|
|
208
|
+
console.log('Transition complete:', event.transition);
|
|
175
209
|
},
|
|
176
210
|
},
|
|
177
|
-
};
|
|
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
|
-
|
|
223
|
+
#### `onBeforeTransition(event)`
|
|
185
224
|
|
|
186
|
-
|
|
225
|
+
Called before a transition. Return `false` to veto (cancel) the transition.
|
|
187
226
|
|
|
188
227
|
#### `onAfterTransition(event)`
|
|
189
228
|
|
|
190
|
-
Called after a successful transition.
|
|
191
|
-
|
|
192
|
-
**Parameters:**
|
|
193
|
-
|
|
194
|
-
- `event`: Object with `transition`, `from`, `to`, and optional `args`
|
|
229
|
+
Called after a successful transition. The FSM state is already updated at this point.
|
|
195
230
|
|
|
196
231
|
## Plugins
|
|
197
232
|
|
|
198
|
-
Plugins extend the FSM with additional
|
|
233
|
+
Plugins extend the FSM with additional methods, grouped under a namespace.
|
|
199
234
|
|
|
200
235
|
### Plugin API
|
|
201
236
|
|
|
202
|
-
|
|
237
|
+
Each plugin receives an `api` object with:
|
|
203
238
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 |
|
|
209
247
|
|
|
210
248
|
### Creating a Plugin
|
|
211
249
|
|
|
212
250
|
```typescript
|
|
213
251
|
import type { FsmLabel, FsmPlugin, FsmTransition } from '@uuxxx/fsm';
|
|
214
252
|
|
|
215
|
-
export const
|
|
253
|
+
export const myPlugin = <TState extends FsmLabel, TTransitions extends Record<string, FsmTransition<TState>>>() =>
|
|
216
254
|
((api) => {
|
|
217
|
-
|
|
255
|
+
let count = 0;
|
|
256
|
+
|
|
257
|
+
api.onAfterTransition(() => {
|
|
258
|
+
count++;
|
|
259
|
+
});
|
|
218
260
|
|
|
219
261
|
return {
|
|
220
|
-
|
|
221
|
-
name: 'plugin-name' as const,
|
|
262
|
+
name: 'counter' as const,
|
|
222
263
|
api: {
|
|
223
|
-
|
|
264
|
+
getCount: () => count,
|
|
224
265
|
},
|
|
225
266
|
};
|
|
226
267
|
}) satisfies FsmPlugin<TState, TTransitions>;
|
|
@@ -229,54 +270,79 @@ export const somePlugin = <TState extends FsmLabel, TTransitions extends Record<
|
|
|
229
270
|
### Using Plugins
|
|
230
271
|
|
|
231
272
|
```typescript
|
|
232
|
-
const
|
|
233
|
-
// ...
|
|
234
|
-
plugins: [
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const fsm = makeFsm(config);
|
|
273
|
+
const fsm = makeFsm({
|
|
274
|
+
// ...
|
|
275
|
+
plugins: [myPlugin()],
|
|
276
|
+
});
|
|
238
277
|
|
|
239
|
-
|
|
240
|
-
|
|
278
|
+
fsm.start();
|
|
279
|
+
fsm.counter.getCount(); // 1
|
|
241
280
|
```
|
|
242
281
|
|
|
282
|
+
Plugin names must be unique — registering two plugins with the same name triggers an error.
|
|
283
|
+
|
|
243
284
|
## Built-in Plugins
|
|
244
285
|
|
|
245
286
|
### History Plugin
|
|
246
287
|
|
|
247
|
-
|
|
288
|
+
Read-only state history tracking with pointer-based navigation.
|
|
289
|
+
|
|
290
|
+
`back()` and `forward()` move an internal pointer and return the state at that position — they do **not** change the FSM state. Use transition methods to actually navigate (e.g. `fsm.goto(fsm.history.back(1))`).
|
|
248
291
|
|
|
249
292
|
```typescript
|
|
250
293
|
import { makeFsm } from '@uuxxx/fsm';
|
|
251
|
-
import {
|
|
294
|
+
import { historyPlugin } from '@uuxxx/fsm-plugins/history';
|
|
252
295
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
296
|
+
const fsm = makeFsm({
|
|
297
|
+
init: 'a',
|
|
298
|
+
states: ['a', 'b', 'c'],
|
|
299
|
+
transitions: {
|
|
300
|
+
goto: { from: '*', to: (s: 'a' | 'b' | 'c') => s },
|
|
301
|
+
},
|
|
302
|
+
plugins: [historyPlugin()],
|
|
303
|
+
});
|
|
257
304
|
|
|
258
|
-
|
|
305
|
+
fsm.goto('b');
|
|
306
|
+
fsm.goto('c');
|
|
307
|
+
fsm.history.get(); // ['a', 'b', 'c'] (returns a copy)
|
|
259
308
|
|
|
260
|
-
//
|
|
261
|
-
fsm.
|
|
262
|
-
fsm.
|
|
309
|
+
fsm.history.back(1); // returns 'b' (pointer moved, FSM state unchanged)
|
|
310
|
+
fsm.history.current(); // 'b'
|
|
311
|
+
fsm.history.canBack(); // true
|
|
312
|
+
fsm.history.canForward(); // true
|
|
313
|
+
fsm.history.forward(1); // returns 'c'
|
|
314
|
+
fsm.goto(fsm.history.current()); // actually transition to 'c'
|
|
315
|
+
```
|
|
263
316
|
|
|
264
|
-
|
|
265
|
-
console.log(fsm.history.get()); // Get all history
|
|
317
|
+
#### History API
|
|
266
318
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
319
|
+
| Method | Returns | Description |
|
|
320
|
+
| ---------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------- |
|
|
321
|
+
| `fsm.history.get()` | `TState[]` | Returns a copy of the full history array |
|
|
322
|
+
| `fsm.history.current()` | `TState` | Returns the state at the current pointer position |
|
|
323
|
+
| `fsm.history.back(steps)` | `TState` | Move pointer back by `steps`, returns the state at that position. Clamps to start. Ignores non-positive values |
|
|
324
|
+
| `fsm.history.forward(steps)` | `TState` | Move pointer forward by `steps`, returns the state at that position. Clamps to end. Ignores non-positive values |
|
|
325
|
+
| `fsm.history.canBack()` | `boolean` | Whether the pointer can move back (pointer > 0) |
|
|
326
|
+
| `fsm.history.canForward()` | `boolean` | Whether the pointer can move forward (pointer < end) |
|
|
270
327
|
|
|
271
|
-
|
|
328
|
+
When a transition occurs, any forward history after the current pointer is discarded (like browser navigation).
|
|
272
329
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
330
|
+
## Exported Types
|
|
331
|
+
|
|
332
|
+
The library exports the following types for use in plugins and generic code:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import type {
|
|
336
|
+
FsmConfig, // Config<TState, TTransitions, TPlugins>
|
|
337
|
+
FsmTransition, // Transition<TState>
|
|
338
|
+
FsmPlugin, // Plugin<TState, TTransitions>
|
|
339
|
+
FsmLabel, // string (state label type)
|
|
340
|
+
} from '@uuxxx/fsm';
|
|
341
|
+
```
|
|
276
342
|
|
|
277
343
|
## TypeScript Support
|
|
278
344
|
|
|
279
|
-
The library is
|
|
345
|
+
The library is built with TypeScript-first design. All types are inferred from config — no manual type annotations needed:
|
|
280
346
|
|
|
281
347
|
```typescript
|
|
282
348
|
const fsm = makeFsm({
|
|
@@ -287,5 +353,12 @@ const fsm = makeFsm({
|
|
|
287
353
|
stop: { from: 'running', to: 'stopped' },
|
|
288
354
|
},
|
|
289
355
|
});
|
|
290
|
-
|
|
356
|
+
|
|
357
|
+
fsm.start(); // ✓ typed — only callable from 'idle'
|
|
358
|
+
fsm.stop(); // ✓ typed — only callable from 'running'
|
|
359
|
+
fsm.state(); // ✓ returns 'idle' | 'running' | 'stopped'
|
|
291
360
|
```
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
[MIT](./LICENSE)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,61 @@
|
|
|
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. */
|
|
8
59
|
onError?: (msg: string) => void;
|
|
9
60
|
};
|
|
10
61
|
type TransitionMethods<TTransitions extends Rec<Transition<Label>>> = { [K in KeyOf<TTransitions>]: TTransitions[K]['to'] extends Label ? () => TTransitions[K]['to'] : TTransitions[K]['to'] };
|
|
@@ -12,5 +63,27 @@ type PluginsMethods<TState extends Label, TTransitions extends Rec<Transition<TS
|
|
|
12
63
|
name: K;
|
|
13
64
|
}>['api'] };
|
|
14
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
|
+
*/
|
|
15
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>;
|
|
16
89
|
export { type Config as FsmConfig, type Label as FsmLabel, type Plugin as FsmPlugin, type Transition as FsmTransition, makeFsm };
|
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uuxxx/fsm",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Lightweight, type-safe finite state machine for TypeScript with plugin support and lifecycle hooks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"finite state machine",
|
|
7
|
-
"fsm"
|
|
7
|
+
"fsm",
|
|
8
|
+
"lifecycle",
|
|
9
|
+
"plugins",
|
|
10
|
+
"state machine",
|
|
11
|
+
"type-safe",
|
|
12
|
+
"typescript"
|
|
8
13
|
],
|
|
9
|
-
"homepage": "https://github.com/uuxxx/fsm/blob/main/README.md",
|
|
14
|
+
"homepage": "https://github.com/uuxxx/fsm/blob/main/packages/core/README.md",
|
|
10
15
|
"license": "MIT",
|
|
11
16
|
"author": "Artem Tryapichnikov <golysheeet@gmail.com>",
|
|
12
17
|
"repository": {
|
|
13
18
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/uuxxx/fsm.git"
|
|
19
|
+
"url": "git+https://github.com/uuxxx/fsm.git",
|
|
20
|
+
"directory": "packages/core"
|
|
15
21
|
},
|
|
16
22
|
"files": [
|
|
17
23
|
"dist",
|
|
@@ -23,49 +29,20 @@
|
|
|
23
29
|
".": {
|
|
24
30
|
"types": "./dist/index.d.ts",
|
|
25
31
|
"default": "./dist/index.js"
|
|
26
|
-
},
|
|
27
|
-
"./history-plugin": {
|
|
28
|
-
"types": "./dist/history-plugin.d.ts",
|
|
29
|
-
"default": "./dist/history-plugin.js"
|
|
30
32
|
}
|
|
31
33
|
},
|
|
32
34
|
"publishConfig": {
|
|
33
35
|
"access": "public"
|
|
34
36
|
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"@uuxxx/utils": "^0.0.3"
|
|
37
|
-
},
|
|
38
37
|
"devDependencies": {
|
|
39
|
-
"@babel/core": "^7.28.4",
|
|
40
|
-
"@babel/preset-env": "^7.28.3",
|
|
41
|
-
"@babel/preset-typescript": "^7.27.1",
|
|
42
|
-
"@changesets/cli": "^2.29.7",
|
|
43
38
|
"@swc/core": "^1.13.5",
|
|
44
|
-
"lefthook": "^2.0.2",
|
|
45
|
-
"oxfmt": "^0.41.0",
|
|
46
|
-
"oxlint": "^1.0.0",
|
|
47
|
-
"oxlint-tsgolint": "^0.17.0",
|
|
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
|
-
"engines": {
|
|
57
|
-
"node": ">= 20",
|
|
58
|
-
"pnpm": ">= 10"
|
|
42
|
+
"tslib": "^2.8.1"
|
|
59
43
|
},
|
|
60
44
|
"scripts": {
|
|
61
45
|
"build": "rolldown -c rolldown.config.ts",
|
|
62
|
-
"changeset:version": "changeset version && git add --all && git commit -m 'chore: bump'",
|
|
63
|
-
"changeset:publish": "changeset publish",
|
|
64
|
-
"test": "vitest run",
|
|
65
|
-
"lint": "oxlint",
|
|
66
|
-
"lint:fix": "oxlint --fix",
|
|
67
|
-
"fmt": "oxfmt --write .",
|
|
68
|
-
"fmt:check": "oxfmt --check .",
|
|
69
46
|
"check:types": "tsc --noEmit"
|
|
70
47
|
}
|
|
71
48
|
}
|
|
@@ -1,39 +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
|
-
onError: (listener: (msg: string) => void) => Noop;
|
|
33
|
-
} & StateMethods<TState> & { [K in KeyOf<LifecycleMethods<TState, TTransitions>>]-?: (listener: LifecycleMethods<TState, TTransitions>[K]) => Noop };
|
|
34
|
-
type PluginApi = {
|
|
35
|
-
name: string;
|
|
36
|
-
api: Rec<AnyFn>;
|
|
37
|
-
};
|
|
38
|
-
type Plugin<TState extends Label = Label, TTransitions extends Rec<Transition<TState>> = Rec<Transition<TState>>> = (api: ApiForPlugin<TState, TTransitions>) => PluginApi;
|
|
39
|
-
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-Dx6AAZEC.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};
|