DEV Community

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

Posted on

1 1

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

Image of AssemblyAI tool

Transforming Interviews into Publishable Stories with AssemblyAI

Insightview is a modern web application that streamlines the interview workflow for journalists. By leveraging AssemblyAI's LeMUR and Universal-2 technology, it transforms raw interview recordings into structured, actionable content, dramatically reducing the time from recording to publication.

Key Features:
🎥 Audio/video file upload with real-time preview
🗣️ Advanced transcription with speaker identification
⭐ Automatic highlight extraction of key moments
✍️ AI-powered article draft generation
📤 Export interview's subtitles in VTT format

Read full post

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay