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,298 lines 94.6 kB
// whargarbl lots of these return arrays could/should be sets import { circular_buffer } from 'circular_buffer_js'; import { FslDirections } from './jssm_types'; import { arrow_direction, arrow_left_kind, arrow_right_kind } from './jssm_arrow'; import { compile, make, wrap_parse } from './jssm_compiler'; import { theme_mapping, base_theme } from './jssm_theme'; import { seq, unique, find_repeated, weighted_rand_select, weighted_sample_select, histograph, weighted_histo_key, array_box_if_string, name_bind_prop_and_state, hook_name, named_hook_name, gen_splitmix32, sleep } from './jssm_util'; import * as constants from './jssm_constants'; const { shapes, gviz_shapes, named_colors } = constants; import { version, build_time } from './version'; // replaced from package.js in build import { JssmError } from './jssm_error'; /********* * * 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 'line-style': 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 'state-label': state_decl.stateLabel = d.value; break; case 'border-color': state_decl.borderColor = d.value; break; case 'state_property': state_decl.property = { name: d.name, value: d.value }; break; default: throw new JssmError(undefined, `Unknown state property: '${JSON.stringify(d)}'`); } }); return state_decl; } function state_style_condense(jssk) { const state_style = {}; if (Array.isArray(jssk)) { jssk.forEach((key, i) => { if (typeof key !== 'object') { throw new JssmError(this, `invalid state item ${i} in state_style_condense list: ${JSON.stringify(key)}`); } switch (key.key) { case 'shape': if (state_style.shape !== undefined) { throw new JssmError(this, `cannot redefine 'shape' in state_style_condense, already defined`); } state_style.shape = key.value; break; case 'color': if (state_style.color !== undefined) { throw new JssmError(this, `cannot redefine 'color' in state_style_condense, already defined`); } state_style.color = key.value; break; case 'text-color': if (state_style.textColor !== undefined) { throw new JssmError(this, `cannot redefine 'text-color' in state_style_condense, already defined`); } state_style.textColor = key.value; break; case 'corners': if (state_style.corners !== undefined) { throw new JssmError(this, `cannot redefine 'corners' in state_style_condense, already defined`); } state_style.corners = key.value; break; case 'line-style': if (state_style.lineStyle !== undefined) { throw new JssmError(this, `cannot redefine 'line-style' in state_style_condense, already defined`); } state_style.lineStyle = key.value; break; case 'background-color': if (state_style.backgroundColor !== undefined) { throw new JssmError(this, `cannot redefine 'background-color' in state_style_condense, already defined`); } state_style.backgroundColor = key.value; break; case 'state-label': if (state_style.stateLabel !== undefined) { throw new JssmError(this, `cannot redefine 'state-label' in state_style_condense, already defined`); } state_style.stateLabel = key.value; break; case 'border-color': if (state_style.borderColor !== undefined) { throw new JssmError(this, `cannot redefine 'border-color' in state_style_condense, already defined`); } state_style.borderColor = key.value; break; default: // TODO do that <never> trick to assert this list is complete throw new JssmError(this, `unknown state style key in condense: ${key.key}`); } }); } else if (jssk === undefined) { // do nothing, undefined is legal and means we should return the empty container above } else { throw new JssmError(this, 'state_style_condense received a non-array'); } return state_style; } // TODO add a lotta docblock here class Machine { // whargarbl this badly needs to be broken up, monolith master constructor({ start_states, end_states = [], initial_state, start_states_no_enforce, complete = [], transitions, machine_author, machine_comment, machine_contributor, machine_definition, machine_language, machine_license, machine_name, machine_version, state_declaration, property_definition, state_property, fsl_version, dot_preamble = undefined, arrange_declaration = [], arrange_start_declaration = [], arrange_end_declaration = [], theme = ['default'], flow = 'down', graph_layout = 'dot', instance_name, history, data, default_state_config, default_active_state_config, default_hooked_state_config, default_terminal_state_config, default_start_state_config, default_end_state_config, allows_override, config_allows_override, rng_seed, time_source, timeout_source, clear_timeout_source }) { this._time_source = () => new Date().getTime(); this._create_started = this._time_source(); this._instance_name = instance_name; 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._start_states = new Set(start_states); this._end_states = new Set(end_states); // todo consider what to do about incorporating complete too 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._themes = 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_after_hooks = false; this._has_global_action_hooks = false; this._has_transition_hooks = true; // no need for a boolean for single hooks, just test for undefinedness this._has_forced_transitions = false; this._hooks = new Map(); this._named_hooks = new Map(); this._entry_hooks = new Map(); this._exit_hooks = new Map(); this._after_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._has_post_hooks = false; this._has_post_basic_hooks = false; this._has_post_named_hooks = false; this._has_post_entry_hooks = false; this._has_post_exit_hooks = false; this._has_post_global_action_hooks = false; this._has_post_transition_hooks = true; // no need for a boolean for single hooks, just test for undefinedness this._code_allows_override = allows_override; this._config_allows_override = config_allows_override; if ((allows_override === false) && (config_allows_override === true)) { throw new JssmError(undefined, "Code specifies no override, but config tries to permit; config may not be less strict than code"); } this._post_hooks = new Map(); this._post_named_hooks = new Map(); this._post_entry_hooks = new Map(); this._post_exit_hooks = new Map(); this._post_global_action_hooks = new Map(); this._post_any_action_hook = undefined; this._post_standard_transition_hook = undefined; this._post_main_transition_hook = undefined; this._post_forced_transition_hook = undefined; this._post_any_transition_hook = undefined; this._data = data; this._property_keys = new Set(); this._default_properties = new Map(); this._state_properties = new Map(); this._required_properties = new Set(); this._state_style = state_style_condense(default_state_config); this._active_state_style = state_style_condense(default_active_state_config); this._hooked_state_style = state_style_condense(default_hooked_state_config); this._terminal_state_style = state_style_condense(default_terminal_state_config); this._start_state_style = state_style_condense(default_start_state_config); this._end_state_style = state_style_condense(default_end_state_config); this._history_length = history || 0; this._history = new circular_buffer(this._history_length); this._state_labels = new Map(); this._rng_seed = rng_seed !== null && rng_seed !== void 0 ? rng_seed : new Date().getTime(); this._rng = gen_splitmix32(this._rng_seed); this._timeout_source = timeout_source !== null && timeout_source !== void 0 ? timeout_source : ((f, a) => setTimeout(f, a)); this._clear_timeout_source = clear_timeout_source !== null && clear_timeout_source !== void 0 ? clear_timeout_source : ((h) => clearTimeout(h)); this._timeout_handle = undefined; this._timeout_target = undefined; this._timeout_target_time = undefined; this._after_mapping = new Map(); // consolidate the state declarations 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)); }); } // walk the decls for labels; aggregate them when found [...this._state_declarations].map(sd => { const [key, decl] = sd, labelled = decl.declarations.filter(d => d.key === 'state-label'); if (labelled.length > 1) { throw new JssmError(this, `state ${key} may only have one state-label; has ${labelled.length}`); } if (labelled.length === 1) { this._state_labels.set(key, labelled[0].value); } }); // walk the transitions 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; if (tr.forced_only) { this._has_forced_transitions = true; } // 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 after mapping, if any if (tr.after_time) { this._after_mapping.set(tr.from, [tr.to, tr.after_time]); } // 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?'); } */ } }); if (Array.isArray(property_definition)) { property_definition.forEach(pr => { this._property_keys.add(pr.name); if (pr.hasOwnProperty('default_value')) { this._default_properties.set(pr.name, pr.default_value); } if (pr.hasOwnProperty('required') && (pr.required === true)) { this._required_properties.add(pr.name); } }); } if (Array.isArray(state_property)) { state_property.forEach(sp => { this._state_properties.set(sp.name, sp.default_value); }); } // set initial state either from the specified or the start state list. validate admission behavior. if (initial_state) { if (!(this._states.has(initial_state))) { throw new JssmError(this, `requested start state ${initial_state} does not exist`); } if ((!(start_states_no_enforce)) && (!(start_states.includes(initial_state)))) { throw new JssmError(this, `requested start state ${initial_state} is not in start state list; add {start_states_no_enforce:true} to constructor options if desired`); } this._state = initial_state; } else { this._state = start_states[0]; } // done building, do checks // assert all props are valid this._state_properties.forEach((_value, key) => { const inside = JSON.parse(key); if (Array.isArray(inside)) { const j_property = inside[0]; if (typeof j_property === 'string') { const j_state = inside[1]; if (typeof j_state === 'string') { if (!(this.known_prop(j_property))) { throw new JssmError(this, `State "${j_state}" has property "${j_property}" which is not globally declared`); } } } } }); // assert all required properties are serviced this._required_properties.forEach(dp_key => { if (this._default_properties.has(dp_key)) { throw new JssmError(this, `The property "${dp_key}" is required, but also has a default; these conflict`); } this.states().forEach(s => { const bound_name = name_bind_prop_and_state(dp_key, s); if (!(this._state_properties.has(bound_name))) { throw new JssmError(this, `State "${s}" is missing required property "${dp_key}"`); } }); }); // assert chosen starting state is valid if (!(this.has_state(this.state()))) { throw new JssmError(this, `Current start state "${this.state()}" does not exist`); } // assert all starting states are valid start_states.forEach((ss, ssi) => { if (!(this.has_state(ss))) { throw new JssmError(this, `Start state ${ssi} "${ss}" does not exist`); } }); // assert chosen starting state is valid if (!(start_states.length === this._start_states.size)) { throw new JssmError(this, `Start states cannot be repeated`); } this._created = this._time_source(); this.auto_set_state_timeout(); this._arrange_declaration.forEach((arrange_pair) => arrange_pair.forEach((possibleState) => { if (!(this._states.has(possibleState))) { throw new JssmError(this, `Cannot arrange state that does not exist "${possibleState}"`); } })); } /******** * * 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 lswitch = jssm.from('on <=> off;'); * console.log( lswitch.state() ); // 'on' * * lswitch.transition('off'); * console.log( lswitch.state() ); // 'off' * ``` * * @typeparam mDT The type of the machine data member; usually omitted * */ state() { return this._state; } /********* * * Get the label for a given state, if any; return `undefined` otherwise. * * ```typescript * import * as jssm from 'jssm'; * * const lswitch = jssm.from('a -> b; state a: { label: "Foo!"; };'); * console.log( lswitch.label_for('a') ); // 'Foo!' * console.log( lswitch.label_for('b') ); // undefined * ``` * * See also {@link display_text}. * * @typeparam mDT The type of the machine data member; usually omitted * */ label_for(state) { return this._state_labels.get(state); } /********* * * Get whatever the node should show as text. * * Currently, this means to get the label for a given state, if any; * otherwise to return the node's name. However, this definition is expected * to grow with time, and it is currently considered ill-advised to manually * parse this text. * * See also {@link label_for}. * * ```typescript * import * as jssm from 'jssm'; * * const lswitch = jssm.from('a -> b; state a: { label: "Foo!"; };'); * console.log( lswitch.display_text('a') ); // 'Foo!' * console.log( lswitch.display_text('b') ); // 'b' * ``` * * @typeparam mDT The type of the machine data member; usually omitted * */ display_text(state) { var _a; return (_a = this._state_labels.get(state)) !== null && _a !== void 0 ? _a : state; } /********* * * Get the current data of a machine. * * ```typescript * import * as jssm from 'jssm'; * * const lswitch = jssm.from('on <=> off;', {data: 1}); * console.log( lswitch.data() ); // 1 * ``` * * @typeparam mDT The type of the machine data member; usually omitted * */ data() { return structuredClone(this._data); } // NEEDS_DOCS /********* * * Get the current value of a given property name. * * ```typescript * * ``` * * @param name The relevant property name to look up * * @returns The value behind the prop name. Because functional props are * evaluated as getters, this can be anything. * */ prop(name) { const bound_name = name_bind_prop_and_state(name, this.state()); if (this._state_properties.has(bound_name)) { return this._state_properties.get(bound_name); } else if (this._default_properties.has(name)) { return this._default_properties.get(name); } else { return undefined; } } // NEEDS_DOCS /********* * * Get the current value of a given property name. If missing on the state * and without a global default, throw, unlike {@link prop}, which would * return `undefined` instead. * * ```typescript * * ``` * * @param name The relevant property name to look up * * @returns The value behind the prop name. Because functional props are * evaluated as getters, this can be anything. * */ strict_prop(name) { const bound_name = name_bind_prop_and_state(name, this.state()); if (this._state_properties.has(bound_name)) { return this._state_properties.get(bound_name); } else if (this._default_properties.has(name)) { return this._default_properties.get(name); } else { throw new JssmError(this, `Strictly requested a prop '${name}' which doesn't exist on current state '${this.state()}' and has no default`); } } // NEEDS_DOCS // COMEBACK add prop_map, sparse_props and strict_props to doc text when implemented /********* * * Get the current value of every prop, as an object. If no current definition * exists for a prop - that is, if the prop was defined without a default and * the current state also doesn't define the prop - then that prop will be listed * in the returned object with a value of `undefined`. * * ```typescript * const traffic_light = sm` * * property can_go default true; * property hesitate default true; * property stop_first default false; * * Off -> Red => Green => Yellow => Red; * [Red Yellow Green] ~> [Off FlashingRed]; * FlashingRed -> Red; * * state Red: { property stop_first true; property can_go false; }; * state Off: { property stop_first true; }; * state FlashingRed: { property stop_first true; }; * state Green: { property hesitate false; }; * * `; * * traffic_light.state(); // Off * traffic_light.props(); // { can_go: true, hesitate: true, stop_first: true; } * * traffic_light.go('Red'); * traffic_light.props(); // { can_go: false, hesitate: true, stop_first: true; } * * traffic_light.go('Green'); * traffic_light.props(); // { can_go: true, hesitate: false, stop_first: false; } * ``` * */ props() { const ret = {}; this.known_props().forEach(p => ret[p] = this.prop(p)); return ret; } // NEEDS_DOCS // TODO COMEBACK /********* * * Get the current value of every prop, as an object. Compare * {@link prop_map}, which returns a `Map`. * * ```typescript * * ``` * */ // sparse_props(name: string): object { // } // NEEDS_DOCS // TODO COMEBACK /********* * * Get the current value of every prop, as an object. Compare * {@link prop_map}, which returns a `Map`. Akin to {@link strict_prop}, * this throws if a required prop is missing. * * ```typescript * * ``` * */ // strict_props(name: string): object { // } /********* * * Check whether a given string is a known property's name. * * ```typescript * const example = sm`property foo default 1; a->b;`; * * example.known_prop('foo'); // true * example.known_prop('bar'); // false * ``` * * @param prop_name The relevant property name to look up * */ known_prop(prop_name) { return this._property_keys.has(prop_name); } // NEEDS_DOCS /********* * * List all known property names. If you'd also like values, use * {@link props} instead. The order of the properties is not defined, and * the properties generally will not be sorted. * * ```typescript * ``` * */ known_props() { return [...this._property_keys]; } /******** * * Check whether a given state is a valid start state (either because it was * explicitly named as such, or because it was the first mentioned state.) * * ```typescript * import { sm, is_start_state } from 'jssm'; * * const example = sm`a -> b;`; * * console.log( final_test.is_start_state('a') ); // true * console.log( final_test.is_start_state('b') ); // false * * const example = sm`start_states: [a b]; a -> b;`; * * console.log( final_test.is_start_state('a') ); // true * console.log( final_test.is_start_state('b') ); // true * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The name of the state to check * */ is_start_state(whichState) { return this._start_states.has(whichState); } /******** * * Check whether a given state is a valid start state (either because it was * explicitly named as such, or because it was the first mentioned state.) * * ```typescript * import { sm, is_end_state } from 'jssm'; * * const example = sm`a -> b;`; * * console.log( final_test.is_start_state('a') ); // false * console.log( final_test.is_start_state('b') ); // true * * const example = sm`end_states: [a b]; a -> b;`; * * console.log( final_test.is_start_state('a') ); // true * console.log( final_test.is_start_state('b') ); // true * ``` * * @typeparam mDT The type of the machine data member; usually omitted * * @param whichState The name of the state to check * */ is_end_state(whichState) { return this._end_states.has(whichState); } /******** * * 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, 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 * ``` * */ is_final() { // return ((!this.is_changing()) && this.state_is_final(this.state())); return this.state_is_final(this.state()); } /******** * * Serialize the current machine, including all defining state but not the * machine string, to a structure. This means you will need the machine * string to recreate (to not waste repeated space;) if you want the machine * string embedded, call {@link serialize_with_string} instead. * * @typeparam mDT The type of the machine data member; usually omitted * */ serialize(comment) { return { comment, state: this._state, data: this._data, jssm_version: version, history: this._history.toArray(), history_capacity: this._history.capacity, timestamp: new Date().getTime(), }; } 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 }; } /********* * * 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 lswitch = jssm.from('on <=> off;'); * console.log( lswitch.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 lswitch = jssm.from('on <=> off;'); * * console.log( lswitch.has_state('off') ); // true * console.log( lswitch.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()); } get uses_actions() { return Array.from(this._actions.keys()).length > 0; } get uses_forced_transitions() { return this._has_forced_transitions; } /********* * * Check if the code that built the machine allows overriding state and data. * */ get code_allows_override() { return this._code_allows_override; } /********* * * Check if the machine config allows overriding state and data. * */ get config_allows_override() { return this._config_allows_override; } /********* * * Check if a machine allows overriding state and data. * */ get allows_override() { // code false? config true, throw. config false, false. config undefined, false. if (this._code_allows_override === false) { /* istanbul ignore next */ if (this._config_allows_override === true) { /* istanbul ignore next */ throw new JssmError(this, "Code specifies no override, but config tries to permit; config may not be less strict than code; should be unreachable"); } else { return false; } } // code true? config true, true. config false, false. config undefined, true. if (this._code_allows_override === true) { if (this._config_allows_override === false) { return false; } else { return true; } } // code must be undefined. config false, false. config true, true. config undefined, false. if (this._config_allows_override === true) { return true; } else { return false; } } all_themes() { return [...theme_mapping.keys()]; // constructor sets this to "default" otherwise } // This will always return an array of FSL themes; the reason we spuriously // add the single type is that the setter and getter need matching accept/return // types, and the setter can take both as a convenience get themes() { return this._themes; // constructor sets this to "default" otherwise } set themes(to) { if (typeof to === 'string') { this._themes = [to]; } else { this._themes = to; } } 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. This list includes both unforced and * forced entrances; if this isn't desired, consider * {@link list_unforced_entrances} or {@link list_forced_entrances} as * appropriate. * * ```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()) { var _a, _b; const guaranteed = ((_a = this._states.get(whichState)) !== null && _a !== void 0 ? _a : { from: undefined }); return (_b = guaranteed.from) !== null && _b !== void 0 ? _b : []; } /******** * * List all exits attached to the current state. Please note that the order * of the list is not defined. This list includes both unforced and forced * exits; if this isn't desired, consider {@link list_unforced_exits} or * {@link list_forced_exits} as appropriate. * * ```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()) { var _a, _b; const guaranteed = ((_a = this._states.get(whichState)) !== null && _a !== void 0 ? _a : { to: undefined }); return (_b = guaranteed.to) !== null && _b !== void 0 ? _b : []; } 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_filtered -> 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()), undefined, this._rng); 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 { if (this.has_state(whichState)) { return []; } 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_hoo