jssm 5.104.2 → 5.112.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.gitattributes +17 -6
  2. package/.log-progress.json +9 -0
  3. package/CHANGELOG.md +130 -45
  4. package/CLAUDE.md +11 -0
  5. package/MIGRATING-jssm-viz.md +67 -0
  6. package/README.md +179 -882
  7. package/dist/es6/fsl_parser.js +1 -1
  8. package/dist/es6/jssm.d.ts +773 -39
  9. package/dist/es6/jssm.js +921 -89
  10. package/dist/es6/jssm_arrow.js +24 -0
  11. package/dist/es6/jssm_compiler.d.ts +17 -2
  12. package/dist/es6/jssm_compiler.js +17 -3
  13. package/dist/es6/jssm_constants.d.ts +27 -0
  14. package/dist/es6/jssm_constants.js +27 -0
  15. package/dist/es6/jssm_error.d.ts +19 -0
  16. package/dist/es6/jssm_error.js +19 -0
  17. package/dist/es6/jssm_theme.d.ts +11 -0
  18. package/dist/es6/jssm_theme.js +11 -0
  19. package/dist/es6/jssm_types.d.ts +29 -3
  20. package/dist/es6/jssm_util.d.ts +161 -9
  21. package/dist/es6/jssm_util.js +174 -17
  22. package/dist/es6/jssm_viz.d.ts +175 -0
  23. package/dist/es6/jssm_viz.js +560 -0
  24. package/dist/es6/jssm_viz_colors.d.ts +63 -0
  25. package/dist/es6/jssm_viz_colors.js +63 -0
  26. package/dist/es6/version.js +1 -1
  27. package/dist/jssm.es5.cjs +1 -1
  28. package/dist/jssm.es5.iife.js +1 -0
  29. package/dist/jssm.es5.nonmin.cjs +2201 -870
  30. package/dist/jssm.es6.mjs +1 -1
  31. package/dist/jssm.es6.nonmin.cjs +2200 -871
  32. package/dist/jssm_viz.cjs +1 -0
  33. package/dist/{jssm.es5.iife.nonmin.cjs → jssm_viz.es5.iife.nonmin.cjs} +2589 -1090
  34. package/dist/jssm_viz.es5.nonmin.cjs +24674 -0
  35. package/dist/jssm_viz.es6.nonmin.cjs +24661 -0
  36. package/dist/jssm_viz.iife.cjs +1 -0
  37. package/dist/jssm_viz.mjs +1 -0
  38. package/jest-dragon.config.cjs +4 -1
  39. package/jest-spec.config.cjs +9 -6
  40. package/jest-stoch.config.cjs +4 -1
  41. package/jest-unicode.config.cjs +4 -1
  42. package/jssm.es5.d.cts +950 -41
  43. package/jssm.es6.d.ts +950 -41
  44. package/jssm_viz.es5.d.cts +2127 -0
  45. package/jssm_viz.es6.d.ts +2127 -0
  46. package/log-progress.data.json +28 -0
  47. package/package.json +56 -23
  48. package/rollup.config.viz.es5.js +46 -0
  49. package/rollup.config.viz.es6.js +46 -0
  50. package/rollup.config.viz.iife.js +36 -0
  51. package/typedoc-options.cjs +4 -3
  52. package/dist/jssm.es5.iife.cjs +0 -1
  53. package/fsl_parser.d.ts +0 -6
  54. package/jssm.d.ts +0 -1141
  55. package/jssm_arrow.d.ts +0 -53
  56. package/jssm_compiler.d.ts +0 -135
  57. package/jssm_constants.d.ts +0 -5
  58. package/jssm_error.d.ts +0 -8
  59. package/jssm_theme.d.ts +0 -4
  60. package/jssm_types.d.ts +0 -378
  61. package/jssm_util.d.ts +0 -106
  62. package/version.d.ts +0 -2
package/dist/es6/jssm.js CHANGED
@@ -45,6 +45,9 @@ function transfer_state_properties(state_decl) {
45
45
  case 'border-color':
46
46
  state_decl.borderColor = d.value;
47
47
  break;
48
+ case 'image':
49
+ state_decl.image = d.value;
50
+ break;
48
51
  case 'state_property':
49
52
  state_decl.property = { name: d.name, value: d.value };
50
53
  break;
@@ -53,65 +56,107 @@ function transfer_state_properties(state_decl) {
53
56
  });
54
57
  return state_decl;
55
58
  }
56
- function state_style_condense(jssk) {
59
+ /**
60
+ *
61
+ * Collapse a list of individual state-style key/value pairs into a single
62
+ * {@link JssmStateConfig} object, remapping FSL-style kebab-case keys to the
63
+ * camelCase field names the runtime uses.
64
+ *
65
+ * The parser emits state styling as a flat array like
66
+ * `[{ key: 'color', value: 'red' }, { key: 'line-style', value: 'dashed' }]`
67
+ * because that is the most natural shape for the grammar to produce. This
68
+ * helper runs once per style bucket during `Machine` construction to turn
69
+ * those arrays into the compact `{ color, lineStyle, ... }` objects the
70
+ * graph-rendering code expects.
71
+ *
72
+ * ```typescript
73
+ * state_style_condense([
74
+ * { key: 'color', value: 'red' },
75
+ * { key: 'shape', value: 'oval' },
76
+ * { key: 'line-style', value: 'dashed' }
77
+ * ]);
78
+ * // => { color: 'red', shape: 'oval', lineStyle: 'dashed' }
79
+ *
80
+ * state_style_condense(undefined);
81
+ * // => {}
82
+ * ```
83
+ *
84
+ * @param jssk The list of style keys to condense. `undefined` is accepted
85
+ * and yields an empty config.
86
+ *
87
+ * @param machine Optional `Machine` reference, used only so that any
88
+ * {@link JssmError} thrown can point at the offending machine in its
89
+ * diagnostic message.
90
+ *
91
+ * @returns A `JssmStateConfig` object containing every key from `jssk`
92
+ * remapped into its camelCase field.
93
+ *
94
+ * @throws {JssmError} If `jssk` is neither an array nor `undefined`, if any
95
+ * element is not an object, if the same key appears more than once, or if a
96
+ * key is not one of the recognized style names.
97
+ *
98
+ * @internal
99
+ *
100
+ */
101
+ function state_style_condense(jssk, machine) {
57
102
  const state_style = {};
58
103
  if (Array.isArray(jssk)) {
59
104
  jssk.forEach((key, i) => {
60
105
  if (typeof key !== 'object') {
61
- throw new JssmError(this, `invalid state item ${i} in state_style_condense list: ${JSON.stringify(key)}`);
106
+ throw new JssmError(machine, `invalid state item ${i} in state_style_condense list: ${JSON.stringify(key)}`);
62
107
  }
63
108
  switch (key.key) {
64
109
  case 'shape':
65
110
  if (state_style.shape !== undefined) {
66
- throw new JssmError(this, `cannot redefine 'shape' in state_style_condense, already defined`);
111
+ throw new JssmError(machine, `cannot redefine 'shape' in state_style_condense, already defined`);
67
112
  }
68
113
  state_style.shape = key.value;
69
114
  break;
70
115
  case 'color':
71
116
  if (state_style.color !== undefined) {
72
- throw new JssmError(this, `cannot redefine 'color' in state_style_condense, already defined`);
117
+ throw new JssmError(machine, `cannot redefine 'color' in state_style_condense, already defined`);
73
118
  }
74
119
  state_style.color = key.value;
75
120
  break;
76
121
  case 'text-color':
77
122
  if (state_style.textColor !== undefined) {
78
- throw new JssmError(this, `cannot redefine 'text-color' in state_style_condense, already defined`);
123
+ throw new JssmError(machine, `cannot redefine 'text-color' in state_style_condense, already defined`);
79
124
  }
80
125
  state_style.textColor = key.value;
81
126
  break;
82
127
  case 'corners':
83
128
  if (state_style.corners !== undefined) {
84
- throw new JssmError(this, `cannot redefine 'corners' in state_style_condense, already defined`);
129
+ throw new JssmError(machine, `cannot redefine 'corners' in state_style_condense, already defined`);
85
130
  }
86
131
  state_style.corners = key.value;
87
132
  break;
88
133
  case 'line-style':
89
134
  if (state_style.lineStyle !== undefined) {
90
- throw new JssmError(this, `cannot redefine 'line-style' in state_style_condense, already defined`);
135
+ throw new JssmError(machine, `cannot redefine 'line-style' in state_style_condense, already defined`);
91
136
  }
92
137
  state_style.lineStyle = key.value;
93
138
  break;
94
139
  case 'background-color':
95
140
  if (state_style.backgroundColor !== undefined) {
96
- throw new JssmError(this, `cannot redefine 'background-color' in state_style_condense, already defined`);
141
+ throw new JssmError(machine, `cannot redefine 'background-color' in state_style_condense, already defined`);
97
142
  }
98
143
  state_style.backgroundColor = key.value;
99
144
  break;
100
145
  case 'state-label':
101
146
  if (state_style.stateLabel !== undefined) {
102
- throw new JssmError(this, `cannot redefine 'state-label' in state_style_condense, already defined`);
147
+ throw new JssmError(machine, `cannot redefine 'state-label' in state_style_condense, already defined`);
103
148
  }
104
149
  state_style.stateLabel = key.value;
105
150
  break;
106
151
  case 'border-color':
107
152
  if (state_style.borderColor !== undefined) {
108
- throw new JssmError(this, `cannot redefine 'border-color' in state_style_condense, already defined`);
153
+ throw new JssmError(machine, `cannot redefine 'border-color' in state_style_condense, already defined`);
109
154
  }
110
155
  state_style.borderColor = key.value;
111
156
  break;
112
157
  default:
113
158
  // TODO do that <never> trick to assert this list is complete
114
- throw new JssmError(this, `unknown state style key in condense: ${key.key}`);
159
+ throw new JssmError(machine, `unknown state style key in condense: ${key.key}`);
115
160
  }
116
161
  });
117
162
  }
@@ -119,11 +164,30 @@ function state_style_condense(jssk) {
119
164
  // do nothing, undefined is legal and means we should return the empty container above
120
165
  }
121
166
  else {
122
- throw new JssmError(this, 'state_style_condense received a non-array');
167
+ throw new JssmError(machine, 'state_style_condense received a non-array');
123
168
  }
124
169
  return state_style;
125
170
  }
126
- // TODO add a lotta docblock here
171
+ /*******
172
+ *
173
+ * Core finite state machine class. Holds the full graph of states and
174
+ * transitions, the current state, hooks, data, properties, and all runtime
175
+ * behavior. Typically created via the {@link sm} tagged template literal
176
+ * rather than constructed directly.
177
+ *
178
+ * ```typescript
179
+ * import { sm } from 'jssm';
180
+ *
181
+ * const light = sm`Red 'next' => Green 'next' => Yellow 'next' => Red;`;
182
+ * light.state(); // 'Red'
183
+ * light.action('next'); // true
184
+ * light.state(); // 'Green'
185
+ * ```
186
+ *
187
+ * @typeparam mDT The machine data type — the type of the value stored in
188
+ * `.data()`. Defaults to `undefined` when no data is used.
189
+ *
190
+ */
127
191
  class Machine {
128
192
  // whargarbl this badly needs to be broken up, monolith master
129
193
  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 }) {
@@ -164,7 +228,7 @@ class Machine {
164
228
  this._has_exit_hooks = false;
165
229
  this._has_after_hooks = false;
166
230
  this._has_global_action_hooks = false;
167
- this._has_transition_hooks = true;
231
+ this._has_transition_hooks = false;
168
232
  // no need for a boolean for single hooks, just test for undefinedness
169
233
  this._has_forced_transitions = false;
170
234
  this._hooks = new Map();
@@ -184,7 +248,7 @@ class Machine {
184
248
  this._has_post_entry_hooks = false;
185
249
  this._has_post_exit_hooks = false;
186
250
  this._has_post_global_action_hooks = false;
187
- this._has_post_transition_hooks = true;
251
+ this._has_post_transition_hooks = false;
188
252
  // no need for a boolean for single hooks, just test for undefinedness
189
253
  this._code_allows_override = allows_override;
190
254
  this._config_allows_override = config_allows_override;
@@ -201,17 +265,21 @@ class Machine {
201
265
  this._post_main_transition_hook = undefined;
202
266
  this._post_forced_transition_hook = undefined;
203
267
  this._post_any_transition_hook = undefined;
268
+ this._pre_everything_hook = undefined;
269
+ this._everything_hook = undefined;
270
+ this._pre_post_everything_hook = undefined;
271
+ this._post_everything_hook = undefined;
204
272
  this._data = data;
205
273
  this._property_keys = new Set();
206
274
  this._default_properties = new Map();
207
275
  this._state_properties = new Map();
208
276
  this._required_properties = new Set();
209
- this._state_style = state_style_condense(default_state_config);
210
- this._active_state_style = state_style_condense(default_active_state_config);
211
- this._hooked_state_style = state_style_condense(default_hooked_state_config);
212
- this._terminal_state_style = state_style_condense(default_terminal_state_config);
213
- this._start_state_style = state_style_condense(default_start_state_config);
214
- this._end_state_style = state_style_condense(default_end_state_config);
277
+ this._state_style = state_style_condense(default_state_config, this);
278
+ this._active_state_style = state_style_condense(default_active_state_config, this);
279
+ this._hooked_state_style = state_style_condense(default_hooked_state_config, this);
280
+ this._terminal_state_style = state_style_condense(default_terminal_state_config, this);
281
+ this._start_state_style = state_style_condense(default_start_state_config, this);
282
+ this._end_state_style = state_style_condense(default_end_state_config, this);
215
283
  this._history_length = history || 0;
216
284
  this._history = new circular_buffer(this._history_length);
217
285
  this._state_labels = new Map();
@@ -446,6 +514,8 @@ class Machine {
446
514
  *
447
515
  * @typeparam mDT The type of the machine data member; usually omitted
448
516
  *
517
+ * @returns The current state name.
518
+ *
449
519
  */
450
520
  state() {
451
521
  return this._state;
@@ -466,6 +536,10 @@ class Machine {
466
536
  *
467
537
  * @typeparam mDT The type of the machine data member; usually omitted
468
538
  *
539
+ * @param state The state to get the label for.
540
+ *
541
+ * @returns The label string, or `undefined` if no label is set.
542
+ *
469
543
  */
470
544
  label_for(state) {
471
545
  return this._state_labels.get(state);
@@ -491,6 +565,10 @@ class Machine {
491
565
  *
492
566
  * @typeparam mDT The type of the machine data member; usually omitted
493
567
  *
568
+ * @param state The state to get display text for.
569
+ *
570
+ * @returns The label if one exists, otherwise the state's name.
571
+ *
494
572
  */
495
573
  display_text(state) {
496
574
  var _a;
@@ -509,23 +587,32 @@ class Machine {
509
587
  *
510
588
  * @typeparam mDT The type of the machine data member; usually omitted
511
589
  *
590
+ * @returns A deep clone of the machine's current data value.
591
+ *
512
592
  */
513
593
  data() {
514
594
  return structuredClone(this._data);
515
595
  }
516
- // NEEDS_DOCS
517
596
  /*********
518
597
  *
519
- * Get the current value of a given property name.
598
+ * Get the current value of a given property name. Checks the current
599
+ * state's properties first, then falls back to the global default.
600
+ * Returns `undefined` if neither exists. For a throwing variant, see
601
+ * {@link strict_prop}.
520
602
  *
521
603
  * ```typescript
604
+ * const m = sm`property color default "grey"; a -> b;
605
+ * state b: { property color "blue"; };`;
522
606
  *
607
+ * m.prop('color'); // 'grey' (default, because state is 'a')
608
+ * m.go('b');
609
+ * m.prop('color'); // 'blue' (state 'b' overrides the default)
610
+ * m.prop('size'); // undefined (no such property)
523
611
  * ```
524
612
  *
525
- * @param name The relevant property name to look up
613
+ * @param name The relevant property name to look up.
526
614
  *
527
- * @returns The value behind the prop name. Because functional props are
528
- * evaluated as getters, this can be anything.
615
+ * @returns The value behind the prop name, or `undefined` if not defined.
529
616
  *
530
617
  */
531
618
  prop(name) {
@@ -540,21 +627,25 @@ class Machine {
540
627
  return undefined;
541
628
  }
542
629
  }
543
- // NEEDS_DOCS
544
630
  /*********
545
631
  *
546
632
  * Get the current value of a given property name. If missing on the state
547
- * and without a global default, throw, unlike {@link prop}, which would
548
- * return `undefined` instead.
633
+ * and without a global default, throws a {@link JssmError}, unlike
634
+ * {@link prop}, which would return `undefined` instead.
549
635
  *
550
636
  * ```typescript
637
+ * const m = sm`property color default "grey"; a -> b;`;
551
638
  *
639
+ * m.strict_prop('color'); // 'grey'
640
+ * m.strict_prop('size'); // throws JssmError
552
641
  * ```
553
642
  *
554
- * @param name The relevant property name to look up
643
+ * @param name The relevant property name to look up.
644
+ *
645
+ * @returns The value behind the prop name.
555
646
  *
556
- * @returns The value behind the prop name. Because functional props are
557
- * evaluated as getters, this can be anything.
647
+ * @throws {JssmError} If the property is not defined on the current state
648
+ * and has no default.
558
649
  *
559
650
  */
560
651
  strict_prop(name) {
@@ -569,13 +660,11 @@ class Machine {
569
660
  throw new JssmError(this, `Strictly requested a prop '${name}' which doesn't exist on current state '${this.state()}' and has no default`);
570
661
  }
571
662
  }
572
- // NEEDS_DOCS
573
- // COMEBACK add prop_map, sparse_props and strict_props to doc text when implemented
574
663
  /*********
575
664
  *
576
665
  * Get the current value of every prop, as an object. If no current definition
577
- * exists for a prop - that is, if the prop was defined without a default and
578
- * the current state also doesn't define the prop - then that prop will be listed
666
+ * exists for a prop that is, if the prop was defined without a default and
667
+ * the current state also doesn't define the prop then that prop will be listed
579
668
  * in the returned object with a value of `undefined`.
580
669
  *
581
670
  * ```typescript
@@ -606,41 +695,20 @@ class Machine {
606
695
  * traffic_light.props(); // { can_go: true, hesitate: false, stop_first: false; }
607
696
  * ```
608
697
  *
698
+ * @returns An object mapping every known property name to its current value
699
+ * (or `undefined` if the property has no default and the current state
700
+ * doesn't define it).
701
+ *
609
702
  */
610
703
  props() {
611
704
  const ret = {};
612
705
  this.known_props().forEach(p => ret[p] = this.prop(p));
613
706
  return ret;
614
707
  }
615
- // NEEDS_DOCS
616
- // TODO COMEBACK
617
- /*********
618
- *
619
- * Get the current value of every prop, as an object. Compare
620
- * {@link prop_map}, which returns a `Map`.
621
- *
622
- * ```typescript
623
- *
624
- * ```
625
- *
626
- */
627
- // sparse_props(name: string): object {
628
- // }
629
- // NEEDS_DOCS
630
- // TODO COMEBACK
631
- /*********
632
- *
633
- * Get the current value of every prop, as an object. Compare
634
- * {@link prop_map}, which returns a `Map`. Akin to {@link strict_prop},
635
- * this throws if a required prop is missing.
636
- *
637
- * ```typescript
638
- *
639
- * ```
640
- *
641
- */
642
- // strict_props(name: string): object {
643
- // }
708
+ // TODO: sparse_props — like props() but omits undefined entries
709
+ // sparse_props(name: string): object { }
710
+ // TODO: strict_props — like props() but throws on any undefined entry
711
+ // strict_props(name: string): object { }
644
712
  /*********
645
713
  *
646
714
  * Check whether a given string is a known property's name.
@@ -658,7 +726,6 @@ class Machine {
658
726
  known_prop(prop_name) {
659
727
  return this._property_keys.has(prop_name);
660
728
  }
661
- // NEEDS_DOCS
662
729
  /*********
663
730
  *
664
731
  * List all known property names. If you'd also like values, use
@@ -666,8 +733,13 @@ class Machine {
666
733
  * the properties generally will not be sorted.
667
734
  *
668
735
  * ```typescript
736
+ * const m = sm`property color default "grey"; property size default 1; a -> b;`;
737
+ *
738
+ * m.known_props(); // ['color', 'size']
669
739
  * ```
670
740
  *
741
+ * @returns An array of all property name strings defined on this machine.
742
+ *
671
743
  */
672
744
  known_props() {
673
745
  return [...this._property_keys];
@@ -777,6 +849,12 @@ class Machine {
777
849
  *
778
850
  * @typeparam mDT The type of the machine data member; usually omitted
779
851
  *
852
+ * @param comment An optional comment string to embed in the serialized
853
+ * output for identification or debugging.
854
+ *
855
+ * @returns A {@link JssmSerialization} object containing the machine's
856
+ * current state, data, and timestamp.
857
+ *
780
858
  */
781
859
  serialize(comment) {
782
860
  return {
@@ -789,48 +867,98 @@ class Machine {
789
867
  timestamp: new Date().getTime(),
790
868
  };
791
869
  }
870
+ /** Get the graph layout direction (e.g. `'LR'`, `'TB'`). Set via the
871
+ * FSL `graph_layout` directive.
872
+ * @returns The layout string, or the default if not set.
873
+ */
792
874
  graph_layout() {
793
875
  return this._graph_layout;
794
876
  }
877
+ /** Get the Graphviz DOT preamble string, injected before the graph body
878
+ * during visualization. Set via the FSL `dot_preamble` directive.
879
+ * @returns The preamble string.
880
+ */
795
881
  dot_preamble() {
796
882
  return this._dot_preamble;
797
883
  }
884
+ /** Get the machine's author list. Set via the FSL `machine_author` directive.
885
+ * @returns An array of author name strings.
886
+ */
798
887
  machine_author() {
799
888
  return this._machine_author;
800
889
  }
890
+ /** Get the machine's comment string. Set via the FSL `machine_comment` directive.
891
+ * @returns The comment string.
892
+ */
801
893
  machine_comment() {
802
894
  return this._machine_comment;
803
895
  }
896
+ /** Get the machine's contributor list. Set via the FSL `machine_contributor` directive.
897
+ * @returns An array of contributor name strings.
898
+ */
804
899
  machine_contributor() {
805
900
  return this._machine_contributor;
806
901
  }
902
+ /** Get the machine's definition string. Set via the FSL `machine_definition` directive.
903
+ * @returns The definition string.
904
+ */
807
905
  machine_definition() {
808
906
  return this._machine_definition;
809
907
  }
908
+ /** Get the machine's language (ISO 639-1). Set via the FSL `machine_language` directive.
909
+ * @returns The language code string.
910
+ */
810
911
  machine_language() {
811
912
  return this._machine_language;
812
913
  }
914
+ /** Get the machine's license string. Set via the FSL `machine_license` directive.
915
+ * @returns The license string.
916
+ */
813
917
  machine_license() {
814
918
  return this._machine_license;
815
919
  }
920
+ /** Get the machine's name. Set via the FSL `machine_name` directive.
921
+ * @returns The machine name string.
922
+ */
816
923
  machine_name() {
817
924
  return this._machine_name;
818
925
  }
926
+ /** Get the machine's version string. Set via the FSL `machine_version` directive.
927
+ * @returns The version string.
928
+ */
819
929
  machine_version() {
820
930
  return this._machine_version;
821
931
  }
932
+ /** Get the raw state declaration objects as parsed from the FSL source.
933
+ * @returns An array of raw state declaration objects.
934
+ */
822
935
  raw_state_declarations() {
823
936
  return this._raw_state_declaration;
824
937
  }
938
+ /** Get the processed state declaration for a specific state.
939
+ * @param which - The state to look up.
940
+ * @returns The {@link JssmStateDeclaration} for the given state.
941
+ */
825
942
  state_declaration(which) {
826
943
  return this._state_declarations.get(which);
827
944
  }
945
+ /** Get all processed state declarations as a Map.
946
+ * @returns A `Map` from state name to {@link JssmStateDeclaration}.
947
+ */
828
948
  state_declarations() {
829
949
  return this._state_declarations;
830
950
  }
951
+ /** Get the FSL language version this machine was compiled under.
952
+ * @returns The FSL version string.
953
+ */
831
954
  fsl_version() {
832
955
  return this._fsl_version;
833
956
  }
957
+ /** Get the complete internal state of the machine as a serializable
958
+ * structure. Includes actions, edges, edge map, named transitions,
959
+ * reverse actions, current state, and states map.
960
+ * @returns A {@link JssmMachineInternalState} snapshot.
961
+ */
834
962
  machine_state() {
835
963
  return {
836
964
  internal_state_impl_version: 1,
@@ -858,10 +986,17 @@ class Machine {
858
986
  *
859
987
  * @typeparam mDT The type of the machine data member; usually omitted
860
988
  *
989
+ * @returns An array of all state names in the machine.
990
+ *
861
991
  */
862
992
  states() {
863
993
  return Array.from(this._states.keys());
864
994
  }
995
+ /** Get the internal state descriptor for a given state name.
996
+ * @param whichState - The state to look up.
997
+ * @returns The {@link JssmGenericState} descriptor.
998
+ * @throws {JssmError} If the state does not exist.
999
+ */
865
1000
  state_for(whichState) {
866
1001
  const state = this._states.get(whichState);
867
1002
  if (state) {
@@ -886,7 +1021,9 @@ class Machine {
886
1021
  *
887
1022
  * @typeparam mDT The type of the machine data member; usually omitted
888
1023
  *
889
- * @param whichState The state to be checked for extance
1024
+ * @param whichState The state to be checked for existence.
1025
+ *
1026
+ * @returns `true` if the state exists, `false` otherwise.
890
1027
  *
891
1028
  */
892
1029
  has_state(whichState) {
@@ -924,19 +1061,33 @@ class Machine {
924
1061
  *
925
1062
  * @typeparam mDT The type of the machine data member; usually omitted
926
1063
  *
1064
+ * @returns An array of all {@link JssmTransition} edge objects.
1065
+ *
927
1066
  */
928
1067
  list_edges() {
929
1068
  return this._edges;
930
1069
  }
1070
+ /** Get the map of named transitions (transitions with explicit names).
1071
+ * @returns A `Map` from transition name to edge index.
1072
+ */
931
1073
  list_named_transitions() {
932
1074
  return this._named_transitions;
933
1075
  }
1076
+ /** List all distinct action names defined anywhere in the machine.
1077
+ * @returns An array of action name strings.
1078
+ */
934
1079
  list_actions() {
935
1080
  return Array.from(this._actions.keys());
936
1081
  }
1082
+ /** Whether any actions are defined on this machine.
1083
+ * @returns `true` if the machine has at least one action.
1084
+ */
937
1085
  get uses_actions() {
938
1086
  return Array.from(this._actions.keys()).length > 0;
939
1087
  }
1088
+ /** Whether any forced (`~>`) transitions exist in this machine.
1089
+ * @returns `true` if at least one forced transition is defined.
1090
+ */
940
1091
  get uses_forced_transitions() {
941
1092
  return this._has_forced_transitions;
942
1093
  }
@@ -944,6 +1095,8 @@ class Machine {
944
1095
  *
945
1096
  * Check if the code that built the machine allows overriding state and data.
946
1097
  *
1098
+ * @returns The override permission from the FSL source code.
1099
+ *
947
1100
  */
948
1101
  get code_allows_override() {
949
1102
  return this._code_allows_override;
@@ -952,13 +1105,19 @@ class Machine {
952
1105
  *
953
1106
  * Check if the machine config allows overriding state and data.
954
1107
  *
1108
+ * @returns The override permission from the runtime config.
1109
+ *
955
1110
  */
956
1111
  get config_allows_override() {
957
1112
  return this._config_allows_override;
958
1113
  }
959
1114
  /*********
960
1115
  *
961
- * Check if a machine allows overriding state and data.
1116
+ * Check if a machine allows overriding state and data. Resolves the
1117
+ * combined effect of code and config permissions — config may not be
1118
+ * less strict than code.
1119
+ *
1120
+ * @returns The effective override permission.
962
1121
  *
963
1122
  */
964
1123
  get allows_override() {
@@ -990,15 +1149,22 @@ class Machine {
990
1149
  return false;
991
1150
  }
992
1151
  }
1152
+ /** List all available theme names.
1153
+ * @returns An array of theme name strings.
1154
+ */
993
1155
  all_themes() {
994
1156
  return [...theme_mapping.keys()]; // constructor sets this to "default" otherwise
995
1157
  }
996
- // This will always return an array of FSL themes; the reason we spuriously
997
- // add the single type is that the setter and getter need matching accept/return
998
- // types, and the setter can take both as a convenience
1158
+ /** Get the active theme(s) for this machine. Always stored as an array
1159
+ * internally; the union return type exists for setter compatibility.
1160
+ * @returns The current theme or array of themes.
1161
+ */
999
1162
  get themes() {
1000
1163
  return this._themes; // constructor sets this to "default" otherwise
1001
1164
  }
1165
+ /** Set the active theme(s). Accepts a single theme name or an array.
1166
+ * @param to - A theme name or array of theme names to apply.
1167
+ */
1002
1168
  set themes(to) {
1003
1169
  if (typeof to === 'string') {
1004
1170
  this._themes = [to];
@@ -1007,9 +1173,19 @@ class Machine {
1007
1173
  this._themes = to;
1008
1174
  }
1009
1175
  }
1176
+ /** Get the flow direction for graph layout (e.g. `'right'`, `'down'`).
1177
+ * Set via the FSL `flow` directive.
1178
+ * @returns The current flow direction.
1179
+ */
1010
1180
  flow() {
1011
1181
  return this._flow;
1012
1182
  }
1183
+ /** Look up a transition's edge index by source and target state names.
1184
+ * @param from - Source state name.
1185
+ * @param to - Target state name.
1186
+ * @returns The edge index in the edges array, or `undefined` if no
1187
+ * such transition exists.
1188
+ */
1013
1189
  get_transition_by_state_names(from, to) {
1014
1190
  const emg = this._edge_map.get(from);
1015
1191
  if (emg) {
@@ -1019,6 +1195,11 @@ class Machine {
1019
1195
  return undefined;
1020
1196
  }
1021
1197
  }
1198
+ /** Look up the full transition object for a given source→target pair.
1199
+ * @param from - Source state name.
1200
+ * @param to - Target state name.
1201
+ * @returns The {@link JssmTransition} object, or `undefined` if none exists.
1202
+ */
1022
1203
  lookup_transition_for(from, to) {
1023
1204
  const id = this.get_transition_by_state_names(from, to);
1024
1205
  return ((id === undefined) || (id === null)) ? undefined : this._edges[id];
@@ -1099,6 +1280,12 @@ class Machine {
1099
1280
  const guaranteed = ((_a = this._states.get(whichState)) !== null && _a !== void 0 ? _a : { to: undefined });
1100
1281
  return (_b = guaranteed.to) !== null && _b !== void 0 ? _b : [];
1101
1282
  }
1283
+ /** Get the transitions available from a state, filtered to those with
1284
+ * probability data. Used by the probabilistic walk system.
1285
+ * @param whichState - The state to inspect.
1286
+ * @returns An array of {@link JssmTransition} edges exiting the state.
1287
+ * @throws {JssmError} If the state does not exist.
1288
+ */
1102
1289
  probable_exits_for(whichState) {
1103
1290
  const wstate = this._states.get(whichState);
1104
1291
  if (!(wstate)) {
@@ -1106,14 +1293,23 @@ class Machine {
1106
1293
  }
1107
1294
  const wstate_to = wstate.to, wtf // wstate_to_filtered -> wtf
1108
1295
  = wstate_to
1109
- .map((ws) => this.lookup_transition_for(this.state(), ws))
1296
+ .map((ws) => this.lookup_transition_for(whichState, ws))
1110
1297
  .filter(Boolean);
1111
1298
  return wtf;
1112
1299
  }
1300
+ /** Take a single random transition from the current state, weighted by
1301
+ * edge probabilities.
1302
+ * @returns `true` if a transition was taken, `false` otherwise.
1303
+ */
1113
1304
  probabilistic_transition() {
1114
1305
  const selected = weighted_rand_select(this.probable_exits_for(this.state()), undefined, this._rng);
1115
1306
  return this.transition(selected.to);
1116
1307
  }
1308
+ /** Take `n` consecutive probabilistic transitions and return the sequence
1309
+ * of states visited (before each transition).
1310
+ * @param n - Number of steps to walk.
1311
+ * @returns An array of state names visited during the walk.
1312
+ */
1117
1313
  probabilistic_walk(n) {
1118
1314
  return seq(n)
1119
1315
  .map(() => {
@@ -1123,6 +1319,11 @@ class Machine {
1123
1319
  })
1124
1320
  .concat([this.state()]);
1125
1321
  }
1322
+ /** Take `n` probabilistic steps and return a histograph of how many times
1323
+ * each state was visited.
1324
+ * @param n - Number of steps to walk.
1325
+ * @returns A `Map` from state name to visit count.
1326
+ */
1126
1327
  probabilistic_histo_walk(n) {
1127
1328
  return histograph(this.probabilistic_walk(n));
1128
1329
  }
@@ -1157,7 +1358,10 @@ class Machine {
1157
1358
  *
1158
1359
  * @typeparam mDT The type of the machine data member; usually omitted
1159
1360
  *
1160
- * @param whichState The state whose actions to have listed
1361
+ * @param whichState The state whose actions to list. Defaults to the
1362
+ * current state.
1363
+ *
1364
+ * @returns An array of action names available from the given state.
1161
1365
  *
1162
1366
  */
1163
1367
  actions(whichState = this.state()) {
@@ -1214,9 +1418,17 @@ class Machine {
1214
1418
  .map( filtered => filtered.from );
1215
1419
  }
1216
1420
  */
1421
+ /** List all action names available as exits from a given state.
1422
+ * @param whichState - The state to inspect. Defaults to the current state.
1423
+ * @returns An array of action name strings.
1424
+ * @throws {JssmError} If the state does not exist.
1425
+ */
1217
1426
  list_exit_actions(whichState = this.state()) {
1218
1427
  const ra_base = this._reverse_actions.get(whichState);
1219
1428
  if (!(ra_base)) {
1429
+ if (this.has_state(whichState)) {
1430
+ return [];
1431
+ }
1220
1432
  throw new JssmError(this, `No such state ${JSON.stringify(whichState)}`);
1221
1433
  }
1222
1434
  return Array.from(ra_base.values())
@@ -1224,9 +1436,17 @@ class Machine {
1224
1436
  .filter((o) => o.from === whichState)
1225
1437
  .map((filtered) => filtered.action);
1226
1438
  }
1439
+ /** List all action exits from a state with their probabilities.
1440
+ * @param whichState - The state to inspect. Defaults to the current state.
1441
+ * @returns An array of `{ action, probability }` objects.
1442
+ * @throws {JssmError} If the state does not exist.
1443
+ */
1227
1444
  probable_action_exits(whichState = this.state()) {
1228
1445
  const ra_base = this._reverse_actions.get(whichState);
1229
1446
  if (!(ra_base)) {
1447
+ if (this.has_state(whichState)) {
1448
+ return [];
1449
+ }
1230
1450
  throw new JssmError(this, `No such state ${JSON.stringify(whichState)}`);
1231
1451
  }
1232
1452
  return Array.from(ra_base.values())
@@ -1237,32 +1457,57 @@ class Machine {
1237
1457
  probability: filtered.probability
1238
1458
  }));
1239
1459
  }
1240
- // TODO FIXME test that is_unenterable on non-state throws
1460
+ /** Check whether a state has no incoming transitions (unreachable after start).
1461
+ * @param whichState - The state to check.
1462
+ * @returns `true` if the state has zero entrances.
1463
+ * @throws {JssmError} If the state does not exist.
1464
+ */
1241
1465
  is_unenterable(whichState) {
1242
1466
  if (!(this.has_state(whichState))) {
1243
1467
  throw new JssmError(this, `No such state ${whichState}`);
1244
1468
  }
1245
1469
  return this.list_entrances(whichState).length === 0;
1246
1470
  }
1471
+ /** Check whether any state in the machine is unenterable.
1472
+ * @returns `true` if at least one state has no incoming transitions.
1473
+ */
1247
1474
  has_unenterables() {
1248
1475
  return this.states().some((x) => this.is_unenterable(x));
1249
1476
  }
1477
+ /** Check whether the current state is terminal (has no exits).
1478
+ * @returns `true` if the current state has zero exits.
1479
+ */
1250
1480
  is_terminal() {
1251
1481
  return this.state_is_terminal(this.state());
1252
1482
  }
1253
- // TODO FIXME test that state_is_terminal on non-state throws
1483
+ /** Check whether a specific state is terminal (has no exits).
1484
+ * @param whichState - The state to check.
1485
+ * @returns `true` if the state has zero exits.
1486
+ * @throws {JssmError} If the state does not exist.
1487
+ */
1254
1488
  state_is_terminal(whichState) {
1255
1489
  if (!(this.has_state(whichState))) {
1256
1490
  throw new JssmError(this, `No such state ${whichState}`);
1257
1491
  }
1258
1492
  return this.list_exits(whichState).length === 0;
1259
1493
  }
1494
+ /** Check whether any state in the machine is terminal.
1495
+ * @returns `true` if at least one state has no exits.
1496
+ */
1260
1497
  has_terminals() {
1261
1498
  return this.states().some((x) => this.state_is_terminal(x));
1262
1499
  }
1500
+ /** Check whether the current state is complete (every exit has an action).
1501
+ * @returns `true` if the current state is complete.
1502
+ */
1263
1503
  is_complete() {
1264
1504
  return this.state_is_complete(this.state());
1265
1505
  }
1506
+ /** Check whether a specific state is complete (every exit has an action).
1507
+ * @param whichState - The state to check.
1508
+ * @returns `true` if the state is complete.
1509
+ * @throws {JssmError} If the state does not exist.
1510
+ */
1266
1511
  state_is_complete(whichState) {
1267
1512
  const wstate = this._states.get(whichState);
1268
1513
  if (wstate) {
@@ -1272,11 +1517,18 @@ class Machine {
1272
1517
  throw new JssmError(this, `No such state ${JSON.stringify(whichState)}`);
1273
1518
  }
1274
1519
  }
1520
+ /** Check whether any state in the machine is complete.
1521
+ * @returns `true` if at least one state is complete.
1522
+ */
1275
1523
  has_completes() {
1276
1524
  return this.states().some((x) => this.state_is_complete(x));
1277
1525
  }
1278
- // basic toolable hook call. convenience wrappers will follow, like
1279
- // hook(from, to, handler) and exit_hook(from, handler) and etc
1526
+ /** Low-level hook registration. Installs a handler described by a
1527
+ * {@link HookDescription} into the appropriate internal map. Prefer the
1528
+ * convenience wrappers ({@link hook}, {@link hook_entry}, etc.) over
1529
+ * calling this directly.
1530
+ * @param HookDesc - A hook descriptor specifying kind, states, and handler.
1531
+ */
1280
1532
  set_hook(HookDesc) {
1281
1533
  switch (HookDesc.kind) {
1282
1534
  case 'hook':
@@ -1380,97 +1632,309 @@ class Machine {
1380
1632
  this._has_post_exit_hooks = true;
1381
1633
  this._has_post_hooks = true;
1382
1634
  break;
1635
+ case 'pre everything':
1636
+ this._pre_everything_hook = HookDesc.handler;
1637
+ this._has_hooks = true;
1638
+ break;
1639
+ case 'everything':
1640
+ this._everything_hook = HookDesc.handler;
1641
+ this._has_hooks = true;
1642
+ break;
1643
+ case 'pre post everything':
1644
+ this._pre_post_everything_hook = HookDesc.handler;
1645
+ this._has_post_hooks = true;
1646
+ break;
1647
+ case 'post everything':
1648
+ this._post_everything_hook = HookDesc.handler;
1649
+ this._has_post_hooks = true;
1650
+ break;
1383
1651
  default:
1384
1652
  throw new JssmError(this, `Unknown hook type ${HookDesc.kind}, should be impossible`);
1385
1653
  }
1386
1654
  }
1655
+ /** Register a pre-transition hook on a specific edge. Fires before
1656
+ * transitioning from `from` to `to`. If the handler returns `false`, the
1657
+ * transition is blocked.
1658
+ *
1659
+ * ```typescript
1660
+ * const m = sm`a -> b -> c;`;
1661
+ * m.hook('a', 'b', () => console.log('a->b'));
1662
+ * ```
1663
+ *
1664
+ * @param from - Source state name.
1665
+ * @param to - Target state name.
1666
+ * @param handler - Callback invoked before the transition.
1667
+ * @returns `this` for chaining.
1668
+ */
1387
1669
  hook(from, to, handler) {
1388
1670
  this.set_hook({ kind: 'hook', from, to, handler });
1389
1671
  return this;
1390
1672
  }
1673
+ /** Register a pre-transition hook on a specific action-labeled edge.
1674
+ * @param from - Source state name.
1675
+ * @param to - Target state name.
1676
+ * @param action - The action label that triggers this hook.
1677
+ * @param handler - Callback invoked before the transition.
1678
+ * @returns `this` for chaining.
1679
+ */
1391
1680
  hook_action(from, to, action, handler) {
1392
1681
  this.set_hook({ kind: 'named', from, to, action, handler });
1393
1682
  return this;
1394
1683
  }
1684
+ /** Register a pre-transition hook on any edge triggered by a specific action.
1685
+ * @param action - The action name to hook.
1686
+ * @param handler - Callback invoked before any transition with this action.
1687
+ * @returns `this` for chaining.
1688
+ */
1395
1689
  hook_global_action(action, handler) {
1396
1690
  this.set_hook({ kind: 'global action', action, handler });
1397
1691
  return this;
1398
1692
  }
1693
+ /** Register a pre-transition hook on any action-driven transition.
1694
+ * @param handler - Callback invoked before any action transition.
1695
+ * @returns `this` for chaining.
1696
+ */
1399
1697
  hook_any_action(handler) {
1400
1698
  this.set_hook({ kind: 'any action', handler });
1401
1699
  return this;
1402
1700
  }
1701
+ /** Register a pre-transition hook on any standard (`->`) transition.
1702
+ * @param handler - Callback invoked before any legal transition.
1703
+ * @returns `this` for chaining.
1704
+ */
1403
1705
  hook_standard_transition(handler) {
1404
1706
  this.set_hook({ kind: 'standard transition', handler });
1405
1707
  return this;
1406
1708
  }
1709
+ /** Register a pre-transition hook on any main-path (`=>`) transition.
1710
+ * @param handler - Callback invoked before any main transition.
1711
+ * @returns `this` for chaining.
1712
+ */
1407
1713
  hook_main_transition(handler) {
1408
1714
  this.set_hook({ kind: 'main transition', handler });
1409
1715
  return this;
1410
1716
  }
1717
+ /** Register a pre-transition hook on any forced (`~>`) transition.
1718
+ * @param handler - Callback invoked before any forced transition.
1719
+ * @returns `this` for chaining.
1720
+ */
1411
1721
  hook_forced_transition(handler) {
1412
1722
  this.set_hook({ kind: 'forced transition', handler });
1413
1723
  return this;
1414
1724
  }
1725
+ /** Register a pre-transition hook on any transition regardless of kind.
1726
+ * @param handler - Callback invoked before every transition.
1727
+ * @returns `this` for chaining.
1728
+ */
1415
1729
  hook_any_transition(handler) {
1416
1730
  this.set_hook({ kind: 'any transition', handler });
1417
1731
  return this;
1418
1732
  }
1733
+ /** Register a hook that fires when entering a specific state.
1734
+ * @param to - The state being entered.
1735
+ * @param handler - Callback invoked on entry.
1736
+ * @returns `this` for chaining.
1737
+ */
1419
1738
  hook_entry(to, handler) {
1420
1739
  this.set_hook({ kind: 'entry', to, handler });
1421
1740
  return this;
1422
1741
  }
1742
+ /** Register a hook that fires when leaving a specific state.
1743
+ * @param from - The state being exited.
1744
+ * @param handler - Callback invoked on exit.
1745
+ * @returns `this` for chaining.
1746
+ */
1423
1747
  hook_exit(from, handler) {
1424
1748
  this.set_hook({ kind: 'exit', from, handler });
1425
1749
  return this;
1426
1750
  }
1751
+ /** Register a hook that fires after leaving a specific state (post-exit).
1752
+ * @param from - The state that was exited.
1753
+ * @param handler - Callback invoked after exit completes.
1754
+ * @returns `this` for chaining.
1755
+ */
1427
1756
  hook_after(from, handler) {
1428
1757
  this.set_hook({ kind: 'after', from, handler });
1429
1758
  return this;
1430
1759
  }
1760
+ /** Post-transition hook on a specific edge. Fires after the transition
1761
+ * from `from` to `to` has completed. Cannot block the transition.
1762
+ * @param from - Source state name.
1763
+ * @param to - Target state name.
1764
+ * @param handler - Callback invoked after the transition.
1765
+ * @returns `this` for chaining.
1766
+ */
1431
1767
  post_hook(from, to, handler) {
1432
1768
  this.set_hook({ kind: 'post hook', from, to, handler });
1433
1769
  return this;
1434
1770
  }
1771
+ /** Post-transition hook on a specific action-labeled edge.
1772
+ * @param from - Source state name.
1773
+ * @param to - Target state name.
1774
+ * @param action - The action label.
1775
+ * @param handler - Callback invoked after the transition.
1776
+ * @returns `this` for chaining.
1777
+ */
1435
1778
  post_hook_action(from, to, action, handler) {
1436
1779
  this.set_hook({ kind: 'post named', from, to, action, handler });
1437
1780
  return this;
1438
1781
  }
1782
+ /** Post-transition hook on any edge triggered by a specific action.
1783
+ * @param action - The action name.
1784
+ * @param handler - Callback invoked after any transition with this action.
1785
+ * @returns `this` for chaining.
1786
+ */
1439
1787
  post_hook_global_action(action, handler) {
1440
1788
  this.set_hook({ kind: 'post global action', action, handler });
1441
1789
  return this;
1442
1790
  }
1791
+ /** Post-transition hook on any action-driven transition.
1792
+ * @param handler - Callback invoked after any action transition.
1793
+ * @returns `this` for chaining.
1794
+ */
1443
1795
  post_hook_any_action(handler) {
1444
1796
  this.set_hook({ kind: 'post any action', handler });
1445
1797
  return this;
1446
1798
  }
1799
+ /** Post-transition hook on any standard (`->`) transition.
1800
+ * @param handler - Callback invoked after any legal transition.
1801
+ * @returns `this` for chaining.
1802
+ */
1447
1803
  post_hook_standard_transition(handler) {
1448
1804
  this.set_hook({ kind: 'post standard transition', handler });
1449
1805
  return this;
1450
1806
  }
1807
+ /** Post-transition hook on any main-path (`=>`) transition.
1808
+ * @param handler - Callback invoked after any main transition.
1809
+ * @returns `this` for chaining.
1810
+ */
1451
1811
  post_hook_main_transition(handler) {
1452
1812
  this.set_hook({ kind: 'post main transition', handler });
1453
1813
  return this;
1454
1814
  }
1815
+ /** Post-transition hook on any forced (`~>`) transition.
1816
+ * @param handler - Callback invoked after any forced transition.
1817
+ * @returns `this` for chaining.
1818
+ */
1455
1819
  post_hook_forced_transition(handler) {
1456
1820
  this.set_hook({ kind: 'post forced transition', handler });
1457
1821
  return this;
1458
1822
  }
1823
+ /** Post-transition hook on any transition regardless of kind.
1824
+ * @param handler - Callback invoked after every transition.
1825
+ * @returns `this` for chaining.
1826
+ */
1459
1827
  post_hook_any_transition(handler) {
1460
1828
  this.set_hook({ kind: 'post any transition', handler });
1461
1829
  return this;
1462
1830
  }
1831
+ /** Post-transition hook that fires after entering a specific state.
1832
+ * @param to - The state that was entered.
1833
+ * @param handler - Callback invoked after entry.
1834
+ * @returns `this` for chaining.
1835
+ */
1463
1836
  post_hook_entry(to, handler) {
1464
1837
  this.set_hook({ kind: 'post entry', to, handler });
1465
1838
  return this;
1466
1839
  }
1840
+ /** Post-transition hook that fires after leaving a specific state.
1841
+ * @param from - The state that was exited.
1842
+ * @param handler - Callback invoked after exit.
1843
+ * @returns `this` for chaining.
1844
+ */
1467
1845
  post_hook_exit(from, handler) {
1468
1846
  this.set_hook({ kind: 'post exit', from, handler });
1469
1847
  return this;
1470
1848
  }
1849
+ /** Register a pre-transition hook that fires **before** all other pre-hooks
1850
+ * on every transition. If the handler returns `false`, the transition is
1851
+ * blocked. The handler receives an {@link EverythingHookContext} whose
1852
+ * `hook_name` is `'pre everything'`.
1853
+ *
1854
+ * ```typescript
1855
+ * const m = sm`a -> b -> c;`;
1856
+ * m.hook_pre_everything(({ hook_name }) => {
1857
+ * console.log(`${hook_name} fired`);
1858
+ * return true;
1859
+ * });
1860
+ * ```
1861
+ *
1862
+ * @param handler - Callback invoked before all other pre-hooks.
1863
+ * @returns `this` for chaining.
1864
+ */
1865
+ hook_pre_everything(handler) {
1866
+ this.set_hook({ kind: 'pre everything', handler });
1867
+ return this;
1868
+ }
1869
+ /** Register a pre-transition hook that fires **after** all other pre-hooks
1870
+ * on every transition. If the handler returns `false`, the transition is
1871
+ * blocked. The handler receives an {@link EverythingHookContext} whose
1872
+ * `hook_name` is `'everything'`.
1873
+ *
1874
+ * ```typescript
1875
+ * const m = sm`a -> b -> c;`;
1876
+ * m.hook_everything(({ hook_name }) => {
1877
+ * console.log(`${hook_name} fired`);
1878
+ * return true;
1879
+ * });
1880
+ * ```
1881
+ *
1882
+ * @param handler - Callback invoked after all other pre-hooks.
1883
+ * @returns `this` for chaining.
1884
+ */
1885
+ hook_everything(handler) {
1886
+ this.set_hook({ kind: 'everything', handler });
1887
+ return this;
1888
+ }
1889
+ /** Register a post-transition hook that fires **after** all other
1890
+ * post-hooks on every transition. Cannot block the transition. The
1891
+ * handler receives an {@link EverythingHookContext} whose `hook_name` is
1892
+ * `'post everything'`.
1893
+ *
1894
+ * ```typescript
1895
+ * const m = sm`a -> b -> c;`;
1896
+ * m.hook_post_everything(({ hook_name }) => {
1897
+ * console.log(`${hook_name} fired`);
1898
+ * });
1899
+ * ```
1900
+ *
1901
+ * @param handler - Callback invoked after all other post-hooks.
1902
+ * @returns `this` for chaining.
1903
+ */
1904
+ hook_post_everything(handler) {
1905
+ this.set_hook({ kind: 'post everything', handler });
1906
+ return this;
1907
+ }
1908
+ /** Register a post-transition hook that fires **before** all other
1909
+ * post-hooks on every transition. Cannot block the transition. The
1910
+ * handler receives an {@link EverythingHookContext} whose `hook_name` is
1911
+ * `'pre post everything'`.
1912
+ *
1913
+ * ```typescript
1914
+ * const m = sm`a -> b -> c;`;
1915
+ * m.hook_pre_post_everything(({ hook_name }) => {
1916
+ * console.log(`${hook_name} fired`);
1917
+ * });
1918
+ * ```
1919
+ *
1920
+ * @param handler - Callback invoked before all other post-hooks.
1921
+ * @returns `this` for chaining.
1922
+ */
1923
+ hook_pre_post_everything(handler) {
1924
+ this.set_hook({ kind: 'pre post everything', handler });
1925
+ return this;
1926
+ }
1927
+ /** Get the current RNG seed used for probabilistic transitions.
1928
+ * @returns The numeric seed value.
1929
+ */
1471
1930
  get rng_seed() {
1472
1931
  return this._rng_seed;
1473
1932
  }
1933
+ /** Set the RNG seed. Pass `undefined` to reseed from the current time.
1934
+ * Resets the internal PRNG so subsequent probabilistic operations use the
1935
+ * new seed.
1936
+ * @param to - The seed value, or `undefined` for time-based seeding.
1937
+ */
1474
1938
  set rng_seed(to) {
1475
1939
  if (typeof to === 'undefined') {
1476
1940
  this._rng_seed = new Date().getTime();
@@ -1478,10 +1942,17 @@ class Machine {
1478
1942
  else {
1479
1943
  this._rng_seed = to;
1480
1944
  }
1945
+ this._rng = gen_splitmix32(this._rng_seed);
1481
1946
  }
1482
1947
  // remove_hook(HookDesc: HookDescription) {
1483
1948
  // throw new JssmError(this, 'TODO: Should remove hook here');
1484
1949
  // }
1950
+ /** Get all edges between two states (there can be multiple with
1951
+ * different actions).
1952
+ * @param from - Source state name.
1953
+ * @param to - Target state name.
1954
+ * @returns An array of matching {@link JssmTransition} objects.
1955
+ */
1485
1956
  edges_between(from, to) {
1486
1957
  return this._edges.filter(edge => ((edge.from === from) && (edge.to === to)));
1487
1958
  }
@@ -1518,9 +1989,50 @@ class Machine {
1518
1989
  throw new JssmError(this, "Code specifies no override, but config tries to permit; config may not be less strict than code");
1519
1990
  }
1520
1991
  }
1992
+ /*********
1993
+ *
1994
+ * Shared transition core used by {@link transition}, {@link force_transition},
1995
+ * and {@link action}. Runs validation, fires the full hook pipeline (pre-
1996
+ * everything, any-action, after, any-transition, exit, named, basic,
1997
+ * edge-type, entry, everything), commits the new state if nothing
1998
+ * rejected, and returns whether the transition succeeded.
1999
+ *
2000
+ * Not meant for external use. Call one of the public wrappers instead:
2001
+ * - `transition` for an ordinary legal transition
2002
+ * - `force_transition` to bypass the legality check
2003
+ * - `action` to dispatch by action name rather than target state
2004
+ *
2005
+ * @remarks
2006
+ * Known sharp edges, carried over from the original `// TODO` comments:
2007
+ * - The forced-ness behavior needs to be cleaned up a lot here.
2008
+ * - The callbacks are not fully correct across the forced / action / plain
2009
+ * cases and should be revisited.
2010
+ * - When multiple edges exist between two states with different `kind`
2011
+ * values, only the first edge's kind is used to pick the edge-type hook.
2012
+ *
2013
+ * @typeparam mDT The type of the machine data member; usually omitted.
2014
+ *
2015
+ * @param newStateOrAction The target state name (for a plain or forced
2016
+ * transition) or the action name (when `wasAction` is true).
2017
+ *
2018
+ * @param newData Optional replacement machine data to install alongside
2019
+ * the transition. Hooks may further override this via complex results.
2020
+ *
2021
+ * @param wasForced `true` if the caller invoked `force_transition`, in
2022
+ * which case legality is checked against `valid_force_transition` rather
2023
+ * than `valid_transition`.
2024
+ *
2025
+ * @param wasAction `true` if the caller invoked `action`, in which case
2026
+ * `newStateOrAction` is an action name and the target state is looked up
2027
+ * via the current action edge.
2028
+ *
2029
+ * @returns `true` if the transition was valid and every hook passed;
2030
+ * `false` if the transition was invalid or any hook rejected.
2031
+ *
2032
+ * @internal
2033
+ *
2034
+ */
1521
2035
  transition_impl(newStateOrAction, newData, wasForced, wasAction) {
1522
- // TODO the forced-ness behavior needs to be cleaned up a lot here
1523
- // TODO all the callbacks are wrong on forced, action, etc
1524
2036
  let valid = false, trans_type, newState, fromAction = undefined;
1525
2037
  if (wasForced) {
1526
2038
  if (this.valid_force_transition(newStateOrAction, newData)) {
@@ -1540,7 +2052,7 @@ class Machine {
1540
2052
  }
1541
2053
  else {
1542
2054
  if (this.valid_transition(newStateOrAction, newData)) {
1543
- if (this._has_transition_hooks) {
2055
+ if (this._has_transition_hooks || this._has_post_transition_hooks) {
1544
2056
  trans_type = this.edges_between(this._state, newStateOrAction)[0].kind; // TODO this won't do the right thing if various edges have different types
1545
2057
  }
1546
2058
  valid = true;
@@ -1568,6 +2080,14 @@ class Machine {
1568
2080
  }
1569
2081
  }
1570
2082
  let data_changed = false;
2083
+ // 0. pre everything hook (fires before all other pre-hooks)
2084
+ if (this._pre_everything_hook !== undefined) {
2085
+ const outcome = abstract_everything_hook_step(this._pre_everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'pre everything' }));
2086
+ if (outcome.pass === false) {
2087
+ return false;
2088
+ }
2089
+ update_fields(outcome);
2090
+ }
1571
2091
  if (wasAction) {
1572
2092
  // 1a. any action hook
1573
2093
  const outcome = abstract_hook_step(this._any_action_hook, hook_args);
@@ -1587,13 +2107,6 @@ class Machine {
1587
2107
  const ah = this._after_hooks.get(newStateOrAction);
1588
2108
  const outcome = abstract_hook_step(ah, hook_args);
1589
2109
  // there's no such thing as after not passing, so, omit the result pass check
1590
- /* istanbul can't trace this through the timer */
1591
- /* istanbul ignore next */
1592
- if (ah !== undefined) {
1593
- /* istanbul can't trace this through the timer */
1594
- /* istanbul ignore next */
1595
- ah({ data: outcome.data, next_data: outcome.next_data });
1596
- }
1597
2110
  update_fields(outcome);
1598
2111
  }
1599
2112
  // 3. any transition hook
@@ -1663,6 +2176,14 @@ class Machine {
1663
2176
  }
1664
2177
  update_fields(outcome);
1665
2178
  }
2179
+ // 9. everything hook (fires after all other pre-hooks)
2180
+ if (this._everything_hook !== undefined) {
2181
+ const outcome = abstract_everything_hook_step(this._everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'everything' }));
2182
+ if (outcome.pass === false) {
2183
+ return false;
2184
+ }
2185
+ update_fields(outcome);
2186
+ }
1666
2187
  // all hooks passed! let's now establish the result
1667
2188
  if (this._history_length) {
1668
2189
  this._history.shove([this._state, this._data]);
@@ -1698,6 +2219,10 @@ class Machine {
1698
2219
  }
1699
2220
  // posthooks begin here
1700
2221
  if (this._has_post_hooks) {
2222
+ // 0. pre post everything hook (fires before all other post-hooks)
2223
+ if (this._pre_post_everything_hook !== undefined) {
2224
+ this._pre_post_everything_hook(Object.assign(Object.assign({}, hook_args), { hook_name: 'pre post everything' }));
2225
+ }
1701
2226
  if (wasAction) {
1702
2227
  // 1. any action posthook
1703
2228
  if (this._post_any_action_hook !== undefined) {
@@ -1762,11 +2287,18 @@ class Machine {
1762
2287
  hook(hook_args);
1763
2288
  }
1764
2289
  }
2290
+ // 9. post everything hook (fires after all other post-hooks)
2291
+ if (this._post_everything_hook !== undefined) {
2292
+ this._post_everything_hook(Object.assign(Object.assign({}, hook_args), { hook_name: 'post everything' }));
2293
+ }
1765
2294
  }
1766
2295
  // possibly re-establish new 'after' clause
1767
2296
  this.auto_set_state_timeout();
1768
2297
  return true;
1769
2298
  }
2299
+ /** If the current state has an `after` timeout configured, schedule it.
2300
+ * Called internally after each transition.
2301
+ */
1770
2302
  auto_set_state_timeout() {
1771
2303
  const after_res = this._after_mapping.get(this._state);
1772
2304
  if (after_res !== undefined) {
@@ -1885,6 +2417,9 @@ class Machine {
1885
2417
  *
1886
2418
  * @param newData The data change to insert during the action
1887
2419
  *
2420
+ * @returns `true` if the action was valid and the transition occurred,
2421
+ * `false` otherwise.
2422
+ *
1888
2423
  */
1889
2424
  action(actionName, newData) {
1890
2425
  return this.transition_impl(actionName, newData, false, true);
@@ -1907,6 +2442,8 @@ class Machine {
1907
2442
  *
1908
2443
  * @typeparam mDT The type of the machine data member; usually omitted
1909
2444
  *
2445
+ * @returns The {@link JssmStateConfig} for standard states.
2446
+ *
1910
2447
  */
1911
2448
  get standard_state_style() {
1912
2449
  return this._state_style;
@@ -1933,6 +2470,8 @@ class Machine {
1933
2470
  *
1934
2471
  * @typeparam mDT The type of the machine data member; usually omitted
1935
2472
  *
2473
+ * @returns The {@link JssmStateConfig} for hooked states.
2474
+ *
1936
2475
  */
1937
2476
  get hooked_state_style() {
1938
2477
  return this._hooked_state_style;
@@ -1958,6 +2497,8 @@ class Machine {
1958
2497
  *
1959
2498
  * @typeparam mDT The type of the machine data member; usually omitted
1960
2499
  *
2500
+ * @returns The {@link JssmStateConfig} for start states.
2501
+ *
1961
2502
  */
1962
2503
  get start_state_style() {
1963
2504
  return this._start_state_style;
@@ -1988,6 +2529,8 @@ class Machine {
1988
2529
  *
1989
2530
  * @typeparam mDT The type of the machine data member; usually omitted
1990
2531
  *
2532
+ * @returns The {@link JssmStateConfig} for end states.
2533
+ *
1991
2534
  */
1992
2535
  get end_state_style() {
1993
2536
  return this._end_state_style;
@@ -2013,6 +2556,8 @@ class Machine {
2013
2556
  *
2014
2557
  * @typeparam mDT The type of the machine data member; usually omitted
2015
2558
  *
2559
+ * @returns The {@link JssmStateConfig} for terminal states.
2560
+ *
2016
2561
  */
2017
2562
  get terminal_state_style() {
2018
2563
  return this._terminal_state_style;
@@ -2035,6 +2580,8 @@ class Machine {
2035
2580
  *
2036
2581
  * @typeparam mDT The type of the machine data member; usually omitted
2037
2582
  *
2583
+ * @returns The {@link JssmStateConfig} for the active state.
2584
+ *
2038
2585
  */
2039
2586
  get active_state_style() {
2040
2587
  return this._active_state_style;
@@ -2063,6 +2610,10 @@ class Machine {
2063
2610
  *
2064
2611
  * @typeparam mDT The type of the machine data member; usually omitted
2065
2612
  *
2613
+ * @param state The state to compute the composite style for.
2614
+ *
2615
+ * @returns The fully composited {@link JssmStateConfig} for the given state.
2616
+ *
2066
2617
  */
2067
2618
  style_for(state) {
2068
2619
  // first look up the themes
@@ -2147,6 +2698,7 @@ class Machine {
2147
2698
  individual_style.lineStyle = decl === null || decl === void 0 ? void 0 : decl.lineStyle;
2148
2699
  individual_style.corners = decl === null || decl === void 0 ? void 0 : decl.corners;
2149
2700
  individual_style.shape = decl === null || decl === void 0 ? void 0 : decl.shape;
2701
+ individual_style.image = decl === null || decl === void 0 ? void 0 : decl.image;
2150
2702
  layers.push(individual_style);
2151
2703
  return layers.reduce((acc, cur) => {
2152
2704
  const composite_state = acc;
@@ -2184,6 +2736,9 @@ class Machine {
2184
2736
  *
2185
2737
  * @param newData The data change to insert during the action
2186
2738
  *
2739
+ * @returns `true` if the action was valid and the transition occurred,
2740
+ * `false` otherwise.
2741
+ *
2187
2742
  */
2188
2743
  do(actionName, newData) {
2189
2744
  return this.transition_impl(actionName, newData, false, true);
@@ -2216,6 +2771,8 @@ class Machine {
2216
2771
  *
2217
2772
  * @param newData The data change to insert during the transition
2218
2773
  *
2774
+ * @returns `true` if the transition was legal and occurred, `false` otherwise.
2775
+ *
2219
2776
  */
2220
2777
  transition(newState, newData) {
2221
2778
  return this.transition_impl(newState, newData, false, false);
@@ -2238,6 +2795,8 @@ class Machine {
2238
2795
  *
2239
2796
  * @param newData The data change to insert during the transition
2240
2797
  *
2798
+ * @returns `true` if the transition was legal and occurred, `false` otherwise.
2799
+ *
2241
2800
  */
2242
2801
  go(newState, newData) {
2243
2802
  return this.transition_impl(newState, newData, false, false);
@@ -2263,16 +2822,28 @@ class Machine {
2263
2822
  *
2264
2823
  * @param newData The data change to insert during the transition
2265
2824
  *
2825
+ * @returns `true` if a transition (forced or otherwise) existed and occurred,
2826
+ * `false` otherwise.
2827
+ *
2266
2828
  */
2267
2829
  force_transition(newState, newData) {
2268
2830
  return this.transition_impl(newState, newData, true, false);
2269
2831
  }
2832
+ /** Get the edge index for an action from the current state.
2833
+ * @param action - The action name.
2834
+ * @returns The edge index, or `undefined` if the action is not available.
2835
+ */
2270
2836
  current_action_for(action) {
2271
2837
  const action_base = this._actions.get(action);
2272
2838
  return action_base
2273
2839
  ? action_base.get(this.state())
2274
2840
  : undefined;
2275
2841
  }
2842
+ /** Get the full transition object for an action from the current state.
2843
+ * @param action - The action name.
2844
+ * @returns The {@link JssmTransition} object.
2845
+ * @throws {JssmError} If the action is not available from the current state.
2846
+ */
2276
2847
  current_action_edge_for(action) {
2277
2848
  const idx = this.current_action_for(action);
2278
2849
  if ((idx === undefined) || (idx === null)) {
@@ -2280,11 +2851,22 @@ class Machine {
2280
2851
  }
2281
2852
  return this._edges[idx];
2282
2853
  }
2854
+ /** Check whether an action is available from the current state.
2855
+ * @param action - The action name to check.
2856
+ * @param _newData - Reserved for future data validation.
2857
+ * @returns `true` if the action can be taken.
2858
+ */
2283
2859
  valid_action(action, _newData) {
2284
2860
  // todo whargarbl implement data stuff
2285
2861
  // todo major incomplete whargarbl comeback
2286
2862
  return this.current_action_for(action) !== undefined;
2287
2863
  }
2864
+ /** Check whether a transition to a given state is legal (non-forced) from
2865
+ * the current state.
2866
+ * @param newState - The target state.
2867
+ * @param _newData - Reserved for future data validation.
2868
+ * @returns `true` if the transition is legal.
2869
+ */
2288
2870
  valid_transition(newState, _newData) {
2289
2871
  // todo whargarbl implement data stuff
2290
2872
  // todo major incomplete whargarbl comeback
@@ -2297,23 +2879,47 @@ class Machine {
2297
2879
  }
2298
2880
  return true;
2299
2881
  }
2882
+ /** Check whether a forced transition to a given state exists from the
2883
+ * current state.
2884
+ * @param newState - The target state.
2885
+ * @param _newData - Reserved for future data validation.
2886
+ * @returns `true` if a forced (or any) transition exists.
2887
+ */
2300
2888
  valid_force_transition(newState, _newData) {
2301
2889
  // todo whargarbl implement data stuff
2302
2890
  // todo major incomplete whargarbl comeback
2303
2891
  return (this.lookup_transition_for(this.state(), newState) !== undefined);
2304
2892
  }
2893
+ /** Get the instance name of this machine, if one was assigned at creation.
2894
+ * @returns The instance name string, or `undefined`.
2895
+ */
2305
2896
  instance_name() {
2306
2897
  return this._instance_name;
2307
2898
  }
2899
+ /** Get the creation date of this machine as a `Date` object.
2900
+ * @returns A `Date` representing when the machine was created.
2901
+ */
2308
2902
  get creation_date() {
2309
2903
  return new Date(Math.floor(this.creation_timestamp));
2310
2904
  }
2905
+ /** Get the creation timestamp (milliseconds since epoch).
2906
+ * @returns The timestamp as a number.
2907
+ */
2311
2908
  get creation_timestamp() {
2312
2909
  return this._created;
2313
2910
  }
2911
+ /** Get the timestamp when construction began (before parsing).
2912
+ * @returns The start-of-construction timestamp as a number.
2913
+ */
2314
2914
  get create_start_time() {
2315
2915
  return this._create_started;
2316
2916
  }
2917
+ /** Schedule an automatic transition to `next_state` after `after_time`
2918
+ * milliseconds. Only one timeout may be active at a time.
2919
+ * @param next_state - The state to transition to when the timer fires.
2920
+ * @param after_time - Delay in milliseconds.
2921
+ * @throws {JssmError} If a timeout is already pending.
2922
+ */
2317
2923
  set_state_timeout(next_state, after_time) {
2318
2924
  if (this._timeout_handle !== undefined) {
2319
2925
  throw new JssmError(this, `Asked to set a state timeout to ${next_state}:${after_time}, but already timing out to ${this._timeout_target}:${this._timeout_target_time}`);
@@ -2336,6 +2942,8 @@ class Machine {
2336
2942
  this._timeout_target = next_state;
2337
2943
  this._timeout_target_time = after_time;
2338
2944
  }
2945
+ /** Cancel any pending state timeout. Safe to call when no timeout is active.
2946
+ */
2339
2947
  clear_state_timeout() {
2340
2948
  if (this._timeout_handle === undefined) {
2341
2949
  return; // calling with no timeout is a no-op, means it can be called glad-handedly
@@ -2345,14 +2953,28 @@ class Machine {
2345
2953
  this._timeout_target = undefined;
2346
2954
  this._timeout_target_time = undefined;
2347
2955
  }
2956
+ /** Get the configured `after` timeout for a given state, if any.
2957
+ * @param which_state - The state to look up.
2958
+ * @returns A `[targetState, delayMs]` tuple, or `undefined` if no timeout
2959
+ * is configured for that state.
2960
+ */
2348
2961
  state_timeout_for(which_state) {
2349
2962
  return this._after_mapping.get(which_state);
2350
2963
  }
2964
+ /** Get the configured `after` timeout for the current state, if any.
2965
+ * @returns A `[targetState, delayMs]` tuple, or `undefined`.
2966
+ */
2351
2967
  current_state_timeout() {
2352
2968
  return (this._timeout_target !== undefined)
2353
2969
  ? [this._timeout_target, this._timeout_target_time]
2354
2970
  : undefined;
2355
2971
  }
2972
+ /** Convenience method to create a new machine from a tagged template literal.
2973
+ * Equivalent to calling the top-level `sm` function.
2974
+ * @param template_strings - The template string array.
2975
+ * @param remainder - Interpolated values.
2976
+ * @returns A new {@link Machine} instance.
2977
+ */
2356
2978
  /* eslint-disable no-use-before-define */
2357
2979
  /* eslint-disable class-methods-use-this */
2358
2980
  sm(template_strings, ...remainder /* , arguments */) {
@@ -2431,14 +3053,70 @@ function from(MachineAsString, ExtraConstructorFields) {
2431
3053
  }
2432
3054
  return new Machine(to_decorate);
2433
3055
  }
3056
+ /**
3057
+ *
3058
+ * Type guard that narrows an unknown value to a {@link HookComplexResult}.
3059
+ *
3060
+ * A hook complex result is an object with at minimum a boolean `pass` field,
3061
+ * and may optionally also carry replacement `data` / `next_data` fields that
3062
+ * the machine should adopt if the hook passes. This helper is used by the
3063
+ * hook-dispatch machinery to tell "hook returned a complex object" from
3064
+ * "hook returned a bare boolean / null / undefined".
3065
+ *
3066
+ * ```typescript
3067
+ * is_hook_complex_result({ pass: true }); // true
3068
+ * is_hook_complex_result({ pass: false, data: { x: 1 }}); // true
3069
+ * is_hook_complex_result(true); // false
3070
+ * is_hook_complex_result(null); // false
3071
+ * is_hook_complex_result({ other: 'thing' }); // false
3072
+ * ```
3073
+ *
3074
+ * @typeparam mDT The type of the machine data member; usually omitted.
3075
+ *
3076
+ * @param hr The value to test.
3077
+ *
3078
+ * @returns `true` if `hr` is a non-null object with a boolean `pass` field;
3079
+ * `false` otherwise. When `true`, TypeScript narrows `hr` to
3080
+ * `HookComplexResult<mDT>`.
3081
+ *
3082
+ */
2434
3083
  function is_hook_complex_result(hr) {
2435
- if (typeof hr === 'object') {
3084
+ if (hr !== null && typeof hr === 'object') {
2436
3085
  if (typeof hr.pass === 'boolean') {
2437
3086
  return true;
2438
3087
  }
2439
3088
  }
2440
3089
  return false;
2441
3090
  }
3091
+ /**
3092
+ *
3093
+ * Normalize any legal hook return value to a single "did it reject?" boolean.
3094
+ *
3095
+ * Hooks in jssm may return any of the following to indicate success:
3096
+ * `true`, `undefined`, or a complex result whose `pass` field is `true`.
3097
+ * They may return any of the following to indicate rejection:
3098
+ * `false`, or a complex result whose `pass` field is `false`. This helper
3099
+ * collapses all of those shapes into one boolean so callers don't have to
3100
+ * re-implement the matrix.
3101
+ *
3102
+ * ```typescript
3103
+ * is_hook_rejection(true); // false (pass)
3104
+ * is_hook_rejection(undefined); // false (pass)
3105
+ * is_hook_rejection(false); // true (reject)
3106
+ * is_hook_rejection({ pass: true }); // false (pass)
3107
+ * is_hook_rejection({ pass: false }); // true (reject)
3108
+ * ```
3109
+ *
3110
+ * @typeparam mDT The type of the machine data member; usually omitted.
3111
+ *
3112
+ * @param hr A hook result of any legal shape.
3113
+ *
3114
+ * @returns `true` if the hook rejected the transition; `false` if it passed.
3115
+ *
3116
+ * @throws {TypeError} If `hr` is not a recognized hook result shape (for
3117
+ * example, a number or a plain object without a `pass` field).
3118
+ *
3119
+ */
2442
3120
  function is_hook_rejection(hr) {
2443
3121
  if (hr === true) {
2444
3122
  return false;
@@ -2454,6 +3132,43 @@ function is_hook_rejection(hr) {
2454
3132
  }
2455
3133
  throw new TypeError('unknown hook rejection type result');
2456
3134
  }
3135
+ /**
3136
+ *
3137
+ * Invoke an optional transition/action hook and normalize its return value
3138
+ * into a {@link HookComplexResult}.
3139
+ *
3140
+ * This is the central adapter the transition pipeline uses to run every
3141
+ * non-"everything" hook kind (basic, named, entry, exit, after, action, etc).
3142
+ * It accepts `undefined` for the hook slot because most hooks are not set on
3143
+ * most machines; when no hook is installed the step is a no-op pass.
3144
+ *
3145
+ * The valid return shapes from a hook and their normalized meanings are:
3146
+ * - `undefined` → `{ pass: true }`
3147
+ * - `true` → `{ pass: true }`
3148
+ * - `false` → `{ pass: false }`
3149
+ * - `null` → `{ pass: false }`
3150
+ * - a complex result object → returned as-is
3151
+ *
3152
+ * Anything else is a programmer error and throws.
3153
+ *
3154
+ * @typeparam mDT The type of the machine data member; usually omitted.
3155
+ *
3156
+ * @param maybe_hook The hook handler to call, or `undefined` for the
3157
+ * "no hook installed" case.
3158
+ *
3159
+ * @param hook_args The context object passed to the hook. Includes the
3160
+ * current and proposed state, current and proposed data, action name, and
3161
+ * transition kind.
3162
+ *
3163
+ * @returns A {@link HookComplexResult} describing whether the hook passed
3164
+ * and, optionally, any data replacements it requested.
3165
+ *
3166
+ * @throws {TypeError} If the hook returns a value that is not one of the
3167
+ * legal shapes listed above.
3168
+ *
3169
+ * @internal
3170
+ *
3171
+ */
2457
3172
  function abstract_hook_step(maybe_hook, hook_args) {
2458
3173
  if (maybe_hook !== undefined) {
2459
3174
  const result = maybe_hook(hook_args);
@@ -2466,6 +3181,67 @@ function abstract_hook_step(maybe_hook, hook_args) {
2466
3181
  if (result === false) {
2467
3182
  return { pass: false };
2468
3183
  }
3184
+ if (result === null) {
3185
+ return { pass: false };
3186
+ }
3187
+ if (is_hook_complex_result(result)) {
3188
+ return result;
3189
+ }
3190
+ throw new TypeError(`Unknown hook result type ${result}`);
3191
+ }
3192
+ else {
3193
+ return { pass: true };
3194
+ }
3195
+ }
3196
+ /**
3197
+ *
3198
+ * Invoke an optional "everything" hook and normalize its return value into
3199
+ * a {@link HookComplexResult}.
3200
+ *
3201
+ * Mechanically identical to {@link abstract_hook_step}, but typed for the
3202
+ * everything-hook family (`pre_everything_hook` and `everything_hook`),
3203
+ * whose context object carries an extra `hook_name` field identifying which
3204
+ * bracket of the pipeline is firing. Separated from `abstract_hook_step`
3205
+ * so TypeScript can enforce that the hook handler and the context object
3206
+ * agree on shape.
3207
+ *
3208
+ * The valid return shapes and their meanings are the same as for
3209
+ * `abstract_hook_step`:
3210
+ * - `undefined` or `true` → `{ pass: true }`
3211
+ * - `false` or `null` → `{ pass: false }`
3212
+ * - a complex result → returned as-is
3213
+ *
3214
+ * @typeparam mDT The type of the machine data member; usually omitted.
3215
+ *
3216
+ * @param maybe_hook The everything-hook handler, or `undefined` when none
3217
+ * is installed.
3218
+ *
3219
+ * @param hook_args The everything-hook context object. Differs from a
3220
+ * normal hook context in that it also includes `hook_name`.
3221
+ *
3222
+ * @returns A {@link HookComplexResult} describing whether the hook passed
3223
+ * and any data replacements it requested.
3224
+ *
3225
+ * @throws {TypeError} If the hook returns a value outside the legal shapes.
3226
+ *
3227
+ * @internal
3228
+ *
3229
+ */
3230
+ function abstract_everything_hook_step(maybe_hook, hook_args) {
3231
+ if (maybe_hook !== undefined) {
3232
+ const result = maybe_hook(hook_args);
3233
+ if (result === undefined) {
3234
+ return { pass: true };
3235
+ }
3236
+ if (result === true) {
3237
+ return { pass: true };
3238
+ }
3239
+ if (result === false) {
3240
+ return { pass: false };
3241
+ }
3242
+ if (result === null) {
3243
+ return { pass: false };
3244
+ }
2469
3245
  if (is_hook_complex_result(result)) {
2470
3246
  return result;
2471
3247
  }
@@ -2475,7 +3251,63 @@ function abstract_hook_step(maybe_hook, hook_args) {
2475
3251
  return { pass: true };
2476
3252
  }
2477
3253
  }
3254
+ /**
3255
+ * Compares two semantic version strings.
3256
+ *
3257
+ * @param {string} v1 - First version string (e.g., "5.104.2")
3258
+ * @param {string} v2 - Second version string (e.g., "5.103.1")
3259
+ *
3260
+ * @returns {number} - Negative if v1 < v2, 0 if equal, positive if v1 > v2
3261
+ *
3262
+ * @example
3263
+ * compareVersions("5.104.2", "5.103.1") // returns 1 (v1 is newer)
3264
+ *
3265
+ * @example
3266
+ * compareVersions("5.104.2", "6.0.0") // returns -1 (v1 is older)
3267
+ *
3268
+ * @example
3269
+ * compareVersions("5.104.2", "5.104.2") // returns 0 (equal)
3270
+ */
3271
+ function compareVersions(v1, v2) {
3272
+ var _a, _b;
3273
+ const parts1 = v1.split('.').map(Number);
3274
+ const parts2 = v2.split('.').map(Number);
3275
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
3276
+ const num1 = (_a = parts1[i]) !== null && _a !== void 0 ? _a : 0;
3277
+ const num2 = (_b = parts2[i]) !== null && _b !== void 0 ? _b : 0;
3278
+ if (num1 !== num2) {
3279
+ return num1 - num2;
3280
+ }
3281
+ }
3282
+ return 0;
3283
+ }
3284
+ /**
3285
+ * Deserializes a previously serialized machine state.
3286
+ *
3287
+ * This function recreates a machine from a serialization object, restoring its
3288
+ * state, data, and history. For security and compatibility reasons, it will
3289
+ * refuse to deserialize data from future versions of the library.
3290
+ *
3291
+ * @typeparam mDT - The type of the machine data member
3292
+ *
3293
+ * @param {string} machine_string - The FSL string defining the machine structure
3294
+ * @param {JssmSerialization<mDT>} ser - The serialization object to restore from
3295
+ *
3296
+ * @returns {Machine<mDT>} - The restored machine instance
3297
+ *
3298
+ * @throws {Error} If the serialization is from a future version
3299
+ *
3300
+ * @example
3301
+ * const machine = jssm.from("a -> b;");
3302
+ * const serialized = machine.serialize();
3303
+ * const restored = jssm.deserialize("a -> b;", serialized);
3304
+ */
2478
3305
  function deserialize(machine_string, ser) {
3306
+ // Refuse to deserialize data from future versions
3307
+ if (compareVersions(ser.jssm_version, version) > 0) {
3308
+ throw new Error(`Cannot deserialize from future version ${ser.jssm_version} ` +
3309
+ `(current version is ${version}). Please upgrade jssm to deserialize this data.`);
3310
+ }
2479
3311
  const machine = from(machine_string, { data: ser.data, history: ser.history_capacity });
2480
3312
  machine._state = ser.state;
2481
3313
  ser.history.forEach(history_item => machine._history.push(history_item));
@@ -2483,6 +3315,6 @@ function deserialize(machine_string, ser) {
2483
3315
  }
2484
3316
  export { version, build_time, transfer_state_properties, Machine, deserialize, make, wrap_parse as parse, compile, sm, from, arrow_direction, arrow_left_kind, arrow_right_kind,
2485
3317
  // WHARGARBL TODO these should be exported to a utility library
2486
- seq, unique, find_repeated, weighted_rand_select, histograph, weighted_sample_select, weighted_histo_key, sleep, constants, shapes, gviz_shapes, named_colors, is_hook_rejection, is_hook_complex_result, abstract_hook_step, state_style_condense, FslDirections
3318
+ seq, unique, find_repeated, weighted_rand_select, histograph, weighted_sample_select, weighted_histo_key, gen_splitmix32, sleep, constants, shapes, gviz_shapes, named_colors, is_hook_rejection, is_hook_complex_result, abstract_hook_step, abstract_everything_hook_step, state_style_condense, FslDirections
2487
3319
  // FslThemes
2488
3320
  };