UNPKG

@fabiospampinato/fsm

Version:

Finite State Machine implementation, with support for guards and enter/exit events.

249 lines (135 loc) 5.55 kB
/* IMPORT */ import compact = require ( 'lodash/compact' ); import get = require ( 'lodash/get' ); import includes = require ( 'lodash/includes' ); import isFunction = require ( 'lodash/isFunction' ); import isString = require ( 'lodash/isString' ); import isUndefined = require ( 'lodash/isUndefined' ); import FIFO, { type as FIFOType } from '@fabiospampinato/fifo'; import Lockable, { type as LockableType } from '@fabiospampinato/lockable'; import {guard, model, state, states, transition, transitionObj, statesObj} from './types'; /* FSM */ //TODO: Add support for actions on submodules, like `history.start`: 'end' //TODO: Add support for an `action` key on the transition object, that would be the executed action class FSM { /* PROPERTIES */ model: model; states: statesObj; queue: FIFOType; initial: state; processing: LockableType; state: state; /* CONSTRUCTOR */ constructor ( model: model, states: statesObj, initial: state ) { this.model = model; this.states = states; this.queue = new FIFO (); this.initial = initial; this.processing = new Lockable (); this.set ( this.initial ); } /* UTILITIES */ _isValidState ( state: state ): boolean { return this.states.hasOwnProperty ( state ); } _isValidTransition ( state: state, transition: transition ): boolean { return this._isValidState ( state ) && !!this._getTransitionState ( state, transition ) && this._isValidTransitionGuard ( state, transition ); } _isValidTransitionGuard ( state: state, transition: transition ): boolean { const guards = this._getTransitionGuard ( state, transition ); if ( !guards ) return true; for ( let guard of guards.split ( '|' ) ) { const parts = guard.match ( /^(!?)(\w+)(?:\.(\w+))?$/ ); if ( !parts ) throw new Error ( '[fsm] Invalid guard' ); const affirmative = ( parts[1] !== '!' ), method = compact ( parts.slice ( 2 ) ).join ( '.' ); if ( !!this._callModel ( method ) !== affirmative ) return false; } return true; } _getTransition ( state: state, transition: transition ): transitionObj | undefined { const stateObj = this.states[state]; if ( !stateObj.hasOwnProperty ( 'transitions' ) ) return; return stateObj.transitions[transition]; } _getTransitionState ( state: state, transition: transition ): state | undefined { const transitionObj = this._getTransition ( state, transition ); if ( isUndefined ( transitionObj ) || isString ( transitionObj ) ) return transitionObj; return transitionObj.state; } _getTransitionGuard ( state: state, transition: transition ): guard | undefined { const transitionObj = this._getTransition ( state, transition ); if ( isUndefined ( transitionObj ) || isString ( transitionObj ) ) return; return transitionObj.guard; } _getExistsEnters ( prevState: state, nextState: state ): [states, states] { // Exists just to provide a DRYer implementation of HSM if ( prevState === nextState ) return [[], []]; return [[prevState], [nextState]]; } _callModel ( path: string, args: any[] = [] ) { const method = get ( this.model, path ); if ( !isFunction ( method ) ) return; const context = includes ( path, '.' ) ? get ( this.model, path.split ( '.' ).slice ( 0, -1 ).join ( '.' ) ) : this.model; return method.apply ( context, args ); } /* GET */ get (): state { return this.state; } /* SET */ set ( state: state ): this { if ( !this._isValidState ( state ) ) throw new Error ( `[fsm] Invalid state "${state}"` ); this.state = state; return this; } /* RESET */ reset (): this { return this.set ( this.initial ); } /* IS */ is ( state: state ): boolean { return this.state === state; } isDoable ( transition: transition ): boolean { return this._isValidTransition ( this.state, transition ); } /* TRANSITION */ do ( ...args ): this { // Just an alias of `transition` return this.transition.call ( this, ...args ); } transition ( transition: transition ): this; transition ( transition: transition, ...args ): this; transition ( transition: transition, ...args ): this { this.queue.add ( [transition, ...args] ); if ( this.processing.isLocked () ) return this; this.processing.lock (); while ( true ) { const next = this.queue.next (); if ( !next ) break; this._transition.apply ( this, next ); } this.processing.unlock (); return this; } _transition ( transition: string, ...args ) { if ( !this.isDoable ( transition ) ) throw new Error ( `[fsm] Invalid transition "${transition}" from state "${this.state}"` ); let nextState = this._getTransitionState ( this.state, transition ); if ( !nextState ) throw new Error ( `[fsm] Invalid transition "${transition}" from state "${this.state}"` ); if ( nextState === '*' ) nextState = this.state; // `*` states always point to the current state const [exits, enters] = this._getExistsEnters ( this.state, nextState ); exits.forEach ( this._exit.bind ( this ) ); this._callModel ( transition, args ); enters.forEach ( this._enter.bind ( this ) ); this.set ( nextState ); } /* EVENTS */ _exit ( state: state ): void { this._callModel ( `${state}Exit` ); } _enter ( state: state ): void { this.set ( state ); this._callModel ( `${state}Enter` ); } } /* EXPORT */ export default FSM;