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
}
}
Top comments (0)