jssm 5.143.10 → 5.143.12

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 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.10 at 6/12/2026, 8:06:22 AM
21
+ * Generated for version 5.143.12 at 6/12/2026, 8:38:46 AM
22
22
 
23
23
  -->
24
- # jssm 5.143.10
24
+ # jssm 5.143.12
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,902 tests at 100.0% line coverage
284
+ library.** 6,952 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,902 tests***, run 75,608 times.
417
+ ***6,952 tests***, run 78,925 times.
418
418
 
419
- - 6,208 specs with 100.0% coverage
420
- - 694 fuzz tests with 63.5% coverage
421
- - 5,805 TypeScript lines - 1.2 tests per line, 13.0 generated tests per line
419
+ - 6,225 specs with 100.0% coverage
420
+ - 727 fuzz tests with 73.1% coverage
421
+ - 5,871 TypeScript lines - 1.2 tests per line, 13.4 generated tests per line
422
422
 
423
423
  [![Actions Status](https://github.com/StoneCypher/jssm/workflows/Node%20CI/badge.svg)](https://github.com/StoneCypher/jssm/actions)
424
424
  [![NPM version](https://img.shields.io/npm/v/jssm.svg)](https://www.npmjs.com/package/jssm)
@@ -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.10";
22791
+ const version = "5.143.12";
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
- if (this.valid_force_transition(newStateOrAction, newData)) {
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
- if (this.valid_action(newStateOrAction, newData)) {
25441
- const edge = this.current_action_edge_for(newStateOrAction);
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
- if (this.valid_transition(newStateOrAction, newData)) {
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`
@@ -25657,6 +25805,7 @@ class Machine {
25657
25805
  this._history.shove([this._state, this._data]);
25658
25806
  }
25659
25807
  this._state = newState;
25808
+ this._state_id = newStateId;
25660
25809
  if (data_changed) {
25661
25810
  this._data = hook_args.data;
25662
25811
  }
@@ -25672,6 +25821,7 @@ class Machine {
25672
25821
  this._history.shove([this._state, this._data]);
25673
25822
  }
25674
25823
  this._state = newState;
25824
+ this._state_id = newStateId;
25675
25825
  // TODO known bug: this gives no way to set data to undefined
25676
25826
  // see https://github.com/StoneCypher/fsl/issues/1264
25677
25827
  if (newData !== undefined) {
@@ -26356,14 +26506,16 @@ class Machine {
26356
26506
  return this.transition_impl(newState, newData, true, false);
26357
26507
  }
26358
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.
26359
26511
  * @param action - The action name.
26360
26512
  * @returns The edge index, or `undefined` if the action is not available.
26361
26513
  */
26362
26514
  current_action_for(action) {
26363
- const action_base = this._actions.get(action);
26364
- return action_base
26365
- ? action_base.get(this.state())
26366
- : undefined;
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));
26367
26519
  }
26368
26520
  /** Get the full transition object for an action from the current state.
26369
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.10";
22816
+ const version = "5.143.12";
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
- if (this.valid_force_transition(newStateOrAction, newData)) {
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
- if (this.valid_action(newStateOrAction, newData)) {
25466
- const edge = this.current_action_edge_for(newStateOrAction);
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
- if (this.valid_transition(newStateOrAction, newData)) {
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`
@@ -25682,6 +25830,7 @@ class Machine {
25682
25830
  this._history.shove([this._state, this._data]);
25683
25831
  }
25684
25832
  this._state = newState;
25833
+ this._state_id = newStateId;
25685
25834
  if (data_changed) {
25686
25835
  this._data = hook_args.data;
25687
25836
  }
@@ -25697,6 +25846,7 @@ class Machine {
25697
25846
  this._history.shove([this._state, this._data]);
25698
25847
  }
25699
25848
  this._state = newState;
25849
+ this._state_id = newStateId;
25700
25850
  // TODO known bug: this gives no way to set data to undefined
25701
25851
  // see https://github.com/StoneCypher/fsl/issues/1264
25702
25852
  if (newData !== undefined) {
@@ -26381,14 +26531,16 @@ class Machine {
26381
26531
  return this.transition_impl(newState, newData, true, false);
26382
26532
  }
26383
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.
26384
26536
  * @param action - The action name.
26385
26537
  * @returns The edge index, or `undefined` if the action is not available.
26386
26538
  */
26387
26539
  current_action_for(action) {
26388
- const action_base = this._actions.get(action);
26389
- return action_base
26390
- ? action_base.get(this.state())
26391
- : undefined;
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));
26392
26544
  }
26393
26545
  /** Get the full transition object for an action from the current state.
26394
26546
  * @param action - The action name.