UNPKG

jssm

Version:

A Javascript finite state machine (FSM) with a terse DSL and a simple API. Most FSMs are one-liners. Fast, easy, powerful, well tested, typed with TypeScript, and visualizations. MIT License.

1,443 lines (1,439 loc) 58 kB
// whargarbl lots of these return arrays could/should be sets import { reduce as reduce_to_639 } from 'reduce-to-639-1'; import { seq, weighted_rand_select, weighted_sample_select, histograph, weighted_histo_key, array_box_if_string, hook_name, named_hook_name } from './jssm_util'; import { shapes, gviz_shapes, named_colors } from './jssm_constants'; import { parse } from './jssm-dot'; import { version } from './version'; // replaced from package.js in build import { JssmError } from './jssm_error'; /* eslint-disable complexity */ /********* * * Return the direction of an arrow - `right`, `left`, or `both`. * * ```typescript * import { arrow_direction } from 'jssm'; * * arrow_direction('->'); // 'right' * arrow_direction('<~=>'); // 'both' * ``` * * @param arrow The arrow to be evaluated * */ function arrow_direction(arrow) { switch (String(arrow)) { case '->': case '→': case '=>': case '⇒': case '~>': case '↛': return 'right'; case '<-': case '←': case '<=': case '⇐': case '<~': case '↚': return 'left'; case '<->': case '↔': case '<-=>': case '←⇒': case '←=>': case '<-⇒': case '<-~>': case '←↛': case '←~>': case '<-↛': case '<=>': case '⇔': case '<=->': case '⇐→': case '⇐->': case '<=→': case '<=~>': case '⇐↛': case '⇐~>': case '<=↛': case '<~>': case '↮': case '<~->': case '↚→': case '↚->': case '<~→': case '<~=>': case '↚⇒': case '↚=>': case '<~⇒': return 'both'; default: throw new JssmError(undefined, `arrow_direction: unknown arrow type ${arrow}`); } } /* eslint-enable complexity */ /* eslint-disable complexity */ /********* * * Return the direction of an arrow - `right`, `left`, or `both`. * * ```typescript * import { arrow_left_kind } from 'jssm'; * * arrow_left_kind('<-'); // 'legal' * arrow_left_kind('<='); // 'main' * arrow_left_kind('<~'); // 'forced' * arrow_left_kind('<->'); // 'legal' * arrow_left_kind('->'); // 'none' * ``` * * @param arrow The arrow to be evaluated * */ function arrow_left_kind(arrow) { switch (String(arrow)) { case '->': case '→': case '=>': case '⇒': case '~>': case '↛': return 'none'; case '<-': case '←': case '<->': case '↔': case '<-=>': case '←⇒': case '<-~>': case '←↛': return 'legal'; case '<=': case '⇐': case '<=>': case '⇔': case '<=->': case '⇐→': case '<=~>': case '⇐↛': return 'main'; case '<~': case '↚': case '<~>': case '↮': case '<~->': case '↚→': case '<~=>': case '↚⇒': return 'forced'; default: throw new JssmError(undefined, `arrow_direction: unknown arrow type ${arrow}`); } } /* eslint-enable complexity */ /* eslint-disable complexity */ /********* * * Return the direction of an arrow - `right`, `left`, or `both`. * * ```typescript * import { arrow_left_kind } from 'jssm'; * * arrow_left_kind('->'); // 'legal' * arrow_left_kind('=>'); // 'main' * arrow_left_kind('~>'); // 'forced' * arrow_left_kind('<->'); // 'legal' * arrow_left_kind('<-'); // 'none' * ``` * * @param arrow The arrow to be evaluated * */ function arrow_right_kind(arrow) { switch (String(arrow)) { case '<-': case '←': case '<=': case '⇐': case '<~': case '↚': return 'none'; case '->': case '→': case '<->': case '↔': case '<=->': case '⇐→': case '<~->': case '↚→': return 'legal'; case '=>': case '⇒': case '<=>': case '⇔': case '<-=>': case '←⇒': case '<~=>': case '↚⇒': return 'main'; case '~>': case '↛': case '<~>': case '↮': case '<-~>': case '←↛': case '<=~>': case '⇐↛': return 'forced'; default: throw new JssmError(undefined, `arrow_direction: unknown arrow type ${arrow}`); } } /* eslint-enable complexity */ /********* * * Internal method meant to perform factory assembly of an edge. Not meant for * external use. * * @internal * * @typeparam mDT The type of the machine data member; usually omitted * */ // TODO add at-param to docblock function makeTransition(this_se, from, to, isRight, _wasList, _wasIndex) { const kind = isRight ? arrow_right_kind(this_se.kind) : arrow_left_kind(this_se.kind), edge = { from, to, kind, forced_only: kind === 'forced', main_path: kind === 'main' }; // if ((wasList !== undefined) && (wasIndex === undefined)) { throw new JssmError(undefined, `Must have an index if transition was in a list"); } // if ((wasIndex !== undefined) && (wasList === undefined)) { throw new JssmError(undefined, `Must be in a list if transition has an index"); } /* if (typeof edge.to === 'object') { if (edge.to.key === 'cycle') { if (wasList === undefined) { throw new JssmError(undefined, "Must have a waslist if a to is type cycle"); } const nextIndex = wrapBy(wasIndex, edge.to.value, wasList.length); edge.to = wasList[nextIndex]; } } */ const action = isRight ? 'r_action' : 'l_action', probability = isRight ? 'r_probability' : 'l_probability'; if (this_se[action]) { edge.action = this_se[action]; } if (this_se[probability]) { edge.probability = this_se[probability]; } return edge; } /********* * * This method wraps the parser call that comes from the peg grammar, * {@link parse}. Generally neither this nor that should be used directly * unless you mean to develop plugins or extensions for the machine. * * Parses the intermediate representation of a compiled string down to a * machine configuration object. If you're using this (probably don't,) you're * probably also using {@link compile} and {@link Machine.constructor}. * * ```typescript * import { parse, compile, Machine } from 'jssm'; * * const intermediate = wrap_parse('a -> b;', {}); * // [ {key:'transition', from:'a', se:{kind:'->',to:'b'}} ] * * const cfg = compile(intermediate); * // { start_states:['a'], transitions: [{ from:'a', to:'b', kind:'legal', forced_only:false, main_path:false }] } * * const machine = new Machine(cfg); * // Machine { _instance_name: undefined, _state: 'a', ... * ``` * * This method is mostly for plugin and intermediate tool authors, or people * who need to work with the machine's intermediate representation. * * # Hey! * * Most people looking at this want either the `sm` operator or method `from`, * which perform all the steps in the chain. The library's author mostly uses * operator `sm`, and mostly falls back to `.from` when needing to parse * strings dynamically instead of from template literals. * * Operator {@link sm}: * * ```typescript * import { sm } from 'jssm'; * * const switch = sm`on <=> off;`; * ``` * * Method {@link from}: * * ```typescript * import * as jssm from 'jssm'; * * const toggle = jssm.from('up <=> down;'); * ``` * * `wrap_parse` itself is an internal convenience method for alting out an * object as the options call. Not generally meant for external use. * * @param input The FSL code to be evaluated * * @param options Things to control about the instance * */ function wrap_parse(input, options) { return parse(input, options || {}); } /********* * * Internal method performing one step in compiling rules for transitions. Not * generally meant for external use. * * @internal * * @typeparam mDT The type of the machine data member; usually omitted * */ function compile_rule_transition_step(acc, from, to, this_se, next_se) { const edges = []; const uFrom = (Array.isArray(from) ? from : [from]), uTo = (Array.isArray(to) ? to : [to]); uFrom.map((f) => { uTo.map((t) => { const right = makeTransition(this_se, f, t, true); if (right.kind !== 'none') { edges.push(right); } const left = makeTransition(this_se, t, f, false); if (left.kind !== 'none') { edges.push(left); } }); }); const new_acc = acc.concat(edges); if (next_se) { return compile_rule_transition_step(new_acc, to, next_se.to, next_se, next_se.se); } else { return new_acc; } } /********* * * Internal method performing one step in compiling rules for transitions. Not * generally meant for external use. * * @internal * */ function compile_rule_handle_transition(rule) { return compile_rule_transition_step([], rule.from, rule.se.to, rule.se, rule.se.se); } /********* * * Internal method performing one step in compiling rules for transitions. Not * generally meant for external use. * * @internal * */ function compile_rule_handler(rule) { if (rule.key === 'transition') { return { agg_as: 'transition', val: compile_rule_handle_transition(rule) }; } if (rule.key === 'machine_language') { return { agg_as: 'machine_language', val: reduce_to_639(rule.value) }; } if (rule.key === 'state_declaration') { if (!rule.name) { throw new JssmError(undefined, 'State declarations must have a name'); } return { agg_as: 'state_declaration', val: { state: rule.name, declarations: rule.value } }; } if (['arrange_declaration', 'arrange_start_declaration', 'arrange_end_declaration'].includes(rule.key)) { return { agg_as: rule.key, val: [rule.value] }; } const tautologies = [ 'graph_layout', 'start_states', 'end_states', 'machine_name', 'machine_version', 'machine_comment', 'machine_author', 'machine_contributor', 'machine_definition', 'machine_reference', 'machine_license', 'fsl_version', 'state_config', 'theme', 'flow', 'dot_preamble' ]; if (tautologies.includes(rule.key)) { return { agg_as: rule.key, val: rule.value }; } throw new JssmError(undefined, `compile_rule_handler: Unknown rule: ${JSON.stringify(rule)}`); } /********* * * Compile a machine's JSON intermediate representation to a config object. If * you're using this (probably don't,) you're probably also using * {@link parse} to get the IR, and the object constructor * {@link Machine.construct} to turn the config object into a workable machine. * * ```typescript * import { parse, compile, Machine } from 'jssm'; * * const intermediate = parse('a -> b;'); * // [ {key:'transition', from:'a', se:{kind:'->',to:'b'}} ] * * const cfg = compile(intermediate); * // { start_states:['a'], transitions: [{ from:'a', to:'b', kind:'legal', forced_only:false, main_path:false }] } * * const machine = new Machine(cfg); * // Machine { _instance_name: undefined, _state: 'a', ... * ``` * * This method is mostly for plugin and intermediate tool authors, or people * who need to work with the machine's intermediate representation. * * # Hey! * * Most people looking at this want either the `sm` operator or method `from`, * which perform all the steps in the chain. The library's author mostly uses * operator `sm`, and mostly falls back to `.from` when needing to parse * strings dynamically instead of from template literals. * * Operator {@link sm}: * * ```typescript * import { sm } from 'jssm'; * * const switch = sm`on <=> off;`; * ``` * * Method {@link from}: * * ```typescript * import * as jssm from 'jssm'; * * const toggle = jssm.from('up <=> down;'); * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param tree The parse tree to be boiled down into a machine config * */ function compile(tree) { const results = { graph_layout: [], transition: [], start_states: [], end_states: [], state_config: [], state_declaration: [], fsl_version: [], machine_author: [], machine_comment: [], machine_contributor: [], machine_definition: [], machine_language: [], machine_license: [], machine_name: [], machine_reference: [], theme: [], flow: [], dot_preamble: [], arrange_declaration: [], arrange_start_declaration: [], arrange_end_declaration: [], machine_version: [] }; tree.map((tr) => { const rule = compile_rule_handler(tr), agg_as = rule.agg_as, val = rule.val; // TODO FIXME no any results[agg_as] = results[agg_as].concat(val); }); const assembled_transitions = [].concat(...results['transition']); const result_cfg = { start_states: results.start_states.length ? results.start_states : [assembled_transitions[0].from], transitions: assembled_transitions }; const oneOnlyKeys = [ 'graph_layout', 'machine_name', 'machine_version', 'machine_comment', 'fsl_version', 'machine_license', 'machine_definition', 'machine_language', 'theme', 'flow', 'dot_preamble' ]; oneOnlyKeys.map((oneOnlyKey) => { if (results[oneOnlyKey].length > 1) { throw new JssmError(undefined, `May only have one ${oneOnlyKey} statement maximum: ${JSON.stringify(results[oneOnlyKey])}`); } else { if (results[oneOnlyKey].length) { result_cfg[oneOnlyKey] = results[oneOnlyKey][0]; } } }); ['arrange_declaration', 'arrange_start_declaration', 'arrange_end_declaration', 'machine_author', 'machine_contributor', 'machine_reference', 'state_declaration'].map((multiKey) => { if (results[multiKey].length) { result_cfg[multiKey] = results[multiKey]; } }); return result_cfg; } /********* * * An internal convenience wrapper for parsing then compiling a machine string. * Not generally meant for external use. Please see {@link compile} or * {@link sm}. * * @typeparam mDT The type of the machine data member; usually omitted * * @param plan The FSL code to be evaluated and built into a machine config * */ function make(plan) { return compile(wrap_parse(plan)); } /********* * * An internal method meant to take a series of declarations and fold them into * a single multi-faceted declaration, in the process of building a state. Not * generally meant for external use. * * @internal * */ function transfer_state_properties(state_decl) { state_decl.declarations.map((d) => { switch (d.key) { case 'shape': state_decl.shape = d.value; break; case 'color': state_decl.color = d.value; break; case 'corners': state_decl.corners = d.value; break; case 'linestyle': state_decl.linestyle = d.value; break; case 'text-color': state_decl.textColor = d.value; break; case 'background-color': state_decl.backgroundColor = d.value; break; case 'border-color': state_decl.borderColor = d.value; break; default: throw new JssmError(undefined, `Unknown state property: '${JSON.stringify(d)}'`); } }); return state_decl; } // TODO add a lotta docblock here class Machine { // whargarbl this badly needs to be broken up, monolith master constructor({ start_states, complete = [], transitions, machine_author, machine_comment, machine_contributor, machine_definition, machine_language, machine_license, machine_name, machine_version, state_declaration, fsl_version, dot_preamble = undefined, arrange_declaration = [], arrange_start_declaration = [], arrange_end_declaration = [], theme = 'default', flow = 'down', graph_layout = 'dot', instance_name, data }) { this._instance_name = instance_name; this._state = start_states[0]; this._states = new Map(); this._state_declarations = new Map(); this._edges = []; this._edge_map = new Map(); this._named_transitions = new Map(); this._actions = new Map(); this._reverse_actions = new Map(); this._reverse_action_targets = new Map(); // todo this._machine_author = array_box_if_string(machine_author); this._machine_comment = machine_comment; this._machine_contributor = array_box_if_string(machine_contributor); this._machine_definition = machine_definition; this._machine_language = machine_language; this._machine_license = machine_license; this._machine_name = machine_name; this._machine_version = machine_version; this._raw_state_declaration = state_declaration || []; this._fsl_version = fsl_version; this._arrange_declaration = arrange_declaration; this._arrange_start_declaration = arrange_start_declaration; this._arrange_end_declaration = arrange_end_declaration; this._dot_preamble = dot_preamble; this._theme = theme; this._flow = flow; this._graph_layout = graph_layout; this._has_hooks = false; this._has_basic_hooks = false; this._has_named_hooks = false; this._has_entry_hooks = false; this._has_exit_hooks = false; this._has_global_action_hooks = false; this._has_transition_hooks = true; // no need for a boolean has any transition hook, as it's one or nothing, so just test that for undefinedness this._hooks = new Map(); this._named_hooks = new Map(); this._entry_hooks = new Map(); this._exit_hooks = new Map(); this._global_action_hooks = new Map(); this._any_action_hook = undefined; this._standard_transition_hook = undefined; this._main_transition_hook = undefined; this._forced_transition_hook = undefined; this._any_transition_hook = undefined; this._standard_transition_hook = undefined; this._data = data; if (state_declaration) { state_declaration.map((state_decl) => { if (this._state_declarations.has(state_decl.state)) { // no repeats throw new JssmError(this, `Added the same state declaration twice: ${JSON.stringify(state_decl.state)}`); } this._state_declarations.set(state_decl.state, transfer_state_properties(state_decl)); }); } transitions.map((tr) => { if (tr.from === undefined) { throw new JssmError(this, `transition must define 'from': ${JSON.stringify(tr)}`); } if (tr.to === undefined) { throw new JssmError(this, `transition must define 'to': ${JSON.stringify(tr)}`); } // get the cursors. what a mess const cursor_from = this._states.get(tr.from) || { name: tr.from, from: [], to: [], complete: complete.includes(tr.from) }; if (!(this._states.has(tr.from))) { this._new_state(cursor_from); } const cursor_to = this._states.get(tr.to) || { name: tr.to, from: [], to: [], complete: complete.includes(tr.to) }; if (!(this._states.has(tr.to))) { this._new_state(cursor_to); } // guard against existing connections being re-added if (cursor_from.to.includes(tr.to)) { throw new JssmError(this, `already has ${JSON.stringify(tr.from)} to ${JSON.stringify(tr.to)}`); } else { cursor_from.to.push(tr.to); cursor_to.from.push(tr.from); } // add the edge; note its id this._edges.push(tr); const thisEdgeId = this._edges.length - 1; // guard against repeating a transition name if (tr.name) { if (this._named_transitions.has(tr.name)) { throw new JssmError(this, `named transition "${JSON.stringify(tr.name)}" already created`); } else { this._named_transitions.set(tr.name, thisEdgeId); } } // set up the mapping, so that edges can be looked up by endpoint pairs const from_mapping = this._edge_map.get(tr.from) || new Map(); if (!(this._edge_map.has(tr.from))) { this._edge_map.set(tr.from, from_mapping); } // const to_mapping = from_mapping.get(tr.to); from_mapping.set(tr.to, thisEdgeId); // already checked that this mapping doesn't exist, above // set up the action mapping, so that actions can be looked up by origin if (tr.action) { // forward mapping first by action name let actionMap = this._actions.get(tr.action); if (!(actionMap)) { actionMap = new Map(); this._actions.set(tr.action, actionMap); } if (actionMap.has(tr.from)) { throw new JssmError(this, `action ${JSON.stringify(tr.action)} already attached to origin ${JSON.stringify(tr.from)}`); } else { actionMap.set(tr.from, thisEdgeId); } // reverse mapping first by state origin name let rActionMap = this._reverse_actions.get(tr.from); if (!(rActionMap)) { rActionMap = new Map(); this._reverse_actions.set(tr.from, rActionMap); } // no need to test for reverse mapping pre-presence; // forward mapping already covers collisions rActionMap.set(tr.action, thisEdgeId); // reverse mapping first by state target name if (!(this._reverse_action_targets.has(tr.to))) { this._reverse_action_targets.set(tr.to, new Map()); } /* todo comeback fundamental problem is roActionMap needs to be a multimap const roActionMap = this._reverse_action_targets.get(tr.to); // wasteful - already did has - refactor if (roActionMap) { if (roActionMap.has(tr.action)) { throw new JssmError(this, `ro-action ${tr.to} already attached to action ${tr.action}`); } else { roActionMap.set(tr.action, thisEdgeId); } } else { throw new JssmError(this, `should be impossible - flow doesn\'t know .set precedes .get yet again. severe error?'); } */ } }); } /******** * * Internal method for fabricating states. Not meant for external use. * * @internal * */ _new_state(state_config) { if (this._states.has(state_config.name)) { throw new JssmError(this, `state ${JSON.stringify(state_config.name)} already exists`); } this._states.set(state_config.name, state_config); return state_config.name; } /********* * * Get the current state of a machine. * * ```typescript * import * as jssm from 'jssm'; * * const switch = jssm.from('on <=> off;'); * console.log( switch.state() ); // 'on' * * switch.transition('off'); * console.log( switch.state() ); // 'off' * ``` * * @typeparam mDT The type of the machine data member; usually omitted * */ state() { return this._state; } /* whargarbl todo major when we reimplement this, reintroduce this change to the is_final call is_changing(): boolean { return true; // todo whargarbl } */ /********* * * Get the current data of a machine. * * ```typescript * import * as jssm from 'jssm'; * * const switch = jssm.from('on <=> off;', {data: 1}); * console.log( switch.data() ); // 1 * ``` * * @typeparam mDT The type of the machine data member; usually omitted * */ data() { return this._data; } /* whargarbl todo major when we reimplement this, reintroduce this change to the is_final call is_changing(): boolean { return true; // todo whargarbl } */ /******** * * Check whether a given state is final (either has no exits or is marked * `complete`.) * * ```typescript * import { sm, state_is_final } from 'jssm'; * * const final_test = sm`first -> second;`; * * console.log( final_test.state_is_final('first') ); // false * console.log( final_test.state_is_final('second') ); // true * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The name of the state to check for finality * */ state_is_final(whichState) { return ((this.state_is_terminal(whichState)) && (this.state_is_complete(whichState))); } /******** * * Check whether the current state is final (either has no exits or is marked * `complete`.) * * ```typescript * import { sm, state_is_final } from 'jssm'; * * const final_test = sm`first -> second;`; * * console.log( final_test.is_final() ); // false * state.transition('second'); * console.log( final_test.is_final() ); // true * ``` * * @typeparam mDT The type of the machine data member; usually omitted * */ is_final() { // return ((!this.is_changing()) && this.state_is_final(this.state())); return this.state_is_final(this.state()); } graph_layout() { return this._graph_layout; } dot_preamble() { return this._dot_preamble; } machine_author() { return this._machine_author; } machine_comment() { return this._machine_comment; } machine_contributor() { return this._machine_contributor; } machine_definition() { return this._machine_definition; } machine_language() { return this._machine_language; } machine_license() { return this._machine_license; } machine_name() { return this._machine_name; } machine_version() { return this._machine_version; } raw_state_declarations() { return this._raw_state_declaration; } state_declaration(which) { return this._state_declarations.get(which); } state_declarations() { return this._state_declarations; } fsl_version() { return this._fsl_version; } machine_state() { return { internal_state_impl_version: 1, actions: this._actions, edge_map: this._edge_map, edges: this._edges, named_transitions: this._named_transitions, reverse_actions: this._reverse_actions, // reverse_action_targets : this._reverse_action_targets, state: this._state, states: this._states }; } /* load_machine_state(): boolean { return false; // todo whargarbl } */ /********* * * List all the states known by the machine. Please note that the order of * these states is not guaranteed. * * ```typescript * import * as jssm from 'jssm'; * * const switch = jssm.from('on <=> off;'); * console.log( switch.states() ); // ['on', 'off'] * ``` * * @typeparam mDT The type of the machine data member; usually omitted * */ states() { return Array.from(this._states.keys()); } state_for(whichState) { const state = this._states.get(whichState); if (state) { return state; } else { throw new JssmError(this, 'No such state', { requested_state: whichState }); } } /********* * * Check whether the machine knows a given state. * * ```typescript * import * as jssm from 'jssm'; * * const switch = jssm.from('on <=> off;'); * * console.log( switch.has_state('off') ); // true * console.log( switch.has_state('dance') ); // false * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The state to be checked for extance * */ has_state(whichState) { return this._states.get(whichState) !== undefined; } /********* * * Lists all edges of a machine. * * ```typescript * import { sm } from 'jssm'; * * const lswitch = sm`on 'toggle' <=> 'toggle' off;`; * * lswitch.list_edges(); * [ * { * from: 'on', * to: 'off', * kind: 'main', * forced_only: false, * main_path: true, * action: 'toggle' * }, * { * from: 'off', * to: 'on', * kind: 'main', * forced_only: false, * main_path: true, * action: 'toggle' * } * ] * ``` * * @typeparam mDT The type of the machine data member; usually omitted * */ list_edges() { return this._edges; } list_named_transitions() { return this._named_transitions; } list_actions() { return Array.from(this._actions.keys()); } theme() { return this._theme; // constructor sets this to "default" otherwise } flow() { return this._flow; } get_transition_by_state_names(from, to) { const emg = this._edge_map.get(from); if (emg) { return emg.get(to); } else { return undefined; } } lookup_transition_for(from, to) { const id = this.get_transition_by_state_names(from, to); return ((id === undefined) || (id === null)) ? undefined : this._edges[id]; } /******** * * List all transitions attached to the current state, sorted by entrance and * exit. The order of each sublist is not defined. A node could appear in * both lists. * * ```typescript * import { sm } from 'jssm'; * * const light = sm`red 'next' -> green 'next' -> yellow 'next' -> red; [red yellow green] 'shutdown' ~> off 'start' -> red;`; * * light.state(); // 'red' * light.list_transitions(); // { entrances: [ 'yellow', 'off' ], exits: [ 'green', 'off' ] } * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The state whose transitions to have listed * */ list_transitions(whichState = this.state()) { return { entrances: this.list_entrances(whichState), exits: this.list_exits(whichState) }; } /******** * * List all entrances attached to the current state. Please note that the * order of the list is not defined. * * ```typescript * import { sm } from 'jssm'; * * const light = sm`red 'next' -> green 'next' -> yellow 'next' -> red; [red yellow green] 'shutdown' ~> off 'start' -> red;`; * * light.state(); // 'red' * light.list_entrances(); // [ 'yellow', 'off' ] * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The state whose entrances to have listed * */ list_entrances(whichState = this.state()) { return (this._states.get(whichState) || { from: undefined }).from || []; } /******** * * List all exits attached to the current state. Please note that the order * of the list is not defined. * * ```typescript * import { sm } from 'jssm'; * * const light = sm`red 'next' -> green 'next' -> yellow 'next' -> red; [red yellow green] 'shutdown' ~> off 'start' -> red;`; * * light.state(); // 'red' * light.list_exits(); // [ 'green', 'off' ] * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The state whose exits to have listed * */ list_exits(whichState = this.state()) { return (this._states.get(whichState) || { to: undefined }).to || []; } probable_exits_for(whichState) { const wstate = this._states.get(whichState); if (!(wstate)) { throw new JssmError(this, `No such state ${JSON.stringify(whichState)} in probable_exits_for`); } const wstate_to = wstate.to, wtf = wstate_to .map((ws) => this.lookup_transition_for(this.state(), ws)) .filter(Boolean); return wtf; } probabilistic_transition() { const selected = weighted_rand_select(this.probable_exits_for(this.state())); return this.transition(selected.to); } probabilistic_walk(n) { return seq(n) .map(() => { const state_was = this.state(); this.probabilistic_transition(); return state_was; }) .concat([this.state()]); } probabilistic_histo_walk(n) { return histograph(this.probabilistic_walk(n)); } /******** * * List all actions available from this state. Please note that the order of * the actions is not guaranteed. * * ```typescript * import { sm } from 'jssm'; * * const machine = sm` * red 'next' -> green 'next' -> yellow 'next' -> red; * [red yellow green] 'shutdown' ~> off 'start' -> red; * `; * * console.log( machine.state() ); // logs 'red' * console.log( machine.actions() ); // logs ['next', 'shutdown'] * * machine.action('next'); // true * console.log( machine.state() ); // logs 'green' * console.log( machine.actions() ); // logs ['next', 'shutdown'] * * machine.action('shutdown'); // true * console.log( machine.state() ); // logs 'off' * console.log( machine.actions() ); // logs ['start'] * * machine.action('start'); // true * console.log( machine.state() ); // logs 'red' * console.log( machine.actions() ); // logs ['next', 'shutdown'] * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The state whose actions to have listed * */ actions(whichState = this.state()) { const wstate = this._reverse_actions.get(whichState); if (wstate) { return Array.from(wstate.keys()); } else { throw new JssmError(this, `No such state ${JSON.stringify(whichState)}`); } } /******** * * List all states that have a specific action attached. Please note that * the order of the states is not guaranteed. * * ```typescript * import { sm } from 'jssm'; * * const machine = sm` * red 'next' -> green 'next' -> yellow 'next' -> red; * [red yellow green] 'shutdown' ~> off 'start' -> red; * `; * * console.log( machine.list_states_having_action('next') ); // ['red', 'green', 'yellow'] * console.log( machine.list_states_having_action('start') ); // ['off'] * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The action to be checked for associated states * */ list_states_having_action(whichState) { const wstate = this._actions.get(whichState); if (wstate) { return Array.from(wstate.keys()); } else { throw new JssmError(this, `No such state ${JSON.stringify(whichState)}`); } } // comeback /* list_entrance_actions(whichState: mNT = this.state() ) : Array<mNT> { return [... (this._reverse_action_targets.get(whichState) || new Map()).values()] // wasteful .map( (edgeId:any) => (this._edges[edgeId] : any)) // whargarbl burn out any .filter( (o:any) => o.to === whichState) .map( filtered => filtered.from ); } */ list_exit_actions(whichState = this.state()) { const ra_base = this._reverse_actions.get(whichState); if (!(ra_base)) { throw new JssmError(this, `No such state ${JSON.stringify(whichState)}`); } return Array.from(ra_base.values()) .map((edgeId) => this._edges[edgeId]) .filter((o) => o.from === whichState) .map((filtered) => filtered.action); } probable_action_exits(whichState = this.state()) { const ra_base = this._reverse_actions.get(whichState); if (!(ra_base)) { throw new JssmError(this, `No such state ${JSON.stringify(whichState)}`); } return Array.from(ra_base.values()) .map((edgeId) => this._edges[edgeId]) .filter((o) => o.from === whichState) .map((filtered) => ({ action: filtered.action, probability: filtered.probability })); } // TODO FIXME test that is_unenterable on non-state throws is_unenterable(whichState) { if (!(this.has_state(whichState))) { throw new JssmError(this, `No such state ${whichState}`); } return this.list_entrances(whichState).length === 0; } has_unenterables() { return this.states().some((x) => this.is_unenterable(x)); } is_terminal() { return this.state_is_terminal(this.state()); } // TODO FIXME test that state_is_terminal on non-state throws state_is_terminal(whichState) { if (!(this.has_state(whichState))) { throw new JssmError(this, `No such state ${whichState}`); } return this.list_exits(whichState).length === 0; } has_terminals() { return this.states().some((x) => this.state_is_terminal(x)); } is_complete() { return this.state_is_complete(this.state()); } state_is_complete(whichState) { const wstate = this._states.get(whichState); if (wstate) { return wstate.complete; } else { throw new JssmError(this, `No such state ${JSON.stringify(whichState)}`); } } has_completes() { return this.states().some((x) => this.state_is_complete(x)); } // basic toolable hook call. convenience wrappers will follow, like // hook(from, to, handler) and exit_hook(from, handler) and etc set_hook(HookDesc) { switch (HookDesc.kind) { case 'hook': this._hooks.set(hook_name(HookDesc.from, HookDesc.to), HookDesc.handler); this._has_hooks = true; this._has_basic_hooks = true; break; case 'named': this._named_hooks.set(named_hook_name(HookDesc.from, HookDesc.to, HookDesc.action), HookDesc.handler); this._has_hooks = true; this._has_named_hooks = true; break; case 'global action': this._global_action_hooks.set(HookDesc.action, HookDesc.handler); this._has_hooks = true; this._has_global_action_hooks = true; break; case 'any action': this._any_action_hook = HookDesc.handler; this._has_hooks = true; break; case 'standard transition': this._standard_transition_hook = HookDesc.handler; this._has_transition_hooks = true; this._has_hooks = true; break; case 'main transition': this._main_transition_hook = HookDesc.handler; this._has_transition_hooks = true; this._has_hooks = true; break; case 'forced transition': this._forced_transition_hook = HookDesc.handler; this._has_transition_hooks = true; this._has_hooks = true; break; case 'any transition': this._any_transition_hook = HookDesc.handler; this._has_hooks = true; break; case 'entry': this._entry_hooks.set(HookDesc.to, HookDesc.handler); this._has_hooks = true; this._has_entry_hooks = true; break; case 'exit': this._exit_hooks.set(HookDesc.from, HookDesc.handler); this._has_hooks = true; this._has_exit_hooks = true; break; default: throw new JssmError(this, `Unknown hook type ${HookDesc.kind}, should be impossible`); } } hook(from, to, handler) { this.set_hook({ kind: 'hook', from, to, handler }); return this; } hook_action(from, to, action, handler) { this.set_hook({ kind: 'named', from, to, action, handler }); return this; } hook_global_action(action, handler) { this.set_hook({ kind: 'global action', action, handler }); return this; } hook_any_action(handler) { this.set_hook({ kind: 'any action', handler }); return this; } hook_standard_transition(handler) { this.set_hook({ kind: 'standard transition', handler }); return this; } hook_main_transition(handler) { this.set_hook({ kind: 'main transition', handler }); return this; } hook_forced_transition(handler) { this.set_hook({ kind: 'forced transition', handler }); return this; } hook_any_transition(handler) { this.set_hook({ kind: 'any transition', handler }); return this; } hook_entry(to, handler) { this.set_hook({ kind: 'entry', to, handler }); return this; } hook_exit(from, handler) { this.set_hook({ kind: 'exit', from, handler }); return this; } // remove_hook(HookDesc: HookDescription) { // throw new JssmError(this, 'TODO: Should remove hook here'); // } edges_between(from, to) { return this._edges.filter(edge => ((edge.from === from) && (edge.to === to))); } transition_impl(newStateOrAction, newData, wasForced, wasAction) { // TODO the forced-ness behavior needs to be cleaned up a lot here // TODO all the callbacks are wrong on forced, action, etc let valid = false, trans_type, newState, fromAction = undefined; if (wasForced) { if (this.valid_force_transition(newStateOrAction, newData)) { valid = true; trans_type = 'forced'; newState = newStateOrAction; } } else if (wasAction) { if (this.valid_action(newStateOrAction, newData)) { const edge = this.current_action_edge_for(newStateOrAction); valid = true; trans_type = edge.kind; newState = edge.to; fromAction = newStateOrAction; } } else { if (this.valid_transition(newStateOrAction, newData)) { if (this._has_transition_hooks) { trans_type = this.edges_between(this._state, newStateOrAction)[0].kind; // TODO this won't do the right thing if various edges have different types } valid = true; newState = newStateOrAction; } } if (valid) { if (this._has_hooks) { const hook_args = { data: this._data, action: fromAction, from: this._state, to: newState, forced: wasForced }; function update_fields(res) { if (res.hasOwnProperty('data')) { hook_args.data = res.data; data_changed = true; } } let data_changed = false; if (wasAction) { // 1. any action hook const outcome = abstract_hook_step(this._any_action_hook, hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); // 2. global specific action hook const outcome2 = abstract_hook_step(this._global_action_hooks.get(newStateOrAction), hook_args); if (outcome2.pass === false) { return false; } update_fields(outcome2); } // 3. any transition hook if (this._any_transition_hook !== undefined) { const outcome = abstract_hook_step(this._any_transition_hook, hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); } // 4. exit hook if (this._has_exit_hooks) { const outcome = abstract_hook_step(this._exit_hooks.get(this._state), hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); } // 5. named transition / action hook if (this._has_named_hooks) { if (wasAction) { const nhn = named_hook_name(this._state, newState, newStateOrAction), outcome = abstract_hook_step(this._named_hooks.get(nhn), hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); } } // 6. regular hook if (this._has_basic_hooks) { const hn = hook_name(this._state, newState), outcome = abstract_hook_step(this._hooks.get(hn), hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); } // 7. edge type hook // 7a. standard transition hook if (trans_type === 'legal') { const outcome = abstract_hook_step(this._standard_transition_hook, hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); } // 7b. main type hook if (trans_type === 'main') { const outcome = abstract_hook_step(this._main_transition_hook, hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); } // 7c. forced transition hook if (trans_type === 'forced') { const outcome = abstract_hook_step(this._forced_transition_hook, hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); } // 8. entry hook if (this._has_entry_hooks) { const outcome = abstract_hook_step(this._entry_hooks.get(newState), hook_args); if (outcome.pass === false) { return false; } update_fields(outcome); } // all hooks passed! let's now establish the result this._state = newState; if (data_changed) { this._data = hook_args.data; } return true; // or without hooks } else { this._state = newState; return true; } // not valid } else { retu