jssm 5.141.4 → 5.141.6

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.141.4 at 6/2/2026, 10:32:57 AM
21
+ * Generated for version 5.141.6 at 6/4/2026, 4:19:34 PM
22
22
 
23
23
  -->
24
- # jssm 5.141.4
24
+ # jssm 5.141.6
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/) ·
@@ -418,7 +418,7 @@ If your contribution is missing here, please open an issue.
418
418
 
419
419
  - 5,971 specs with 100.0% coverage
420
420
  - 513 fuzz tests with 3.4% coverage
421
- - 5,573 TypeScript lines - 1.2 tests per line, 10.3 generated tests per line
421
+ - 5,590 TypeScript lines - 1.2 tests per line, 10.2 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)
@@ -21327,7 +21327,7 @@ var constants = /*#__PURE__*/Object.freeze({
21327
21327
  * Useful for runtime diagnostics and for embedding in serialized machine
21328
21328
  * snapshots so that deserializers can detect version-skew.
21329
21329
  */
21330
- const version = "5.141.4";
21330
+ const version = "5.141.6";
21331
21331
 
21332
21332
  // whargarbl lots of these return arrays could/should be sets
21333
21333
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
@@ -23046,6 +23046,62 @@ class Machine {
23046
23046
  this._event_listener_count++;
23047
23047
  return () => { this._unsubscribe_entry(set, entry); };
23048
23048
  }
23049
+ /**
23050
+ * Invoke a single event-handler entry, respecting its filter, once-removal
23051
+ * semantics, and the error re-fire / recursion-guard logic. Extracted so
23052
+ * {@link _fire} can share identical behavior between the size-1 fast-path
23053
+ * and the general snapshotted loop.
23054
+ *
23055
+ * @param entry - The subscriber descriptor to invoke.
23056
+ * @param set - The live Set that owns `entry`; needed for once-removal.
23057
+ * @param name - The event name being dispatched (used in error re-fires).
23058
+ * @param detail - The event payload forwarded to the handler.
23059
+ *
23060
+ * @internal
23061
+ */
23062
+ _fire_one(entry, set, name, detail) {
23063
+ // filter check
23064
+ if (entry.filter !== undefined) {
23065
+ for (const k of Object.keys(entry.filter)) {
23066
+ if (entry.filter[k] !== detail[k]) {
23067
+ return;
23068
+ }
23069
+ }
23070
+ }
23071
+ // once removal happens BEFORE invocation so a throwing handler still
23072
+ // gets removed and so re-entrant `on` calls during the handler see
23073
+ // the post-removal state.
23074
+ if (entry.once) {
23075
+ this._unsubscribe_entry(set, entry);
23076
+ }
23077
+ try {
23078
+ entry.handler(detail);
23079
+ }
23080
+ catch (err) {
23081
+ if (name === 'error' || this._firing_error) {
23082
+ // surface to stderr as a last resort but never recurse;
23083
+ // `console` is in the JS standard library and present in every
23084
+ // supported runtime, so guarding it would just add an untestable
23085
+ // branch. See #638.
23086
+ // eslint-disable-next-line no-console
23087
+ console.error(err);
23088
+ }
23089
+ else {
23090
+ this._firing_error = true;
23091
+ try {
23092
+ this._fire('error', {
23093
+ error: err,
23094
+ source_event: name,
23095
+ source_detail: detail,
23096
+ handler: entry.handler
23097
+ });
23098
+ }
23099
+ finally {
23100
+ this._firing_error = false;
23101
+ }
23102
+ }
23103
+ }
23104
+ }
23049
23105
  /**
23050
23106
  * Dispatch an event to every registered subscriber in registration
23051
23107
  * order. Filters are checked first; non-matching handlers are skipped
@@ -23057,6 +23113,11 @@ class Machine {
23057
23113
  * handler throws, the new exception is swallowed rather than rebroadcast
23058
23114
  * to avoid an infinite loop.
23059
23115
  *
23116
+ * When exactly one subscriber is registered the common case avoids the
23117
+ * `Array.from(set)` snapshot allocation by capturing the lone entry into a
23118
+ * local first — equivalent to a 1-element snapshot but allocation-free.
23119
+ * The general path still snapshots for re-entrancy safety.
23120
+ *
23060
23121
  * @internal
23061
23122
  */
23062
23123
  _fire(name, detail) {
@@ -23064,55 +23125,19 @@ class Machine {
23064
23125
  if (set === undefined || set.size === 0) {
23065
23126
  return;
23066
23127
  }
23067
- // Snapshot so handlers can `off()` mid-loop without disturbing iteration.
23128
+ // Fast-path: single subscriber capture entry before invoking so that
23129
+ // even if the handler mutates `set` (via off/once auto-removal) we hold a
23130
+ // stable reference. Behaviorally identical to a 1-element snapshot.
23131
+ if (set.size === 1) {
23132
+ const only = set.values().next().value;
23133
+ this._fire_one(only, set, name, detail);
23134
+ return;
23135
+ }
23136
+ // General path: snapshot so handlers can `off()` mid-loop without
23137
+ // disturbing iteration.
23068
23138
  const entries = Array.from(set);
23069
23139
  for (const entry of entries) {
23070
- // filter check
23071
- if (entry.filter !== undefined) {
23072
- let matched = true;
23073
- for (const k of Object.keys(entry.filter)) {
23074
- if (entry.filter[k] !== detail[k]) {
23075
- matched = false;
23076
- break;
23077
- }
23078
- }
23079
- if (!matched) {
23080
- continue;
23081
- }
23082
- }
23083
- // once removal happens BEFORE invocation so a throwing handler still
23084
- // gets removed and so re-entrant `on` calls during the handler see
23085
- // the post-removal state.
23086
- if (entry.once) {
23087
- this._unsubscribe_entry(set, entry);
23088
- }
23089
- try {
23090
- entry.handler(detail);
23091
- }
23092
- catch (err) {
23093
- if (name === 'error' || this._firing_error) {
23094
- // surface to stderr as a last resort but never recurse;
23095
- // `console` is in the JS standard library and present in every
23096
- // supported runtime, so guarding it would just add an untestable
23097
- // branch. See #638.
23098
- // eslint-disable-next-line no-console
23099
- console.error(err);
23100
- }
23101
- else {
23102
- this._firing_error = true;
23103
- try {
23104
- this._fire('error', {
23105
- error: err,
23106
- source_event: name,
23107
- source_detail: detail,
23108
- handler: entry.handler
23109
- });
23110
- }
23111
- finally {
23112
- this._firing_error = false;
23113
- }
23114
- }
23115
- }
23140
+ this._fire_one(entry, set, name, detail);
23116
23141
  }
23117
23142
  }
23118
23143
  /** Low-level hook registration. Installs a handler described by a
@@ -23823,6 +23848,44 @@ class Machine {
23823
23848
  throw new JssmError(this, "Code specifies no override, but config tries to permit; config may not be less strict than code");
23824
23849
  }
23825
23850
  }
23851
+ /*********
23852
+ *
23853
+ * Fire a `'rejection'` event caused by a hook vetoing a pending transition.
23854
+ * Extracted from the per-call closures inside {@link transition_impl} so
23855
+ * that it is allocated once at class-definition time rather than on every
23856
+ * hooked transition.
23857
+ *
23858
+ * @param hook_name Name of the hook that rejected (e.g. `'exit'`).
23859
+ * @param fromState State the machine was in when the transition was
23860
+ * attempted; used as the `from` field of the rejection event.
23861
+ * @param newState State that would have been entered had the hook
23862
+ * passed; used as the `to` field of the rejection event.
23863
+ * @param fromAction Action name when the transition was initiated by an
23864
+ * action call; `undefined` for plain state transitions.
23865
+ * @param oldData Machine data at the moment the transition was
23866
+ * attempted, before any hook mutations.
23867
+ * @param newData The `next_data` value passed to the transition call.
23868
+ * @param wasForced Whether the transition was attempted via
23869
+ * `force_transition`.
23870
+ *
23871
+ * @see transition_impl
23872
+ * @see _fire
23873
+ *
23874
+ * @internal
23875
+ *
23876
+ */
23877
+ _fire_hook_rejection(hook_name, fromState, newState, fromAction, oldData, newData, wasForced) {
23878
+ this._fire('rejection', {
23879
+ from: fromState,
23880
+ to: newState,
23881
+ action: fromAction,
23882
+ data: oldData,
23883
+ next_data: newData,
23884
+ reason: 'hook',
23885
+ hook_name,
23886
+ forced: wasForced
23887
+ });
23888
+ }
23826
23889
  /*********
23827
23890
  *
23828
23891
  * Shared transition core used by {@link transition}, {@link force_transition},
@@ -23931,75 +23994,68 @@ class Machine {
23931
23994
  if (this._has_hooks) {
23932
23995
  // once validity is known, clear old 'after' timeout clause
23933
23996
  this.clear_state_timeout();
23934
- function update_fields(res) {
23935
- if (res.hasOwnProperty('data')) {
23936
- hook_args.data = res.data;
23937
- hook_args.next_data = res.next_data;
23938
- data_changed = true;
23939
- }
23940
- }
23941
23997
  let data_changed = false;
23942
- const fire_rejection = (hook_name) => {
23943
- this._fire('rejection', {
23944
- from: fromState,
23945
- to: newState,
23946
- action: fromAction,
23947
- data: oldData,
23948
- next_data: newData,
23949
- reason: 'hook',
23950
- hook_name,
23951
- forced: wasForced
23952
- });
23953
- };
23954
23998
  // 0. pre everything hook (fires before all other pre-hooks)
23955
23999
  if (this._pre_everything_hook !== undefined) {
23956
24000
  const outcome = abstract_everything_hook_step(this._pre_everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'pre everything' }));
23957
24001
  if (outcome.pass === false) {
23958
- fire_rejection('pre everything');
24002
+ this._fire_hook_rejection('pre everything', fromState, newState, fromAction, oldData, newData, wasForced);
23959
24003
  return false;
23960
24004
  }
23961
- update_fields(outcome);
24005
+ if (_update_hook_fields(hook_args, outcome)) {
24006
+ data_changed = true;
24007
+ }
23962
24008
  }
23963
24009
  if (wasAction) {
23964
24010
  // 1a. any action hook
23965
24011
  const outcome = abstract_hook_step(this._any_action_hook, hook_args);
23966
24012
  if (outcome.pass === false) {
23967
- fire_rejection('any action');
24013
+ this._fire_hook_rejection('any action', fromState, newState, fromAction, oldData, newData, wasForced);
23968
24014
  return false;
23969
24015
  }
23970
- update_fields(outcome);
24016
+ if (_update_hook_fields(hook_args, outcome)) {
24017
+ data_changed = true;
24018
+ }
23971
24019
  // 1b. global specific action hook
23972
24020
  const outcome2 = abstract_hook_step(this._global_action_hooks.get(newStateOrAction), hook_args);
23973
24021
  if (outcome2.pass === false) {
23974
- fire_rejection('global action');
24022
+ this._fire_hook_rejection('global action', fromState, newState, fromAction, oldData, newData, wasForced);
23975
24023
  return false;
23976
24024
  }
23977
- update_fields(outcome2);
24025
+ if (_update_hook_fields(hook_args, outcome2)) {
24026
+ data_changed = true;
24027
+ }
23978
24028
  }
23979
24029
  // 2. after hook
23980
24030
  if (this._has_after_hooks) {
23981
24031
  const ah = this._after_hooks.get(newStateOrAction);
23982
24032
  const outcome = abstract_hook_step(ah, hook_args);
23983
- // there's no such thing as after not passing, so, omit the result pass check
23984
- update_fields(outcome);
24033
+ // there's no such thing as after not passing, so, omit the result pass check.
24034
+ // after hooks are post-exit and informational — their outcome never changes
24035
+ // data, so there's no data_changed branch here (it would be unreachable).
24036
+ _update_hook_fields(hook_args, outcome);
23985
24037
  }
23986
24038
  // 3. any transition hook
23987
24039
  if (this._any_transition_hook !== undefined) {
23988
24040
  const outcome = abstract_hook_step(this._any_transition_hook, hook_args);
23989
24041
  if (outcome.pass === false) {
23990
- fire_rejection('any transition');
24042
+ this._fire_hook_rejection('any transition', fromState, newState, fromAction, oldData, newData, wasForced);
23991
24043
  return false;
23992
24044
  }
23993
- update_fields(outcome);
24045
+ if (_update_hook_fields(hook_args, outcome)) {
24046
+ data_changed = true;
24047
+ }
23994
24048
  }
23995
24049
  // 4. exit hook
23996
24050
  if (this._has_exit_hooks) {
23997
24051
  const outcome = abstract_hook_step(this._exit_hooks.get(this._state), hook_args);
23998
24052
  if (outcome.pass === false) {
23999
- fire_rejection('exit');
24053
+ this._fire_hook_rejection('exit', fromState, newState, fromAction, oldData, newData, wasForced);
24000
24054
  return false;
24001
24055
  }
24002
- update_fields(outcome);
24056
+ if (_update_hook_fields(hook_args, outcome)) {
24057
+ data_changed = true;
24058
+ }
24003
24059
  }
24004
24060
  // 5. named transition / action hook
24005
24061
  if (this._has_named_hooks) {
@@ -24011,10 +24067,12 @@ class Machine {
24011
24067
  const nh = byAct === undefined ? undefined : byAct.get(newStateOrAction);
24012
24068
  const outcome = abstract_hook_step(nh, hook_args);
24013
24069
  if (outcome.pass === false) {
24014
- fire_rejection('named');
24070
+ this._fire_hook_rejection('named', fromState, newState, fromAction, oldData, newData, wasForced);
24015
24071
  return false;
24016
24072
  }
24017
- update_fields(outcome);
24073
+ if (_update_hook_fields(hook_args, outcome)) {
24074
+ data_changed = true;
24075
+ }
24018
24076
  }
24019
24077
  }
24020
24078
  // 6. regular hook
@@ -24024,56 +24082,68 @@ class Machine {
24024
24082
  const h = byTo === undefined ? undefined : byTo.get(newState);
24025
24083
  const outcome = abstract_hook_step(h, hook_args);
24026
24084
  if (outcome.pass === false) {
24027
- fire_rejection('hook');
24085
+ this._fire_hook_rejection('hook', fromState, newState, fromAction, oldData, newData, wasForced);
24028
24086
  return false;
24029
24087
  }
24030
- update_fields(outcome);
24088
+ if (_update_hook_fields(hook_args, outcome)) {
24089
+ data_changed = true;
24090
+ }
24031
24091
  }
24032
24092
  // 7. edge type hook
24033
24093
  // 7a. standard transition hook
24034
24094
  if (trans_type === 'legal') {
24035
24095
  const outcome = abstract_hook_step(this._standard_transition_hook, hook_args);
24036
24096
  if (outcome.pass === false) {
24037
- fire_rejection('standard transition');
24097
+ this._fire_hook_rejection('standard transition', fromState, newState, fromAction, oldData, newData, wasForced);
24038
24098
  return false;
24039
24099
  }
24040
- update_fields(outcome);
24100
+ if (_update_hook_fields(hook_args, outcome)) {
24101
+ data_changed = true;
24102
+ }
24041
24103
  }
24042
24104
  // 7b. main type hook
24043
24105
  if (trans_type === 'main') {
24044
24106
  const outcome = abstract_hook_step(this._main_transition_hook, hook_args);
24045
24107
  if (outcome.pass === false) {
24046
- fire_rejection('main transition');
24108
+ this._fire_hook_rejection('main transition', fromState, newState, fromAction, oldData, newData, wasForced);
24047
24109
  return false;
24048
24110
  }
24049
- update_fields(outcome);
24111
+ if (_update_hook_fields(hook_args, outcome)) {
24112
+ data_changed = true;
24113
+ }
24050
24114
  }
24051
24115
  // 7c. forced transition hook
24052
24116
  if (trans_type === 'forced') {
24053
24117
  const outcome = abstract_hook_step(this._forced_transition_hook, hook_args);
24054
24118
  if (outcome.pass === false) {
24055
- fire_rejection('forced transition');
24119
+ this._fire_hook_rejection('forced transition', fromState, newState, fromAction, oldData, newData, wasForced);
24056
24120
  return false;
24057
24121
  }
24058
- update_fields(outcome);
24122
+ if (_update_hook_fields(hook_args, outcome)) {
24123
+ data_changed = true;
24124
+ }
24059
24125
  }
24060
24126
  // 8. entry hook
24061
24127
  if (this._has_entry_hooks) {
24062
24128
  const outcome = abstract_hook_step(this._entry_hooks.get(newState), hook_args);
24063
24129
  if (outcome.pass === false) {
24064
- fire_rejection('entry');
24130
+ this._fire_hook_rejection('entry', fromState, newState, fromAction, oldData, newData, wasForced);
24065
24131
  return false;
24066
24132
  }
24067
- update_fields(outcome);
24133
+ if (_update_hook_fields(hook_args, outcome)) {
24134
+ data_changed = true;
24135
+ }
24068
24136
  }
24069
24137
  // 9. everything hook (fires after all other pre-hooks)
24070
24138
  if (this._everything_hook !== undefined) {
24071
24139
  const outcome = abstract_everything_hook_step(this._everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'everything' }));
24072
24140
  if (outcome.pass === false) {
24073
- fire_rejection('everything');
24141
+ this._fire_hook_rejection('everything', fromState, newState, fromAction, oldData, newData, wasForced);
24074
24142
  return false;
24075
24143
  }
24076
- update_fields(outcome);
24144
+ if (_update_hook_fields(hook_args, outcome)) {
24145
+ data_changed = true;
24146
+ }
24077
24147
  }
24078
24148
  // all hooks passed! let's now establish the result
24079
24149
  if (this._history_length) {
@@ -24999,6 +25069,46 @@ function is_hook_complex_result(hr) {
24999
25069
  }
25000
25070
  return false;
25001
25071
  }
25072
+ /**
25073
+ *
25074
+ * Apply any data-field updates from a hook's complex result into `hook_args`,
25075
+ * and return whether data actually changed.
25076
+ *
25077
+ * This is the hoisted, allocation-free replacement for the `update_fields`
25078
+ * inner function that used to be re-created on every hooked transition inside
25079
+ * {@link Machine.transition_impl}. By moving it to module scope the function
25080
+ * object is allocated once at module load time.
25081
+ *
25082
+ * When the result does not carry a `data` property (the common case —
25083
+ * most hooks return `true` or `undefined`) the function returns `false`
25084
+ * immediately without touching `hook_args`.
25085
+ *
25086
+ * ```typescript
25087
+ * const args = { data: 'old', next_data: undefined, ... };
25088
+ * const changed = _update_hook_fields(args, { pass: true, data: 'new', next_data: undefined });
25089
+ * // changed === true, args.data === 'new'
25090
+ * ```
25091
+ *
25092
+ * @param hook_args The shared hook-argument object for the current
25093
+ * transition. Mutated in-place when the result carries `data`.
25094
+ * @param res The normalised complex result returned by
25095
+ * {@link abstract_hook_step} or {@link abstract_everything_hook_step}.
25096
+ *
25097
+ * @returns `true` if `res` contained a `data` property (i.e. the hook
25098
+ * mutated the machine's data); `false` otherwise.
25099
+ *
25100
+ * @see Machine.transition_impl
25101
+ * @see abstract_hook_step
25102
+ *
25103
+ */
25104
+ function _update_hook_fields(hook_args, res) {
25105
+ if (Object.prototype.hasOwnProperty.call(res, 'data')) {
25106
+ hook_args.data = res.data;
25107
+ hook_args.next_data = res.next_data;
25108
+ return true;
25109
+ }
25110
+ return false;
25111
+ }
25002
25112
  /**
25003
25113
  *
25004
25114
  * Invoke an optional transition/action hook and normalize its return value