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
JavaScript
// 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