smplie finite state machine for EcmaScript
Published: 2022-09-15 14:07:08


# Mach: flexible FSM in ECMAScript

Hi there,

I just developped a simple Finite State Machine in JS. My initial goal was to  integrate it within a SvelteKit component to provide UI state management.


## Design and usage

this is a single ECMAScript file; simply import it in yout code, and call the FSM () constructor with its parameters:

```
    import { FSM } from './FSM.js'
    
    const fsm = FSM ({
        debug: true,
        jumps: ({ state }) => console.log ({ state }),
        success: ({ state }) => console.log ('success'),
        failure: ({ state }) => console.log ('failure')
    })
```

Then you can simply define states and transition arrows:

```
    fsm.state ({ name: 'INIT', init: true })
    fsm.state ({ name: 'TERM', term: true })
    fsm.state ({ name: 'OFF' })
    fsm.state ({ name: 'ON' })

    fsm.arrow ({ from: 'INIT', accept: 'activate', to: 'OFF'})
    fsm.arrow ({ from: 'OFF', accept: 'switch', to: 'ON'})
    fsm.arrow ({ from: 'ON', accept: 'switch', to: 'OFF'})
    fsm.arrow ({ from: 'ON', accept: "kick", to: 'TERM'})
```

Of course with this way of doing things, you can freely hydrate states and transition arrows from an external data store such a database; or modularize you FSM design according to logial tests !

After all, pack the machine to run it, and read some entries:

```
    fsm.pack ({ state: 'INIT '})
    
    fsm.accept ({ word: 'activate'})
    fsm.accept ({ word: 'on'})
    fsm.accept ({ word: 'kick'})
 
    // console.log ('success')    
```


## Implementation details

all entries (STATES and reanition arrows) are stored in the same array structure, as duck blocks. The items list is doubled by a lookup map which holds pairs (name) => [array-of-indexes] for all name  inside the machine (UPPERCASED for STATES and lowercased for arrows). This shorts the time used to locate states/words blocks .


## Further steps

adding support for asynchronous transduction operations.

```
    /// Here's the complete code for the Mach FSM ...


export const FSM = ({ jumping, success, failure, debug } = {}) => {

    // holds STATE and ARROW items
    const entries = [{ uid: 0, def: 'ROOT' }]

    // aliases lookups table
    const lookups = new Map ()

    const runtime = {
        GUID: 0,
        state: null,  
        trace: [],
        data: null
    }
    
    // state definition
    const state = ({ name, init, term, payload }) => {
        const def = 'STATE'
        const alias = ('' + name).toUpperCase ()
        const uid = ++runtime.GUID
        const uids = lookups.get(alias) || []
        const len = uids.length

        const item = {
            uid, def, alias, init, term, payload
        }

        if (len === 0) {
            entries.push (item)       
            lookups.set (alias, [uid])
        } else {
            throw new Error ('duplicate entry ' + uis + ' ' + alias)
        }

        if (debug) {
            if (len === 0) {
                console.log ('creating new state', { alias, uid })
            } else {
                console.log ('replacing item', { alias, uid })
            }
        }
    }

    // arrow definition
    const arrow = ({ from, accept, to, ops }) => {
        const def = 'ARROW'
        const alias = ('' + accept).toLowerCase ()
        const uid = ++runtime.GUID
        const uids = lookups.get(alias) || []
        const len = uids.length

        const item = {
            uid, def, alias, from, accept, to, ops 
        }

        entries.push (item)
        uids.push (uid)
        lookups.set (alias, uids)

//        console.log ('craating arrow', { item })
   }

    // ready to run !!!
    const pack = ({ state, data }) => {
        const alias = ('' + state).toUpperCase ()

        runtime.state = alias
        runtime.data = data

        if (debug) {
            console.log('Finite State Machine packing', alias) 
        }
    }

    // read entry word
    const read = ({ word, data }) => {
        const alias = ('' + word).toLowerCase ()
        const uids = lookups.get (alias) || []
        let accepted = false 

        console.log ('read', { [alias]: uids.join(', ') })

        uids.map ((uid) => entries [uid])
            .map ((item) => {
                console.log ('MAP', { item })
                return item
            })
            .filter ((item) => accepted === false)
            .filter ((item) => item.def === 'ARROW')
            .filter ((item) => item.from === runtime.state)
            .filter ((item) => item.accept === alias)
            .map ((item) => {
                const suids = lookups.get (item.to) || []
                const final = entries [suids[0]]                

                runtime.state = item.to
                runtime.trace.push (alias)
                accepted = true

                jumping && jumping.call && jumping.call (null, {
                    trace: runtime.trace,
                    result: runtime.data,
                    state: runtime.state,
                    word: alias                   
                })

                item.ops && item.ios.call && item.ops.call (null, { 
                    data, 
                    trace: runtime.trace,
                    result: runtime.data,
                    state: runtime.state,
                    word: alias                   
                })

                if (final.term) {
                    success && success.call (null, { 
                        data, 
                        trace: runtime.trace,
                        result: runtime.data,
                        state: runtime.state,
                        word: alias                   
                    })
                }

                return true
            })

        if (accepted === false) {
            failure && failure.call (null, { 
                data,                      
                trace: runtime.trace,
                result: runtime.data,
                state: runtime.state,
                word: alias                   
             })
        }
    }

    // return debug table as string
    const table = () => {
        const texts = []

        texts.push ('~~~ Finistamach: lightweight Finite State Machine ~~~')
        texts.push ('')
        texts.push ('uid\tdef\talias\t\tmore info...')
        texts.push ('--------'.repeat(5))

        entries.map ((item) => {

            if (item.def === 'STATE') {
                texts.push ([
                    item.uid, 'STATE', item.alias + '\t', 
                    (item.init ? '*init*' : '') +  (item.term ? "*term*" : "")
                ].join('\t'))
            }

            if (item.def === 'ARROW') {
                texts.push ([
                    item.uid, 'ARROW', item.alias + '\t', 
                    item.from + ' --{' + item.accept +  '}-> ' +  item.to
                ].join('\t'))            }
        })

        return texts.join('\n')
    }


    return {
        state, arrow,  pack, read, table
    }
}


```