machina 4.0.2 → 6.1.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 +1 -1
- package/README.md +202 -414
- package/dist/index.cjs +661 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +654 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +654 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +655 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -141
- package/changelog.md +0 -190
- package/lib/machina.js +0 -744
- package/lib/machina.min.js +0 -2
- package/lib/machina.min.js.map +0 -1
- package/lib/machina_OLD.js +0 -612
package/README.md
CHANGED
|
@@ -1,493 +1,281 @@
|
|
|
1
|
-
# machina
|
|
1
|
+
# machina
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Machina.js is a JavaScript framework for highly customizable finite state machines (FSMs). Many of the ideas for machina have been *loosely* inspired by the Erlang/OTP FSM behaviors.
|
|
3
|
+
Focused finite state machines for JavaScript and TypeScript. States in, states out.
|
|
5
4
|
|
|
6
|
-
##
|
|
7
|
-
Finite state machines are a great conceptual model for many concerns facing developers – from conditional UI, connectivity monitoring & management to initialization and more. State machines can simplify tangled paths of asynchronous code, they're easy to test, and they inherently lend themselves to helping you avoid unexpected edge-case-state pitfalls. machina aims to give you the tools you need to model state machines in JavaScript, without being too prescriptive on the problem domain you're solving for.
|
|
5
|
+
## Install
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* online/offline connectivity management
|
|
12
|
-
* conditional UI (menus, navigation, workflow)
|
|
13
|
-
* initialization of node.js processes or single-page-apps
|
|
14
|
-
* responding to user input devices (remotes, keyboard, mouse, etc.)
|
|
15
|
-
|
|
16
|
-
### Quick Example
|
|
17
|
-
|
|
18
|
-
First - you need to include it in your environment (browser, node, etc.):
|
|
19
|
-
|
|
20
|
-
```javascript
|
|
21
|
-
// If you're not using an AMD loader, machina is available on the window
|
|
22
|
-
// Just make sure you have lodash loaded before machina
|
|
23
|
-
var MyFsm = machina.Fsm.extend({ /* your stuff */});
|
|
24
|
-
|
|
25
|
-
// If you're using an AMD loader:
|
|
26
|
-
require(['machina'], function(machina){
|
|
27
|
-
return machina.Fsm.extend({ /* your stuff */});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// node.js/CommonJS:
|
|
31
|
-
var machina = require('machina');
|
|
32
|
-
|
|
33
|
-
// FYI machina v0.3.x & earlier returned a factory
|
|
34
|
-
// function in CommonJS environments:
|
|
35
|
-
var lodash = require('lodash');
|
|
36
|
-
var machina = require('machina')(lodash);
|
|
37
|
-
var MyFsm = machina.Fsm.extend({ /* your stuff */});
|
|
7
|
+
```
|
|
8
|
+
npm install machina
|
|
38
9
|
```
|
|
39
10
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```javascript
|
|
43
|
-
var vehicleSignal = new machina.Fsm( {
|
|
11
|
+
## Quick start — `createFsm`
|
|
44
12
|
|
|
45
|
-
|
|
46
|
-
// instance is constructed, giving you a place for any
|
|
47
|
-
// setup behavior, etc. It receives the same arguments
|
|
48
|
-
// (options) as the constructor function.
|
|
49
|
-
initialize: function( options ) {
|
|
50
|
-
// your setup code goes here...
|
|
51
|
-
},
|
|
13
|
+
`createFsm` is the standard choice: one config, one FSM instance, one internal context object. Handler signatures receive a `{ ctx, inputName, defer, emit }` args object — no `this`, works with arrow functions.
|
|
52
14
|
|
|
53
|
-
|
|
15
|
+
Handlers return a state name to transition, or nothing to stay put.
|
|
54
16
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// this value will throw an exception in v1.0+
|
|
58
|
-
initialState: "uninitialized",
|
|
17
|
+
```ts
|
|
18
|
+
import { createFsm } from "machina";
|
|
59
19
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
20
|
+
const light = createFsm({
|
|
21
|
+
id: "traffic-light",
|
|
22
|
+
initialState: "green",
|
|
23
|
+
context: { tickCount: 0 },
|
|
64
24
|
states: {
|
|
65
|
-
uninitialized: {
|
|
66
|
-
// Input handlers are usually functions. They can
|
|
67
|
-
// take arguments, too (even though this one doesn't)
|
|
68
|
-
// The "*" handler is special (more on that in a bit)
|
|
69
|
-
"*": function() {
|
|
70
|
-
this.deferUntilTransition();
|
|
71
|
-
// the `transition` method takes a target state (as a string)
|
|
72
|
-
// and transitions to it. You should NEVER directly assign the
|
|
73
|
-
// state property on an FSM. Also - while it's certainly OK to
|
|
74
|
-
// call `transition` externally, you usually end up with the
|
|
75
|
-
// cleanest approach if you endeavor to transition *internally*
|
|
76
|
-
// and just pass input to the FSM.
|
|
77
|
-
this.transition( "green" );
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
25
|
green: {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
_onEnter: function() {
|
|
84
|
-
this.timer = setTimeout( function() {
|
|
85
|
-
this.handle( "timeout" );
|
|
86
|
-
}.bind( this ), 30000 );
|
|
87
|
-
this.emit( "vehicles", { status: "GREEN" } );
|
|
26
|
+
_onEnter({ ctx }) {
|
|
27
|
+
ctx.tickCount = 0;
|
|
88
28
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
timeout
|
|
93
|
-
|
|
94
|
-
this.deferUntilTransition( "green-interruptible" );
|
|
29
|
+
tick({ ctx }) {
|
|
30
|
+
ctx.tickCount++;
|
|
31
|
+
},
|
|
32
|
+
timeout({ ctx }) {
|
|
33
|
+
if (ctx.tickCount >= 5) return "yellow";
|
|
95
34
|
},
|
|
96
|
-
// _onExit is a special handler that is invoked just before
|
|
97
|
-
// the FSM leaves the current state and transitions to another
|
|
98
|
-
_onExit: function() {
|
|
99
|
-
clearTimeout( this.timer );
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
"green-interruptible": {
|
|
103
|
-
pedestrianWaiting: "yellow"
|
|
104
35
|
},
|
|
105
36
|
yellow: {
|
|
106
|
-
|
|
107
|
-
this.timer = setTimeout( function() {
|
|
108
|
-
this.handle( "timeout" );
|
|
109
|
-
}.bind( this ), 5000 );
|
|
110
|
-
// machina FSMs are event emitters. Here we're
|
|
111
|
-
// emitting a custom event and data, etc.
|
|
112
|
-
this.emit( "vehicles", { status: "YELLOW" } );
|
|
113
|
-
},
|
|
114
|
-
timeout: "red",
|
|
115
|
-
_onExit: function() {
|
|
116
|
-
clearTimeout( this.timer );
|
|
117
|
-
}
|
|
37
|
+
timeout: "red", // string shorthand — always transitions
|
|
118
38
|
},
|
|
119
39
|
red: {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.handle( "timeout" );
|
|
123
|
-
}.bind( this ), 1000 );
|
|
124
|
-
this.emit( "vehicles", { status: "RED" } );
|
|
125
|
-
},
|
|
126
|
-
_reset: "green",
|
|
127
|
-
_onExit: function() {
|
|
128
|
-
clearTimeout(this.timer);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
// While you can call the FSM's `handle` method externally, it doesn't
|
|
134
|
-
// make for a terribly expressive API. As a general rule, you wrap calls
|
|
135
|
-
// to `handle` with more semantically meaningful method calls like these:
|
|
136
|
-
reset: function() {
|
|
137
|
-
this.handle( "_reset" );
|
|
40
|
+
timeout: "green",
|
|
41
|
+
},
|
|
138
42
|
},
|
|
43
|
+
});
|
|
139
44
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} );
|
|
144
|
-
|
|
145
|
-
// Now, to use it:
|
|
146
|
-
// This call causes the FSM to transition from uninitialized -> green
|
|
147
|
-
// & queues up pedestrianWaiting input, which replays after the timeout
|
|
148
|
-
// causes a transition to green-interruptible....which immediately
|
|
149
|
-
// transitions to yellow since we have a pedestrian waiting. After the
|
|
150
|
-
// next timeout, we end up in "red".
|
|
151
|
-
vehicleSignal.pedestrianWaiting();
|
|
152
|
-
// Once the FSM is in the "red" state, we can reset it to "green" by calling:
|
|
153
|
-
vehicleSignal.reset();
|
|
154
|
-
```
|
|
45
|
+
light.handle("tick");
|
|
46
|
+
light.handle("tick");
|
|
47
|
+
light.handle("timeout"); // stays green — tickCount is 2, not >= 5
|
|
155
48
|
|
|
156
|
-
|
|
49
|
+
light.currentState(); // "green"
|
|
50
|
+
light.compositeState(); // "green" (dot-delimited path, useful with child FSMs)
|
|
51
|
+
light.canHandle("timeout"); // true
|
|
157
52
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
* It starts in the `uninitialized` state.
|
|
162
|
-
* It accepts input either by calling `handle` directly and passing the input type as a string (plus any arguments), or by calling top level methods you put on your FSM's prototype that wrap the calls to `handle` with a more expressive API.
|
|
163
|
-
* You do *not* assign the state value of the FSM directly, instead, you use `transition(stateName)` to transition to a different state.
|
|
164
|
-
* Special "input handlers" exist in machina: `_onEnter`, `_onExit` and `*`. In fact, the very first state (`uninitialized`) in this FSM is using `*`. It's the "catch-all" handler which, if provided, will match any input in that state that's not explicitly matched by name. In this case, any input handled in `uninitialized` will cause the FSM to defer the input (queue it up for replay after transitioning), and immediately transfer to `green`. (This is just to demonstrate how a start-up-only state can automatically transfer into active state(s) as clients begin using the FSM. )
|
|
53
|
+
light.reset(); // back to initialState, fires _onEnter
|
|
54
|
+
light.dispose(); // tears down; all subsequent calls are silent no-ops
|
|
55
|
+
```
|
|
165
56
|
|
|
166
|
-
|
|
57
|
+
### Public API — `Fsm`
|
|
167
58
|
|
|
168
|
-
|
|
169
|
-
|
|
59
|
+
| Method | Description |
|
|
60
|
+
| ---------------------------- | ------------------------------------------------------------------ |
|
|
61
|
+
| `handle(inputName, ...args)` | Dispatch an input to the current state's handler |
|
|
62
|
+
| `canHandle(inputName)` | True if the current state has a handler (or `"*"`) for this input |
|
|
63
|
+
| `transition(toState)` | Directly transition; fires `_onExit`, `_onEnter`, lifecycle events |
|
|
64
|
+
| `reset()` | Transition back to `initialState` |
|
|
65
|
+
| `currentState()` | Returns the current state name |
|
|
66
|
+
| `compositeState()` | Dot-delimited path including active child FSM states |
|
|
67
|
+
| `on(eventName, callback)` | Subscribe to a lifecycle event (returns `{ off() }`) |
|
|
68
|
+
| `emit(eventName, data?)` | Emit a custom event |
|
|
69
|
+
| `dispose(options?)` | Permanently shut down; cascades to child FSMs by default |
|
|
170
70
|
|
|
171
|
-
|
|
172
|
-
`BehavioralFsm` is new to machina as of v1.0 (though the `Fsm` constructor now inherits from it). The `BehavioralFsm` constructor lets you create an FSM that defines *behavior* (hence the name) that you want applied to multiple, separate instances of *state*. A `BehavioralFsm` instance does not (should not!) track state locally, on itself. For example, consider this scenario....where we get to twist our `vehicleSignal` FSM beyond reason: :smile:
|
|
71
|
+
## `createBehavioralFsm`
|
|
173
72
|
|
|
174
|
-
|
|
175
|
-
var vehicleSignal = new machina.BehavioralFsm( {
|
|
73
|
+
`createBehavioralFsm` defines behavior once and applies it to any number of independent client objects. Per-client state lives in a `WeakMap` — no properties are stamped onto the client. The client object IS the context; handlers receive it as `ctx`.
|
|
176
74
|
|
|
177
|
-
|
|
178
|
-
// your setup code goes here...
|
|
179
|
-
},
|
|
75
|
+
The client type is provided as an explicit type parameter since it can't be inferred from the config.
|
|
180
76
|
|
|
181
|
-
|
|
77
|
+
```ts
|
|
78
|
+
import { createBehavioralFsm } from "machina";
|
|
182
79
|
|
|
183
|
-
|
|
80
|
+
interface Connection {
|
|
81
|
+
url: string;
|
|
82
|
+
retries: number;
|
|
83
|
+
}
|
|
184
84
|
|
|
85
|
+
const connFsm = createBehavioralFsm<Connection>({
|
|
86
|
+
id: "connectivity",
|
|
87
|
+
initialState: "disconnected",
|
|
185
88
|
states: {
|
|
186
|
-
|
|
187
|
-
"
|
|
188
|
-
this.deferUntilTransition( client );
|
|
189
|
-
this.transition( client, "green" );
|
|
190
|
-
}
|
|
89
|
+
disconnected: {
|
|
90
|
+
connect: "connecting",
|
|
191
91
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
92
|
+
connecting: {
|
|
93
|
+
connected: "online",
|
|
94
|
+
failed({ ctx }) {
|
|
95
|
+
ctx.retries++;
|
|
96
|
+
if (ctx.retries >= 3) return "error";
|
|
97
|
+
return "disconnected";
|
|
198
98
|
},
|
|
199
|
-
timeout: "green-interruptible",
|
|
200
|
-
pedestrianWaiting: function( client ) {
|
|
201
|
-
this.deferUntilTransition( client, "green-interruptible" );
|
|
202
|
-
},
|
|
203
|
-
_onExit: function( client ) {
|
|
204
|
-
clearTimeout( client.timer );
|
|
205
|
-
}
|
|
206
99
|
},
|
|
207
|
-
|
|
208
|
-
|
|
100
|
+
online: {
|
|
101
|
+
disconnect: "disconnected",
|
|
209
102
|
},
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}.bind( this ), 5000 );
|
|
215
|
-
this.emit( "vehicles", { client: client, status: YELLOW } );
|
|
103
|
+
error: {
|
|
104
|
+
reset({ ctx }) {
|
|
105
|
+
ctx.retries = 0;
|
|
106
|
+
return "disconnected";
|
|
216
107
|
},
|
|
217
|
-
timeout: "red",
|
|
218
|
-
_onExit: function( client ) {
|
|
219
|
-
clearTimeout( client.timer );
|
|
220
|
-
}
|
|
221
108
|
},
|
|
222
|
-
red: {
|
|
223
|
-
_onEnter: function( client ) {
|
|
224
|
-
client.timer = setTimeout( function() {
|
|
225
|
-
this.handle( client, "timeout" );
|
|
226
|
-
}.bind( this ), 1000 );
|
|
227
|
-
},
|
|
228
|
-
_reset: "green",
|
|
229
|
-
_onExit: function( client ) {
|
|
230
|
-
clearTimeout( client.timer );
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
109
|
},
|
|
110
|
+
});
|
|
234
111
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
},
|
|
112
|
+
const connA = { url: "wss://host-a.example.com", retries: 0 };
|
|
113
|
+
const connB = { url: "wss://host-b.example.com", retries: 0 };
|
|
238
114
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
} );
|
|
243
|
-
|
|
244
|
-
// Now we can have multiple 'instances' of traffic lights that all share the same FSM:
|
|
245
|
-
var light1 = { location: "Dijsktra Ave & Hunt Blvd", direction: "north-south" };
|
|
246
|
-
var light2 = { location: "Dijsktra Ave & Hunt Blvd", direction: "east-west" };
|
|
247
|
-
|
|
248
|
-
// to use the behavioral fsm, we pass the "client" in as the first arg to API calls:
|
|
249
|
-
vehicleSignal.pedestrianWaiting( light1 );
|
|
250
|
-
|
|
251
|
-
// Now let's signal a pedestrian waiting at light2
|
|
252
|
-
vehicleSignal.pedestrianWaiting( light2 );
|
|
253
|
-
|
|
254
|
-
// if you were to inspect light1 and light2, you'd see they both have
|
|
255
|
-
// a __machina__ property, which contains metadata related to this FSM.
|
|
256
|
-
// For example, light1.__machina__.vehicleSignal.state might be "green"
|
|
257
|
-
// and light2.__machina__.vehicleSignal.state might be "yellow" (depending
|
|
258
|
-
// on when you check). The point is - the "clients' state" is tracked
|
|
259
|
-
// separately from each other, and from the FSM. Here's a snapshot of
|
|
260
|
-
// light1 right after the vehicleSignal.pedestrianWaiting( light1 ) call:
|
|
261
|
-
|
|
262
|
-
{
|
|
263
|
-
"location": "Dijsktra Ave & Hunt Blvd",
|
|
264
|
-
"direction": "north-south",
|
|
265
|
-
"__machina__": {
|
|
266
|
-
"vehicle-signal": {
|
|
267
|
-
"inputQueue": [
|
|
268
|
-
{
|
|
269
|
-
"type": "transition",
|
|
270
|
-
"untilState": "green-interruptible",
|
|
271
|
-
"args": [
|
|
272
|
-
{
|
|
273
|
-
"inputType": "pedestrianWaiting",
|
|
274
|
-
"delegated": false
|
|
275
|
-
}
|
|
276
|
-
]
|
|
277
|
-
}
|
|
278
|
-
],
|
|
279
|
-
"targetReplayState": "green",
|
|
280
|
-
"state": "green",
|
|
281
|
-
"priorState": "uninitialized",
|
|
282
|
-
"priorAction": "",
|
|
283
|
-
"currentAction": "",
|
|
284
|
-
"currentActionArgs": [
|
|
285
|
-
{
|
|
286
|
-
"inputType": "pedestrianWaiting",
|
|
287
|
-
"delegated": false
|
|
288
|
-
}
|
|
289
|
-
],
|
|
290
|
-
"inExitHandler": false
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
"timer": 11
|
|
294
|
-
}
|
|
115
|
+
connFsm.handle(connA, "connect");
|
|
116
|
+
connFsm.handle(connB, "connect");
|
|
117
|
+
connFsm.handle(connB, "failed");
|
|
295
118
|
|
|
119
|
+
connFsm.currentState(connA); // "connecting"
|
|
120
|
+
connFsm.currentState(connB); // "disconnected"
|
|
296
121
|
```
|
|
297
122
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
#### The Fsm Constructor
|
|
301
|
-
If you've used machina prior to v1.0, the `Fsm` constructor is what you're familiar with. It's functionally equivalent to the `BehavioralFsm` (in fact, it inherits from it), except that it can only deal with one client: *itself*. There's no need to pass a `client` argument to the API calls on an `Fsm` instance, since it only acts on itself. All of the metadata that was stamped on our `light1` and `light2` clients above (under the `__machina__` property) is at the instance level on an `Fsm` (as it has been historically for this constructor).
|
|
123
|
+
`BehavioralFsm` has the same API as `Fsm`, except every method takes the client object as its first argument.
|
|
302
124
|
|
|
303
|
-
|
|
304
|
-
machina's FSM constructor functions are simple to extend. If you don't need an instance, but just want a modified constructor function to use later to create instances, you can do something like this:
|
|
125
|
+
## Hierarchical states
|
|
305
126
|
|
|
306
|
-
|
|
307
|
-
var TrafficLightFsm = machina.Fsm.extend({ /* your options */ });
|
|
127
|
+
Attach a child FSM to any state via `_child`. Inputs are dispatched to the child first; unhandled inputs bubble up to the parent.
|
|
308
128
|
|
|
309
|
-
|
|
310
|
-
|
|
129
|
+
```ts
|
|
130
|
+
import { createFsm } from "machina";
|
|
311
131
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// I'd like to know when the transition event occurs
|
|
324
|
-
trafficLight.on("transition", function (data){
|
|
325
|
-
console.log("we just transitioned from " + data.fromState + " to " + data.toState);
|
|
132
|
+
const childFsm = createFsm({
|
|
133
|
+
id: "upload-phases",
|
|
134
|
+
initialState: "preparing",
|
|
135
|
+
context: {},
|
|
136
|
+
states: {
|
|
137
|
+
preparing: { ready: "uploading" },
|
|
138
|
+
uploading: { done: "verifying" },
|
|
139
|
+
verifying: { verified: "complete" },
|
|
140
|
+
complete: {},
|
|
141
|
+
},
|
|
326
142
|
});
|
|
327
143
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
144
|
+
const uploader = createFsm({
|
|
145
|
+
id: "uploader",
|
|
146
|
+
initialState: "idle",
|
|
147
|
+
context: {},
|
|
148
|
+
states: {
|
|
149
|
+
idle: {
|
|
150
|
+
start: "active",
|
|
151
|
+
},
|
|
152
|
+
active: {
|
|
153
|
+
_child: childFsm, // delegates inputs here first
|
|
154
|
+
cancel: "idle", // "cancel" is not on childFsm, so it bubbles up here
|
|
155
|
+
},
|
|
156
|
+
},
|
|
331
157
|
});
|
|
332
158
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
Unsubscribing can be done a couple of ways:
|
|
159
|
+
uploader.handle("start");
|
|
160
|
+
uploader.compositeState(); // "active.preparing"
|
|
336
161
|
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
var sub = trafficLight.on("transition", someCallback);
|
|
340
|
-
sub.off(); // unsubscribes the handler
|
|
162
|
+
uploader.handle("ready");
|
|
163
|
+
uploader.compositeState(); // "active.uploading"
|
|
341
164
|
|
|
342
|
-
|
|
343
|
-
//
|
|
344
|
-
trafficLight.off("transition", someCallback);
|
|
345
|
-
// remove all transition subscribers
|
|
346
|
-
trafficLight.off("transition");
|
|
347
|
-
// remove ALL subscribers, period:
|
|
348
|
-
trafficLight.off();
|
|
165
|
+
uploader.handle("cancel");
|
|
166
|
+
uploader.compositeState(); // "idle"
|
|
349
167
|
```
|
|
350
168
|
|
|
351
|
-
|
|
169
|
+
The parent re-enters `initialState` of the child FSM whenever the parent transitions into the state that owns `_child`.
|
|
352
170
|
|
|
353
|
-
|
|
354
|
-
One of the most exciting additions in v1.0: machina now supports hierarchical state machines. Remember our earlier example of the `vehicleSignal` FSM? Well, that's only *part* of a pedestrian crosswalk. Pedestrians need their own signal as well - typically a sign that signals "Walk" and "Do Not Walk". Let's peek at what an FSM for this might look like:
|
|
171
|
+
## Deferred input
|
|
355
172
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
},
|
|
173
|
+
Call `defer()` inside a handler to queue the current input for replay after the next transition. Pass `{ until: "stateName" }` to replay only on entry to a specific state.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const fsm = createFsm({
|
|
177
|
+
id: "loader",
|
|
178
|
+
initialState: "loading",
|
|
179
|
+
context: {},
|
|
363
180
|
states: {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
},
|
|
370
|
-
walking: {
|
|
371
|
-
_onEnter: function() {
|
|
372
|
-
this.timer = setTimeout( function() {
|
|
373
|
-
this.handle( "timeout" );
|
|
374
|
-
}.bind( this ), 30000 );
|
|
375
|
-
this.emit( "pedestrians", { status: WALK } );
|
|
181
|
+
loading: {
|
|
182
|
+
_onEnter() {
|
|
183
|
+
// simulate async load completing
|
|
184
|
+
setTimeout(() => fsm.handle("loaded"), 100);
|
|
376
185
|
},
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
flashing: {
|
|
383
|
-
_onEnter: function() {
|
|
384
|
-
this.timer = setTimeout( function() {
|
|
385
|
-
this.handle( "timeout" );
|
|
386
|
-
}.bind( this ), 5000 );
|
|
387
|
-
this.emit( "pedestrians", { status: DO_NOT_WALK, flashing: true } );
|
|
186
|
+
// Can't process "save" while still loading — replay it later
|
|
187
|
+
save({ defer }) {
|
|
188
|
+
defer({ until: "ready" });
|
|
388
189
|
},
|
|
389
|
-
|
|
390
|
-
_onExit: function() {
|
|
391
|
-
clearTimeout( this.timer );
|
|
392
|
-
}
|
|
190
|
+
loaded: "ready",
|
|
393
191
|
},
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}.bind( this ), 1000 );
|
|
192
|
+
ready: {
|
|
193
|
+
save() {
|
|
194
|
+
// deferred "save" replays here automatically after transitioning in
|
|
195
|
+
console.log("saving");
|
|
399
196
|
},
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
} )
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
In many ways, our `pedestrianSignal` is similar to the `vehicleSignal` FSM:
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
410
200
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
201
|
+
fsm.handle("save"); // deferred — not yet in "ready"
|
|
202
|
+
// after "loaded" fires and transitions to "ready", "save" replays automatically
|
|
203
|
+
```
|
|
414
204
|
|
|
415
|
-
|
|
205
|
+
## Events
|
|
416
206
|
|
|
417
|
-
|
|
418
|
-
var crosswalk = new machina.Fsm( {
|
|
419
|
-
namespace: "crosswalk",
|
|
420
|
-
initialState: "vehiclesEnabled",
|
|
421
|
-
states: {
|
|
422
|
-
vehiclesEnabled: {
|
|
423
|
-
_child: vehicleSignal,
|
|
424
|
-
_onEnter: function() {
|
|
425
|
-
this.emit( "pedestrians", { status: DO_NOT_WALK } );
|
|
426
|
-
},
|
|
427
|
-
timeout: "pedestriansEnabled"
|
|
428
|
-
},
|
|
429
|
-
pedestriansEnabled: {
|
|
430
|
-
_child: pedestrianSignal,
|
|
431
|
-
_onEnter: function() {
|
|
432
|
-
this.emit( "vehicles", { status: RED } );
|
|
433
|
-
},
|
|
434
|
-
timeout: "vehiclesEnabled"
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
} );
|
|
438
|
-
```
|
|
439
|
-
Notice how each state has a `_child` property? This property can be used to assign an FSM instance to act as a child FSM for this parent state (or a factory function that produces an instance to be used, etc.). Here's how it works:
|
|
207
|
+
Both `Fsm` and `BehavioralFsm` emit lifecycle events you can subscribe to with `on()`. Use `"*"` to catch everything.
|
|
440
208
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
* As the parent state transitions into any of its states, it will tell the child FSM to handle a `_reset` input. This gives you a hook to move the child FSM to the correct state before handling any further input. For example, you'll notice our `pedestrianSignal` FSM has a `_reset` input handler in the `dontwalk` state, which transitions the FSM to the `walking` state.
|
|
209
|
+
```ts
|
|
210
|
+
const sub = light.on("transitioned", ({ fromState, toState }) => {
|
|
211
|
+
console.log(`${fromState} -> ${toState}`);
|
|
212
|
+
});
|
|
446
213
|
|
|
447
|
-
|
|
214
|
+
sub.off(); // unsubscribe
|
|
448
215
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
console.log(
|
|
216
|
+
// wildcard — receives every event
|
|
217
|
+
light.on("*", (eventName, data) => {
|
|
218
|
+
console.log(eventName, data);
|
|
219
|
+
});
|
|
220
|
+
```
|
|
452
221
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
222
|
+
### Built-in events
|
|
223
|
+
|
|
224
|
+
| Event | Payload | Fired when |
|
|
225
|
+
| --------------- | ------------------------ | --------------------------------------- |
|
|
226
|
+
| `transitioning` | `{ fromState, toState }` | A transition is about to occur |
|
|
227
|
+
| `transitioned` | `{ fromState, toState }` | A transition completed |
|
|
228
|
+
| `handling` | `{ inputName }` | An input is about to be dispatched |
|
|
229
|
+
| `handled` | `{ inputName }` | An input was successfully handled |
|
|
230
|
+
| `nohandler` | `{ inputName, args }` | No handler found in current state |
|
|
231
|
+
| `invalidstate` | `{ stateName }` | Transition targeted a nonexistent state |
|
|
232
|
+
| `deferred` | `{ inputName }` | An input was deferred |
|
|
233
|
+
|
|
234
|
+
`BehavioralFsm` events include a `client` field in every payload to identify which client the event pertains to.
|
|
235
|
+
|
|
236
|
+
## TypeScript
|
|
237
|
+
|
|
238
|
+
machina is written in TypeScript. State names, input names, and handler signatures are all inferred from the config object — no manual type parameters needed for `createFsm`.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import type {
|
|
242
|
+
FsmConfig,
|
|
243
|
+
FsmEventMap,
|
|
244
|
+
BehavioralFsmEventMap,
|
|
245
|
+
HandlerArgs,
|
|
246
|
+
HandlerFn,
|
|
247
|
+
StateNamesOf,
|
|
248
|
+
InputNamesOf,
|
|
249
|
+
Subscription,
|
|
250
|
+
DisposeOptions,
|
|
251
|
+
} from "machina";
|
|
252
|
+
|
|
253
|
+
// StateNamesOf and InputNamesOf extract literal unions from your states config:
|
|
254
|
+
// type MyStates = StateNamesOf<typeof myConfig.states>; // "green" | "yellow" | "red"
|
|
255
|
+
// type MyInputs = InputNamesOf<typeof myConfig.states>; // "tick" | "timeout"
|
|
456
256
|
```
|
|
457
257
|
|
|
458
|
-
|
|
258
|
+
Transition targets in string shorthand are validated against actual state keys at compile time. A typo like `timeout: "yellw"` is a type error.
|
|
459
259
|
|
|
460
|
-
|
|
461
|
-
The top level `machina` object has the following members:
|
|
260
|
+
## Disposal
|
|
462
261
|
|
|
463
|
-
|
|
464
|
-
* `BehavioralFsm` – the constructor function used to create BehavioralFSM instances.
|
|
465
|
-
* `utils` - contains helper functions that can be overridden to change default behavior(s) in machina:
|
|
466
|
-
* `makeFsmNamespace` - function that provides a default "channel" or "exchange" for an FSM instance. (e.g. - fsm.0, fsm.1, etc.)
|
|
467
|
-
* `on` - method used to subscribe a callback to top-level machina events (currently the only event published at this level is `newFsm`)
|
|
468
|
-
* `off` - method used to unsubscribe a callback to top-level machina events.
|
|
469
|
-
* `emit` - top-level method used to emit events.
|
|
470
|
-
* `eventListeners` - an object literal containing the susbcribers to any top-level events.
|
|
262
|
+
`dispose()` permanently shuts down an FSM. All subsequent method calls are silent no-ops. Child FSMs are disposed by default.
|
|
471
263
|
|
|
472
|
-
|
|
473
|
-
|
|
264
|
+
```ts
|
|
265
|
+
fsm.dispose(); // also disposes child FSMs
|
|
266
|
+
|
|
267
|
+
fsm.dispose({ preserveChildren: true }); // leave child FSMs running
|
|
268
|
+
```
|
|
474
269
|
|
|
475
|
-
|
|
476
|
-
* run `npm install` & `bower install` to install all dependencies
|
|
477
|
-
* To build, run `npm run build` - then check the lib folder for the output
|
|
478
|
-
* To run the examples:
|
|
479
|
-
* `npm start`
|
|
480
|
-
* navigate in your browser to <http://localhost:3080/>
|
|
481
|
-
* To run tests & examples:
|
|
482
|
-
* To run node-based tests: `npm run test`
|
|
483
|
-
* To run istanbul (code test coverage): `npm run coverage`
|
|
484
|
-
* To see a browser-based istanbul report: `npm run show-coverage`
|
|
270
|
+
## Companion tools
|
|
485
271
|
|
|
272
|
+
- [machina-inspect](https://www.npmjs.com/package/machina-inspect) — static analysis for FSM configs: unreachable states, `_onEnter` loops, missing handlers.
|
|
273
|
+
- [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina) — ESLint plugin wrapping machina-inspect for inline editor feedback.
|
|
486
274
|
|
|
487
|
-
##
|
|
275
|
+
## Full documentation
|
|
488
276
|
|
|
489
|
-
|
|
277
|
+
https://machina-js.org
|
|
490
278
|
|
|
491
|
-
##
|
|
279
|
+
## License
|
|
492
280
|
|
|
493
|
-
|
|
281
|
+
MIT
|