/ *
A very simple Finite State Machine system . If you ' re struggling with managing
a mixture of JavaScript API styles ( Promise , callback , events ) then this will
help get things organized . The best example is in the ` client/components/HLSVideo.svelte ` file .
The main advantage an FSM has is it 's ability to constrain and organize random events, and it' s
ability to fully log what ' s happening in a stream of random events . The ` HLSVideo.svelte ` example
is a great demonstration of this because it has to juggle events from two different styles of APIs ,
network events , user UI events , and _ _video _ _ loading events . Since all of these can come in at
random the FSM will help order them and do the right thing at the right time .
# # # Theory
One way to look at a Finite State Machine is an _ _ _explicit if - while - loop _ _ _ .
If you have a ` while-loop ` that is doing a lot of processing based on
variables , and you ' re using a lot of ` if-statements ` or ` switch-case `
statements , then the ` FSM ` simply formalizes exactly _ _what _ _ states and when
they can run .
For example , if you have a mess of nested ` if-statements ` , and each one might
not cover every possible variable , then you ' ll have bugs as different
branches of the ` if-statements ` run or don ' t run . The ` FSM ` simply says , " The only
allowed states at this point in time are X , Y , and Z . The only events I expect are
event1 , event2 , and event3 . If I get anything else that ' s an error . "
It ' s a different way to think about data processing , but it is very reliable _and _
easy to debug because you can log every event , state , and all the data while processing
random events .
# # # FSMs in User Interfaces
Most UIs are simple enough that you can get away with the basic Svelte variables updating
the state . When UIs get more complex -- and especially when they trigger network events -- you
run into problems with random events requiring different updates to the UI . For example , if
a user clicks on a button that loads a video , but they ' re already loading a different video ,
then you have to deal with the current click , the old video loading state , and the new video
playing state . An FSM would be able to "serialize" the click , old video load , and new video
load and play better than the usual Svelte variables .
In short , if you ' re struggling with properly updating a user interface as random events come
in from the user and the network , then switch to an FSM .
# # # Usage
The ` class FSM ` takes two things : data and a event transition object . The ` data ` is only
for logging and debugging the FSM , as it gets printed out with each log message . The core
of the FSM is your event handler , which has functions named after each event :
` ` ` javascript
class VideoEvents {
async mount ( state ) {
case "START" :
return "COUNTING_DOWN" ;
default :
return "ERROR" ;
}
}
` ` `
In this snippet the ` class VideoEvents ` has a ` async mount ` event that works like this :
+ This ` mount() ` event takes a single ` state ` which is just a string for the state the FSM is currently in .
+ You then figure out what to do based on this ` state ` , typically using a ` switch/case ` .
+ Based on what you ' ve done , you return the next state as another string .
+ The ` FSM ` then waits for a new event , and calls that function with this new state .
To trigger this ` mount ` event you would then write :
` ` ` javascript
await fsm . do ( "mount" ) ;
` ` `
You can pass additional arguments to the ` do ` function and they will be passed to the
event as additional arguments :
` ` ` javascript
// if fsm is in "START" state
await fsm . do ( "mount" , 1 , 2 , 3 ) ;
// then becomes
ev . mount ( "START" , 1 , 2 , 3 ) ;
` ` `
# # # Logging Events
The ` FSM ` class will already call ` log.debug ` on many things , but if you want to do your own logging
you can use the ` onTransition ` callback :
` ` ` javascript
fsm = new FSM ( video _config , new VideoEvents ( ) ) ;
fsm . onTransition ( fsm => {
video _config . state = fsm . state
log . info ( "I'm logging something else too" , the _other _thing ) ;
} ) ;
` ` `
This is a slight modification of how the FSM is used in ` HLSVideo.svelte ` . Remember that the ` video_config ` is only passed to ` FSM() ` so that it gets logged while the ` FSM ` is running . This is _incredibly _ useful when debugging why your ` FSM ` is doing something .
In the ` onTransition ` callback I ' m simply updating the ` state ` in ` video_config ` and then I added a little ` log.info ` to show you could log anything else you wanted there .
# # # Internal Transitions
If I 'm remembering my graduate courses right, this is called an "epsilon transition", but it' s
basically triggering another event inside the current event , effectively changing state without
waiting for a new event . I could be wrong on the terminology , but you do this by returning an
` Array[state, cb()] ` pair . The ` FSM ` will then set state to ` state ` and call your callback so
you can do further processing .
_ _ _BUG _ _ _ : This could be improved but for now it works . Look at ` HLSVideo.svelte:detect_video ` for an example of doing this .
# # # State Names
The first state is "START" by default , but after that you can use any string . I prefer to make
the states UPPERCASE so they aren ' t confused with events , which are ` lowercase() ` function
names in the event handler class ( ` VideoEvents ` above ) .
* /
import assert from "./assert.js" ;
import { log } from "./logging.js" ;
/ *
A very simple Finite State Machine class . Nothing fancy , just a state
and some transition callbacks based on the function names found in ` this.events ` .
* /
export class FSM {
/ *
Sets up this FSM to use the ` events ` callback as the transitions , and then
"START" as the first state . It does _ _ _not _ _ _ call any initial transitions
so you have to do the first call to ` FSM.do() ` . The ` data ` isn ' t used
internally and only passed to logging functions for debugging .
# # # Event Class Signature
The ` events ` can be anything , and you construct it so it will have your own data .
The only requirement is that the functions have a ` name(state) ` signature and that
` state ` is going to be a string . It should then return the next state string for
the next event to receive .
+ ` data Anything ` -- Whatever you want to log while debugging .
+ ` events Event Object ` -- An object / class that can handle event calls sent to it .
* /
constructor ( data , events ) {
// data is really only for logging/debugging
this . data = data ;
this . events = events ;
this . state = "START" ;
this . on _state = undefined ;
}
/ *
A callback that runs whenever an event handler returns a state . The ` cb ` callback
receives this ` FSM ` so it can debug it or do more to it .
_ _ _WARNING _ _ _ : This is called called even if the state didn ' t change , which
might confuse people if they think it ' s only when a "transition" happens .
+ ` cb(FSM) ` -- Callback after the transition . Receives this ` FSM ` .
* /
onTransition ( cb ) {
this . transition _cb = cb ;
}
/ *
Mostly used internally to perform the transition then call the ` onTransition ` callback ,
but sometimes you might need to force a new state externally .
+ ` next_state string ` -- The new state name .
* /
transition ( next _state ) {
this . state = next _state ;
if ( this . transition _cb ) {
this . transition _cb ( this ) ;
}
}
/ *
All event names available in the ` this.events ` class .
* /
event _names ( ) {
return Object . getOwnPropertyNames ( Object . getPrototypeOf ( this . events ) ) . filter ( k => k !== "constructor" ) ;
}
/ *
The core of this ` FSM ` is the ` do() ` function . It handles finding the next
event by it ' s name , loading it , and calling it with the given additional
arguments .
+ ` event string ` -- The _ _name _ _ of the target event , not the actual function . It ' s a string .
+ ` ...args ` -- Additional arguments to pass to the event .
+ _ _ _return _ _ _ ` this.state string ` -- The current state of this ` FSM ` .
* /
async do ( event , ... args ) {
const evhandler = this . events [ event ] ;
assert ( evhandler !== undefined , ` Invalid event ${ event } . Available ones are ' ${ this . event _names ( ) } '. ` ) ;
// NOTE: you have to use .call to pass in the this.events object or else this === undefined in the call
const next _state = await evhandler . call ( this . events , this . state , ... args ) ;
assert ( next _state , ` The event " ${ event } " returned " ${ next _state } " but must be a truthy true state. ` ) ;
if ( Array . isArray ( next _state ) ) {
assert ( next _state . length == 2 , ` Returning an array only allows 2 elements (state, func) but you returned ${ next _state } ` ) ;
let [ state , func ] = next _state ;
log . debug ( ` FSM ${ this . events . constructor . name } : ( ${ event } ) = ${ this . state } -> ${ state } ( ${ args } ) ` , "DATA:" , this . data , func ? ` THEN ${ func . name } () ` : undefined ) ;
this . transition ( state ) ;
await func ( ) ;
} else {
log . debug ( ` FSM ${ this . events . constructor . name } : ( ${ event } ) = ${ this . state } -> ${ next _state } ( ${ args } ) ` , "DATA:" , this . data ) ;
this . transition ( next _state ) ;
}
return this . state ;
}
}
export default FSM ;