jssm 5.162.4 → 5.162.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.
@@ -883,6 +883,10 @@ type AfterHook<mDT> = {
883
883
  from: string;
884
884
  handler: HookHandler<mDT>;
885
885
  };
886
+ type AfterAnyHook<mDT> = {
887
+ kind: 'after any';
888
+ handler: HookHandler<mDT>;
889
+ };
886
890
  type PostBasicHookDescription<mDT> = {
887
891
  kind: 'post hook';
888
892
  from: string;
@@ -956,12 +960,12 @@ type PostEverythingHook<mDT> = {
956
960
  *
957
961
  * Pre-transition variants (`'hook'`, `'named'`, `'standard transition'`,
958
962
  * `'main transition'`, `'forced transition'`, `'any transition'`,
959
- * `'global action'`, `'any action'`, `'entry'`, `'exit'`, `'after'`)
960
- * may return a falsy value to veto a transition. Post-transition
963
+ * `'global action'`, `'any action'`, `'entry'`, `'exit'`, `'after'`,
964
+ * `'after any'`) may return a falsy value to veto a transition. Post-transition
961
965
  * variants (`'post *'`) cannot veto and are invoked only after a
962
966
  * successful transition.
963
967
  */
964
- type HookDescription<mDT> = BasicHookDescription<mDT> | HookDescriptionWithAction<mDT> | GlobalActionHook<mDT> | AnyActionHook<mDT> | StandardTransitionHook<mDT> | MainTransitionHook<mDT> | ForcedTransitionHook<mDT> | AnyTransitionHook<mDT> | EntryHook<mDT> | ExitHook<mDT> | AfterHook<mDT> | PostBasicHookDescription<mDT> | PostHookDescriptionWithAction<mDT> | PostGlobalActionHook<mDT> | PostAnyActionHook<mDT> | PostStandardTransitionHook<mDT> | PostMainTransitionHook<mDT> | PostForcedTransitionHook<mDT> | PostAnyTransitionHook<mDT> | PostEntryHook<mDT> | PostExitHook<mDT> | PreEverythingHook<mDT> | EverythingHook<mDT> | PrePostEverythingHook<mDT> | PostEverythingHook<mDT>;
968
+ type HookDescription<mDT> = BasicHookDescription<mDT> | HookDescriptionWithAction<mDT> | GlobalActionHook<mDT> | AnyActionHook<mDT> | StandardTransitionHook<mDT> | MainTransitionHook<mDT> | ForcedTransitionHook<mDT> | AnyTransitionHook<mDT> | EntryHook<mDT> | ExitHook<mDT> | AfterHook<mDT> | AfterAnyHook<mDT> | PostBasicHookDescription<mDT> | PostHookDescriptionWithAction<mDT> | PostGlobalActionHook<mDT> | PostAnyActionHook<mDT> | PostStandardTransitionHook<mDT> | PostMainTransitionHook<mDT> | PostForcedTransitionHook<mDT> | PostAnyTransitionHook<mDT> | PostEntryHook<mDT> | PostExitHook<mDT> | PreEverythingHook<mDT> | EverythingHook<mDT> | PrePostEverythingHook<mDT> | PostEverythingHook<mDT>;
965
969
  /**
966
970
  * Whether an observational hook runs in the pre-transition phase (where it
967
971
  * may veto/mutate the transition) or the post-transition phase (a pure
@@ -1235,7 +1239,9 @@ type JssmErrorEventDetail = {
1235
1239
  /**
1236
1240
  * Detail payload fired with a `data-change` event. Fires whenever the
1237
1241
  * machine's data payload is replaced. `old_data` is the value before the
1238
- * change; `new_data` is the value after.
1242
+ * change; `new_data` is the value after. `cause` names the API family that
1243
+ * performed the replacement: a data-bearing `transition`, an `override`, or
1244
+ * a direct `set_data` call.
1239
1245
  */
1240
1246
  type JssmDataChangeEventDetail<mDT> = {
1241
1247
  from?: StateType;
@@ -1243,7 +1249,7 @@ type JssmDataChangeEventDetail<mDT> = {
1243
1249
  action?: StateType;
1244
1250
  old_data: mDT;
1245
1251
  new_data: mDT;
1246
- cause: 'transition' | 'override';
1252
+ cause: 'transition' | 'override' | 'set_data';
1247
1253
  };
1248
1254
  /**
1249
1255
  * Detail payload fired with an `override` event. Distinguishes a forced
@@ -23790,7 +23790,7 @@ function fslSemanticSpans(text) {
23790
23790
  * Useful for runtime diagnostics and for embedding in serialized machine
23791
23791
  * snapshots so that deserializers can detect version-skew.
23792
23792
  */
23793
- const version = "5.162.4";
23793
+ const version = "5.162.6";
23794
23794
 
23795
23795
  /**
23796
23796
  * The FSL Markdown fence convention parser — pure, host-agnostic logic that
@@ -23970,6 +23970,7 @@ const hook_required_fields = {
23970
23970
  'entry': ['to'],
23971
23971
  'exit': ['from'],
23972
23972
  'after': ['from'],
23973
+ 'after any': [],
23973
23974
  'post hook': ['from', 'to'],
23974
23975
  'post named': ['from', 'to', 'action'],
23975
23976
  'post global action': ['action'],
@@ -24338,6 +24339,7 @@ class Machine {
24338
24339
  this._entry_hooks = new Map();
24339
24340
  this._exit_hooks = new Map();
24340
24341
  this._after_hooks = new Map();
24342
+ this._after_any_hook = undefined;
24341
24343
  this._global_action_hooks = new Map();
24342
24344
  this._any_action_hook = undefined;
24343
24345
  this._standard_transition_hook = undefined;
@@ -24852,6 +24854,53 @@ class Machine {
24852
24854
  data() {
24853
24855
  return structuredClone(this._data);
24854
24856
  }
24857
+ /*********
24858
+ *
24859
+ * Replace the machine's data in place, without a transition. This is the
24860
+ * practical way to assign any value — including `undefined`, `null`, or
24861
+ * `false` — outside a hook's complex return, closing the gap where an
24862
+ * `undefined` assignment had no direct API (StoneCypher/fsl#1264). Fires
24863
+ * a `data-change` event with cause `'set_data'` when the value actually
24864
+ * changes; unlike {@link override} it requires no `allows_override`
24865
+ * config, because it never moves the state.
24866
+ *
24867
+ * ```typescript
24868
+ * import * as jssm from 'jssm';
24869
+ *
24870
+ * const lswitch = jssm.from('on <=> off;', {data: 1});
24871
+ * console.log( lswitch.data() ); // 1
24872
+ *
24873
+ * lswitch.set_data(2);
24874
+ * console.log( lswitch.data() ); // 2
24875
+ *
24876
+ * lswitch.set_data(undefined);
24877
+ * console.log( lswitch.data() ); // undefined
24878
+ * ```
24879
+ *
24880
+ * @typeParam mDT The type of the machine data member; usually omitted
24881
+ *
24882
+ * @param newData The value to install as the machine's data.
24883
+ *
24884
+ * @returns The machine, for chaining.
24885
+ *
24886
+ * @see Machine.data
24887
+ * @see override
24888
+ *
24889
+ */
24890
+ set_data(newData) {
24891
+ const oldData = this._data;
24892
+ this._data = newData;
24893
+ if (oldData !== newData) {
24894
+ this._fire('data-change', {
24895
+ from: this._state,
24896
+ to: this._state,
24897
+ old_data: oldData,
24898
+ new_data: newData,
24899
+ cause: 'set_data'
24900
+ });
24901
+ }
24902
+ return this;
24903
+ }
24855
24904
  /**
24856
24905
  * The machine's current data by REFERENCE — no clone. The public
24857
24906
  * {@link Machine.data} contract is a deep clone per call (a mutation
@@ -26629,6 +26678,11 @@ class Machine {
26629
26678
  this._has_hooks = true;
26630
26679
  this._has_after_hooks = true;
26631
26680
  break;
26681
+ case 'after any':
26682
+ this._after_any_hook = HookDesc.handler;
26683
+ this._has_hooks = true;
26684
+ this._has_after_hooks = true;
26685
+ break;
26632
26686
  case 'post hook': {
26633
26687
  // Numeric pair key; same rationale as 'hook' (#729).
26634
26688
  this._post_hooks.set(pair_key(this._state_interner.intern(HookDesc.from), this._state_interner.intern(HookDesc.to)), HookDesc.handler);
@@ -26800,6 +26854,12 @@ class Machine {
26800
26854
  case 'after':
26801
26855
  removed = this._after_hooks.delete(HookDesc.from);
26802
26856
  break;
26857
+ case 'after any':
26858
+ if (this._after_any_hook !== undefined) {
26859
+ this._after_any_hook = undefined;
26860
+ removed = true;
26861
+ }
26862
+ break;
26803
26863
  case 'post hook': {
26804
26864
  const fid = this._state_interner.id_of(HookDesc.from), tid = this._state_interner.id_of(HookDesc.to);
26805
26865
  removed = (fid !== undefined) && (tid !== undefined) && this._post_hooks.delete(pair_key(fid, tid));
@@ -27017,6 +27077,36 @@ class Machine {
27017
27077
  this.set_hook({ kind: 'after', from, handler });
27018
27078
  return this;
27019
27079
  }
27080
+ /** Register a hook that fires when ANY state's `after` timer elapses — the
27081
+ * whole-machine companion to {@link hook_after}, mirroring how
27082
+ * {@link hook_any_transition} companions {@link hook}. When the elapsing
27083
+ * state also has a specific {@link hook_after}, the specific hook fires
27084
+ * first and this one fires second; a specific after hook firing always
27085
+ * implies the any-after hook fires too (StoneCypher/fsl#1299). Like
27086
+ * `hook_after` it is informational — its outcome cannot reject the timed
27087
+ * transition — and it does NOT fire on ordinary dispatch.
27088
+ * @param handler - Callback invoked whenever any `after` timer fires, just
27089
+ * before the timed transition is taken.
27090
+ * @returns `this` for chaining.
27091
+ *
27092
+ * @example
27093
+ * const m = sm`a after 1000 -> b; a -> c; c -> a;`;
27094
+ * let calls = 0;
27095
+ * m.hook_after_any(() => { calls += 1; });
27096
+ * m.go('c');
27097
+ * m.go('a');
27098
+ * // ordinary dispatch never fires it; only a timer elapsing does:
27099
+ * calls; // => 0
27100
+ * m.clear_state_timeout();
27101
+ *
27102
+ * @see hook_after
27103
+ * @see hook_any_transition
27104
+ * @see set_state_timeout
27105
+ */
27106
+ hook_after_any(handler) {
27107
+ this.set_hook({ kind: 'after any', handler });
27108
+ return this;
27109
+ }
27020
27110
  /** Post-transition hook on a specific edge. Fires after the transition
27021
27111
  * from `from` to `to` has completed. Cannot block the transition.
27022
27112
  * @param from - Source state name.
@@ -27243,7 +27333,13 @@ class Machine {
27243
27333
  }
27244
27334
  /*********
27245
27335
  *
27246
- * Replace the current state and data with no regard to the graph.
27336
+ * Replace the current state and, when a data argument is provided, the
27337
+ * data — with no regard to the graph.
27338
+ *
27339
+ * The data argument is arity-detected: omitting it preserves the current
27340
+ * data, while explicitly passing `undefined` really sets the data to
27341
+ * `undefined` (StoneCypher/fsl#1264). Before 5.163 an omitted data
27342
+ * argument silently cleared the data.
27247
27343
  *
27248
27344
  * ```typescript
27249
27345
  * import { sm } from 'jssm';
@@ -27259,22 +27355,37 @@ class Machine {
27259
27355
  * console.log( machine.state() ); // 'a'
27260
27356
  * ```
27261
27357
  *
27358
+ * @param newState The state to teleport to; must exist in the graph.
27359
+ *
27360
+ * @param newData Replacement data. Omit to keep the current data; pass
27361
+ * `undefined` explicitly to clear it.
27362
+ *
27363
+ * @throws {JssmError} If the machine's config does not set
27364
+ * `allows_override: true`, or if `newState` does not exist.
27365
+ *
27366
+ * @see set_data
27367
+ *
27262
27368
  */
27263
27369
  override(newState, newData) {
27370
+ // arity, not undefined-comparison: an omitted argument preserves the
27371
+ // data, an explicit `undefined` clears it (StoneCypher/fsl#1264)
27372
+ const dataProvided = arguments.length >= 2;
27264
27373
  if (this.allows_override) {
27265
27374
  if (this._states.has(newState)) {
27266
27375
  const fromState = this._state;
27267
27376
  const oldData = this._data;
27268
27377
  this._state = newState;
27269
27378
  this._state_id = this._state_interner.intern(newState);
27270
- this._data = newData;
27379
+ if (dataProvided) {
27380
+ this._data = newData;
27381
+ }
27271
27382
  this._fire('override', {
27272
27383
  from: fromState,
27273
27384
  to: newState,
27274
27385
  old_data: oldData,
27275
- new_data: newData
27386
+ new_data: this._data
27276
27387
  });
27277
- if (oldData !== newData) {
27388
+ if (dataProvided && (oldData !== newData)) {
27278
27389
  this._fire('data-change', {
27279
27390
  from: fromState,
27280
27391
  to: newState,
@@ -27482,13 +27593,20 @@ class Machine {
27482
27593
  * `newStateOrAction` is an action name and the target state is looked up
27483
27594
  * via the current action edge.
27484
27595
  *
27596
+ * @param dataProvided `true` when the caller explicitly supplied a data
27597
+ * argument — even an explicitly-`undefined` one, which commits `undefined`
27598
+ * as the new data (StoneCypher/fsl#1264). When `false` the current data
27599
+ * is preserved. The public wrappers derive this from call arity; the
27600
+ * default reproduces the old `!== undefined` inference for any direct
27601
+ * callers.
27602
+ *
27485
27603
  * @returns `true` if the transition was valid and every hook passed;
27486
27604
  * `false` if the transition was invalid or any hook rejected.
27487
27605
  *
27488
27606
  * @internal
27489
27607
  *
27490
27608
  */
27491
- transition_impl(newStateOrAction, newData, wasForced, wasAction) {
27609
+ transition_impl(newStateOrAction, newData, wasForced, wasAction, dataProvided = newData !== undefined) {
27492
27610
  let valid = false, trans_type, newState, newStateId = NaN, actionId = NaN, fromAction = undefined;
27493
27611
  if (wasForced) {
27494
27612
  // numeric inline of valid_force_transition: any existing edge
@@ -27753,7 +27871,7 @@ class Machine {
27753
27871
  if (data_changed) {
27754
27872
  this._data = hook_args.data;
27755
27873
  }
27756
- else if (newData !== undefined) {
27874
+ else if (dataProvided) {
27757
27875
  this._data = newData;
27758
27876
  }
27759
27877
  // success fallthrough to posthooks; intentionally no return here
@@ -27766,9 +27884,9 @@ class Machine {
27766
27884
  }
27767
27885
  this._state = newState;
27768
27886
  this._state_id = newStateId;
27769
- // TODO known bug: this gives no way to set data to undefined
27770
- // see https://github.com/StoneCypher/fsl/issues/1264
27771
- if (newData !== undefined) {
27887
+ // provision is detected by caller arity, so an explicit `undefined`
27888
+ // commits while an omitted argument preserves (StoneCypher/fsl#1264)
27889
+ if (dataProvided) {
27772
27890
  this._data = newData;
27773
27891
  }
27774
27892
  // success fallthrough to posthooks; intentionally no return here
@@ -28081,7 +28199,9 @@ class Machine {
28081
28199
  *
28082
28200
  */
28083
28201
  action(actionName, newData) {
28084
- return this.transition_impl(actionName, newData, false, true);
28202
+ // arity, not undefined-comparison: an explicit `undefined` is a real
28203
+ // data assignment (StoneCypher/fsl#1264)
28204
+ return this.transition_impl(actionName, newData, false, true, arguments.length >= 2);
28085
28205
  }
28086
28206
  /********
28087
28207
  *
@@ -28345,6 +28465,7 @@ class Machine {
28345
28465
  push_global(this._main_transition_hook, 'main transition', 'pre');
28346
28466
  push_global(this._forced_transition_hook, 'forced transition', 'pre');
28347
28467
  push_global(this._any_transition_hook, 'any transition', 'pre');
28468
+ push_global(this._after_any_hook, 'after any', 'pre');
28348
28469
  push_global(this._pre_everything_hook, 'pre everything', 'pre');
28349
28470
  push_global(this._everything_hook, 'everything', 'pre');
28350
28471
  // post-phase, edge- and state-keyed tables
@@ -28859,7 +28980,7 @@ class Machine {
28859
28980
  *
28860
28981
  */
28861
28982
  do(actionName, newData) {
28862
- return this.transition_impl(actionName, newData, false, true);
28983
+ return this.transition_impl(actionName, newData, false, true, arguments.length >= 2);
28863
28984
  }
28864
28985
  /********
28865
28986
  *
@@ -28893,7 +29014,7 @@ class Machine {
28893
29014
  *
28894
29015
  */
28895
29016
  transition(newState, newData) {
28896
- return this.transition_impl(newState, newData, false, false);
29017
+ return this.transition_impl(newState, newData, false, false, arguments.length >= 2);
28897
29018
  }
28898
29019
  /********
28899
29020
  *
@@ -28917,7 +29038,7 @@ class Machine {
28917
29038
  *
28918
29039
  */
28919
29040
  go(newState, newData) {
28920
- return this.transition_impl(newState, newData, false, false);
29041
+ return this.transition_impl(newState, newData, false, false, arguments.length >= 2);
28921
29042
  }
28922
29043
  /********
28923
29044
  *
@@ -28945,7 +29066,7 @@ class Machine {
28945
29066
  *
28946
29067
  */
28947
29068
  force_transition(newState, newData) {
28948
- return this.transition_impl(newState, newData, true, false);
29069
+ return this.transition_impl(newState, newData, true, false, arguments.length >= 2);
28949
29070
  }
28950
29071
  /** Get the edge index for an action from the current state.
28951
29072
  * Interned dispatch: resolves via the numeric (action, from) index —
@@ -29058,6 +29179,11 @@ class Machine {
29058
29179
  if (ah !== undefined) {
29059
29180
  ah({ data: this._data, next_data: this._data });
29060
29181
  }
29182
+ // a specific after hook firing implies the any-after hook fires too,
29183
+ // afterward; and it also fires alone (StoneCypher/fsl#1299)
29184
+ if (this._after_any_hook !== undefined) {
29185
+ this._after_any_hook({ data: this._data, next_data: this._data });
29186
+ }
29061
29187
  }
29062
29188
  this._fire('timeout', { from: from_state, to: next_state, after_time });
29063
29189
  this.go(next_state);