UNPKG

automata

Version:

Automata is a Deterministic Finite State Machine automata framework featuring: a JSON based automata definition, timed transitions, sub-states, guards, FSM registry, etc.

559 lines (436 loc) 19.3 kB
# Automata - A finite state machine framework. Current state of automata is version 2.x.x, which is not backward compatible with 1.x.x. ## Description Automata is a formal finite state machine (FDA) framework. It aims at offering a totally decoupled management of logic and data. It features all the needed elements to have a modern and flexible finite state machine framework like * FDA registry * Timed transitions * Auto transition * Sub states * Guards * FDA Session as message chroreographer * Asynchronous execution ## How to Automata works on browsers or Node and has no dependencies. ### To get it: * npm install automata or * include automata.js script file Automata will then expose an object with some functions: #### Typescript ```typescript export class Automata { static RegisterFSM( file : string|FSMJson ); static CreateSession<T>( controller : T, fsm_name : string, o? : SessionObserver<T> ) : SessionConsumeMessagePromise<T>; } ``` #### Javascript ```javascript { Automata : { CreateSession(controller, name, session_observer?), RegisterFSM( automata_def ) } } ``` ## How it works In Automata, FDA (finite deterministic automaton) are declaratively defined. Their definition can be found in `FSMDefinition` object. The Automata definition will be unique, and properly registered in an internal registry. `Session` objects will be created from the automata. Think of the FDA as the class, and the `Session` as the object. For example, an FDA defines a Scrabble game. The sessions will be specific Scrabble games. Sessions keep track of the current State as well as per-session Data associated with a session controller object. This controller is an arbitrary object you supply at session creation time. A minimal example state machine could be: ```javascript Automata.RegisterFSM({ name : "Test1", state : ["a","b","c"], initial_state : "a", transition : [ { event : "ab", from : "a", to : "b" }, { event : "bc", from : "b", to : "c" } ] }); ``` To start using this machine, a `Session` must be created from a registered FDA. For example: ```typescript const Controller = (function () { function Controller() { } Controller.prototype.a_enter = function (session, state, msg) { console.log(state + " enter "); }; ; Controller.prototype.a_exit = function (session, state, msg) { console.log(state + " exit "); }; ; Controller.prototype.b_enter = function (session, state, msg) { console.log(state + " enter "); }; ; Controller.prototype.b_exit = function (session, state, msg) { console.log(state + " exit "); }; ; Controller.prototype.c_exit = function (session, state, msg) { console.log(state + " exit"); }; ; Controller.prototype.c_enter = function (session, state, msg) { console.log(state + " enter"); }; ; Controller.prototype.ab_transition = function (session, state, msg) { console.log("transition: " + msg.msgId); }; ; Controller.prototype.bc_transition = function (session, state, msg) { console.log("transition: " + msg.msgId); }; ; Controller.prototype.Test1_enter = function (session, state, msg) { console.log(state + " enter "); }; ; Controller.prototype.Test1_exit = function (session, state, msg) { console.log(state + " exit "); }; ; return Controller; }()); Automata.CreateSession( new Controller(), "Test1" ).then( function success( s : Session<Controller>, m : Message ) { // session has been created and the controller object correctly attached. }, function error( s : Session<Controller>, m : Error ) { // something went wrong. } ); ``` To send notification events to a session object, call dispatchMessage method: ```typescript session.dispatchMessage( { msgId: "12" } ); ``` This is the most basic workflow, but some things must be taken into account: ### Why create a session then success. Can't it be a synchronous call ? Session creation may internally trigger state changes, so you never may be sure what Transition or State code defined in the Controller object will do. Also, you can dispatch a message to a session that is currently executing other dispatched messages, or there may be a few other messages queued for the session. By nature, dispatching a message is totally asynchronous. ### How can i be notified of all FSM activity. You can be notified of all the FSM activity bound to a `Session` by attaching an observer to the `Session` object. Since starting a `Session` may trigger internal state changes, you may want to pass the listener as the third optional parameter of type `SessionObserver<T>` to `Automata.CreateSession` like: ```typescript Automata.CreateSession( new Controller(), "Test1", { contextCreated : ( e : SessionObserverEvent<T> ) => {}, contextDestroyed : ( e : SessionObserverEvent<T> ) => {}, sessionEnded : ( e : SessionObserverEvent<T> ) => {}, customEvent : ( e : SessionObserverEvent<T> ) => {}, stateChanged : ( e : SessionObserverEvent<T> ) => {} } ).then( function success( s : Session<Controller>, m : Message ) { // session has been created and the controller object correctly attached. }, function error( s : Session<Controller>, m : Error ) { // something went wrong. } ); ``` or Attach a listener at a later time by calling ```typescript session.addObserver( ... ) ``` ### FDA messages The `session.dispatchMessage` and `session.postMessage` methods accepts as a valid message any object which conforms to ```json { msgId : string, data? : object } ``` msgId's values must be the value defined in the **event** attribute present in the Transition FDA definition block. So, for a given `State`, a call to `session.dispatchMessage` will match an exit transition with the passed-in message. * If found, it will start the process of State transition. * If not found, the message will be discarded by notifying an Error of `unknown exit transition for State 's'` A session accepts messages until it has reached a final State. From then and beyond, the session will toss Errors any message is dispatched or posted to it. ### Dispatch vs Post message. A session exposes two well defined method to interact with it. `session.postMessage`, is expected to by the internal submission point of messages for a given Session. Controller methods that wanted to trigger a State change request must call this method. `session.dispatchMessage`, is expected to be the external submission point of messages for a given Session. Each call to `dispatchMessage` will create an internal messages queue. This queue will be fed with all the internal State change requests, which means `postMessage` will add messages to the currently executing Session's messages queue. The signature for `postMessage` is: `postMessage( m : Message )` while the signature for `dispatchMessage` is: `dispatchMessage<U extends Message>( m : U ) : SessionConsumeMessagePromise<T>` or in javascript> `dispatchMessage( message ) : SessionConsumeMessagePromise` The promise-like object returned by dispatchMessage will be invoked when the associated messages queue gets empty. This brings a level of execution isolation where once a message has been dispatched, all activity derived from it will be treated as an asynchronous atomic operation. It is thus guaranteed that this object will be notified only after all internal transitions have ended. ## Controller object The FDA logic and state are isolated. The developer supplies a custom FDA controller object when the `Session` is created. The `controller` object contains per session data, like for example the cards dealt in a game, the authorization credentials, or any other Session specific information. It also has callback functions for FDA specific hook points like entering/exiting a `State` or executing a `Transition`. For both, State and Transitions, the calling **this** scope will be the logic object itself. ## Activy hooks Automata offers many activity hooks: State and FDA: * **_enter**. Code fired on state enter. * **_exit**. Code fired on state exit. Transition: * **_transition**. Code fired when the transition fires. * **_preGuard**. Code fired on transition fire but previously to onTransition. It can veto transition fire. * **_postGuard**. Code fired after onTransition execution. Could veto transition fire by issuing an auto-transition. A natural transition flow of executed actions for a transition from StateA to StateB will be: ``` // For an automata defined as follows: ... state : ['a', 'b', 'c'], initial_state : 'a', transition : [ { from : 'a', to : 'b', event : 'ab' } ] ... StateA.onExit() -> Transition.onTransition() -> StateB.onEnter() // which translate into the following Controller methods (should they exist) a_exit( ... ); ab_transition( ... ); b_enter( ... ); ``` The controller object can only be notified automatically about Session changes by Convention: the framework will automatically try to find methods in the controller object as follows: * * State enter: state.getName() + "_enter" * * State exit: state.getName() + "_exit" * * Transition fire: transition.getEvent() + "_transition" * * Transition pre-guard: transition.getEvent() + "_preGuard" * * Transition post-guard: transition.getEvent() + "_postGuard" State and Transition activity callbacks are of the form: ```javascript ( session : Session<Controller_Type>, state : string, msg : Message ) => void; ``` Those functions will be automatically called **only if** they exist in the logic object. ## Guards Guards prevent a transition from being fired. In Automata there are two available guard points out of the box. One on preTransitionFire and the other on postTransitionFire. The difference is straight: * The **pre-transition guard**, if fired, aborts the transition firing procedure as if it had never occurred. That means, that neither the onExit function, nor a self transition event will be fired by the engine. A good usage of this situation is for counting states. For example, in a multi-player game where 3 players must be present to start the game, a transition from state WaitPlayers to state StartGame will be defined. The pre-transition guard will allow to set a count up, so that whenever a new player enters the game, the count increments, and will fail until the desired amount is reached. This procedure won't affect the state machine, nor its observers. * The **post-transition guard**, if fired, makes the transition behave as a self-transition trigger. For a Transition form State A to State B, a post-transition-guard would fire the following action sequence: Exit_State_A, Transition Fire, Enter_State_A. As opposed to Exit_State_A, Transition Fire, Enter_State_B. A natural transition flow of executed actions for a transition from StateA to StateB with preGuard and postGuard actions will be: ``` if preGuard throws guard-exception // nothing will happen nil; else if postGuard throws guard-exception // auto-transition. State change to StateA will be notified to observers. StateA.onExit -> transition.onTransition -> StateA.onEnter else // this is the regular execution path for a non-guarded transition. State change to // StateB will be notified to observers. StateA.onExit -> Transition.onTransition -> StateB.onEnter endif endif ``` The way to instrument the engine that a guard veto has been fired, will be by throwing an exception from the pre/post-transition functions. A Guard is expected to throw an Error object by calling. Guards are optional, and will be invoked only if they exist in the Controller object. The method names must be of the form: ``` <event>_preGuard / <event>_postGuard. ``` Event is the 'event' defined in the FSM definition transition block. ## Timed transitions Automata offers out of the box timed transitions by defining an **timeout** block in a transition definition block. For example: ```javascript fsmContext.registerFSM( { ..., transition : [ { event : "ab", from : "a", to : "b", timeout : { millis : 4000, data : {} } }, ... } ); ``` This instruments the engine that after 4 seconds of entering `state a`, an event `{msgId: "ab"}` will be dispatched to the `Session`. The timer is handled automatically, and set/canceled on state enter/exit respectively. Timers are set by calling `setTimeout`, and automatically handled by the javascript engine. ## SubStates Automata allows to nest as much as needed substates. In fact, by defining a single FDA, the engine stacks two levels, one for the FDA, and the other, initially for the FDA's initial state. To define different levels, you must register more than one FDA in the registry, and then reference one of them as a substate in the "state" section: ```typescript // Register one FSM model. Automata.RegisterFSM( { name : "SubStateTest", state : ["_1","_2","_3"], initial_state : "_1", transition : [ { event : "12", from : "_1", to : "_2" }, { event : "23", from : "_2", to : "_3" } ] } ); ``` To reference another Automata as substate, use the prefix `FSM:` for the state name: ```typescript Automata.RegisterFSM( { name : "Test4", state : ["a","b","FSM:SubStateTest","c"], initial_state : "a", transition : [ { event : "ab", from : "a", to : "b", }, { event : "bc", from : "b", to : "SubStateTest", }, { event : "cd", from : "SubStateTest", to : "c", } ] } ); ``` ## Transition from Substates The way in which Automata manages state changes is made hierarchycally. That means, the engine will try to find a suitable transition for a given incoming message regardless of its depth level. So for any given FDA stacktrace, the engine will traverse upwards trying to find a suitable state to fire a transition for the dispatched event. (Warning, offending ascii art. States between parenthesis, transitions between square brackets.) <pre> (ROOT) | | v (S1) --[T_S1_S2]--> (SUB_STATE) --[T_SS_S3]--> (S3) | +---> (SS1) --[TSS1_SS2]--> (SS2) </pre> For example, to a `Session` which is currently in `State S1`, ```typescript session.dispatchMessage( {msgId : "T_S1_S2" } ); ``` will make the session change state to `SS1`, and the stackTrace will be the following: `ROOT, SUB_STATE, SS1` By calling ```javascript session.dispatchMessage( {msgId : "T_SS_S3" } ); ``` on the session at state SS1, SS1 will be removed from the stack (since SS2 is a final state), and the session will transize to S3 state. Additionally, this session will be finished since S3 is a final State (this nesting level will be removed from the stack too), and so it is ROOT, which causes the session to be emptied. ## FDA listeners Any FDA `Session` activity can be monitored by adding one or more observers to it. For example: ```javascript session.addObserver( { contextCreated( e : SessionObserverEvent<T> ) : void; // fired when the Session creates a new depth level contextDestroyed( e : SessionObserverEvent<T> ) : void; // fired when the Session destroys a depth level sessionEnded( e : SessionObserverEvent<T> ) : void; // session reached one of its final states. customEvent( e : SessionObserverEvent<T> ) : void; // fire something by calling session.fireCustomEvent. // for example, from the Controller object stateChanged( e : SessionObserverEvent<T> ) : void; // Session changed state (new and previous are available). }); ``` The event parameter for the observer methods is: ```typescript export interface SessionObserverEvent<T> { session : Session<T>; message : Message; custom_message? : Message; current_state_name : string; prev_state_name : string; } ``` ## Custom events The preferred way for sending custom events will be by calling: ```javascript session.fireCustomEvent( a_json_object ); ``` and have a listener/observer object attached to the sending FDA session. This method will be notified on the method ```javascript customEvent : function( ev : FSM.CustomEvent ) { ``` # Samples ## Sample 1 - Simple FDA This sample shows how to define common FDA session callback points. Either on logic object, or by defining a callback. In either case, **this** is defined to be the session's logic object. ## Sample 2 - FDA with timed events This sample show how to define a timed transition. ## Sample 3 - Guards This sample shows how transition guards work on Automata. To fire a transition, first of all an optional **pre-guard** function is tested. If this function throws an exception, Automata interprets a veto on this transition fire. During pre-guard stage, a veto means transition disposal, so no auto-transition is performed. This is useful for example, in a multiplayer game where while playing, a user abandons the game and the game can continue playing. So instead of transitioning from State-playing to State-EndGame, a guard can decide to veto the transition. By definition, a guard **should not** modify the model, in this case, a Logic object. In the example, the guard will fail two times until the count reaches 3. At this moment, the transition is fired (its onTransition method is executed if exists), and after that, the **post-guard** condition is checked. PostGuard semantics are completely different. After firing the transition, the postGuard is checked. If this function **throws an exception** the transition turns into auto-transition, that means firing state change to current-state, and entering again current state. If not, the transition continues its natural flow and transition's next state is set as current state. ## Sample 4 - SubStates Sub States is an Automata feature which allows to nest different registered FDA as states of other FDA. The mechanism is straightforward, just define a **substate** block in an FDA **state** definition block. Automata will handle automatically all the nesting procedure, call the FDA action hooks and set the system's new current state. A substate, or a FDA does not define neither onEnter nor onExit function callbacks.