machina 4.0.2 → 6.0.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 CHANGED
@@ -1,493 +1,276 @@
1
- # machina v4.0.2
1
+ # machina
2
2
 
3
- ## What is it?
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
- ## Why Would I Use It?
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
- Some frequent use cases for machina:
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
- Great, now that we know how to pull it in, let's create an FSM to represent a vehicle traffic light at a pedestrian crosswalk:
41
-
42
- ```javascript
43
- var vehicleSignal = new machina.Fsm( {
11
+ ## Quick start — `createFsm`
44
12
 
45
- // the initialize method is called right after the FSM
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
- namespace: "vehicle-signal",
15
+ Handlers return a state name to transition, or nothing to stay put.
54
16
 
55
- // `initialState` tells machina what state to start the FSM in.
56
- // The default value is "uninitialized". Not providing
57
- // this value will throw an exception in v1.0+
58
- initialState: "uninitialized",
17
+ ```ts
18
+ import { createFsm } from "machina";
59
19
 
60
- // The states object's top level properties are the
61
- // states in which the FSM can exist. Each state object
62
- // contains input handlers for the different inputs
63
- // handled while in that state.
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
- // _onEnter is a special handler that is invoked
82
- // immediately as the FSM transitions into the new state
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
- // If all you need to do is transition to a new state
90
- // inside an input handler, you can provide the string
91
- // name of the state in place of the input handler function.
92
- timeout: "green-interruptible",
93
- pedestrianWaiting: function() {
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
- _onEnter: function() {
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
- _onEnter: function() {
121
- this.timer = setTimeout( function() {
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
- pedestrianWaiting: function() {
141
- this.handle( "pedestrianWaiting" );
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
- Though the code comments give you a lot of detail, let's break down what's happening in the above FSM:
49
+ light.currentState(); // "green"
50
+ light.compositeState(); // "green" (dot-delimited path, useful with child FSMs)
51
+ light.canHandle("timeout"); // true
157
52
 
158
- * When you are creating an FSM, the constructor takes one argument, the `options` arg - which is an object that contains (at least) the `states` & `initialState` values for your FSM, as well as an optional `initialize` method (which is invoked at the end of the underlying constructor function) and any additional properties or methods you want on the FSM.
159
- * It can exist in one of five possible states: `uninitialized`, `green`, `green-interruptible`, `yellow` and `red`. (Only one state can be active at a time.)
160
- * The states themselves are objects under the `states` property on the FSM, and contain handlers whose names match the input types that the FSM accepts while in that state.
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
- >Note - input handlers can return values. Just be aware that this is not reliable in hierarchical FSMs.
57
+ ### Public API — `Fsm`
167
58
 
168
- ### Going Further
169
- machina provides two constructor functions for creating an FSM: `machina.Fsm` and `machina.BehavioralFsm`:
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
- #### The BehavioralFsm Constructor
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
- ```javascript
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
- initialize: function( options ) {
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
- namespace: "vehicle-signal",
77
+ ```ts
78
+ import { createBehavioralFsm } from "machina";
182
79
 
183
- initialState: "uninitialized",
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
- uninitialized: {
187
- "*": function( client ) {
188
- this.deferUntilTransition( client );
189
- this.transition( client, "green" );
190
- }
89
+ disconnected: {
90
+ connect: "connecting",
191
91
  },
192
- green: {
193
- _onEnter: function( client ) {
194
- client.timer = setTimeout( function() {
195
- this.handle( client, "timeout" );
196
- }.bind( this ), 30000 );
197
- this.emit( "vehicles", { client: client, status: GREEN } );
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
- "green-interruptible": {
208
- pedestrianWaiting: "yellow"
100
+ online: {
101
+ disconnect: "disconnected",
209
102
  },
210
- yellow: {
211
- _onEnter: function( client ) {
212
- client.timer = setTimeout( function() {
213
- this.handle( client, "timeout" );
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
- reset: function( client ) {
236
- this.handle( client, "_reset" );
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
- pedestrianWaiting: function( client ) {
240
- this.handle( client, "pedestrianWaiting" );
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
- Though we're using the *same FSM for behavior*, the *state is tracked separately*. This enables you to keep a smaller memory footprint, especially in situations where you'd otherwise have lots of individual instances of the same FSM in play. More importantly, though, it allows you to take a more functional approach to FSM behavior and state should you prefer to do so. (As a side note, it also makes it much simpler to store a client's state and re-load it later and have the FSM pick up where it left off, etc.)
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
- ### Wait - What's This About Inheritance?
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
- ```javascript
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
- // later/elsewhere in your code:
310
- var trafficLight = new TrafficLightFsm();
129
+ ```ts
130
+ import { createFsm } from "machina";
311
131
 
312
- // you can also override any of the options:
313
- var anotherLight = new TrafficLightFsm({ initialState: "go" });
314
- ```
315
-
316
- The `extend` method works similar to other frameworks (like Backbone, for example). The primary difference is this: *the states object will be deep merged across the prototype chain* into an instance-level `states` property (so it doesn't mutate the prototype chain). This means you can add new states as well as add new (or override existing) handlers to existing states as you inherit from "parent" FSMs. This can be very useful, but – as with all things inheritance-related – use with caution!
317
-
318
- ### And You Mentioned Events?
319
- machina FSMs are event emitters, and subscribing to them is pretty easy:
320
-
321
- ```javascript
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
- // Or, maybe I want to know when ANY event occurs
329
- trafficLight.on("*", function (eventName, data){
330
- console.log("this thing happened:", eventName);
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
- ```javascript
338
- //each listener gets a return value
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
- // OR, we can use the FSM's prototype method -
343
- // remove this specific subscription:
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
- You can emit your own custom events in addition to the built-in events machina emits. To read more about these events, see the [wiki](https://github.com/ifandelse/machina.js/wiki).
169
+ The parent re-enters `initialState` of the child FSM whenever the parent transitions into the state that owns `_child`.
352
170
 
353
- ### Things Suddenly Got Hierarchical!
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
- ```javascript
357
- var pedestrianSignal = new machina.Fsm( {
358
- namespace: "pedestrian-signal",
359
- initialState: "uninitialized",
360
- reset: function() {
361
- this.transition( "walking" );
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
- uninitialized: {
365
- "*": function() {
366
- this.deferUntilTransition();
367
- this.transition( "walking" );
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
- timeout: "flashing",
378
- _onExit: function() {
379
- clearTimeout( this.timer );
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
- timeout: "dontwalk",
390
- _onExit: function() {
391
- clearTimeout( this.timer );
392
- }
190
+ loaded: "ready",
393
191
  },
394
- dontwalk: {
395
- _onEnter: function() {
396
- this.timer = setTimeout( function() {
397
- this.handle( "timeout" );
398
- }.bind( this ), 1000 );
192
+ ready: {
193
+ save() {
194
+ // deferred "save" replays here automatically after transitioning in
195
+ console.log("saving");
399
196
  },
400
- _reset: "walking",
401
- _onExit: function() {
402
- clearTimeout( this.timer );
403
- }
404
- }
405
- }
406
- } )
407
- ```
408
-
409
- In many ways, our `pedestrianSignal` is similar to the `vehicleSignal` FSM:
410
-
411
- * It starts in the `uninitialized` state, and the first input causes it to transition to `walking` before actually processing the input.
412
- * It can only be in one of four states: `uninitialized`, `walking`, `flashing` and `dontwalk`.
413
- * This FSM's input is primarily internally-executed, based on timers (`setTimeout` calls).
414
-
415
- Now - we *could* stand up an instance of `pedestrianSignal` and `vehicleSignal`, and subscribe them to each other's `transition` events. This would make them "siblings" - where `pedestrianSignal` could, for example, only transition to `walking` when `vehicleSignal` is in the `red` state, etc. While there are scenarios where this sort of "sibling" approach is useful, what we really have is a hierarchy. There are two higher level states that each FSM represents, a "vehicles-can-cross" state and a "pedestrians-can-cross" state. With machina v1.0, we can create an FSM to model these higher states, and attach our `pedestrianSignal` and `vehicleSignal` FSMs to their parent states:
416
-
417
- ```javascript
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
197
  },
429
- pedestriansEnabled: {
430
- _child: pedestrianSignal,
431
- _onEnter: function() {
432
- this.emit( "vehicles", { status: RED } );
433
- },
434
- timeout: "vehiclesEnabled"
435
- }
436
- }
437
- } );
198
+ },
199
+ });
200
+
201
+ fsm.handle("save"); // deferred — not yet in "ready"
202
+ // after "loaded" fires and transitions to "ready", "save" replays automatically
438
203
  ```
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:
440
204
 
441
- * When an FSM is handling input, it attempts to let the child FSM handle it first. If the child emits a `nohandler` event, the parent FSM will take over and attempt to handle it. For example - if a `pedestrianWaiting` input is fed to the above FSM while in the `vehiclesEnabled` state, it will be passed on to the `vehicleSignal` FSM to be handled there.
442
- * Events emitted from the child FSM are bubbled up to be emitted by the top level parent (except for the `nohandler` event).
443
- * If a child FSM handles input that it does not have a handler for, it will bubble the input up to the parent FSM to be handled there. Did you notice that both our `pedestrianSignal` and `vehicleSignal` FSMs queue up a `timeout` input in the `dontwalk` and `red` states, respectively? However, neither of those FSMs have an input handler for `timeout` in those states. When these FSMs become part of the hierarchy above, as children of the `crosswalk` FSM, the `timeout` input will bubble up to the parent FSM to be handled, where there *are* handlers for it.
444
- * When the parent FSM transitions to a new state, any child FSM from a previous state is ignored entirely (i.e. - events emitted, or input bubbled, will *not* be handled in the parent). If the parent FSM transitions back to that state, it will resume listening to the child FSM, etc.
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.
205
+ ## Events
206
+
207
+ Both `Fsm` and `BehavioralFsm` emit lifecycle events you can subscribe to with `on()`. Use `"*"` to catch everything.
446
208
 
447
- In v1.1.0, machina added the `compositeState()` method to the `BehavioralFsm` and `Fsm` prototypes. This means you can get the current state of the FSM hierarchy. For example:
209
+ ```ts
210
+ const sub = light.on("transitioned", ({ fromState, toState }) => {
211
+ console.log(`${fromState} -> ${toState}`);
212
+ });
448
213
 
449
- ```javascript
450
- // calling compositeState on Fsm instances
451
- console.log( crosswalk.compositeState() ); // vehiclesEnabled.green
214
+ sub.off(); // unsubscribe
452
215
 
453
- // calling compositeState on BehavioralFsm instances
454
- // (you have to pass the client arg)
455
- console.log( crosswalk.compositeState( fsmClient ) ); // pedestriansEnabled.walking
216
+ // wildcard — receives every event
217
+ light.on("*", (eventName, data) => {
218
+ console.log(eventName, data);
219
+ });
456
220
  ```
457
221
 
458
- >Caveats: This feature is very new to machina, so expect it to evolve a bit. I plan to fine-tune how events bubble in a hierarchy a bit more.
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"
256
+ ```
459
257
 
460
- ### The Top Level machina object
461
- The top level `machina` object has the following members:
258
+ Transition targets in string shorthand are validated against actual state keys at compile time. A typo like `timeout: "yellw"` is a type error.
462
259
 
463
- * `Fsm` - the constructor function used to create FSMs.
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.
260
+ ## Disposal
471
261
 
472
- ## Build, Tests & Examples
473
- machina.js uses [gulp.js](http://gulpjs.com/) to build.
262
+ `dispose()` permanently shuts down an FSM. All subsequent method calls are silent no-ops. Child FSMs are disposed by default.
474
263
 
475
- * Install node.js (and consider using [nvm](https://github.com/creationix/nvm) to manage your node versions)
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`
264
+ ```ts
265
+ fsm.dispose(); // also disposes child FSMs
485
266
 
267
+ fsm.dispose({ preserveChildren: true }); // leave child FSMs running
268
+ ```
486
269
 
487
- ## Release Notes
270
+ ## Full documentation
488
271
 
489
- Go [here](https://github.com/ifandelse/machina.js/blob/master/changelog.md) to see the changelog.
272
+ https://ifandelse.github.io/machina.js/
490
273
 
491
- ## Have More Questions?
274
+ ## License
492
275
 
493
- Read the [wiki](https://github.com/ifandelse/machina.js/wiki) and the source – you might find your answer and more! Check out the [issue](https://github.com/ifandelse/machina.js/issues/4) opened by @burin - a *great* example of how to use github issues to ask questions, provide sample code, etc. I only ask that if you open an issue, that it be *focused on a specific problem or bug* (not wide-open ambiguity, please).
276
+ MIT