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.
863 lines (681 loc) • 24.8 kB
text/typescript
/**
* Created by ibon on 2/8/16.
*/
export interface Message {
msgId : string;
data? : any;
}
export type GenericMap<T> = { [name : string] : T };
export type Action = <T>(session: Session<T>, message : Message) => T;
export interface AutoTransitionJson {
millis : number,
data? : any;
}
export interface TransitionJson {
from : string;
to : string;
event : string;
timeout? : AutoTransitionJson
}
export interface FSMJson {
name : string;
state : string[];
initial_state : string;
transition : TransitionJson[];
}
export type SessionMessageCallback = <T>(session : Session<T>, message? : Message) => void;
export type SessionMessageCallbackError = <T>(session : Session<T>, message? : Error) => void;
export class SessionConsumeMessagePromise<T> {
_success : SessionMessageCallback;
_error : SessionMessageCallbackError;
constructor() {
}
then( ok : SessionMessageCallback, error? : SessionMessageCallbackError ) {
this._success = ok;
this._error = error;
return this;
}
__success( s : Session<T>, m : Message ) {
this._success && this._success( s, m );
}
__error( s : Session<T>, message? : Error ) {
this._error && this._error( s, message );
}
}
export class FSMRegistry {
static _fsm : GenericMap<FSM> = {};
static FSMFromId( id : string ) : FSM {
return FSMRegistry._fsm[id];
}
static register( fsm_json : FSMJson ) : FSM {
let ret : FSM = null;
try {
let fsm = new FSM(fsm_json);
FSMRegistry._fsm[fsm.name] = fsm;
console.log("Registered Automata '"+fsm.name+"'");
ret=fsm;
} catch( e ) {
console.error(e);
}
return ret;
}
static createSession<T>( session_controller : T, fsm_id : string, o? : SessionObserver<T> ) : SessionConsumeMessagePromise<T> {
const promise : SessionConsumeMessagePromise<T> = new SessionConsumeMessagePromise<T>();
const fsm = FSMRegistry._fsm[ fsm_id ];
if ( fsm ) {
const session = new Session( session_controller );
if ( o ) {
session.addObserver(o);
}
session.__initialize( fsm ).then(
(session : Session<T>, m: Message) : void => {
promise.__success( session, m );
},
(session : Session<T>, m : Error) : void => {
promise.__error( session, m );
}
);
} else {
setImmediate( function() {
promise.__error( null, new Error("Unkonwn automata: '"+fsm_id+"'") );
} );
}
return promise;
}
}
export interface StateAutoTransitionElement {
millis : number;
message?: Message;
timer_id? : number;
}
export class State {
_name : string;
_exit_transitions : GenericMap<Transition>;
_exit_transitions_count : number;
_enter_action : Action;
_exit_action : Action;
_auto_transition : StateAutoTransitionElement[];
constructor( name : string ) {
this._name = name;
this._exit_transitions = {};
this._exit_transitions_count = 0;
this._enter_action = null;
this._exit_action = null;
this._auto_transition = [];
}
transitionForMessage( m : Message ) {
const tr : Transition = this._exit_transitions[ m.msgId ];
return tr||null;
}
addExitTransition( t : Transition ) {
this._exit_transitions[t.event] = t;
this._exit_transitions_count += 1;
}
get name() {
return this._name;
}
__onExit<T>( s : Session<T>, m : Message ) : boolean {
if ( this._exit_action!==null ) {
this._exit_action( s, m );
}
this.__stopTimeoutTransitionElements();
return this._exit_action!==null;
}
__onEnter<T>( s : Session<T>, m : Message ) {
if ( this._enter_action!==null ) {
this._enter_action( s, m);
}
this.__startTimeoutTransitionElements(s);
return this._enter_action!==null;
}
__startTimeoutTransitionElements<T>( s : Session<T> ) {
this._auto_transition.forEach( (sate) : void => {
sate.timer_id = setTimeout(
this.__notifyTimeoutEvent.bind(this,s, sate.message),
sate.millis
);
});
}
__stopTimeoutTransitionElements() {
this._auto_transition.forEach( (sate) : void => {
if ( sate.timer_id !==-1 ) {
clearTimeout(sate.timer_id);
sate.timer_id = -1;
}
});
}
__notifyTimeoutEvent<T>( s : Session<T>, m : Message ) {
this.__stopTimeoutTransitionElements();
s.dispatchMessage( m );
}
__setTimeoutTransitionInfo( millis : number, message : Message ) {
this._auto_transition.push( {
millis : millis,
message : message,
timer_id : -1
} );
}
isFinal() : boolean {
return this._exit_transitions_count === 0;
}
toString() : string {
return this._name;
}
}
export class FSM extends State {
_states : State[];
_transitions : Transition[];
_initial_state : State;
constructor( fsm : FSMJson ) {
super( fsm.name );
this._states = [];
this._transitions = [];
this._initial_state = null;
this.__createStates( fsm.state, fsm.initial_state );
this.__createTransitions( fsm.transition );
}
get initial_state() {
return this._initial_state;
}
serialize() : FSMJson {
return {
name : this._name,
state : this._states.map( st => st._name ),
initial_state : this._initial_state._name,
transition : this._transitions.map( tr => {
return {
event: tr._event,
from: tr._initial_state._name,
to: tr._final_state._name
}
})
};
}
__createStates( states : string[], initial : string ) {
for( let name of states ) {
let st: State;
if ( name.lastIndexOf("FSM:")===-1 ) {
st= new State(name);
} else {
const fsmname = name.substring(4);
st= FSMRegistry._fsm[fsmname];
if ( !st ) {
throw "Automata '"+this._name+"' referencing other non existent automata: '"+name+"'";
}
}
this._states.push( st );
if ( st.name === initial ) {
this.__setInitialState( st );
}
}
}
__setInitialState( st : State ) {
this._initial_state = st;
this.__createInitialTransition();
this.__createEnterAction();
}
__createInitialTransition() {
this.addExitTransition(
new Transition(
this,
this._initial_state,
Transition.__InitialTransitionEvent ) );
}
__createEnterAction() {
this._enter_action = <T>(session : Session<T>, message : Message ) : void => {
session.postMessage( Transition.__InitialTransitionMessage );
}
}
__findStateByName( n : string ) : State {
for( let s of this._states ) {
if ( s.name===n ) {
return s;
}
}
return null;
}
__createTransitions( transitions : TransitionJson[] ) {
transitions.forEach( (v:TransitionJson /*, index:number, arr:TransitionJson[] */ ) : void => {
const f : State = this.__findStateByName( v.from );
const t : State = this.__findStateByName( v.to );
const e : string = v.event;
if ( !f || !t ) {
throw `Wrongly defined Automata '${this.name}'. Transition '${v.event}' refers unknown state:'${(!f ? v.from : v.to)}'`;
}
this._transitions.push( new Transition( f, t, e ) );
// auto transition behavior.
if ( typeof v.timeout!=="undefined" ) {
f.__setTimeoutTransitionInfo( v.timeout.millis, {
msgId : e,
data : v.timeout.data
} );
}
});
}
}
export class Transition {
static __InitialTransitionEvent : string = "__INITIAL_EVENT";
static __InitialTransitionMessage : Message = { msgId : Transition.__InitialTransitionEvent };
_event : string;
_initial_state : State;
_final_state : State;
constructor( from : State, to : State, event : string ) {
this._event= event;
this._initial_state = from;
this._final_state = to;
if ( from ) {
from.addExitTransition( this );
}
}
get event() {
return this._event;
}
get final_state() {
return this._final_state;
}
toString() : string {
return this._event;
}
}
export interface SerializedSessionContext {
current_state : string;
prev_state : string;
}
export class SessionContext {
_current_state : State;
_prev_state : State;
constructor( c:State, p:State ) {
this._current_state = c;
this._prev_state = p;
}
serialize() : SerializedSessionContext {
return {
current_state : this._current_state._name,
prev_state : this._prev_state ? this._prev_state._name : "",
};
}
get current_state() : State {
return this._current_state;
}
get prev_state() : State {
return this._prev_state;
}
currentStateName() : string {
return this._current_state && this._current_state.name;
}
prevStateName() : string {
return this._prev_state && this._prev_state.name;
}
printStackTrace() {
console.log(" "+this._current_state.name );
}
}
export interface SessionObserverEvent<T> {
session : Session<T>;
message : Message;
custom_message? : Message;
current_state_name : string;
prev_state_name : string;
}
export interface SessionObserver<T> {
contextCreated( e : SessionObserverEvent<T> ) : void;
contextDestroyed( e : SessionObserverEvent<T> ) : void;
sessionEnded( e : SessionObserverEvent<T> ) : void;
customEvent( e : SessionObserverEvent<T> ) : void;
stateChanged( e : SessionObserverEvent<T> ) : void;
}
export interface SerializedSession {
ended : boolean,
controller : any,
states : SerializedSessionContext[],
fsm : FSMJson | string
}
export class Session<T> {
_fsm : FSM;
_session_controller : T;
_states : SessionContext[];
_ended : boolean;
_messages_controller : SessionMessagesController<T>;
_observers : SessionObserver<T>[];
_sessionEndPromise : SessionConsumeMessagePromise<T>;
constructor( session_controller : T ) {
this._states = [];
this._session_controller = session_controller;
this._messages_controller = new SessionMessagesController(this);
this._observers = [];
this._fsm = null;
this._ended = false;
this._sessionEndPromise = null;
}
__initialize( fsm : FSM ) : SessionConsumeMessagePromise<T> {
this._fsm = fsm;
this._states.push( new SessionContext(fsm, null) );
this.__invoke( fsm.name + "_enter", Transition.__InitialTransitionMessage );
const promise =this.dispatchMessage( Transition.__InitialTransitionMessage );
this._sessionEndPromise = promise;
return promise;
}
__serializeController() : any {
var sc = <any>this._session_controller;
if ( sc.serialize && typeof sc.serialize==="function" ) {
return sc.serialize();
}
return {};
}
serialize( from_registry?:boolean ) : SerializedSession {
const serializedController = this.__serializeController();
return {
ended : this._ended,
fsm : from_registry ? this._fsm.name : this._fsm.serialize(),
states : this._states.map( st => st.serialize() ),
controller: serializedController
};
}
static deserialize<T,U>( s : SerializedSession, deserializer : (sg : U) => T, from_registry?:boolean ) : Session<T> {
const controller : T = deserializer( s.controller );
const session : Session<T> = new Session( controller );
session.__deserialize( s, from_registry );
return session;
}
__deserialize( s : SerializedSession, from_registry?:boolean ) {
if ( !from_registry ) {
this._fsm = FSMRegistry.register(s.fsm as FSMJson);
} else {
if ( typeof s.fsm==='string' ) {
// try automata saved as string.
this._fsm = FSMRegistry.FSMFromId(s.fsm as string);
} else {
// if not, assume it was saved as fully serialized automata, but loading it was saved as automata name.
this._fsm = FSMRegistry.FSMFromId((s.fsm as FSMJson).name);
}
}
this._ended = s.ended;
this._states = s.states.map( e => {
const c : State = e.current_state === this._fsm.name ?
this._fsm :
this._fsm._states.filter( s => s._name===e.current_state )[0];
const p : State = e.prev_state === "" ?
null :
this._fsm._states.filter( s => s._name===e.prev_state )[0];
return new SessionContext(c, p)
} );
}
addObserver( o : SessionObserver<T> ) {
this._observers.push( o );
}
/**
* User side message.
*/
dispatchMessage<U extends Message>( m : U ) : SessionConsumeMessagePromise<T> {
const c : SessionConsumeMessagePromise<T> = new SessionConsumeMessagePromise();
if ( this._ended ) {
setTimeout( ()=> {
c._error(this, new Error('Session ended'));
}, 0 );
} else {
this._messages_controller.dispatchMessage(m, c);
}
return c;
}
/**
* From SessionController internals.
*/
postMessage( m : Message ) {
this._messages_controller.postMessage( m );
}
__messageImpl( m : Message ) {
if ( m === Transition.__InitialTransitionMessage ) {
this.__consumeMessageForFSM( m );
} else {
this.__consumeMessageForState( m );
}
}
get current_state() : State {
return this._states.length ?
this._states[ this._states.length-1 ].current_state :
null;
}
get prev_state() : State {
return this._states.length ?
this._states[ this._states.length-1 ].prev_state :
null;
}
__onEnter( m : Message ) {
const cs : State = this.current_state;
if ( cs!==null && !cs.__onEnter(this, m) ) {
this.__invoke( cs.name+"_enter", m );
}
}
__onExit( m : Message ) {
const cs = this.current_state;
if ( cs!==null && !cs.__onExit( this, m ) ) {
this.__invoke( cs.name+"_exit", m );
}
}
__invoke( method : string, m : Message ) : any {
return (<any>this._session_controller)[method] && (<any>this._session_controller)[method]( this, this.current_state_name, m );
}
__consumeMessageForFSM( m : Message ) {
const cs = this.current_state;
const fsm = <FSM>cs;
const new_current_state = fsm.initial_state;
this._states.push( new SessionContext( new_current_state, this.current_state ) );
this.__notifyContextCreated( m );
this.__onEnter( m );
}
__findStateWithTransitionForMessage( m : Message ) : State {
const sc = this._states;
let state : State = null;
for( let i= sc.length-1; i>=0; i-- ) {
const current_state : State = sc[i].current_state;
const tr : Transition= current_state.transitionForMessage( m );
if ( tr!==null ) {
state= current_state;
break;
}
}
return state;
}
__exitAllStatesUpToStateWithTransitionForMessage( stateWitTransition : State, m : Message ) {
while( this._states.length ) {
let cs : SessionContext= this._states[ this._states.length - 1 ];
this.__onExit( m );
if ( cs.current_state!==stateWitTransition ) {
this._states.pop();
this.__notifyContextDestroyed( m );
} else {
break;
}
}
}
__popAllStates( m : Message ) {
while( this._states.length ) {
this.__onExit(m);
this._states.pop();
this.__notifyContextDestroyed( m );
}
}
__setCurrentState( s :State, m : Message ) {
let prev : State = null;
if ( this._states.length ) {
prev= this._states.pop().current_state;
}
this._states.push( new SessionContext( s, prev ) );
this.__notifyStateChange( m );
this.__onEnter( m );
}
__endSession( m : Message ) {
this._ended= true;
this.__notifySessionEnded( m );
}
get current_state_name() {
return this._states.length ?
this._states[ this._states.length - 1].currentStateName() :
"<No current state>";
}
get prev_state_name() {
return this._states.length ?
this._states[ this._states.length - 1].prevStateName() :
"<No prev state>";
}
__consumeMessageForState( m : Message ) {
if ( !this._ended ) {
const state_for_message:State = this.__findStateWithTransitionForMessage(m);
if (null !== state_for_message) {
this.__processMessage(state_for_message, m);
} else {
throw new Error(`No message: '${m.msgId}' for state: '${this.current_state_name}'`);
}
} else {
throw new Error(`Session is ended. Message ${m.msgId} is discarded.`);
}
}
__processMessage( state_for_message : State, m : Message ) {
const tr : Transition = state_for_message.transitionForMessage(m);
const transition_event : string = tr.event;
if ( !this.__invoke(transition_event+"_preGuard", m) ) {
this.__exitAllStatesUpToStateWithTransitionForMessage(state_for_message, m);
this.__invoke(transition_event + "_transition", m);
let next:State;
if (! this.__invoke(transition_event + "_postGuard", m) ) {
next = tr.final_state;
} else {
next = state_for_message;
}
this.__setCurrentState(next, m);
if (next.isFinal()) {
// this.__popAllStates(m);
this.__endSession(m);
}
}
}
fireCustomEvent( message : any ) {
for( let o of this._observers ) {
o.customEvent( {
session : this,
message : null,
current_state_name : this.current_state_name,
prev_state_name : this.prev_state.name,
custom_message : message
} );
}
}
__notifySessionEnded( m : Message ) {
this.__notify( m, "sessionEnded" );
}
__notifyContextCreated( m : Message ) {
this.__notify( m, "contextCreated" );
}
__notifyContextDestroyed( m : Message ) {
this.__notify( m, "contextDestroyed");
}
__notifyStateChange( m : Message ) {
this.__notify( m, "stateChanged" );
}
__notify( m : Message, method : string ) {
for( let o of this._observers ) {
(<any>o)[method] && (<any>o)[method]( {
session : this,
message : m,
current_state_name : this.current_state_name,
prev_state_name : this.prev_state_name
} );
}
}
get controller() {
return this._session_controller;
}
printStackTrace() {
if ( this._states.length===0 ) {
console.log("session empty");
} else {
console.log("session stack trace:");
this._states.forEach( function( s ) {
s.printStackTrace();
});
}
}
}
export class SessionMessageControllerMessageQueue<T> {
_session : Session<T>;
_triggering_message : Message;
_messages_queue : Message[];
_callback : SessionConsumeMessagePromise<T>;
constructor( session : Session<T>, m : Message, callback? : SessionConsumeMessagePromise<T> ) {
this._session = session;
this._callback = typeof callback!=="undefined" ? callback : null;
this._triggering_message = m;
this._messages_queue = [ m ];
}
postMessage( m : Message ) {
this._messages_queue.push( m );
}
__consumeMessage() : boolean {
let ret : boolean;
if ( this._messages_queue.length ) {
const m = this._messages_queue.shift();
try {
this._session.__messageImpl(m);
ret = false;
} catch (e) {
// console.error(`consume for message '${m.msgId}' got exception: `, e);
this._messages_queue= [];
this._callback.__error( this._session, e );
ret = true;
}
} else {
ret = true;
if ( this._callback ) {
this._callback.__success( this._session, this._triggering_message );
}
}
return ret;
}
}
export class SessionMessagesController<T> {
_session : Session<T>;
_message_queues : SessionMessageControllerMessageQueue<T>[];
_consuming : boolean;
constructor( session : Session<T> ) {
this._message_queues = [];
this._session = session;
this._consuming = false;
}
dispatchMessage( m : Message, callback? : SessionConsumeMessagePromise<T> ) {
this._message_queues.push( new SessionMessageControllerMessageQueue(this._session, m, callback) );
this.__consumeMessage();
}
postMessage( m : Message ) {
this._message_queues[0].postMessage( m );
this.__consumeMessage();
}
__consumeMessage() {
if ( !this._consuming ) {
this._consuming = true;
setImmediate( this.__consumeOne.bind(this) );
}
}
__consumeOne() {
if (this._message_queues.length) {
if ( this._message_queues[0].__consumeMessage() ) {
this._message_queues.shift();
}
}
if ( this._message_queues.length ) {
setImmediate( this.__consumeOne.bind(this) );
} else {
this._consuming = false;
}
}
}
export class Automata {
static RegisterFSM( file : string|FSMJson ) {
if ( typeof file==="string" ) {
} else {
FSMRegistry.register( file );
}
}
static CreateSession<T>( controller : T, fsm_name : string, o? : SessionObserver<T> ) : SessionConsumeMessagePromise<T> {
return FSMRegistry.createSession( controller, fsm_name, o );
}
}