jssm 5.143.9 → 5.143.11
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/README.md +7 -7
- package/dist/cdn/instance.js +168 -12
- package/dist/cdn/viz.js +168 -12
- package/dist/cli/fsl-render.cjs +1 -1
- package/dist/cli/fsl.cjs +1 -1
- package/dist/deno/README.md +7 -7
- package/dist/deno/jssm.d.ts +9 -0
- package/dist/deno/jssm.js +1 -1
- package/dist/deno/jssm_intern.d.ts +93 -0
- package/dist/jssm.es5.cjs +1 -1
- package/dist/jssm.es5.iife.js +1 -1
- package/dist/jssm.es6.mjs +1 -1
- package/dist/jssm_viz.cjs +1 -1
- package/dist/jssm_viz.iife.cjs +1 -1
- package/dist/jssm_viz.mjs +1 -1
- package/jssm.es5.d.cts +75 -0
- package/jssm.es6.d.ts +75 -0
- package/jssm_viz.es5.d.cts +75 -0
- package/jssm_viz.es6.d.ts +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,10 +18,10 @@ Please edit the file it's derived from, instead: `./src/md/readme_base.md`
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
* Generated for version 5.143.
|
|
21
|
+
* Generated for version 5.143.11 at 6/12/2026, 8:20:42 AM
|
|
22
22
|
|
|
23
23
|
-->
|
|
24
|
-
# jssm 5.143.
|
|
24
|
+
# jssm 5.143.11
|
|
25
25
|
|
|
26
26
|
[**Try the live editor**](https://stonecypher.github.io/jssm-viz-demo/graph_explorer.html) ·
|
|
27
27
|
[Documentation](https://stonecypher.github.io/jssm/docs/) ·
|
|
@@ -281,7 +281,7 @@ That decision shows up everywhere downstream:
|
|
|
281
281
|
or run `npm run benny` against your own machine.
|
|
282
282
|
|
|
283
283
|
- **More thoroughly tested than any other JavaScript state-machine
|
|
284
|
-
library.** 6,
|
|
284
|
+
library.** 6,919 tests at 100.0% line coverage
|
|
285
285
|
([report](https://coveralls.io/github/StoneCypher/jssm)), plus
|
|
286
286
|
fuzz testing via `fast-check`, with parser test data across ten natural
|
|
287
287
|
languages and Emoji.
|
|
@@ -414,11 +414,11 @@ If your contribution is missing here, please open an issue.
|
|
|
414
414
|
|
|
415
415
|
<br/>
|
|
416
416
|
|
|
417
|
-
***6,
|
|
417
|
+
***6,919 tests***, run 75,625 times.
|
|
418
418
|
|
|
419
|
-
- 6,
|
|
420
|
-
-
|
|
421
|
-
- 5,
|
|
419
|
+
- 6,225 specs with 100.0% coverage
|
|
420
|
+
- 694 fuzz tests with 63.9% coverage
|
|
421
|
+
- 5,871 TypeScript lines - 1.2 tests per line, 12.9 generated tests per line
|
|
422
422
|
|
|
423
423
|
[](https://github.com/StoneCypher/jssm/actions)
|
|
424
424
|
[](https://www.npmjs.com/package/jssm)
|
package/dist/cdn/instance.js
CHANGED
|
@@ -22560,6 +22560,119 @@ theme_mapping.set('ocean', ocean_theme);
|
|
|
22560
22560
|
theme_mapping.set('plain', plain_theme);
|
|
22561
22561
|
theme_mapping.set('bold', bold_theme);
|
|
22562
22562
|
|
|
22563
|
+
/**
|
|
22564
|
+
* String interning support for the jssm machine internals.
|
|
22565
|
+
*
|
|
22566
|
+
* State and action names are interned to dense integer ids at machine
|
|
22567
|
+
* construction so that per-transition dispatch can use numeric map keys
|
|
22568
|
+
* (integer hashing) instead of repeated string-keyed lookups. Internal
|
|
22569
|
+
* machinery only — deliberately not re-exported from the `jssm` public
|
|
22570
|
+
* surface, so the public API is unchanged.
|
|
22571
|
+
*
|
|
22572
|
+
* @internal
|
|
22573
|
+
*/
|
|
22574
|
+
/**
|
|
22575
|
+
* A string↔integer bimap. Assigns dense ids (0, 1, 2, …) in first-seen
|
|
22576
|
+
* order; lookups are O(1) both directions. Grows monotonically — there is
|
|
22577
|
+
* no removal, matching machine semantics (states and actions are fixed
|
|
22578
|
+
* after construction; late interning only happens for never-matching
|
|
22579
|
+
* lookups such as hook registrations naming unknown states).
|
|
22580
|
+
*
|
|
22581
|
+
* @example
|
|
22582
|
+
* const i = new Interner();
|
|
22583
|
+
* i.intern('red'); // 0
|
|
22584
|
+
* i.intern('green'); // 1
|
|
22585
|
+
* i.intern('red'); // 0 (idempotent)
|
|
22586
|
+
* i.id_of('green'); // 1
|
|
22587
|
+
* i.name_of(0); // 'red'
|
|
22588
|
+
*
|
|
22589
|
+
* @see pair_key
|
|
22590
|
+
*/
|
|
22591
|
+
class Interner {
|
|
22592
|
+
constructor() {
|
|
22593
|
+
this.ids = new Map();
|
|
22594
|
+
this.names = [];
|
|
22595
|
+
}
|
|
22596
|
+
/**
|
|
22597
|
+
* Return the id for `name`, assigning the next dense id if the name has
|
|
22598
|
+
* not been seen before.
|
|
22599
|
+
*
|
|
22600
|
+
* @param name - The string to intern.
|
|
22601
|
+
* @returns The (possibly newly assigned) integer id.
|
|
22602
|
+
*
|
|
22603
|
+
* @example
|
|
22604
|
+
* interner.intern('red'); // 0 on first call, 0 on every later call
|
|
22605
|
+
*/
|
|
22606
|
+
intern(name) {
|
|
22607
|
+
const existing = this.ids.get(name);
|
|
22608
|
+
if (existing !== undefined) {
|
|
22609
|
+
return existing;
|
|
22610
|
+
}
|
|
22611
|
+
const id = this.names.length;
|
|
22612
|
+
this.ids.set(name, id);
|
|
22613
|
+
this.names.push(name);
|
|
22614
|
+
return id;
|
|
22615
|
+
}
|
|
22616
|
+
/**
|
|
22617
|
+
* Return the id for `name` without interning, or `undefined` when the
|
|
22618
|
+
* name has never been interned. This is the hot-path probe for
|
|
22619
|
+
* user-supplied names.
|
|
22620
|
+
*
|
|
22621
|
+
* @param name - The string to look up.
|
|
22622
|
+
*
|
|
22623
|
+
* @example
|
|
22624
|
+
* interner.id_of('mauve'); // undefined — never interned
|
|
22625
|
+
*/
|
|
22626
|
+
id_of(name) {
|
|
22627
|
+
return this.ids.get(name);
|
|
22628
|
+
}
|
|
22629
|
+
/**
|
|
22630
|
+
* Return the name for `id`, or `undefined` for an id never assigned.
|
|
22631
|
+
*
|
|
22632
|
+
* @param id - The integer id to invert.
|
|
22633
|
+
*
|
|
22634
|
+
* @example
|
|
22635
|
+
* interner.name_of(0); // 'red'
|
|
22636
|
+
*/
|
|
22637
|
+
name_of(id) {
|
|
22638
|
+
return this.names[id];
|
|
22639
|
+
}
|
|
22640
|
+
/** The count of distinct interned names. */
|
|
22641
|
+
get size() {
|
|
22642
|
+
return this.names.length;
|
|
22643
|
+
}
|
|
22644
|
+
}
|
|
22645
|
+
/**
|
|
22646
|
+
* Szudzik pairing: packs two non-negative integers into one unique number,
|
|
22647
|
+
* order-sensitively, with no dependence on a fixed table size — so interners
|
|
22648
|
+
* may keep growing without invalidating existing keys. Values stay exact
|
|
22649
|
+
* for ids below 2^26 (the result is bounded by roughly max(a,b)^2), far
|
|
22650
|
+
* beyond any real machine's state count.
|
|
22651
|
+
*
|
|
22652
|
+
* NaN deliberately propagates: probing with an unknown name's id
|
|
22653
|
+
* (`id_of(...) ?? NaN`) yields a NaN key, which can never match a stored
|
|
22654
|
+
* key, so the lookup misses — exactly the behavior of the string-keyed maps
|
|
22655
|
+
* it replaces. Do NOT use a negative sentinel instead: Szudzik is only
|
|
22656
|
+
* injective over the naturals, and a negative input can collide with a real
|
|
22657
|
+
* stored key (e.g. szudzik(-1, 2) === szudzik(1, 1) === 3), which would make
|
|
22658
|
+
* lookups from an unknown state falsely succeed.
|
|
22659
|
+
*
|
|
22660
|
+
* @param a - First non-negative integer (or NaN as a deliberate miss).
|
|
22661
|
+
* @param b - Second non-negative integer (or NaN as a deliberate miss).
|
|
22662
|
+
* @returns A number unique to the ordered pair `(a, b)` over the naturals.
|
|
22663
|
+
*
|
|
22664
|
+
* @example
|
|
22665
|
+
* pair_key(2, 5); // 27
|
|
22666
|
+
* pair_key(5, 2); // 32 — order-sensitive
|
|
22667
|
+
*
|
|
22668
|
+
* @see Interner
|
|
22669
|
+
*/
|
|
22670
|
+
function pair_key(a, b) {
|
|
22671
|
+
return (a >= b)
|
|
22672
|
+
? (a * a) + a + b
|
|
22673
|
+
: (b * b) + a;
|
|
22674
|
+
}
|
|
22675
|
+
|
|
22563
22676
|
/*******
|
|
22564
22677
|
*
|
|
22565
22678
|
* Convenience aliases for common mathematical and numeric constants from
|
|
@@ -22675,7 +22788,7 @@ var constants = /*#__PURE__*/Object.freeze({
|
|
|
22675
22788
|
* Useful for runtime diagnostics and for embedding in serialized machine
|
|
22676
22789
|
* snapshots so that deserializers can detect version-skew.
|
|
22677
22790
|
*/
|
|
22678
|
-
const version = "5.143.
|
|
22791
|
+
const version = "5.143.11";
|
|
22679
22792
|
|
|
22680
22793
|
// whargarbl lots of these return arrays could/should be sets
|
|
22681
22794
|
const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
|
|
@@ -22927,6 +23040,12 @@ class Machine {
|
|
|
22927
23040
|
this._actions = new Map();
|
|
22928
23041
|
this._reverse_actions = new Map();
|
|
22929
23042
|
this._reverse_action_targets = new Map(); // todo
|
|
23043
|
+
this._state_interner = new Interner();
|
|
23044
|
+
this._action_interner = new Interner();
|
|
23045
|
+
this._state_id = NaN;
|
|
23046
|
+
this._edge_id_by_pair = new Map();
|
|
23047
|
+
this._edge_id_by_action_pair = new Map();
|
|
23048
|
+
this._edge_to_ids = [];
|
|
22930
23049
|
this._start_states = new Set(start_states);
|
|
22931
23050
|
this._end_states = new Set(end_states); // todo consider what to do about incorporating complete too
|
|
22932
23051
|
this._failed_outputs = new Set(failed_outputs);
|
|
@@ -23113,6 +23232,13 @@ class Machine {
|
|
|
23113
23232
|
}
|
|
23114
23233
|
// const to_mapping = from_mapping.get(tr.to);
|
|
23115
23234
|
from_mapping.set(tr.to, thisEdgeId); // already checked that this mapping doesn't exist, above
|
|
23235
|
+
// numeric mirror of the (from, to) endpoint mapping. intern() rather
|
|
23236
|
+
// than id_of(): idempotent, and returns number (not number|undefined)
|
|
23237
|
+
// since both endpoints were just created above if missing.
|
|
23238
|
+
const from_id = this._state_interner.intern(tr.from);
|
|
23239
|
+
const to_id = this._state_interner.intern(tr.to);
|
|
23240
|
+
this._edge_id_by_pair.set(pair_key(from_id, to_id), thisEdgeId);
|
|
23241
|
+
this._edge_to_ids[thisEdgeId] = to_id;
|
|
23116
23242
|
// outbound adjacency: every edge originating at tr.from, regardless of action/target.
|
|
23117
23243
|
// _edge_map above keys a single edge per (from, to) and overwrites on collision, which
|
|
23118
23244
|
// is fine for lookup_transition_for but loses information for edges_between when several
|
|
@@ -23147,6 +23273,9 @@ class Machine {
|
|
|
23147
23273
|
// no need to test for reverse mapping pre-presence;
|
|
23148
23274
|
// forward mapping already covers collisions
|
|
23149
23275
|
rActionMap.set(tr.action, thisEdgeId);
|
|
23276
|
+
// numeric mirror of the (action, from) dispatch mapping
|
|
23277
|
+
const action_id = this._action_interner.intern(tr.action);
|
|
23278
|
+
this._edge_id_by_action_pair.set(pair_key(action_id, from_id), thisEdgeId);
|
|
23150
23279
|
// reverse mapping first by state target name
|
|
23151
23280
|
if (!(this._reverse_action_targets.has(tr.to))) {
|
|
23152
23281
|
this._reverse_action_targets.set(tr.to, new Map());
|
|
@@ -23191,9 +23320,11 @@ class Machine {
|
|
|
23191
23320
|
throw new JssmError(this, `requested start state ${initial_state} is not in start state list; add {start_states_no_enforce:true} to constructor options if desired`);
|
|
23192
23321
|
}
|
|
23193
23322
|
this._state = initial_state;
|
|
23323
|
+
this._state_id = this._state_interner.intern(this._state);
|
|
23194
23324
|
}
|
|
23195
23325
|
else {
|
|
23196
23326
|
this._state = start_states[0];
|
|
23327
|
+
this._state_id = this._state_interner.intern(this._state);
|
|
23197
23328
|
}
|
|
23198
23329
|
// done building, do checks
|
|
23199
23330
|
// assert all props are valid
|
|
@@ -23281,6 +23412,7 @@ class Machine {
|
|
|
23281
23412
|
throw new JssmError(this, `state ${JSON.stringify(state_config.name)} already exists`);
|
|
23282
23413
|
}
|
|
23283
23414
|
this._states.set(state_config.name, state_config);
|
|
23415
|
+
this._state_interner.intern(state_config.name);
|
|
23284
23416
|
return state_config.name;
|
|
23285
23417
|
}
|
|
23286
23418
|
/*********
|
|
@@ -25321,6 +25453,7 @@ class Machine {
|
|
|
25321
25453
|
const fromState = this._state;
|
|
25322
25454
|
const oldData = this._data;
|
|
25323
25455
|
this._state = newState;
|
|
25456
|
+
this._state_id = this._state_interner.intern(newState);
|
|
25324
25457
|
this._data = newData;
|
|
25325
25458
|
this._fire('override', {
|
|
25326
25459
|
from: fromState,
|
|
@@ -25428,30 +25561,45 @@ class Machine {
|
|
|
25428
25561
|
*
|
|
25429
25562
|
*/
|
|
25430
25563
|
transition_impl(newStateOrAction, newData, wasForced, wasAction) {
|
|
25431
|
-
let valid = false, trans_type, newState, fromAction = undefined;
|
|
25564
|
+
let valid = false, trans_type, newState, newStateId = NaN, fromAction = undefined;
|
|
25432
25565
|
if (wasForced) {
|
|
25433
|
-
|
|
25566
|
+
// numeric inline of valid_force_transition: any existing edge
|
|
25567
|
+
// qualifies, forced or not. one string probe (the user's target name)
|
|
25568
|
+
// plus one numeric probe, replacing two string probes.
|
|
25569
|
+
const to_id = this._state_interner.id_of(newStateOrAction);
|
|
25570
|
+
const edgeId = (to_id === undefined) ? undefined : this._edge_id_by_pair.get(pair_key(this._state_id, to_id));
|
|
25571
|
+
if (edgeId !== undefined) {
|
|
25434
25572
|
valid = true;
|
|
25435
25573
|
trans_type = 'forced';
|
|
25436
25574
|
newState = newStateOrAction;
|
|
25575
|
+
newStateId = to_id;
|
|
25437
25576
|
}
|
|
25438
25577
|
}
|
|
25439
25578
|
else if (wasAction) {
|
|
25440
|
-
|
|
25441
|
-
|
|
25579
|
+
// single numeric resolution: the old path looked the action up twice,
|
|
25580
|
+
// once inside valid_action and again inside current_action_edge_for
|
|
25581
|
+
const edgeId = this.current_action_for(newStateOrAction);
|
|
25582
|
+
if ((edgeId !== undefined) && (edgeId !== null)) {
|
|
25583
|
+
const edge = this._edges[edgeId];
|
|
25442
25584
|
valid = true;
|
|
25443
25585
|
trans_type = edge.kind;
|
|
25444
25586
|
newState = edge.to;
|
|
25587
|
+
newStateId = this._edge_to_ids[edgeId];
|
|
25445
25588
|
fromAction = newStateOrAction;
|
|
25446
25589
|
}
|
|
25447
25590
|
}
|
|
25448
25591
|
else {
|
|
25449
|
-
|
|
25592
|
+
// numeric inline of valid_transition: the edge must exist and must not
|
|
25593
|
+
// be forced_only (truthiness, matching the old refusal exactly)
|
|
25594
|
+
const to_id = this._state_interner.id_of(newStateOrAction);
|
|
25595
|
+
const edgeId = (to_id === undefined) ? undefined : this._edge_id_by_pair.get(pair_key(this._state_id, to_id));
|
|
25596
|
+
if ((edgeId !== undefined) && (!(this._edges[edgeId].forced_only))) {
|
|
25450
25597
|
if (this._has_transition_hooks || this._has_post_transition_hooks) {
|
|
25451
25598
|
trans_type = this.edges_between(this._state, newStateOrAction)[0].kind; // TODO this won't do the right thing if various edges have different types
|
|
25452
25599
|
}
|
|
25453
25600
|
valid = true;
|
|
25454
25601
|
newState = newStateOrAction;
|
|
25602
|
+
newStateId = to_id;
|
|
25455
25603
|
}
|
|
25456
25604
|
}
|
|
25457
25605
|
// hook_args is read only inside the `_has_hooks` / `_has_post_hooks`
|
|
@@ -25494,9 +25642,13 @@ class Machine {
|
|
|
25494
25642
|
const fromState = this._state;
|
|
25495
25643
|
const oldData = this._data;
|
|
25496
25644
|
if (valid) {
|
|
25645
|
+
// once validity is known, clear old 'after' timeout clause. This must
|
|
25646
|
+
// happen for hook-free machines too: leaving it inside the hooks branch
|
|
25647
|
+
// let a pending 'after' timer survive a manual transition away, firing a
|
|
25648
|
+
// ghost go() later and crashing re-entry to the after-state with
|
|
25649
|
+
// "already timing out".
|
|
25650
|
+
this.clear_state_timeout();
|
|
25497
25651
|
if (this._has_hooks) {
|
|
25498
|
-
// once validity is known, clear old 'after' timeout clause
|
|
25499
|
-
this.clear_state_timeout();
|
|
25500
25652
|
let data_changed = false;
|
|
25501
25653
|
// 0. pre everything hook (fires before all other pre-hooks)
|
|
25502
25654
|
if (this._pre_everything_hook !== undefined) {
|
|
@@ -25653,6 +25805,7 @@ class Machine {
|
|
|
25653
25805
|
this._history.shove([this._state, this._data]);
|
|
25654
25806
|
}
|
|
25655
25807
|
this._state = newState;
|
|
25808
|
+
this._state_id = newStateId;
|
|
25656
25809
|
if (data_changed) {
|
|
25657
25810
|
this._data = hook_args.data;
|
|
25658
25811
|
}
|
|
@@ -25668,6 +25821,7 @@ class Machine {
|
|
|
25668
25821
|
this._history.shove([this._state, this._data]);
|
|
25669
25822
|
}
|
|
25670
25823
|
this._state = newState;
|
|
25824
|
+
this._state_id = newStateId;
|
|
25671
25825
|
// TODO known bug: this gives no way to set data to undefined
|
|
25672
25826
|
// see https://github.com/StoneCypher/fsl/issues/1264
|
|
25673
25827
|
if (newData !== undefined) {
|
|
@@ -26352,14 +26506,16 @@ class Machine {
|
|
|
26352
26506
|
return this.transition_impl(newState, newData, true, false);
|
|
26353
26507
|
}
|
|
26354
26508
|
/** Get the edge index for an action from the current state.
|
|
26509
|
+
* Interned dispatch: resolves via the numeric (action, from) index —
|
|
26510
|
+
* unknown action names miss without throwing.
|
|
26355
26511
|
* @param action - The action name.
|
|
26356
26512
|
* @returns The edge index, or `undefined` if the action is not available.
|
|
26357
26513
|
*/
|
|
26358
26514
|
current_action_for(action) {
|
|
26359
|
-
const
|
|
26360
|
-
return
|
|
26361
|
-
?
|
|
26362
|
-
:
|
|
26515
|
+
const action_id = this._action_interner.id_of(action);
|
|
26516
|
+
return (action_id === undefined)
|
|
26517
|
+
? undefined
|
|
26518
|
+
: this._edge_id_by_action_pair.get(pair_key(action_id, this._state_id));
|
|
26363
26519
|
}
|
|
26364
26520
|
/** Get the full transition object for an action from the current state.
|
|
26365
26521
|
* @param action - The action name.
|
package/dist/cdn/viz.js
CHANGED
|
@@ -22585,6 +22585,119 @@ theme_mapping.set('ocean', ocean_theme);
|
|
|
22585
22585
|
theme_mapping.set('plain', plain_theme);
|
|
22586
22586
|
theme_mapping.set('bold', bold_theme);
|
|
22587
22587
|
|
|
22588
|
+
/**
|
|
22589
|
+
* String interning support for the jssm machine internals.
|
|
22590
|
+
*
|
|
22591
|
+
* State and action names are interned to dense integer ids at machine
|
|
22592
|
+
* construction so that per-transition dispatch can use numeric map keys
|
|
22593
|
+
* (integer hashing) instead of repeated string-keyed lookups. Internal
|
|
22594
|
+
* machinery only — deliberately not re-exported from the `jssm` public
|
|
22595
|
+
* surface, so the public API is unchanged.
|
|
22596
|
+
*
|
|
22597
|
+
* @internal
|
|
22598
|
+
*/
|
|
22599
|
+
/**
|
|
22600
|
+
* A string↔integer bimap. Assigns dense ids (0, 1, 2, …) in first-seen
|
|
22601
|
+
* order; lookups are O(1) both directions. Grows monotonically — there is
|
|
22602
|
+
* no removal, matching machine semantics (states and actions are fixed
|
|
22603
|
+
* after construction; late interning only happens for never-matching
|
|
22604
|
+
* lookups such as hook registrations naming unknown states).
|
|
22605
|
+
*
|
|
22606
|
+
* @example
|
|
22607
|
+
* const i = new Interner();
|
|
22608
|
+
* i.intern('red'); // 0
|
|
22609
|
+
* i.intern('green'); // 1
|
|
22610
|
+
* i.intern('red'); // 0 (idempotent)
|
|
22611
|
+
* i.id_of('green'); // 1
|
|
22612
|
+
* i.name_of(0); // 'red'
|
|
22613
|
+
*
|
|
22614
|
+
* @see pair_key
|
|
22615
|
+
*/
|
|
22616
|
+
class Interner {
|
|
22617
|
+
constructor() {
|
|
22618
|
+
this.ids = new Map();
|
|
22619
|
+
this.names = [];
|
|
22620
|
+
}
|
|
22621
|
+
/**
|
|
22622
|
+
* Return the id for `name`, assigning the next dense id if the name has
|
|
22623
|
+
* not been seen before.
|
|
22624
|
+
*
|
|
22625
|
+
* @param name - The string to intern.
|
|
22626
|
+
* @returns The (possibly newly assigned) integer id.
|
|
22627
|
+
*
|
|
22628
|
+
* @example
|
|
22629
|
+
* interner.intern('red'); // 0 on first call, 0 on every later call
|
|
22630
|
+
*/
|
|
22631
|
+
intern(name) {
|
|
22632
|
+
const existing = this.ids.get(name);
|
|
22633
|
+
if (existing !== undefined) {
|
|
22634
|
+
return existing;
|
|
22635
|
+
}
|
|
22636
|
+
const id = this.names.length;
|
|
22637
|
+
this.ids.set(name, id);
|
|
22638
|
+
this.names.push(name);
|
|
22639
|
+
return id;
|
|
22640
|
+
}
|
|
22641
|
+
/**
|
|
22642
|
+
* Return the id for `name` without interning, or `undefined` when the
|
|
22643
|
+
* name has never been interned. This is the hot-path probe for
|
|
22644
|
+
* user-supplied names.
|
|
22645
|
+
*
|
|
22646
|
+
* @param name - The string to look up.
|
|
22647
|
+
*
|
|
22648
|
+
* @example
|
|
22649
|
+
* interner.id_of('mauve'); // undefined — never interned
|
|
22650
|
+
*/
|
|
22651
|
+
id_of(name) {
|
|
22652
|
+
return this.ids.get(name);
|
|
22653
|
+
}
|
|
22654
|
+
/**
|
|
22655
|
+
* Return the name for `id`, or `undefined` for an id never assigned.
|
|
22656
|
+
*
|
|
22657
|
+
* @param id - The integer id to invert.
|
|
22658
|
+
*
|
|
22659
|
+
* @example
|
|
22660
|
+
* interner.name_of(0); // 'red'
|
|
22661
|
+
*/
|
|
22662
|
+
name_of(id) {
|
|
22663
|
+
return this.names[id];
|
|
22664
|
+
}
|
|
22665
|
+
/** The count of distinct interned names. */
|
|
22666
|
+
get size() {
|
|
22667
|
+
return this.names.length;
|
|
22668
|
+
}
|
|
22669
|
+
}
|
|
22670
|
+
/**
|
|
22671
|
+
* Szudzik pairing: packs two non-negative integers into one unique number,
|
|
22672
|
+
* order-sensitively, with no dependence on a fixed table size — so interners
|
|
22673
|
+
* may keep growing without invalidating existing keys. Values stay exact
|
|
22674
|
+
* for ids below 2^26 (the result is bounded by roughly max(a,b)^2), far
|
|
22675
|
+
* beyond any real machine's state count.
|
|
22676
|
+
*
|
|
22677
|
+
* NaN deliberately propagates: probing with an unknown name's id
|
|
22678
|
+
* (`id_of(...) ?? NaN`) yields a NaN key, which can never match a stored
|
|
22679
|
+
* key, so the lookup misses — exactly the behavior of the string-keyed maps
|
|
22680
|
+
* it replaces. Do NOT use a negative sentinel instead: Szudzik is only
|
|
22681
|
+
* injective over the naturals, and a negative input can collide with a real
|
|
22682
|
+
* stored key (e.g. szudzik(-1, 2) === szudzik(1, 1) === 3), which would make
|
|
22683
|
+
* lookups from an unknown state falsely succeed.
|
|
22684
|
+
*
|
|
22685
|
+
* @param a - First non-negative integer (or NaN as a deliberate miss).
|
|
22686
|
+
* @param b - Second non-negative integer (or NaN as a deliberate miss).
|
|
22687
|
+
* @returns A number unique to the ordered pair `(a, b)` over the naturals.
|
|
22688
|
+
*
|
|
22689
|
+
* @example
|
|
22690
|
+
* pair_key(2, 5); // 27
|
|
22691
|
+
* pair_key(5, 2); // 32 — order-sensitive
|
|
22692
|
+
*
|
|
22693
|
+
* @see Interner
|
|
22694
|
+
*/
|
|
22695
|
+
function pair_key(a, b) {
|
|
22696
|
+
return (a >= b)
|
|
22697
|
+
? (a * a) + a + b
|
|
22698
|
+
: (b * b) + a;
|
|
22699
|
+
}
|
|
22700
|
+
|
|
22588
22701
|
/*******
|
|
22589
22702
|
*
|
|
22590
22703
|
* Convenience aliases for common mathematical and numeric constants from
|
|
@@ -22700,7 +22813,7 @@ var constants = /*#__PURE__*/Object.freeze({
|
|
|
22700
22813
|
* Useful for runtime diagnostics and for embedding in serialized machine
|
|
22701
22814
|
* snapshots so that deserializers can detect version-skew.
|
|
22702
22815
|
*/
|
|
22703
|
-
const version = "5.143.
|
|
22816
|
+
const version = "5.143.11";
|
|
22704
22817
|
|
|
22705
22818
|
// whargarbl lots of these return arrays could/should be sets
|
|
22706
22819
|
const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
|
|
@@ -22952,6 +23065,12 @@ class Machine {
|
|
|
22952
23065
|
this._actions = new Map();
|
|
22953
23066
|
this._reverse_actions = new Map();
|
|
22954
23067
|
this._reverse_action_targets = new Map(); // todo
|
|
23068
|
+
this._state_interner = new Interner();
|
|
23069
|
+
this._action_interner = new Interner();
|
|
23070
|
+
this._state_id = NaN;
|
|
23071
|
+
this._edge_id_by_pair = new Map();
|
|
23072
|
+
this._edge_id_by_action_pair = new Map();
|
|
23073
|
+
this._edge_to_ids = [];
|
|
22955
23074
|
this._start_states = new Set(start_states);
|
|
22956
23075
|
this._end_states = new Set(end_states); // todo consider what to do about incorporating complete too
|
|
22957
23076
|
this._failed_outputs = new Set(failed_outputs);
|
|
@@ -23138,6 +23257,13 @@ class Machine {
|
|
|
23138
23257
|
}
|
|
23139
23258
|
// const to_mapping = from_mapping.get(tr.to);
|
|
23140
23259
|
from_mapping.set(tr.to, thisEdgeId); // already checked that this mapping doesn't exist, above
|
|
23260
|
+
// numeric mirror of the (from, to) endpoint mapping. intern() rather
|
|
23261
|
+
// than id_of(): idempotent, and returns number (not number|undefined)
|
|
23262
|
+
// since both endpoints were just created above if missing.
|
|
23263
|
+
const from_id = this._state_interner.intern(tr.from);
|
|
23264
|
+
const to_id = this._state_interner.intern(tr.to);
|
|
23265
|
+
this._edge_id_by_pair.set(pair_key(from_id, to_id), thisEdgeId);
|
|
23266
|
+
this._edge_to_ids[thisEdgeId] = to_id;
|
|
23141
23267
|
// outbound adjacency: every edge originating at tr.from, regardless of action/target.
|
|
23142
23268
|
// _edge_map above keys a single edge per (from, to) and overwrites on collision, which
|
|
23143
23269
|
// is fine for lookup_transition_for but loses information for edges_between when several
|
|
@@ -23172,6 +23298,9 @@ class Machine {
|
|
|
23172
23298
|
// no need to test for reverse mapping pre-presence;
|
|
23173
23299
|
// forward mapping already covers collisions
|
|
23174
23300
|
rActionMap.set(tr.action, thisEdgeId);
|
|
23301
|
+
// numeric mirror of the (action, from) dispatch mapping
|
|
23302
|
+
const action_id = this._action_interner.intern(tr.action);
|
|
23303
|
+
this._edge_id_by_action_pair.set(pair_key(action_id, from_id), thisEdgeId);
|
|
23175
23304
|
// reverse mapping first by state target name
|
|
23176
23305
|
if (!(this._reverse_action_targets.has(tr.to))) {
|
|
23177
23306
|
this._reverse_action_targets.set(tr.to, new Map());
|
|
@@ -23216,9 +23345,11 @@ class Machine {
|
|
|
23216
23345
|
throw new JssmError(this, `requested start state ${initial_state} is not in start state list; add {start_states_no_enforce:true} to constructor options if desired`);
|
|
23217
23346
|
}
|
|
23218
23347
|
this._state = initial_state;
|
|
23348
|
+
this._state_id = this._state_interner.intern(this._state);
|
|
23219
23349
|
}
|
|
23220
23350
|
else {
|
|
23221
23351
|
this._state = start_states[0];
|
|
23352
|
+
this._state_id = this._state_interner.intern(this._state);
|
|
23222
23353
|
}
|
|
23223
23354
|
// done building, do checks
|
|
23224
23355
|
// assert all props are valid
|
|
@@ -23306,6 +23437,7 @@ class Machine {
|
|
|
23306
23437
|
throw new JssmError(this, `state ${JSON.stringify(state_config.name)} already exists`);
|
|
23307
23438
|
}
|
|
23308
23439
|
this._states.set(state_config.name, state_config);
|
|
23440
|
+
this._state_interner.intern(state_config.name);
|
|
23309
23441
|
return state_config.name;
|
|
23310
23442
|
}
|
|
23311
23443
|
/*********
|
|
@@ -25346,6 +25478,7 @@ class Machine {
|
|
|
25346
25478
|
const fromState = this._state;
|
|
25347
25479
|
const oldData = this._data;
|
|
25348
25480
|
this._state = newState;
|
|
25481
|
+
this._state_id = this._state_interner.intern(newState);
|
|
25349
25482
|
this._data = newData;
|
|
25350
25483
|
this._fire('override', {
|
|
25351
25484
|
from: fromState,
|
|
@@ -25453,30 +25586,45 @@ class Machine {
|
|
|
25453
25586
|
*
|
|
25454
25587
|
*/
|
|
25455
25588
|
transition_impl(newStateOrAction, newData, wasForced, wasAction) {
|
|
25456
|
-
let valid = false, trans_type, newState, fromAction = undefined;
|
|
25589
|
+
let valid = false, trans_type, newState, newStateId = NaN, fromAction = undefined;
|
|
25457
25590
|
if (wasForced) {
|
|
25458
|
-
|
|
25591
|
+
// numeric inline of valid_force_transition: any existing edge
|
|
25592
|
+
// qualifies, forced or not. one string probe (the user's target name)
|
|
25593
|
+
// plus one numeric probe, replacing two string probes.
|
|
25594
|
+
const to_id = this._state_interner.id_of(newStateOrAction);
|
|
25595
|
+
const edgeId = (to_id === undefined) ? undefined : this._edge_id_by_pair.get(pair_key(this._state_id, to_id));
|
|
25596
|
+
if (edgeId !== undefined) {
|
|
25459
25597
|
valid = true;
|
|
25460
25598
|
trans_type = 'forced';
|
|
25461
25599
|
newState = newStateOrAction;
|
|
25600
|
+
newStateId = to_id;
|
|
25462
25601
|
}
|
|
25463
25602
|
}
|
|
25464
25603
|
else if (wasAction) {
|
|
25465
|
-
|
|
25466
|
-
|
|
25604
|
+
// single numeric resolution: the old path looked the action up twice,
|
|
25605
|
+
// once inside valid_action and again inside current_action_edge_for
|
|
25606
|
+
const edgeId = this.current_action_for(newStateOrAction);
|
|
25607
|
+
if ((edgeId !== undefined) && (edgeId !== null)) {
|
|
25608
|
+
const edge = this._edges[edgeId];
|
|
25467
25609
|
valid = true;
|
|
25468
25610
|
trans_type = edge.kind;
|
|
25469
25611
|
newState = edge.to;
|
|
25612
|
+
newStateId = this._edge_to_ids[edgeId];
|
|
25470
25613
|
fromAction = newStateOrAction;
|
|
25471
25614
|
}
|
|
25472
25615
|
}
|
|
25473
25616
|
else {
|
|
25474
|
-
|
|
25617
|
+
// numeric inline of valid_transition: the edge must exist and must not
|
|
25618
|
+
// be forced_only (truthiness, matching the old refusal exactly)
|
|
25619
|
+
const to_id = this._state_interner.id_of(newStateOrAction);
|
|
25620
|
+
const edgeId = (to_id === undefined) ? undefined : this._edge_id_by_pair.get(pair_key(this._state_id, to_id));
|
|
25621
|
+
if ((edgeId !== undefined) && (!(this._edges[edgeId].forced_only))) {
|
|
25475
25622
|
if (this._has_transition_hooks || this._has_post_transition_hooks) {
|
|
25476
25623
|
trans_type = this.edges_between(this._state, newStateOrAction)[0].kind; // TODO this won't do the right thing if various edges have different types
|
|
25477
25624
|
}
|
|
25478
25625
|
valid = true;
|
|
25479
25626
|
newState = newStateOrAction;
|
|
25627
|
+
newStateId = to_id;
|
|
25480
25628
|
}
|
|
25481
25629
|
}
|
|
25482
25630
|
// hook_args is read only inside the `_has_hooks` / `_has_post_hooks`
|
|
@@ -25519,9 +25667,13 @@ class Machine {
|
|
|
25519
25667
|
const fromState = this._state;
|
|
25520
25668
|
const oldData = this._data;
|
|
25521
25669
|
if (valid) {
|
|
25670
|
+
// once validity is known, clear old 'after' timeout clause. This must
|
|
25671
|
+
// happen for hook-free machines too: leaving it inside the hooks branch
|
|
25672
|
+
// let a pending 'after' timer survive a manual transition away, firing a
|
|
25673
|
+
// ghost go() later and crashing re-entry to the after-state with
|
|
25674
|
+
// "already timing out".
|
|
25675
|
+
this.clear_state_timeout();
|
|
25522
25676
|
if (this._has_hooks) {
|
|
25523
|
-
// once validity is known, clear old 'after' timeout clause
|
|
25524
|
-
this.clear_state_timeout();
|
|
25525
25677
|
let data_changed = false;
|
|
25526
25678
|
// 0. pre everything hook (fires before all other pre-hooks)
|
|
25527
25679
|
if (this._pre_everything_hook !== undefined) {
|
|
@@ -25678,6 +25830,7 @@ class Machine {
|
|
|
25678
25830
|
this._history.shove([this._state, this._data]);
|
|
25679
25831
|
}
|
|
25680
25832
|
this._state = newState;
|
|
25833
|
+
this._state_id = newStateId;
|
|
25681
25834
|
if (data_changed) {
|
|
25682
25835
|
this._data = hook_args.data;
|
|
25683
25836
|
}
|
|
@@ -25693,6 +25846,7 @@ class Machine {
|
|
|
25693
25846
|
this._history.shove([this._state, this._data]);
|
|
25694
25847
|
}
|
|
25695
25848
|
this._state = newState;
|
|
25849
|
+
this._state_id = newStateId;
|
|
25696
25850
|
// TODO known bug: this gives no way to set data to undefined
|
|
25697
25851
|
// see https://github.com/StoneCypher/fsl/issues/1264
|
|
25698
25852
|
if (newData !== undefined) {
|
|
@@ -26377,14 +26531,16 @@ class Machine {
|
|
|
26377
26531
|
return this.transition_impl(newState, newData, true, false);
|
|
26378
26532
|
}
|
|
26379
26533
|
/** Get the edge index for an action from the current state.
|
|
26534
|
+
* Interned dispatch: resolves via the numeric (action, from) index —
|
|
26535
|
+
* unknown action names miss without throwing.
|
|
26380
26536
|
* @param action - The action name.
|
|
26381
26537
|
* @returns The edge index, or `undefined` if the action is not available.
|
|
26382
26538
|
*/
|
|
26383
26539
|
current_action_for(action) {
|
|
26384
|
-
const
|
|
26385
|
-
return
|
|
26386
|
-
?
|
|
26387
|
-
:
|
|
26540
|
+
const action_id = this._action_interner.id_of(action);
|
|
26541
|
+
return (action_id === undefined)
|
|
26542
|
+
? undefined
|
|
26543
|
+
: this._edge_id_by_action_pair.get(pair_key(action_id, this._state_id));
|
|
26388
26544
|
}
|
|
26389
26545
|
/** Get the full transition object for an action from the current state.
|
|
26390
26546
|
* @param action - The action name.
|