Novel Personal Mantras

    fsm-as-promised
    TypeScript icon, indicating that this package has built-in type declarations

    0.17.1 • Public • Published

    Finite State Machine as Promised

    A minimalistic finite state machine library for browser and node implemented using promises.

    NPM Version NPM License Build Status Coverage Status Code Climate Greenkeeper badge Known Vulnerabilities NPM Downloads

    Gitter

    📢 For Visual Studio Code users: checkout the UML visualization extension.

    How to use

    Installation and Setup

    Run npm install fsm-as-promised to get up and running. Then:

    // ES5
    const StateMachine = require('fsm-as-promised');
    
    // ES6
    import StateMachine from 'fsm-as-promised';

    Configuring promise library

    StateMachine.Promise = YourChoiceForPromise

    You can choose from the following promise libraries:

    If the environment does not provide Promise support, the default implementation is es6-promise.

    The library works also with the promise implementation bundled with es6-shim.

    Create finite state machine

    A state machine object can be created by providing a configuration object:

    const fsm = StateMachine({
      events: [
        { name: 'wait', from: 'here'},
        { name: 'jump', from: 'here', to: 'there' },
        { name: 'walk', from: ['there', 'somewhere'], to: 'here' }
      ],
      callbacks: {
        onwait: function () {
          // do something when executing the transition
        },
        onleavehere: function () {
          // do something when leaving state here
        },
        onleave: function () {
          // do something when leaving any state
        },
        onentersomewhere: function () {
          // do something when entering state somewhere
        },
        onenter: function () {
          // do something when entering any state
        },
        onenteredsomewhere: function () {
          // do something after entering state somewhere
          // transition is complete and events can be triggered safely
        },
        onentered: function () {
          // do something after entering any state
          // transition is complete and events can be triggered safely
        }
      }
    });

    Define events

    The state machine configuration contains an array of event that convey information about what transitions are possible. Typically a transition is triggered by an event identified by name, and happens between from and to states.

    Define callbacks

    The state machine configuration can define callback functions that are invoked when leaving or entering a state, or during the transition between the respective states. The callbacks must return promises or be thenable.

    Initialisation options

    Initial state

    You can define the initial state by setting the initial property:

    const fsm = StateMachine({
      initial: 'here',
      events: [
        { name: 'jump', from: 'here', to: 'there' }
      ]
    });
    
    console.log(fsm.current);
    // here

    otherwise the finite state machine's initial state is none.

    Final states

    You can define the final state or states by setting the final property:

    const fsm = StateMachine({
      initial: 'here',
      final: 'there', //can be a string or array
      events: [
        { name: 'jump', from: 'here', to: 'there' }
      ]
    });

    Target

    An existing object can be augmented with a finite state machine:

    const target = {
          key: 'value'
        };
    
    StateMachine({
      events: [
        { name: 'jump', from: 'here', to: 'there' }
      ],
      callbacks: {
        onjump: function (options) {
          // accessing target properties
          console.log(target.key === this.key);
        }
      }
    }, target);
    
    target.jump();

    Custom error handler

    You can override the default library error handler by setting the error property:

    const fsm = StateMachine({
      initial: 'red',
      events: [
        { name: 'red', from: 'green', to: 'red' }
      ],
      error: function customErrorHandler(msg, options) {
        throw new Error('my error');
      }
    });

    The value of the error property is a function that expects two arguments:

    • msg a string containing the error reason
    • options an object havin as properties the name of the transition and the from state when the error occurred.

    Callbacks

    Arguments

    The following arguments are passed to the callbacks:

    const fsm = StateMachine({
        events: [
          { name: 'jump', from: 'here', to: 'there' }
        ],
        callbacks: {
          onjump: function (options) {
            // do something with jump arguments
            console.log(options.args);
    
            // do something with event name
            console.log(options.name);
    
            // do something with from state
            console.log(options.from);
    
            // do something with to state
            console.log(options.to);
    
            return options;
          }
        }
      });
    
    fsm.jump('first', 'second');

    Synchronous

    You can define synchronous callbacks as long as the callback returns the options object that is going to be passed to the next callback in the chain:

    const fsm = StateMachine({
        events: [
          { name: 'jump', from: 'here', to: 'there' }
        ],
        callbacks: {
          onjump: function (options) {
            // do something
    
            return options;
          }
        }
      });
    
    fsm.jump();

    Asynchronous

    You can define asynchronous callbacks as long as the callback returns a new promise that resolves with the options object when the asynchronous operation is completed. If the asynchronous operation is unsuccessful, you can throw an error that will be propagated throughout the chain.

    const fsm = StateMachine({
        events: [
          { name: 'jump', from: 'here', to: 'there' }
        ],
        callbacks: {
          onjump: function (options) {
            return new Promise(function (resolve, reject) {
              // do something
              resolve(options);
            });
          }
        }
      });
    
    await fsm.jump();

    Call order

    The callbacks are called in the following order:

    callback state in which the callback executes
    onleave{stateName} from
    onleave from
    on{eventName} from
    onenter{stateName} from
    onenter from
    onentered{stateName} to
    onentered to

    A state is locked if there is an ongoing transition between two different states. While the state is locked no other transitions are allowed.

    If the transition is not successful (e.g. an error is thrown from any callback), the state machine returns to the state in which it is executed.

    Returned values

    By default, each callback in the promise chain is called with the options object.

    Passing data between callbacks

    Callbacks can pass values that can be used by subsequent callbacks in the promise chain.

    const fsm = StateMachine({
      initial: 'one',
      events: [
        { name: 'start', from: 'one', to: 'another' }
      ],
      callbacks: {
        onleave: function (options) {
          options.foo = 2;
        },
        onstart: function (options) {
          // can use options.foo value here
          if (options.foo === 2) {
            options.foo++;
          }
        },
        onenter: function (options) {
          // options.foo === 3
        }
      }
    });

    This also includes callbacks added to the chain by the user.

    fsm.start().then(function (options) {
      // options.foo === 3
    });

    Beyond the library boundary

    The options object can be hidden from the promises added by the end user by setting the options.res property. This way the subsequent promises that are not part of the state machine do not receive the options object.

    const fsm = StateMachine({
      initial: 'one',
      events: [
        { name: 'start', from: 'one', to: 'another' }
      ],
      callbacks: {
        onstart: function (options) {
          options.res = {
            val: 'result of running start'
          };
        }
      }
    });
    
    const result = await fsm.start();
    
    console.log(result);

    Configuring callback prefix

    By default, the callback names start with on. You can omit the prefix by setting it to empty string or assign any other prefix:

    StateMachine.callbackPrefix = 'customPrefix';

    Common Methods

    The library adds the following utilities to the finite state machine object:

    • can(event) checks if the event can be triggered in the current state,
    • cannot(event) checks if the event cannot be triggered in the current state,
    • is(state) checks if the state is the current state,
    • isFinal(state) checks if the state is final state. If no state is provided the current state is checked.
    • hasState(state) checks if the finite state machine has the state.
    • instanceId returns the uuid of the instance

    Emitted Events

    The finite state machine object is an EventEmitter. By default, the library emits state event whenever the state machine enters a new state.

    You can define and emit new events.

    Handling Errors

    Errors thrown by any of the callbacks called during a transition are propagated through the promise chain and can be handled like this:

    fsm.jump().catch(function (err) {
      // do something with err...
      // err.trigger - the event that triggered the error
      // err.current - the current state of the state machine
      // err.message - described bellow...
      // err.pending - an object containing the description of intra-state pending transitions
    });

    The library throws errors with the following messages:

    message explanation note
    Ambigous transition The state machine has one transition that starts from one state and ends in multiple must be fixed during design time
    Previous transition pending The previous intra-state transition(s) is in progress preventing new ones until it has completed -
    Previous inter-state transition started Inter-state transition started -
    Invalid event in current state The state machine is in a state that does not allow the requested transition -

    ⚠️ Unhandled errors may lead to inconsistent state machine. If you reserved resources as part of a transition, you have to release them if an error occured.

    Graceful error recovery

    It is not advisable to let the errors that can be handled gracefully at callback level to propagate to the end of the promise chain.

    The following is an example where the error is handled inside a synchronous callback:

    const fsm = StateMachine({
      initial: 'green',
      events: [
        { name: 'warn',  from: 'green',  to: 'yellow' }
      ],
      callbacks: {
        onwarn: function (options) {
          try {
            throw new Error('TestError');
          } catch (err) {
            // handle error
            return options;
          }
        }
      }
    });
    
    await fsm.warn()
    
    fsm.current === 'yellow';
    // true

    The same inside an asynchronous callback:

    const fsm = StateMachine({
      initial: 'green',
      events: [
        { name: 'warn',  from: 'green',  to: 'yellow' }
      ],
      callbacks: {
        onwarn: function (options) {
          return new StateMachine.Promise(function (resolve, reject) {
            reject(new Error('TestError'));
          }).catch(function (err) {
            // handle error
            return options;
          });
        }
      }
    });
      
    await fsm.warn()
    
    fsm.current === 'yellow';
    // true

    Recipes

    Conditional transitions

    The library provides a way to define conditional transitions:

    StateMachine({
      events: [
        { name: 'conditional',
          from: 'init',
          to: ['one', 'two'],
          condition: function (options) {
            return 0; // transition to state 'one'
          }
        }
      ]
    });

    The above is equivalent to:

    StateMachine({
      events: [
        { name: 'conditional',
          from: 'init',
          to: ['one', 'two'],
          condition: function (options) {
            return 'one'; // transition to state 'one'
          }
        }
      ]
    });

    The condition callback must return the to Array's index of the selected state, the name of the selected state, or a promise which resolves to either. The condition callback is executed after on{eventName} callback.

    If the above is not suitable, complex conditional transitions can be achieved through transitioning explicitly to a pseudo state where the condition is checked, then the appropriate event is triggered:

    StateMachine({
      events: [
        { name: 'trigger', from: 'existing', to: 'pseudo' },
        { name: 'triggerOptionA', from: 'pseudo', to: 'option-a' },
        { name: 'triggerOptionB', from: 'pseudo', to: 'option-b' }
      ],
      callbacks: {
        onenteredpseudo: function () {
          if (condition) {
            this.triggerOptionA();
          } else {
            this.triggerOptionB();
          }
        }
      }
    });

    If your pseudo state's callback returns a Promise, you must return the call to the event function; e.g. return this.triggerOptionA().

    Tooling

    Visual Studio Code extension

    You can visualize the state machine as a UML diagram in vscode using the Finite state machine viewer extension.

    UML visualization

    The state machine definitions can be visualized as UML diagrams using fsm2dot.

    Install fsm2dot and graphviz, then:

    fsm2dot -f fsm.js -o fsm.dot
    dot -Tpdf fsm.dot -o fsm.pdf

    Contributing

    Install the library and run tests:

    npm install
    npm test
    

    License

    The library is available under the MIT license.

    Credits

    The framework is heavily influenced by Jake Gordon's javascript-state-machine.

    Install

    npm i fsm-as-promised

    DownloadsWeekly Downloads

    4,610

    Version

    0.17.1

    License

    MIT

    Unpacked Size

    42.8 kB

    Total Files

    8

    Last publish

    Collaborators

    • vstirbu