DEV Community

François-Emmanuel CORTES
François-Emmanuel CORTES

Posted on

smplie finite state machine for EcmaScript

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')
    })
Enter fullscreen mode Exit fullscreen mode

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'})
Enter fullscreen mode Exit fullscreen mode

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')    
Enter fullscreen mode Exit fullscreen mode

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
    }
}


Enter fullscreen mode Exit fullscreen mode

Top comments (0)