jssm 5.133.0 → 5.134.0

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.133.0 at 5/27/2026, 9:02:28 PM
21
+ * Generated for version 5.134.0 at 5/27/2026, 10:16:05 PM
22
22
 
23
23
  -->
24
- # jssm 5.133.0
24
+ # jssm 5.134.0
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,157 tests at 100.0% line coverage
284
+ library.** 6,220 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,157 tests***, run 56,944 times.
417
+ ***6,220 tests***, run 57,007 times.
418
418
 
419
- - 5,644 specs with 100.0% coverage
420
- - 513 fuzz tests with 4.3% coverage
421
- - 4,415 TypeScript lines - 1.4 tests per line, 12.9 generated tests per line
419
+ - 5,707 specs with 100.0% coverage
420
+ - 513 fuzz tests with 4.0% coverage
421
+ - 4,856 TypeScript lines - 1.3 tests per line, 11.7 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)
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.133.0";
21355
+ const version = "5.134.0";
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;
@@ -21646,6 +21646,8 @@ class Machine {
21646
21646
  this._timeout_target = undefined;
21647
21647
  this._timeout_target_time = undefined;
21648
21648
  this._after_mapping = new Map();
21649
+ this._event_handlers = new Map();
21650
+ this._firing_error = false;
21649
21651
  // consolidate the state declarations
21650
21652
  if (state_declaration) {
21651
21653
  state_declaration.map((state_decl) => {
@@ -22971,6 +22973,141 @@ class Machine {
22971
22973
  has_completes() {
22972
22974
  return this.states().some((x) => this.state_is_complete(x));
22973
22975
  }
22976
+ on(name, filterOrFn, maybeFn) {
22977
+ return this._subscribe(name, filterOrFn, maybeFn, false);
22978
+ }
22979
+ once(name, filterOrFn, maybeFn) {
22980
+ return this._subscribe(name, filterOrFn, maybeFn, true);
22981
+ }
22982
+ /**
22983
+ * Remove a previously-registered event handler. Match is by reference —
22984
+ * the same function value passed to {@link Machine.on} or
22985
+ * {@link Machine.once}. Returns `true` if a subscription was found and
22986
+ * removed, `false` otherwise.
22987
+ *
22988
+ * ```typescript
22989
+ * const fn = (e: any) => console.log(e);
22990
+ * m.on('transition', fn);
22991
+ * m.off('transition', fn); // true
22992
+ * m.off('transition', fn); // false
22993
+ * ```
22994
+ *
22995
+ * @param name The event name.
22996
+ * @param handler The handler reference to remove.
22997
+ * @returns `true` if removed, `false` if no match was registered.
22998
+ */
22999
+ off(name, handler) {
23000
+ const set = this._event_handlers.get(name);
23001
+ if (set === undefined) {
23002
+ return false;
23003
+ }
23004
+ for (const entry of set) {
23005
+ if (entry.handler === handler) {
23006
+ set.delete(entry);
23007
+ return true;
23008
+ }
23009
+ }
23010
+ return false;
23011
+ }
23012
+ /**
23013
+ * Shared registration core used by {@link Machine.on} and
23014
+ * {@link Machine.once}. Normalizes the optional filter argument and
23015
+ * installs the entry into the per-event subscription set.
23016
+ *
23017
+ * @internal
23018
+ */
23019
+ _subscribe(name, filterOrFn, maybeFn, once) {
23020
+ let filter;
23021
+ let handler;
23022
+ if (typeof filterOrFn === 'function') {
23023
+ filter = undefined;
23024
+ handler = filterOrFn;
23025
+ }
23026
+ else {
23027
+ filter = filterOrFn;
23028
+ handler = maybeFn;
23029
+ }
23030
+ if (typeof handler !== 'function') {
23031
+ throw new JssmError(this, `event handler for "${name}" must be a function`);
23032
+ }
23033
+ let set = this._event_handlers.get(name);
23034
+ if (set === undefined) {
23035
+ set = new Set();
23036
+ this._event_handlers.set(name, set);
23037
+ }
23038
+ const entry = { handler, filter, once };
23039
+ set.add(entry);
23040
+ return () => { set.delete(entry); };
23041
+ }
23042
+ /**
23043
+ * Dispatch an event to every registered subscriber in registration
23044
+ * order. Filters are checked first; non-matching handlers are skipped
23045
+ * without invoking the handler. Exceptions thrown by a handler are
23046
+ * caught and re-emitted as an `error` event so subsequent handlers
23047
+ * still run.
23048
+ *
23049
+ * Re-entry into the `error` event itself is guarded — if an `error`
23050
+ * handler throws, the new exception is swallowed rather than rebroadcast
23051
+ * to avoid an infinite loop.
23052
+ *
23053
+ * @internal
23054
+ */
23055
+ _fire(name, detail) {
23056
+ const set = this._event_handlers.get(name);
23057
+ if (set === undefined || set.size === 0) {
23058
+ return;
23059
+ }
23060
+ // Snapshot so handlers can `off()` mid-loop without disturbing iteration.
23061
+ const entries = Array.from(set);
23062
+ for (const entry of entries) {
23063
+ // filter check
23064
+ if (entry.filter !== undefined) {
23065
+ let matched = true;
23066
+ for (const k of Object.keys(entry.filter)) {
23067
+ if (entry.filter[k] !== detail[k]) {
23068
+ matched = false;
23069
+ break;
23070
+ }
23071
+ }
23072
+ if (!matched) {
23073
+ continue;
23074
+ }
23075
+ }
23076
+ // once removal happens BEFORE invocation so a throwing handler still
23077
+ // gets removed and so re-entrant `on` calls during the handler see
23078
+ // the post-removal state.
23079
+ if (entry.once) {
23080
+ set.delete(entry);
23081
+ }
23082
+ try {
23083
+ entry.handler(detail);
23084
+ }
23085
+ catch (err) {
23086
+ if (name === 'error' || this._firing_error) {
23087
+ // surface to stderr as a last resort but never recurse;
23088
+ // `console` is in the JS standard library and present in every
23089
+ // supported runtime, so guarding it would just add an untestable
23090
+ // branch. See #638.
23091
+ // eslint-disable-next-line no-console
23092
+ console.error(err);
23093
+ }
23094
+ else {
23095
+ this._firing_error = true;
23096
+ try {
23097
+ this._fire('error', {
23098
+ error: err,
23099
+ source_event: name,
23100
+ source_detail: detail,
23101
+ handler: entry.handler
23102
+ });
23103
+ }
23104
+ finally {
23105
+ this._firing_error = false;
23106
+ }
23107
+ }
23108
+ }
23109
+ }
23110
+ }
22974
23111
  /** Low-level hook registration. Installs a handler described by a
22975
23112
  * {@link HookDescription} into the appropriate internal map. Prefer the
22976
23113
  * convenience wrappers ({@link hook}, {@link hook_entry}, etc.) over
@@ -23137,6 +23274,179 @@ class Machine {
23137
23274
  default:
23138
23275
  throw new JssmError(this, `Unknown hook type ${HookDesc.kind}, should be impossible`);
23139
23276
  }
23277
+ // fire the registration event for inspector tools (#638)
23278
+ this._fire('hook-registration', { description: HookDesc });
23279
+ }
23280
+ /**
23281
+ * Remove a previously-registered hook described by a
23282
+ * {@link HookDescription}. Match is by `kind` + identifying keys
23283
+ * (`from`/`to`/`action`/etc.), not by handler reference — there is one
23284
+ * hook per slot in the registry, so the description uniquely identifies
23285
+ * which one to clear. Fires a `hook-removal` event for inspector tools.
23286
+ *
23287
+ * This is the symmetric counterpart of {@link Machine.set_hook} for the
23288
+ * event-bridging use case (#638). Reasoning about hooks via observation
23289
+ * events requires being able to observe their disappearance too.
23290
+ *
23291
+ * ```typescript
23292
+ * const m = sm`a -> b;`;
23293
+ * const fn = () => true;
23294
+ * m.set_hook({ kind: 'hook', from: 'a', to: 'b', handler: fn });
23295
+ * m.remove_hook({ kind: 'hook', from: 'a', to: 'b', handler: fn });
23296
+ * ```
23297
+ *
23298
+ * @param HookDesc - A hook descriptor identifying the hook to remove.
23299
+ * @returns `true` if a hook was removed, `false` otherwise.
23300
+ */
23301
+ remove_hook(HookDesc) {
23302
+ let removed = false;
23303
+ switch (HookDesc.kind) {
23304
+ case 'hook': {
23305
+ const inner = this._hooks.get(HookDesc.from);
23306
+ if (inner !== undefined && inner.has(HookDesc.to)) {
23307
+ inner.delete(HookDesc.to);
23308
+ removed = true;
23309
+ }
23310
+ break;
23311
+ }
23312
+ case 'named': {
23313
+ const inner = this._named_hooks.get(HookDesc.from);
23314
+ const inner2 = inner === undefined ? undefined : inner.get(HookDesc.to);
23315
+ if (inner2 !== undefined && inner2.has(HookDesc.action)) {
23316
+ inner2.delete(HookDesc.action);
23317
+ removed = true;
23318
+ }
23319
+ break;
23320
+ }
23321
+ case 'global action':
23322
+ removed = this._global_action_hooks.delete(HookDesc.action);
23323
+ break;
23324
+ case 'any action':
23325
+ if (this._any_action_hook !== undefined) {
23326
+ this._any_action_hook = undefined;
23327
+ removed = true;
23328
+ }
23329
+ break;
23330
+ case 'standard transition':
23331
+ if (this._standard_transition_hook !== undefined) {
23332
+ this._standard_transition_hook = undefined;
23333
+ removed = true;
23334
+ }
23335
+ break;
23336
+ case 'main transition':
23337
+ if (this._main_transition_hook !== undefined) {
23338
+ this._main_transition_hook = undefined;
23339
+ removed = true;
23340
+ }
23341
+ break;
23342
+ case 'forced transition':
23343
+ if (this._forced_transition_hook !== undefined) {
23344
+ this._forced_transition_hook = undefined;
23345
+ removed = true;
23346
+ }
23347
+ break;
23348
+ case 'any transition':
23349
+ if (this._any_transition_hook !== undefined) {
23350
+ this._any_transition_hook = undefined;
23351
+ removed = true;
23352
+ }
23353
+ break;
23354
+ case 'entry':
23355
+ removed = this._entry_hooks.delete(HookDesc.to);
23356
+ break;
23357
+ case 'exit':
23358
+ removed = this._exit_hooks.delete(HookDesc.from);
23359
+ break;
23360
+ case 'after':
23361
+ removed = this._after_hooks.delete(HookDesc.from);
23362
+ break;
23363
+ case 'post hook': {
23364
+ const inner = this._post_hooks.get(HookDesc.from);
23365
+ if (inner !== undefined && inner.has(HookDesc.to)) {
23366
+ inner.delete(HookDesc.to);
23367
+ removed = true;
23368
+ }
23369
+ break;
23370
+ }
23371
+ case 'post named': {
23372
+ const inner = this._post_named_hooks.get(HookDesc.from);
23373
+ const inner2 = inner === undefined ? undefined : inner.get(HookDesc.to);
23374
+ if (inner2 !== undefined && inner2.has(HookDesc.action)) {
23375
+ inner2.delete(HookDesc.action);
23376
+ removed = true;
23377
+ }
23378
+ break;
23379
+ }
23380
+ case 'post global action':
23381
+ removed = this._post_global_action_hooks.delete(HookDesc.action);
23382
+ break;
23383
+ case 'post any action':
23384
+ if (this._post_any_action_hook !== undefined) {
23385
+ this._post_any_action_hook = undefined;
23386
+ removed = true;
23387
+ }
23388
+ break;
23389
+ case 'post standard transition':
23390
+ if (this._post_standard_transition_hook !== undefined) {
23391
+ this._post_standard_transition_hook = undefined;
23392
+ removed = true;
23393
+ }
23394
+ break;
23395
+ case 'post main transition':
23396
+ if (this._post_main_transition_hook !== undefined) {
23397
+ this._post_main_transition_hook = undefined;
23398
+ removed = true;
23399
+ }
23400
+ break;
23401
+ case 'post forced transition':
23402
+ if (this._post_forced_transition_hook !== undefined) {
23403
+ this._post_forced_transition_hook = undefined;
23404
+ removed = true;
23405
+ }
23406
+ break;
23407
+ case 'post any transition':
23408
+ if (this._post_any_transition_hook !== undefined) {
23409
+ this._post_any_transition_hook = undefined;
23410
+ removed = true;
23411
+ }
23412
+ break;
23413
+ case 'post entry':
23414
+ removed = this._post_entry_hooks.delete(HookDesc.to);
23415
+ break;
23416
+ case 'post exit':
23417
+ removed = this._post_exit_hooks.delete(HookDesc.from);
23418
+ break;
23419
+ case 'pre everything':
23420
+ if (this._pre_everything_hook !== undefined) {
23421
+ this._pre_everything_hook = undefined;
23422
+ removed = true;
23423
+ }
23424
+ break;
23425
+ case 'everything':
23426
+ if (this._everything_hook !== undefined) {
23427
+ this._everything_hook = undefined;
23428
+ removed = true;
23429
+ }
23430
+ break;
23431
+ case 'pre post everything':
23432
+ if (this._pre_post_everything_hook !== undefined) {
23433
+ this._pre_post_everything_hook = undefined;
23434
+ removed = true;
23435
+ }
23436
+ break;
23437
+ case 'post everything':
23438
+ if (this._post_everything_hook !== undefined) {
23439
+ this._post_everything_hook = undefined;
23440
+ removed = true;
23441
+ }
23442
+ break;
23443
+ default:
23444
+ throw new JssmError(this, `Unknown hook type ${HookDesc.kind}, should be impossible`);
23445
+ }
23446
+ if (removed) {
23447
+ this._fire('hook-removal', { description: HookDesc });
23448
+ }
23449
+ return removed;
23140
23450
  }
23141
23451
  /** Register a pre-transition hook on a specific edge. Fires before
23142
23452
  * transitioning from `from` to `to`. If the handler returns `false`, the
@@ -23478,8 +23788,25 @@ class Machine {
23478
23788
  override(newState, newData) {
23479
23789
  if (this.allows_override) {
23480
23790
  if (this._states.has(newState)) {
23791
+ const fromState = this._state;
23792
+ const oldData = this._data;
23481
23793
  this._state = newState;
23482
23794
  this._data = newData;
23795
+ this._fire('override', {
23796
+ from: fromState,
23797
+ to: newState,
23798
+ old_data: oldData,
23799
+ new_data: newData
23800
+ });
23801
+ if (oldData !== newData) {
23802
+ this._fire('data-change', {
23803
+ from: fromState,
23804
+ to: newState,
23805
+ old_data: oldData,
23806
+ new_data: newData,
23807
+ cause: 'override'
23808
+ });
23809
+ }
23483
23810
  }
23484
23811
  else {
23485
23812
  throw new JssmError(this, `Cannot override state to "${newState}", a state that does not exist`);
@@ -23568,6 +23895,21 @@ class Machine {
23568
23895
  forced: wasForced,
23569
23896
  trans_type
23570
23897
  };
23898
+ // 'action' event fires when an action is attempted, regardless of whether
23899
+ // it ultimately succeeds — matches the issue spec for observation events.
23900
+ if (wasAction) {
23901
+ this._fire('action', {
23902
+ action: newStateOrAction,
23903
+ from: this._state,
23904
+ to: newState,
23905
+ data: this._data,
23906
+ next_data: newData
23907
+ });
23908
+ }
23909
+ // Captured pre-transition source state so 'data-change' detail and similar
23910
+ // events can name where we came from.
23911
+ const fromState = this._state;
23912
+ const oldData = this._data;
23571
23913
  if (valid) {
23572
23914
  if (this._has_hooks) {
23573
23915
  // once validity is known, clear old 'after' timeout clause
@@ -23580,10 +23922,23 @@ class Machine {
23580
23922
  }
23581
23923
  }
23582
23924
  let data_changed = false;
23925
+ const fire_rejection = (hook_name) => {
23926
+ this._fire('rejection', {
23927
+ from: fromState,
23928
+ to: newState,
23929
+ action: fromAction,
23930
+ data: oldData,
23931
+ next_data: newData,
23932
+ reason: 'hook',
23933
+ hook_name,
23934
+ forced: wasForced
23935
+ });
23936
+ };
23583
23937
  // 0. pre everything hook (fires before all other pre-hooks)
23584
23938
  if (this._pre_everything_hook !== undefined) {
23585
23939
  const outcome = abstract_everything_hook_step(this._pre_everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'pre everything' }));
23586
23940
  if (outcome.pass === false) {
23941
+ fire_rejection('pre everything');
23587
23942
  return false;
23588
23943
  }
23589
23944
  update_fields(outcome);
@@ -23592,12 +23947,14 @@ class Machine {
23592
23947
  // 1a. any action hook
23593
23948
  const outcome = abstract_hook_step(this._any_action_hook, hook_args);
23594
23949
  if (outcome.pass === false) {
23950
+ fire_rejection('any action');
23595
23951
  return false;
23596
23952
  }
23597
23953
  update_fields(outcome);
23598
23954
  // 1b. global specific action hook
23599
23955
  const outcome2 = abstract_hook_step(this._global_action_hooks.get(newStateOrAction), hook_args);
23600
23956
  if (outcome2.pass === false) {
23957
+ fire_rejection('global action');
23601
23958
  return false;
23602
23959
  }
23603
23960
  update_fields(outcome2);
@@ -23613,6 +23970,7 @@ class Machine {
23613
23970
  if (this._any_transition_hook !== undefined) {
23614
23971
  const outcome = abstract_hook_step(this._any_transition_hook, hook_args);
23615
23972
  if (outcome.pass === false) {
23973
+ fire_rejection('any transition');
23616
23974
  return false;
23617
23975
  }
23618
23976
  update_fields(outcome);
@@ -23621,6 +23979,7 @@ class Machine {
23621
23979
  if (this._has_exit_hooks) {
23622
23980
  const outcome = abstract_hook_step(this._exit_hooks.get(this._state), hook_args);
23623
23981
  if (outcome.pass === false) {
23982
+ fire_rejection('exit');
23624
23983
  return false;
23625
23984
  }
23626
23985
  update_fields(outcome);
@@ -23635,6 +23994,7 @@ class Machine {
23635
23994
  const nh = byAct === undefined ? undefined : byAct.get(newStateOrAction);
23636
23995
  const outcome = abstract_hook_step(nh, hook_args);
23637
23996
  if (outcome.pass === false) {
23997
+ fire_rejection('named');
23638
23998
  return false;
23639
23999
  }
23640
24000
  update_fields(outcome);
@@ -23647,6 +24007,7 @@ class Machine {
23647
24007
  const h = byTo === undefined ? undefined : byTo.get(newState);
23648
24008
  const outcome = abstract_hook_step(h, hook_args);
23649
24009
  if (outcome.pass === false) {
24010
+ fire_rejection('hook');
23650
24011
  return false;
23651
24012
  }
23652
24013
  update_fields(outcome);
@@ -23656,6 +24017,7 @@ class Machine {
23656
24017
  if (trans_type === 'legal') {
23657
24018
  const outcome = abstract_hook_step(this._standard_transition_hook, hook_args);
23658
24019
  if (outcome.pass === false) {
24020
+ fire_rejection('standard transition');
23659
24021
  return false;
23660
24022
  }
23661
24023
  update_fields(outcome);
@@ -23664,6 +24026,7 @@ class Machine {
23664
24026
  if (trans_type === 'main') {
23665
24027
  const outcome = abstract_hook_step(this._main_transition_hook, hook_args);
23666
24028
  if (outcome.pass === false) {
24029
+ fire_rejection('main transition');
23667
24030
  return false;
23668
24031
  }
23669
24032
  update_fields(outcome);
@@ -23672,6 +24035,7 @@ class Machine {
23672
24035
  if (trans_type === 'forced') {
23673
24036
  const outcome = abstract_hook_step(this._forced_transition_hook, hook_args);
23674
24037
  if (outcome.pass === false) {
24038
+ fire_rejection('forced transition');
23675
24039
  return false;
23676
24040
  }
23677
24041
  update_fields(outcome);
@@ -23680,6 +24044,7 @@ class Machine {
23680
24044
  if (this._has_entry_hooks) {
23681
24045
  const outcome = abstract_hook_step(this._entry_hooks.get(newState), hook_args);
23682
24046
  if (outcome.pass === false) {
24047
+ fire_rejection('entry');
23683
24048
  return false;
23684
24049
  }
23685
24050
  update_fields(outcome);
@@ -23688,6 +24053,7 @@ class Machine {
23688
24053
  if (this._everything_hook !== undefined) {
23689
24054
  const outcome = abstract_everything_hook_step(this._everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'everything' }));
23690
24055
  if (outcome.pass === false) {
24056
+ fire_rejection('everything');
23691
24057
  return false;
23692
24058
  }
23693
24059
  update_fields(outcome);
@@ -23723,6 +24089,15 @@ class Machine {
23723
24089
  // not valid
23724
24090
  }
23725
24091
  else {
24092
+ this._fire('rejection', {
24093
+ from: fromState,
24094
+ to: newStateOrAction,
24095
+ action: fromAction,
24096
+ data: oldData,
24097
+ next_data: newData,
24098
+ reason: 'invalid',
24099
+ forced: wasForced
24100
+ });
23726
24101
  return false;
23727
24102
  }
23728
24103
  // posthooks begin here
@@ -23805,6 +24180,47 @@ class Machine {
23805
24180
  this._post_everything_hook(Object.assign(Object.assign({}, hook_args), { hook_name: 'post everything' }));
23806
24181
  }
23807
24182
  }
24183
+ // observation events: fire after the state has been committed and all
24184
+ // hooks (pre and post) have completed, matching the semantics that
24185
+ // events observe rather than intercept (#638).
24186
+ const newData_after = this._data;
24187
+ this._fire('exit', {
24188
+ state: fromState,
24189
+ to: newState,
24190
+ action: fromAction,
24191
+ data: newData_after
24192
+ });
24193
+ this._fire('transition', {
24194
+ from: fromState,
24195
+ to: newState,
24196
+ action: fromAction,
24197
+ data: newData_after,
24198
+ next_data: newData,
24199
+ trans_type,
24200
+ forced: wasForced
24201
+ });
24202
+ this._fire('entry', {
24203
+ state: newState,
24204
+ from: fromState,
24205
+ action: fromAction,
24206
+ data: newData_after
24207
+ });
24208
+ if (oldData !== newData_after) {
24209
+ this._fire('data-change', {
24210
+ from: fromState,
24211
+ to: newState,
24212
+ action: fromAction,
24213
+ old_data: oldData,
24214
+ new_data: newData_after,
24215
+ cause: 'transition'
24216
+ });
24217
+ }
24218
+ if (this.state_is_terminal(newState)) {
24219
+ this._fire('terminal', { state: newState, data: newData_after });
24220
+ }
24221
+ if (this.state_is_complete(newState)) {
24222
+ this._fire('complete', { state: newState, data: newData_after });
24223
+ }
23808
24224
  // possibly re-establish new 'after' clause
23809
24225
  this.auto_set_state_timeout();
23810
24226
  return true;
@@ -24433,14 +24849,17 @@ class Machine {
24433
24849
  // this is enforced by the "after mapping runs normally with very short time" tests in after_mapping.spec
24434
24850
  // we'll mark it no-check so that our coverage numbers aren't wrecked
24435
24851
  /* istanbul ignore next */
24852
+ /* v8 ignore next 10 */
24436
24853
  () => {
24854
+ const from_state = this.state();
24437
24855
  this.clear_state_timeout();
24438
24856
  if (this._has_after_hooks) {
24439
- const ah = this._after_hooks.get(this.state());
24857
+ const ah = this._after_hooks.get(from_state);
24440
24858
  if (ah !== undefined) {
24441
24859
  ah({ data: this._data, next_data: this._data });
24442
24860
  }
24443
24861
  }
24862
+ this._fire('timeout', { from: from_state, to: next_state, after_time });
24444
24863
  this.go(next_state);
24445
24864
  }, after_time);
24446
24865
  this._timeout_target = next_state;