/* * Vaucanson FSM * author: hefeust * 2021-09-30 */ const make_fsm = ({ debug, acceptor, rejector }) => { // the states definitions list const states_entries = [] // states definitions fast access lookup const states_lookups = new Map() // gets a state, given its state_name const safe_get_entry = ({ state_name }) => { const entry_index = states_lookups.get(state_name) const entry = states_entries[entry_index] if (false === states_lookups.has(state_name)) { console.log('states_lookups no valid index for name: ' + state_name) } if (!entry) { console.log('no entry found for entry_index: ' + entry_index) } return entry } // user defines a state with this... const def = (state_props = {}) => { const { state_name, /* string */ init, /* boolean */ term, /* boolean */ text, value } = state_props const change_lookups = new Map() const change_entries = [] const entry_index = states_entries.length const state_entry = { state_props, change_lookups, change_entries } states_entries.push(state_entry) states_lookups.set(state_name, entry_index) /* "on" fluent interface */ return def_returns({ state_name }) } /* user defines changes */ const def_change = ({ state_name, change_props} ) => { const { read, jump, action } = change_props const state_entry = safe_get_entry({ state_name }) const { change_lookups, change_entries } = state_entry const change_index = change_entries.length change_entries.push(change_props) change_lookups.set(read, change_index) /* async part */ change_props.action = () => { const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 1000) }) return promise } return def_returns({ state_name }) } /* fluent interface **/ const def_returns = ({ state_name }) => { return { on: (change_props) => def_change({ state_name, change_props }) } } // // runtime // const pending = [] const trace = [] const runtime = { active_sate_name: '#NO-STATE!', debug_flag: false, accepts_counter: 0, synchro_counter: 0, running: false } // user starts the machine const start = ({ state_name, context, debug }) => { runtime.active_state_name = state_name runtime.debug_flag = debug ? debug : debug_flag runtime.accepts_counter = 0 runtime.synchro_counter = 0 runtime.running = true if (false === states_lookups.has(runtime.active_state_name)) { throw new Error('state not found: ' + runtime.active_state_name) } } // get state own,ed changes const safe_get_change = ({ state_name, read }) => { // console.log('safe_get_change', { state_name, read }) const prev_state_entry = safe_get_entry({ state_name }) const { change_entries, change_lookups } = prev_state_entry const change_index = change_lookups.get(read) const change = change_entries[change_index] // console.log({ change }) return change } // user accepts external input const accept = ({ count, read, payload }) => { const accepts = { counter: runtime.accepts_counter, active_state_name: runtime.active_state_name, read } if(false === runtime.running) { throw new Error('Not yet running or just stopped...') } // console.log('ACCEPTS:', { accepts }) pending.push(accepts) runtime.accepts_counter++ trace.push(read) run_queue() } // run the promsie queue const run_queue = () => { if (pending.length) { const accepts = pending.shift() const change = safe_get_change({ state_name: runtime.active_state_name, read: accepts.read }) if (change) { change.action.call(null).then((result) => { runtime.active_state_name = change.jump runtime.synchro_counter++ run_queue() }) } else { const lag = runtime.accepts_counter > 2 * runtime.synchro_counter console.log({ lag }) console.log({ state_name: runtime.active_state_name, counter: runtime.accepts_counter, NO_CHANGE: runtime.synchro_counter }) pending.unshift(accepts) } } else { if (runtime.accepts_counter > 0) { callbacks.acceptor.call(null, trace) } } } // display full transition (changes) table const log_table = () => { const title = 'Vaucanson Finite State Machine' const empty = '' const heading = 'STATE\tREAD\tJUMP' const passline = '----------------------' return [ title, empty, heading, passline, ...states_entries.reduce((acc_se, se, se_idx) => { return [...acc_se, ...se.change_entries.map((ce) => { return `${se.state_props.state_name}\t${ce.read}\t${ce.jump}` })] }, []), empty ].reduce((acc_lines, line) => acc_lines + '\n' + line, '') } // default log changes const log_change = ({ prev_state_name, read, next_state_name }) => { const psn = prev_state_name const nsm = next_state_name console.log( `CHANGE_FROM "${psn}" ON_READ "${ read }" JUMP_TO "${nsm }"` ) } // default callbacks const callbacks = { acceptor: (trace) => { if (runtime.debug_flag) { console.log('Read: ' + trace.join(', ')) console.log('(accepted) Done.') } return true }, rejector: (trace) => { if (runtime.debug_flag) { console.log('ERROR! (rejected)') console.log('Read: ' + trace.join(', ')) } return false } } // exported API const fsm = { def, start, accept, callbacks, log_table, log_change, } callbacks.acceptor = acceptor || callbacks.acceptor callbacks.rejector = rejector || callbacks.rejector Object.defineProperty(fsm, 'active_state_name', { get: () => { const active_state_index = states_lookups.get(active_state_name) const active_state = states_entries[active_state_index] return active_state.state_props.state_name } }) return fsm } // test code const async_log = (message) => { return () => { const promise = new Promise((resolve, reject) => { setTimeout(() => { console.log(message) resolve() }, 1000) }) return promise } } // definitions phase const fsm = make_fsm({ debug: true }) fsm.def({ state_name: 'A'}) .on({ read: 'b', jump: 'B', action: fsm.log_change }) .on({ read: 'c', jump: 'C', action: fsm.log_change }) fsm.def({ state_name: 'B'}) .on({ read: 'a', jump: 'A', action: fsm.log_change}) .on({ read: 'c', jump: 'C', action: fsm.log_change}) .on({ read: 'd', jump: 'D', action: fsm.log_change }) fsm.def({ state_name: 'C'}) .on({ read: 'a', jump: 'A', action: fsm.log_change }) .on({ read: 'b', jump: 'B', action: fsm.log_change }) .on({ read: 'd', jump: 'D', action: fsm.log_change }) fsm.def({ state_name: 'D' }) .on({ read: 'e', jump: 'E' }) fsm.def({ state_name: 'E', term: true }) //console.log(fsm.log_table()) // running phase fsm.start({ state_name: 'A', debug: true }) fsm.accept({ read: 'c' }) fsm.accept({ read: 'a' }) fsm.accept({ read: 'b' }) fsm.accept({ read: 'a' }) fsm.accept({ read: 'b' }) fsm.accept({ read: 'a' }) fsm.accept({ read: 'b' }) fsm.accept({ read: 'd' }) fsm.accept({ read: 'e' }) // fsm.accept({ read: 'z' })