jssm 5.141.3 → 5.141.5

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.3 at 6/2/2026, 9:10:12 AM
21
+ * Generated for version 5.141.5 at 6/4/2026, 3:42:26 PM
22
22
 
23
23
  -->
24
- # jssm 5.141.3
24
+ # jssm 5.141.5
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,482 tests at 100.0% line coverage
284
+ library.** 6,484 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,482 tests***, run 57,269 times.
417
+ ***6,484 tests***, run 57,271 times.
418
418
 
419
- - 5,969 specs with 100.0% coverage
419
+ - 5,971 specs with 100.0% coverage
420
420
  - 513 fuzz tests with 3.4% coverage
421
- - 5,566 TypeScript lines - 1.2 tests per line, 10.3 generated tests per line
421
+ - 5,581 TypeScript lines - 1.2 tests per line, 10.3 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.3";
21330
+ const version = "5.141.5";
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;
@@ -21643,6 +21643,11 @@ class Machine {
21643
21643
  this._state_labels.set(key, labelled[0].value);
21644
21644
  }
21645
21645
  });
21646
+ // O(1) duplicate-edge guard for the construction loop below: from -> Set<to>.
21647
+ // Keyed by source state; mirrors each state's `to` array with constant-time
21648
+ // membership so the dedup check is O(1) per edge rather than an O(out-degree)
21649
+ // array scan (which made construction O(V*E) on dense graphs). #673
21650
+ const seen_edges = new Map();
21646
21651
  // walk the transitions
21647
21652
  transitions.map((tr) => {
21648
21653
  if (tr.from === undefined) {
@@ -21662,11 +21667,20 @@ class Machine {
21662
21667
  if (!(this._states.has(tr.to))) {
21663
21668
  this._new_state(cursor_to);
21664
21669
  }
21665
- // guard against existing connections being re-added
21666
- if (cursor_from.to.includes(tr.to)) {
21670
+ // guard against existing connections being re-added — O(1) via the
21671
+ // from -> Set<to> index instead of an O(out-degree) `cursor_from.to`
21672
+ // array scan. Behaviour is identical: the same duplicate (from, to)
21673
+ // pair throws the same JssmError. #673
21674
+ let seen_to = seen_edges.get(tr.from);
21675
+ if (seen_to === undefined) {
21676
+ seen_to = new Set();
21677
+ seen_edges.set(tr.from, seen_to);
21678
+ }
21679
+ if (seen_to.has(tr.to)) {
21667
21680
  throw new JssmError(this, `already has ${JSON.stringify(tr.from)} to ${JSON.stringify(tr.to)}`);
21668
21681
  }
21669
21682
  else {
21683
+ seen_to.add(tr.to);
21670
21684
  cursor_from.to.push(tr.to);
21671
21685
  cursor_to.from.push(tr.from);
21672
21686
  }
@@ -23032,6 +23046,62 @@ class Machine {
23032
23046
  this._event_listener_count++;
23033
23047
  return () => { this._unsubscribe_entry(set, entry); };
23034
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
+ }
23035
23105
  /**
23036
23106
  * Dispatch an event to every registered subscriber in registration
23037
23107
  * order. Filters are checked first; non-matching handlers are skipped
@@ -23043,6 +23113,11 @@ class Machine {
23043
23113
  * handler throws, the new exception is swallowed rather than rebroadcast
23044
23114
  * to avoid an infinite loop.
23045
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
+ *
23046
23121
  * @internal
23047
23122
  */
23048
23123
  _fire(name, detail) {
@@ -23050,55 +23125,19 @@ class Machine {
23050
23125
  if (set === undefined || set.size === 0) {
23051
23126
  return;
23052
23127
  }
23053
- // 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.
23054
23138
  const entries = Array.from(set);
23055
23139
  for (const entry of entries) {
23056
- // filter check
23057
- if (entry.filter !== undefined) {
23058
- let matched = true;
23059
- for (const k of Object.keys(entry.filter)) {
23060
- if (entry.filter[k] !== detail[k]) {
23061
- matched = false;
23062
- break;
23063
- }
23064
- }
23065
- if (!matched) {
23066
- continue;
23067
- }
23068
- }
23069
- // once removal happens BEFORE invocation so a throwing handler still
23070
- // gets removed and so re-entrant `on` calls during the handler see
23071
- // the post-removal state.
23072
- if (entry.once) {
23073
- this._unsubscribe_entry(set, entry);
23074
- }
23075
- try {
23076
- entry.handler(detail);
23077
- }
23078
- catch (err) {
23079
- if (name === 'error' || this._firing_error) {
23080
- // surface to stderr as a last resort but never recurse;
23081
- // `console` is in the JS standard library and present in every
23082
- // supported runtime, so guarding it would just add an untestable
23083
- // branch. See #638.
23084
- // eslint-disable-next-line no-console
23085
- console.error(err);
23086
- }
23087
- else {
23088
- this._firing_error = true;
23089
- try {
23090
- this._fire('error', {
23091
- error: err,
23092
- source_event: name,
23093
- source_detail: detail,
23094
- handler: entry.handler
23095
- });
23096
- }
23097
- finally {
23098
- this._firing_error = false;
23099
- }
23100
- }
23101
- }
23140
+ this._fire_one(entry, set, name, detail);
23102
23141
  }
23103
23142
  }
23104
23143
  /** Low-level hook registration. Installs a handler described by a
package/dist/cdn/viz.js CHANGED
@@ -21352,7 +21352,7 @@ var constants = /*#__PURE__*/Object.freeze({
21352
21352
  * Useful for runtime diagnostics and for embedding in serialized machine
21353
21353
  * snapshots so that deserializers can detect version-skew.
21354
21354
  */
21355
- const version = "5.141.3";
21355
+ const version = "5.141.5";
21356
21356
 
21357
21357
  // whargarbl lots of these return arrays could/should be sets
21358
21358
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
@@ -21668,6 +21668,11 @@ class Machine {
21668
21668
  this._state_labels.set(key, labelled[0].value);
21669
21669
  }
21670
21670
  });
21671
+ // O(1) duplicate-edge guard for the construction loop below: from -> Set<to>.
21672
+ // Keyed by source state; mirrors each state's `to` array with constant-time
21673
+ // membership so the dedup check is O(1) per edge rather than an O(out-degree)
21674
+ // array scan (which made construction O(V*E) on dense graphs). #673
21675
+ const seen_edges = new Map();
21671
21676
  // walk the transitions
21672
21677
  transitions.map((tr) => {
21673
21678
  if (tr.from === undefined) {
@@ -21687,11 +21692,20 @@ class Machine {
21687
21692
  if (!(this._states.has(tr.to))) {
21688
21693
  this._new_state(cursor_to);
21689
21694
  }
21690
- // guard against existing connections being re-added
21691
- if (cursor_from.to.includes(tr.to)) {
21695
+ // guard against existing connections being re-added — O(1) via the
21696
+ // from -> Set<to> index instead of an O(out-degree) `cursor_from.to`
21697
+ // array scan. Behaviour is identical: the same duplicate (from, to)
21698
+ // pair throws the same JssmError. #673
21699
+ let seen_to = seen_edges.get(tr.from);
21700
+ if (seen_to === undefined) {
21701
+ seen_to = new Set();
21702
+ seen_edges.set(tr.from, seen_to);
21703
+ }
21704
+ if (seen_to.has(tr.to)) {
21692
21705
  throw new JssmError(this, `already has ${JSON.stringify(tr.from)} to ${JSON.stringify(tr.to)}`);
21693
21706
  }
21694
21707
  else {
21708
+ seen_to.add(tr.to);
21695
21709
  cursor_from.to.push(tr.to);
21696
21710
  cursor_to.from.push(tr.from);
21697
21711
  }
@@ -23057,6 +23071,62 @@ class Machine {
23057
23071
  this._event_listener_count++;
23058
23072
  return () => { this._unsubscribe_entry(set, entry); };
23059
23073
  }
23074
+ /**
23075
+ * Invoke a single event-handler entry, respecting its filter, once-removal
23076
+ * semantics, and the error re-fire / recursion-guard logic. Extracted so
23077
+ * {@link _fire} can share identical behavior between the size-1 fast-path
23078
+ * and the general snapshotted loop.
23079
+ *
23080
+ * @param entry - The subscriber descriptor to invoke.
23081
+ * @param set - The live Set that owns `entry`; needed for once-removal.
23082
+ * @param name - The event name being dispatched (used in error re-fires).
23083
+ * @param detail - The event payload forwarded to the handler.
23084
+ *
23085
+ * @internal
23086
+ */
23087
+ _fire_one(entry, set, name, detail) {
23088
+ // filter check
23089
+ if (entry.filter !== undefined) {
23090
+ for (const k of Object.keys(entry.filter)) {
23091
+ if (entry.filter[k] !== detail[k]) {
23092
+ return;
23093
+ }
23094
+ }
23095
+ }
23096
+ // once removal happens BEFORE invocation so a throwing handler still
23097
+ // gets removed and so re-entrant `on` calls during the handler see
23098
+ // the post-removal state.
23099
+ if (entry.once) {
23100
+ this._unsubscribe_entry(set, entry);
23101
+ }
23102
+ try {
23103
+ entry.handler(detail);
23104
+ }
23105
+ catch (err) {
23106
+ if (name === 'error' || this._firing_error) {
23107
+ // surface to stderr as a last resort but never recurse;
23108
+ // `console` is in the JS standard library and present in every
23109
+ // supported runtime, so guarding it would just add an untestable
23110
+ // branch. See #638.
23111
+ // eslint-disable-next-line no-console
23112
+ console.error(err);
23113
+ }
23114
+ else {
23115
+ this._firing_error = true;
23116
+ try {
23117
+ this._fire('error', {
23118
+ error: err,
23119
+ source_event: name,
23120
+ source_detail: detail,
23121
+ handler: entry.handler
23122
+ });
23123
+ }
23124
+ finally {
23125
+ this._firing_error = false;
23126
+ }
23127
+ }
23128
+ }
23129
+ }
23060
23130
  /**
23061
23131
  * Dispatch an event to every registered subscriber in registration
23062
23132
  * order. Filters are checked first; non-matching handlers are skipped
@@ -23068,6 +23138,11 @@ class Machine {
23068
23138
  * handler throws, the new exception is swallowed rather than rebroadcast
23069
23139
  * to avoid an infinite loop.
23070
23140
  *
23141
+ * When exactly one subscriber is registered the common case avoids the
23142
+ * `Array.from(set)` snapshot allocation by capturing the lone entry into a
23143
+ * local first — equivalent to a 1-element snapshot but allocation-free.
23144
+ * The general path still snapshots for re-entrancy safety.
23145
+ *
23071
23146
  * @internal
23072
23147
  */
23073
23148
  _fire(name, detail) {
@@ -23075,55 +23150,19 @@ class Machine {
23075
23150
  if (set === undefined || set.size === 0) {
23076
23151
  return;
23077
23152
  }
23078
- // Snapshot so handlers can `off()` mid-loop without disturbing iteration.
23153
+ // Fast-path: single subscriber capture entry before invoking so that
23154
+ // even if the handler mutates `set` (via off/once auto-removal) we hold a
23155
+ // stable reference. Behaviorally identical to a 1-element snapshot.
23156
+ if (set.size === 1) {
23157
+ const only = set.values().next().value;
23158
+ this._fire_one(only, set, name, detail);
23159
+ return;
23160
+ }
23161
+ // General path: snapshot so handlers can `off()` mid-loop without
23162
+ // disturbing iteration.
23079
23163
  const entries = Array.from(set);
23080
23164
  for (const entry of entries) {
23081
- // filter check
23082
- if (entry.filter !== undefined) {
23083
- let matched = true;
23084
- for (const k of Object.keys(entry.filter)) {
23085
- if (entry.filter[k] !== detail[k]) {
23086
- matched = false;
23087
- break;
23088
- }
23089
- }
23090
- if (!matched) {
23091
- continue;
23092
- }
23093
- }
23094
- // once removal happens BEFORE invocation so a throwing handler still
23095
- // gets removed and so re-entrant `on` calls during the handler see
23096
- // the post-removal state.
23097
- if (entry.once) {
23098
- this._unsubscribe_entry(set, entry);
23099
- }
23100
- try {
23101
- entry.handler(detail);
23102
- }
23103
- catch (err) {
23104
- if (name === 'error' || this._firing_error) {
23105
- // surface to stderr as a last resort but never recurse;
23106
- // `console` is in the JS standard library and present in every
23107
- // supported runtime, so guarding it would just add an untestable
23108
- // branch. See #638.
23109
- // eslint-disable-next-line no-console
23110
- console.error(err);
23111
- }
23112
- else {
23113
- this._firing_error = true;
23114
- try {
23115
- this._fire('error', {
23116
- error: err,
23117
- source_event: name,
23118
- source_detail: detail,
23119
- handler: entry.handler
23120
- });
23121
- }
23122
- finally {
23123
- this._firing_error = false;
23124
- }
23125
- }
23126
- }
23165
+ this._fire_one(entry, set, name, detail);
23127
23166
  }
23128
23167
  }
23129
23168
  /** Low-level hook registration. Installs a handler described by a