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.
- package/.gitattributes +17 -6
- package/.log-progress.json +9 -0
- package/CHANGELOG.md +130 -45
- package/CLAUDE.md +11 -0
- package/MIGRATING-jssm-viz.md +67 -0
- package/README.md +179 -882
- package/dist/es6/fsl_parser.js +1 -1
- package/dist/es6/jssm.d.ts +773 -39
- package/dist/es6/jssm.js +921 -89
- package/dist/es6/jssm_arrow.js +24 -0
- package/dist/es6/jssm_compiler.d.ts +17 -2
- package/dist/es6/jssm_compiler.js +17 -3
- package/dist/es6/jssm_constants.d.ts +27 -0
- package/dist/es6/jssm_constants.js +27 -0
- package/dist/es6/jssm_error.d.ts +19 -0
- package/dist/es6/jssm_error.js +19 -0
- package/dist/es6/jssm_theme.d.ts +11 -0
- package/dist/es6/jssm_theme.js +11 -0
- package/dist/es6/jssm_types.d.ts +29 -3
- package/dist/es6/jssm_util.d.ts +161 -9
- package/dist/es6/jssm_util.js +174 -17
- package/dist/es6/jssm_viz.d.ts +175 -0
- package/dist/es6/jssm_viz.js +560 -0
- package/dist/es6/jssm_viz_colors.d.ts +63 -0
- package/dist/es6/jssm_viz_colors.js +63 -0
- package/dist/es6/version.js +1 -1
- package/dist/jssm.es5.cjs +1 -1
- package/dist/jssm.es5.iife.js +1 -0
- package/dist/jssm.es5.nonmin.cjs +2201 -870
- package/dist/jssm.es6.mjs +1 -1
- package/dist/jssm.es6.nonmin.cjs +2200 -871
- package/dist/jssm_viz.cjs +1 -0
- package/dist/{jssm.es5.iife.nonmin.cjs → jssm_viz.es5.iife.nonmin.cjs} +2589 -1090
- package/dist/jssm_viz.es5.nonmin.cjs +24674 -0
- package/dist/jssm_viz.es6.nonmin.cjs +24661 -0
- package/dist/jssm_viz.iife.cjs +1 -0
- package/dist/jssm_viz.mjs +1 -0
- package/jest-dragon.config.cjs +4 -1
- package/jest-spec.config.cjs +9 -6
- package/jest-stoch.config.cjs +4 -1
- package/jest-unicode.config.cjs +4 -1
- package/jssm.es5.d.cts +950 -41
- package/jssm.es6.d.ts +950 -41
- package/jssm_viz.es5.d.cts +2127 -0
- package/jssm_viz.es6.d.ts +2127 -0
- package/log-progress.data.json +28 -0
- package/package.json +56 -23
- package/rollup.config.viz.es5.js +46 -0
- package/rollup.config.viz.es6.js +46 -0
- package/rollup.config.viz.iife.js +36 -0
- package/typedoc-options.cjs +4 -3
- package/dist/jssm.es5.iife.cjs +0 -1
- package/fsl_parser.d.ts +0 -6
- package/jssm.d.ts +0 -1141
- package/jssm_arrow.d.ts +0 -53
- package/jssm_compiler.d.ts +0 -135
- package/jssm_constants.d.ts +0 -5
- package/jssm_error.d.ts +0 -8
- package/jssm_theme.d.ts +0 -4
- package/jssm_types.d.ts +0 -378
- package/jssm_util.d.ts +0 -106
- 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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
167
|
+
throw new JssmError(machine, 'state_style_condense received a non-array');
|
|
123
168
|
}
|
|
124
169
|
return state_style;
|
|
125
170
|
}
|
|
126
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
* @
|
|
557
|
-
*
|
|
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
|
|
578
|
-
* the current state also doesn't define the prop
|
|
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
|
-
//
|
|
616
|
-
//
|
|
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
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
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
|
};
|