jssm 5.162.5 → 5.162.7

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
@@ -21912,6 +21912,14 @@ function nth_matching_loc(tree, predicate, n) {
21912
21912
  *
21913
21913
  */
21914
21914
  function makeTransition(this_se, from, to, isRight, _wasList, _wasIndex) {
21915
+ // the explicit quotation syntax lets `""` through the grammar; a nameless
21916
+ // state can never be addressed, so reject at edge assembly (fsl#653)
21917
+ if (from === '') {
21918
+ throw new JssmError(undefined, 'A state name may not be the empty string (transition source)');
21919
+ }
21920
+ if (to === '') {
21921
+ throw new JssmError(undefined, 'A state name may not be the empty string (transition target)');
21922
+ }
21915
21923
  const kind = isRight
21916
21924
  ? arrow_right_kind(this_se.kind)
21917
21925
  : arrow_left_kind(this_se.kind),
@@ -21951,6 +21959,11 @@ function makeTransition(this_se, from, to, isRight, _wasList, _wasIndex) {
21951
21959
  if (this_se[probability] != null) {
21952
21960
  edge.probability = this_se[probability];
21953
21961
  }
21962
+ // same rejection for `''` action quotation — an action nobody can name
21963
+ // can never be dispatched (fsl#653)
21964
+ if (edge.action === '') {
21965
+ throw new JssmError(undefined, 'An action name may not be the empty string');
21966
+ }
21954
21967
  return edge;
21955
21968
  }
21956
21969
  /*********
@@ -23790,7 +23803,7 @@ function fslSemanticSpans(text) {
23790
23803
  * Useful for runtime diagnostics and for embedding in serialized machine
23791
23804
  * snapshots so that deserializers can detect version-skew.
23792
23805
  */
23793
- const version = "5.162.5";
23806
+ const version = "5.162.7";
23794
23807
 
23795
23808
  /**
23796
23809
  * The FSL Markdown fence convention parser — pure, host-agnostic logic that
@@ -23970,6 +23983,7 @@ const hook_required_fields = {
23970
23983
  'entry': ['to'],
23971
23984
  'exit': ['from'],
23972
23985
  'after': ['from'],
23986
+ 'after any': [],
23973
23987
  'post hook': ['from', 'to'],
23974
23988
  'post named': ['from', 'to', 'action'],
23975
23989
  'post global action': ['action'],
@@ -24338,6 +24352,7 @@ class Machine {
24338
24352
  this._entry_hooks = new Map();
24339
24353
  this._exit_hooks = new Map();
24340
24354
  this._after_hooks = new Map();
24355
+ this._after_any_hook = undefined;
24341
24356
  this._global_action_hooks = new Map();
24342
24357
  this._any_action_hook = undefined;
24343
24358
  this._standard_transition_hook = undefined;
@@ -24852,6 +24867,53 @@ class Machine {
24852
24867
  data() {
24853
24868
  return structuredClone(this._data);
24854
24869
  }
24870
+ /*********
24871
+ *
24872
+ * Replace the machine's data in place, without a transition. This is the
24873
+ * practical way to assign any value — including `undefined`, `null`, or
24874
+ * `false` — outside a hook's complex return, closing the gap where an
24875
+ * `undefined` assignment had no direct API (StoneCypher/fsl#1264). Fires
24876
+ * a `data-change` event with cause `'set_data'` when the value actually
24877
+ * changes; unlike {@link override} it requires no `allows_override`
24878
+ * config, because it never moves the state.
24879
+ *
24880
+ * ```typescript
24881
+ * import * as jssm from 'jssm';
24882
+ *
24883
+ * const lswitch = jssm.from('on <=> off;', {data: 1});
24884
+ * console.log( lswitch.data() ); // 1
24885
+ *
24886
+ * lswitch.set_data(2);
24887
+ * console.log( lswitch.data() ); // 2
24888
+ *
24889
+ * lswitch.set_data(undefined);
24890
+ * console.log( lswitch.data() ); // undefined
24891
+ * ```
24892
+ *
24893
+ * @typeParam mDT The type of the machine data member; usually omitted
24894
+ *
24895
+ * @param newData The value to install as the machine's data.
24896
+ *
24897
+ * @returns The machine, for chaining.
24898
+ *
24899
+ * @see Machine.data
24900
+ * @see override
24901
+ *
24902
+ */
24903
+ set_data(newData) {
24904
+ const oldData = this._data;
24905
+ this._data = newData;
24906
+ if (oldData !== newData) {
24907
+ this._fire('data-change', {
24908
+ from: this._state,
24909
+ to: this._state,
24910
+ old_data: oldData,
24911
+ new_data: newData,
24912
+ cause: 'set_data'
24913
+ });
24914
+ }
24915
+ return this;
24916
+ }
24855
24917
  /**
24856
24918
  * The machine's current data by REFERENCE — no clone. The public
24857
24919
  * {@link Machine.data} contract is a deep clone per call (a mutation
@@ -26551,11 +26613,14 @@ class Machine {
26551
26613
  }
26552
26614
  for (const field of hook_spatial_fields) {
26553
26615
  const needed = required.includes(field);
26554
- const present = HookDesc[field] !== undefined;
26555
- if (needed && !present) {
26556
- throw new JssmError(this, `${HookDesc.kind} hook requires '${field}'`);
26557
- }
26558
- if (!needed && present) {
26616
+ const value = HookDesc[field];
26617
+ // a required spatial field must be a usable key: a non-empty string.
26618
+ // presence alone isn't enough `action: false` or `from: ''` would
26619
+ // register a hook nothing can ever fire (fsl#653, fsl#659)
26620
+ if (needed && ((typeof value !== 'string') || (value === ''))) {
26621
+ throw new JssmError(this, `${HookDesc.kind} hook requires '${field}' to be a non-empty string`);
26622
+ }
26623
+ if (!needed && (value !== undefined)) {
26559
26624
  throw new JssmError(this, `${HookDesc.kind} hook does not take '${field}'`);
26560
26625
  }
26561
26626
  }
@@ -26629,6 +26694,11 @@ class Machine {
26629
26694
  this._has_hooks = true;
26630
26695
  this._has_after_hooks = true;
26631
26696
  break;
26697
+ case 'after any':
26698
+ this._after_any_hook = HookDesc.handler;
26699
+ this._has_hooks = true;
26700
+ this._has_after_hooks = true;
26701
+ break;
26632
26702
  case 'post hook': {
26633
26703
  // Numeric pair key; same rationale as 'hook' (#729).
26634
26704
  this._post_hooks.set(pair_key(this._state_interner.intern(HookDesc.from), this._state_interner.intern(HookDesc.to)), HookDesc.handler);
@@ -26800,6 +26870,12 @@ class Machine {
26800
26870
  case 'after':
26801
26871
  removed = this._after_hooks.delete(HookDesc.from);
26802
26872
  break;
26873
+ case 'after any':
26874
+ if (this._after_any_hook !== undefined) {
26875
+ this._after_any_hook = undefined;
26876
+ removed = true;
26877
+ }
26878
+ break;
26803
26879
  case 'post hook': {
26804
26880
  const fid = this._state_interner.id_of(HookDesc.from), tid = this._state_interner.id_of(HookDesc.to);
26805
26881
  removed = (fid !== undefined) && (tid !== undefined) && this._post_hooks.delete(pair_key(fid, tid));
@@ -27017,6 +27093,36 @@ class Machine {
27017
27093
  this.set_hook({ kind: 'after', from, handler });
27018
27094
  return this;
27019
27095
  }
27096
+ /** Register a hook that fires when ANY state's `after` timer elapses — the
27097
+ * whole-machine companion to {@link hook_after}, mirroring how
27098
+ * {@link hook_any_transition} companions {@link hook}. When the elapsing
27099
+ * state also has a specific {@link hook_after}, the specific hook fires
27100
+ * first and this one fires second; a specific after hook firing always
27101
+ * implies the any-after hook fires too (StoneCypher/fsl#1299). Like
27102
+ * `hook_after` it is informational — its outcome cannot reject the timed
27103
+ * transition — and it does NOT fire on ordinary dispatch.
27104
+ * @param handler - Callback invoked whenever any `after` timer fires, just
27105
+ * before the timed transition is taken.
27106
+ * @returns `this` for chaining.
27107
+ *
27108
+ * @example
27109
+ * const m = sm`a after 1000 -> b; a -> c; c -> a;`;
27110
+ * let calls = 0;
27111
+ * m.hook_after_any(() => { calls += 1; });
27112
+ * m.go('c');
27113
+ * m.go('a');
27114
+ * // ordinary dispatch never fires it; only a timer elapsing does:
27115
+ * calls; // => 0
27116
+ * m.clear_state_timeout();
27117
+ *
27118
+ * @see hook_after
27119
+ * @see hook_any_transition
27120
+ * @see set_state_timeout
27121
+ */
27122
+ hook_after_any(handler) {
27123
+ this.set_hook({ kind: 'after any', handler });
27124
+ return this;
27125
+ }
27020
27126
  /** Post-transition hook on a specific edge. Fires after the transition
27021
27127
  * from `from` to `to` has completed. Cannot block the transition.
27022
27128
  * @param from - Source state name.
@@ -27243,7 +27349,13 @@ class Machine {
27243
27349
  }
27244
27350
  /*********
27245
27351
  *
27246
- * Replace the current state and data with no regard to the graph.
27352
+ * Replace the current state and, when a data argument is provided, the
27353
+ * data — with no regard to the graph.
27354
+ *
27355
+ * The data argument is arity-detected: omitting it preserves the current
27356
+ * data, while explicitly passing `undefined` really sets the data to
27357
+ * `undefined` (StoneCypher/fsl#1264). Before 5.163 an omitted data
27358
+ * argument silently cleared the data.
27247
27359
  *
27248
27360
  * ```typescript
27249
27361
  * import { sm } from 'jssm';
@@ -27259,22 +27371,37 @@ class Machine {
27259
27371
  * console.log( machine.state() ); // 'a'
27260
27372
  * ```
27261
27373
  *
27374
+ * @param newState The state to teleport to; must exist in the graph.
27375
+ *
27376
+ * @param newData Replacement data. Omit to keep the current data; pass
27377
+ * `undefined` explicitly to clear it.
27378
+ *
27379
+ * @throws {JssmError} If the machine's config does not set
27380
+ * `allows_override: true`, or if `newState` does not exist.
27381
+ *
27382
+ * @see set_data
27383
+ *
27262
27384
  */
27263
27385
  override(newState, newData) {
27386
+ // arity, not undefined-comparison: an omitted argument preserves the
27387
+ // data, an explicit `undefined` clears it (StoneCypher/fsl#1264)
27388
+ const dataProvided = arguments.length >= 2;
27264
27389
  if (this.allows_override) {
27265
27390
  if (this._states.has(newState)) {
27266
27391
  const fromState = this._state;
27267
27392
  const oldData = this._data;
27268
27393
  this._state = newState;
27269
27394
  this._state_id = this._state_interner.intern(newState);
27270
- this._data = newData;
27395
+ if (dataProvided) {
27396
+ this._data = newData;
27397
+ }
27271
27398
  this._fire('override', {
27272
27399
  from: fromState,
27273
27400
  to: newState,
27274
27401
  old_data: oldData,
27275
- new_data: newData
27402
+ new_data: this._data
27276
27403
  });
27277
- if (oldData !== newData) {
27404
+ if (dataProvided && (oldData !== newData)) {
27278
27405
  this._fire('data-change', {
27279
27406
  from: fromState,
27280
27407
  to: newState,
@@ -27482,13 +27609,20 @@ class Machine {
27482
27609
  * `newStateOrAction` is an action name and the target state is looked up
27483
27610
  * via the current action edge.
27484
27611
  *
27612
+ * @param dataProvided `true` when the caller explicitly supplied a data
27613
+ * argument — even an explicitly-`undefined` one, which commits `undefined`
27614
+ * as the new data (StoneCypher/fsl#1264). When `false` the current data
27615
+ * is preserved. The public wrappers derive this from call arity; the
27616
+ * default reproduces the old `!== undefined` inference for any direct
27617
+ * callers.
27618
+ *
27485
27619
  * @returns `true` if the transition was valid and every hook passed;
27486
27620
  * `false` if the transition was invalid or any hook rejected.
27487
27621
  *
27488
27622
  * @internal
27489
27623
  *
27490
27624
  */
27491
- transition_impl(newStateOrAction, newData, wasForced, wasAction) {
27625
+ transition_impl(newStateOrAction, newData, wasForced, wasAction, dataProvided = newData !== undefined) {
27492
27626
  let valid = false, trans_type, newState, newStateId = NaN, actionId = NaN, fromAction = undefined;
27493
27627
  if (wasForced) {
27494
27628
  // numeric inline of valid_force_transition: any existing edge
@@ -27753,7 +27887,7 @@ class Machine {
27753
27887
  if (data_changed) {
27754
27888
  this._data = hook_args.data;
27755
27889
  }
27756
- else if (newData !== undefined) {
27890
+ else if (dataProvided) {
27757
27891
  this._data = newData;
27758
27892
  }
27759
27893
  // success fallthrough to posthooks; intentionally no return here
@@ -27766,9 +27900,9 @@ class Machine {
27766
27900
  }
27767
27901
  this._state = newState;
27768
27902
  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) {
27903
+ // provision is detected by caller arity, so an explicit `undefined`
27904
+ // commits while an omitted argument preserves (StoneCypher/fsl#1264)
27905
+ if (dataProvided) {
27772
27906
  this._data = newData;
27773
27907
  }
27774
27908
  // success fallthrough to posthooks; intentionally no return here
@@ -28081,7 +28215,9 @@ class Machine {
28081
28215
  *
28082
28216
  */
28083
28217
  action(actionName, newData) {
28084
- return this.transition_impl(actionName, newData, false, true);
28218
+ // arity, not undefined-comparison: an explicit `undefined` is a real
28219
+ // data assignment (StoneCypher/fsl#1264)
28220
+ return this.transition_impl(actionName, newData, false, true, arguments.length >= 2);
28085
28221
  }
28086
28222
  /********
28087
28223
  *
@@ -28345,6 +28481,7 @@ class Machine {
28345
28481
  push_global(this._main_transition_hook, 'main transition', 'pre');
28346
28482
  push_global(this._forced_transition_hook, 'forced transition', 'pre');
28347
28483
  push_global(this._any_transition_hook, 'any transition', 'pre');
28484
+ push_global(this._after_any_hook, 'after any', 'pre');
28348
28485
  push_global(this._pre_everything_hook, 'pre everything', 'pre');
28349
28486
  push_global(this._everything_hook, 'everything', 'pre');
28350
28487
  // post-phase, edge- and state-keyed tables
@@ -28859,7 +28996,7 @@ class Machine {
28859
28996
  *
28860
28997
  */
28861
28998
  do(actionName, newData) {
28862
- return this.transition_impl(actionName, newData, false, true);
28999
+ return this.transition_impl(actionName, newData, false, true, arguments.length >= 2);
28863
29000
  }
28864
29001
  /********
28865
29002
  *
@@ -28893,7 +29030,7 @@ class Machine {
28893
29030
  *
28894
29031
  */
28895
29032
  transition(newState, newData) {
28896
- return this.transition_impl(newState, newData, false, false);
29033
+ return this.transition_impl(newState, newData, false, false, arguments.length >= 2);
28897
29034
  }
28898
29035
  /********
28899
29036
  *
@@ -28917,7 +29054,7 @@ class Machine {
28917
29054
  *
28918
29055
  */
28919
29056
  go(newState, newData) {
28920
- return this.transition_impl(newState, newData, false, false);
29057
+ return this.transition_impl(newState, newData, false, false, arguments.length >= 2);
28921
29058
  }
28922
29059
  /********
28923
29060
  *
@@ -28945,7 +29082,7 @@ class Machine {
28945
29082
  *
28946
29083
  */
28947
29084
  force_transition(newState, newData) {
28948
- return this.transition_impl(newState, newData, true, false);
29085
+ return this.transition_impl(newState, newData, true, false, arguments.length >= 2);
28949
29086
  }
28950
29087
  /** Get the edge index for an action from the current state.
28951
29088
  * Interned dispatch: resolves via the numeric (action, from) index —
@@ -29058,6 +29195,11 @@ class Machine {
29058
29195
  if (ah !== undefined) {
29059
29196
  ah({ data: this._data, next_data: this._data });
29060
29197
  }
29198
+ // a specific after hook firing implies the any-after hook fires too,
29199
+ // afterward; and it also fires alone (StoneCypher/fsl#1299)
29200
+ if (this._after_any_hook !== undefined) {
29201
+ this._after_any_hook({ data: this._data, next_data: this._data });
29202
+ }
29061
29203
  }
29062
29204
  this._fire('timeout', { from: from_state, to: next_state, after_time });
29063
29205
  this.go(next_state);