stepstate

0.0.1 • Public • Published

Stepstate (Finite State Machine Framework)

A convention for defining stateful attributes and a small functional framework for stepping stateful objects through a finite state machine which you define.

Install

npm install --save stepstate

Include

const Steps= require('stepstate')

Philosophy of Use

A finite state machine is a good way to define complex data transitions. This framework allows you to use a regular object and provides a concise way to define your (async) state handlers. Once defined, the state transition is accomplished through a single async function which takes in the current state and returns the new state. Each step checks the "state" attribute on your data, to know which state it is currently in. Each transition to a new state is recorded in the "history" array. Call step iteratively until your state reaches its terminating point which can be read as the "done" data attribute. Outside of the "state", "done", "history" attributes, you can include any other custom data you need for your transitions.

Stateful Object Schema

As long as your objects conform to the following interface, they can be transitioned in the state machine handlers. We propose a very minimal data structure which also can store past state transition.

  // State Schema
  {
    state:string //Name of state, destination in state machine
    history:array[{state,updated}] //History of past states, most recent first
    done:boolean //If state has terminated, done=true otherwise done=false
    updated: unix timestamp in ms //Last time state was touched
  }

  //State History Schema
  {
    state:string
    updated:unix timestamp in ms
  }

State Machine Handlers

State machine step handlers require user defined functions to describe how state is handled at each state, transitioned and ended. Transitions are returned as a string. If nothing is returned, the state will not change. State termination is signified but setting "done=true" on the stateful object. Once done, the object will no longer be sent through the state machine. Handlers can be async promises. Muating object can cause side effects, its recommended you clone it before passing in.

  const Steps = require('stepstate')

  //you can return the next state
  //or set obj.state = 'NextState'
  //and return object
  const steps = {
    Start:obj=>{
      return 'Next'
    },
    Next:obj=>{
      return 'Finalize'
    },
    Finalize:obj=>{
      return 'Done'
    },
    Done:obj=>{
      obj.done = true
      return 'Success'
    }
  }

  const step = Steps(steps)

  async function run(myObject){
    //myObject now has state=='Next'
    //its history will show it was in state 'Start'
    myObject = await step(myObject)

    //myObject now has state=='Finalize'
    //its history will show it was in state 'Start' and 'Next'
    myObject = await step(myObject)

    //myObject now has state=='Done'
    //its history will show it was in state 'Start' and 'Next' and 'Finalize'
    myObject = await step(myObject)

    //myObject now has state=='Success' and done == true
    //this object has reached terminating state
    myObject = await step(myObject)

    return myObject
  }

  let myObject = {state:'Start'}

  run(myObject).then(finalObject=>{
    //done
  })
  

Error Handler

Any errors thrown in handlers can be intercepted and processed on a central error callback. If no error handler is given, errors will be thrown. Unknown state transitions will not be passed into your error handler but thrown and must be caught outside the call to handle.

const steps = {
  //...other state handlers
  //catch is a reserved word used for catching errors thrown in handlers
  catch:(err,object)=>{
    //handle errors here, log them, maybe set the state of the object
    console.log('Error in statemachine',err)
    object.state = 'Error'
    object.error = err
    object.done = true
  }
}

const step = Steps(steps)

Creating a Stateful object

Small utility function for attaching stateful properties to any object. If no object is provided it will just return a bare starting state object. By default the starting state is "Start", but you can specify your own starting state in the object.

  const transaction = Steps.State({
    id:'tx1',
    amount:1,
    to:'user1',
    from:'user2',
  })

  console.log(transaction)
  //{
  //  id:'tx1',
  //  amount:1,
  //  to:'user1',
  //  from:'user2',
  //  state:'Start',
  //  done:false,
  //  history:[],
  //  updated:12343234,
  //}

Providing Context to your States

You may need to access other types of state within your handlers. This is an example of how to provide that within this framework.

  const Context = {
    multiplier:100,
    wallets: {...}//Assume some kind of wallets interface
  }

  function steps(context){
    return {
      Start: tx =>{
        const {wallets, multiplier} = context
        tx.amount *= multiplier
        //not enough funds
        if(!wallet.hasAmount(tx.from,tx.amount)){
          tx.state = 'Not Enough Funds'
          tx.done = true
          return tx
        }
        tx.state = 'Transact'
        return tx
      },
      Transact: tx =>{
        const {wallets} = context

        //continue transaction
        wallet.deduct(tx.from,tx.amount)
        wallet.add(tx.to,tx.amount)
        tx.done = true
        tx.state = 'Success'
        return tx
      },
    }
  }

  const step = Steps(steps(Context))

API

Defines how to use the Stepstate API.

Initialization

Create an instance of your "step" function with your states.

const Steps= require('stepstate')
const step = Steps(stateHandlers)

function(object:stateHandlers) => async function(object:statefulObject, ...arguments)

  • stateHandlers:object - an object containing keys of state names and functions for values. See State Handlers
  const Steps = require('stepstate')

  const steps = {
    'Start': x=> {
      return 'Middle'
    },
    'Middle': x=>{
      return 'End'
    },
    'End': x=>{
      x.done = true
    }
  }

  const step = Steps(steps)

State Handlers

You must create state handlers to pass into the Steps framework. These handlers have a specific interface they must conform to in order for them to be compatible with the framework. Handlers can return string, nothing or the data object,

State handler object follow this pattern:

object['string'] = async function(data, ...arguments)

Each function has these parameters

async function (object,...arguments) => string | undefined

  • data : stateful object - Your stateful data object. You should mutate this as needed, and set done=true when the data no longer needs to run in state machine.
  • ...arguments : any - any arguments you passed in along with your data when calling step
  • return => Return nothing, a string representing the state name to transition to, or the original data object.
const handlers = {
  'Start':async function(data, ...arguments)...,
  'Middle': async function(data, ...arguments)...,
  'End':async function(data, ...arguments)...,
}

State Handler Errors

If any state handler has an uncaught error, you can intercept it with the catch state. This is a reserved word within this framework, so your object states should not use catch. If no catch state is supplied error will be thrown.

function (error, object, ...arguments) => string | undefined

  • error: Error object - This is the error which was thrown
  • object: Stateful object - This is the object state which caused the error
  • ...arguments: any - Arguments passed into the step function
  • return => Return nothing, a string to transition to a new state, or the original data object.

Step

This is an async function. Once you have a step function instance, call it with a stateful object as first parameter. Other arguments will be passed through to your state handlers.

async function(object:statefulObject, ...arguments) => object:statefulObject

Call the step function with these parameters

  • data: stateful object - Your stateful data object which you want to transition to the next step.
  • ...arguments - any arguments you want to inject into the state function
  let statefulObject = {
    state:'Start',
  }

  const step = ... //see Initialization

  async function run(){

     //when done == true your object is done being processed by this state machine
    while(!statefulObject.done){
      //calling step with stateful object and some random data which is passed through
      statefulObject = await step(statefulObject,Date.now(),'test')
    }
  }

  run()

Creating Default Stateful Object

This library has a helper function to create a default state compatible with the Step framework.

Steps.State(data) => stateful object

  • data:object - a non stateful object
  • returns:stateful object => which will be returned with merged stateful data.

Stateful Object Schema

A stateful object is just a regular js object with some extra properties: state, history, done, updated

Object

{
  state:string,
  history:array[{state,updated}],
  done:boolean,
  updated:Unix Timestamp,
}

Package Sidebar

Install

npm i stepstate

Weekly Downloads

1

Version

0.0.1

License

ISC

Unpacked Size

13 kB

Total Files

6

Last publish

Collaborators

  • daywiss