jssm 5.162.26 → 5.162.28

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.
@@ -24454,7 +24454,7 @@ function fslSemanticSpans(text) {
24454
24454
  * Useful for runtime diagnostics and for embedding in serialized machine
24455
24455
  * snapshots so that deserializers can detect version-skew.
24456
24456
  */
24457
- const version = "5.162.26";
24457
+ const version = "5.162.28";
24458
24458
 
24459
24459
  /**
24460
24460
  * The FSL Markdown fence convention parser — pure, host-agnostic logic that
@@ -24994,7 +24994,23 @@ const STOCHASTIC_DEFAULT_MAX_STEPS = 1000;
24994
24994
  * @internal
24995
24995
  */
24996
24996
  const DEFAULT_TIME_SOURCE = () => Date.now();
24997
- const DEFAULT_TIMEOUT_SOURCE = (f, a) => setTimeout(f, a);
24997
+ const DEFAULT_TIMEOUT_SOURCE = (f, a) => {
24998
+ const handle = setTimeout(f, a);
24999
+ // In Node, setTimeout returns a Timeout with .unref(), so a pending `after`
25000
+ // timer does NOT by itself keep the process alive -- an abandoned machine can
25001
+ // be collected and the process can exit instead of hanging until the timer
25002
+ // fires go() on it. The browser returns a plain number with no such method.
25003
+ // A consumer who wants the timer to hold the loop open can supply their own
25004
+ // timeout_source. StoneCypher/fsl#1952
25005
+ const maybe_unref = handle;
25006
+ // The no-unref path is the browser's numeric handle; it can't be reached in
25007
+ // the node-only coverage environment, so the false branch is ignored here.
25008
+ /* v8 ignore next */
25009
+ if (typeof maybe_unref.unref === 'function') {
25010
+ maybe_unref.unref();
25011
+ }
25012
+ return handle;
25013
+ };
24998
25014
  const DEFAULT_CLEAR_TIMEOUT_SOURCE = (h) => clearTimeout(h);
24999
25015
  class Machine {
25000
25016
  // whargarbl this badly needs to be broken up, monolith master
@@ -25172,6 +25188,7 @@ class Machine {
25172
25188
  this._event_handlers = new Map();
25173
25189
  this._event_listener_count = 0;
25174
25190
  this._firing_error = false;
25191
+ this._committing_transition = false;
25175
25192
  // Boundary-hook action cascade guard. Limit defaults to 100 but is
25176
25193
  // configurable via the `boundary_depth_limit` constructor option so tests
25177
25194
  // can tighten the cap and deep pipelines can raise it.
@@ -28110,10 +28127,25 @@ class Machine {
28110
28127
  * @returns `true` if the transition was valid and every hook passed;
28111
28128
  * `false` if the transition was invalid or any hook rejected.
28112
28129
  *
28130
+ * @throws {JssmError} If called reentrantly from inside a hook that is still
28131
+ * running in the enclosing transition's pre-commit pipeline — a hook that
28132
+ * calls `transition`/`go`/`do`/`action`. Committing the inner transition
28133
+ * and then the outer one would silently discard the inner result, so the
28134
+ * reentry is rejected instead (StoneCypher/fsl#1953). Post-commit reentry
28135
+ * (from a post-hook or the boundary-action cascade) is permitted.
28136
+ *
28113
28137
  * @internal
28114
28138
  *
28115
28139
  */
28116
28140
  transition_impl(newStateOrAction, newData, wasForced, wasAction, dataProvided = newData !== undefined) {
28141
+ // Reject reentry from inside the pre-commit hook pipeline. Without this, a
28142
+ // hook that itself transitions the machine would commit an inner transition
28143
+ // that this outer, not-yet-committed frame then silently overwrites. Post-
28144
+ // commit reentry (post-hooks, the boundary-action cascade) is fine: the flag
28145
+ // is already cleared by then. StoneCypher/fsl#1953
28146
+ if (this._committing_transition) {
28147
+ throw new JssmError(this, 'cannot start a transition from within a transition hook: the enclosing transition has not committed yet, so the inner result would be silently discarded');
28148
+ }
28117
28149
  let valid = false,
28118
28150
  // deliberately `string`, not `JssmArrowKind`, though only arrow kinds are
28119
28151
  // ever assigned: declaring this local as the 4-member union makes tsc's
@@ -28222,185 +28254,203 @@ class Machine {
28222
28254
  const oldData = this._data;
28223
28255
  if (valid) {
28224
28256
  if (this._has_hooks) {
28225
- let data_changed = false;
28226
- // 0. pre everything hook (fires before all other pre-hooks)
28227
- if (this._pre_everything_hook !== undefined) {
28228
- const outcome = abstract_everything_hook_step(this._pre_everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'pre everything' }));
28229
- if (!outcome.pass) {
28230
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'pre everything', fromState, newState, fromAction, oldData, newData, wasForced);
28231
- return false;
28232
- }
28233
- if (_update_hook_fields(hook_args, outcome)) {
28234
- data_changed = true;
28235
- }
28236
- }
28237
- if (wasAction) {
28238
- // 1a. any action hook
28239
- const outcome = abstract_hook_step(this._any_action_hook, hook_args);
28240
- if (!outcome.pass) {
28241
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'any action', fromState, newState, fromAction, oldData, newData, wasForced);
28242
- return false;
28243
- }
28244
- if (_update_hook_fields(hook_args, outcome)) {
28245
- data_changed = true;
28246
- }
28247
- // 1b. global specific action hook
28248
- const outcome2 = abstract_hook_step(this._global_action_hooks.get(actionId), hook_args);
28249
- if (!outcome2.pass) {
28250
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'global action', fromState, newState, fromAction, oldData, newData, wasForced);
28251
- return false;
28252
- }
28253
- if (_update_hook_fields(hook_args, outcome2)) {
28254
- data_changed = true;
28255
- }
28256
- }
28257
- // 2. (removed) After hooks do NOT fire on dispatch. They are the
28258
- // `after`-timer's companion (fsl#698: "delay over!") and fire only from
28259
- // the state-timeout path. Through v5.143.28 a probe here keyed on
28260
- // newStateOrAction spuriously fired them on entering the hooked state —
28261
- // or on a same-named action — making one timer elapse read as two
28262
- // handler calls (StoneCypher/fsl#1327).
28263
- // 3. any transition hook
28264
- if (this._any_transition_hook !== undefined) {
28265
- const outcome = abstract_hook_step(this._any_transition_hook, hook_args);
28266
- if (!outcome.pass) {
28267
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'any transition', fromState, newState, fromAction, oldData, newData, wasForced);
28268
- return false;
28269
- }
28270
- if (_update_hook_fields(hook_args, outcome)) {
28271
- data_changed = true;
28272
- }
28273
- }
28274
- // 4. exit hook
28275
- if (this._has_exit_hooks) {
28276
- const outcome = abstract_hook_step(this._exit_hooks.get(this._state_id), hook_args);
28277
- if (!outcome.pass) {
28278
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'exit', fromState, newState, fromAction, oldData, newData, wasForced);
28279
- return false;
28280
- }
28281
- if (_update_hook_fields(hook_args, outcome)) {
28282
- data_changed = true;
28283
- }
28284
- }
28285
- // shared by steps 5 and 6: pre-commit, this._state_id is still the
28286
- // from-state, so both probes key on the same pair; compute it once
28287
- const pre_pair_id = pair_key(this._state_id, newStateId);
28288
- // 5. named transition / action hook
28289
- if (this._has_named_hooks && wasAction) {
28290
- // Numeric pair probe, then the action id captured at dispatch (#729).
28291
- const byPair = this._named_hooks.get(pre_pair_id);
28292
- const nh = byPair === undefined ? undefined : byPair.get(actionId);
28293
- const outcome = abstract_hook_step(nh, hook_args);
28294
- if (!outcome.pass) {
28295
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'named', fromState, newState, fromAction, oldData, newData, wasForced);
28296
- return false;
28257
+ // Open the pre-commit window: from here until the commit below, any
28258
+ // reentrant transition_impl call (a hook transitioning the machine)
28259
+ // throws instead of being silently reverted. The `finally` below closes
28260
+ // it on every exit path; #fire_hook_rejection additionally clears it
28261
+ // before firing the rejection event so a rejection listener may itself
28262
+ // transition. The pipeline body is intentionally left at its original
28263
+ // indentation to keep this fix's diff focused. #1953
28264
+ this._committing_transition = true;
28265
+ try {
28266
+ let data_changed = false;
28267
+ // 0. pre everything hook (fires before all other pre-hooks)
28268
+ if (this._pre_everything_hook !== undefined) {
28269
+ const outcome = abstract_everything_hook_step(this._pre_everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'pre everything' }));
28270
+ if (!outcome.pass) {
28271
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'pre everything', fromState, newState, fromAction, oldData, newData, wasForced);
28272
+ return false;
28273
+ }
28274
+ if (_update_hook_fields(hook_args, outcome)) {
28275
+ data_changed = true;
28276
+ }
28297
28277
  }
28298
- if (_update_hook_fields(hook_args, outcome)) {
28299
- data_changed = true;
28278
+ if (wasAction) {
28279
+ // 1a. any action hook
28280
+ const outcome = abstract_hook_step(this._any_action_hook, hook_args);
28281
+ if (!outcome.pass) {
28282
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'any action', fromState, newState, fromAction, oldData, newData, wasForced);
28283
+ return false;
28284
+ }
28285
+ if (_update_hook_fields(hook_args, outcome)) {
28286
+ data_changed = true;
28287
+ }
28288
+ // 1b. global specific action hook
28289
+ const outcome2 = abstract_hook_step(this._global_action_hooks.get(actionId), hook_args);
28290
+ if (!outcome2.pass) {
28291
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'global action', fromState, newState, fromAction, oldData, newData, wasForced);
28292
+ return false;
28293
+ }
28294
+ if (_update_hook_fields(hook_args, outcome2)) {
28295
+ data_changed = true;
28296
+ }
28300
28297
  }
28301
- }
28302
- // 6. regular hook
28303
- if (this._has_basic_hooks) {
28304
- // Numeric pair probe (#729); one integer hash replaces two string maps.
28305
- const h = this._hooks.get(pre_pair_id);
28306
- const outcome = abstract_hook_step(h, hook_args);
28307
- if (!outcome.pass) {
28308
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'hook', fromState, newState, fromAction, oldData, newData, wasForced);
28309
- return false;
28298
+ // 2. (removed) After hooks do NOT fire on dispatch. They are the
28299
+ // `after`-timer's companion (fsl#698: "delay over!") and fire only from
28300
+ // the state-timeout path. Through v5.143.28 a probe here keyed on
28301
+ // newStateOrAction spuriously fired them on entering the hooked state
28302
+ // or on a same-named action — making one timer elapse read as two
28303
+ // handler calls (StoneCypher/fsl#1327).
28304
+ // 3. any transition hook
28305
+ if (this._any_transition_hook !== undefined) {
28306
+ const outcome = abstract_hook_step(this._any_transition_hook, hook_args);
28307
+ if (!outcome.pass) {
28308
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'any transition', fromState, newState, fromAction, oldData, newData, wasForced);
28309
+ return false;
28310
+ }
28311
+ if (_update_hook_fields(hook_args, outcome)) {
28312
+ data_changed = true;
28313
+ }
28310
28314
  }
28311
- if (_update_hook_fields(hook_args, outcome)) {
28312
- data_changed = true;
28315
+ // 4. exit hook
28316
+ if (this._has_exit_hooks) {
28317
+ const outcome = abstract_hook_step(this._exit_hooks.get(this._state_id), hook_args);
28318
+ if (!outcome.pass) {
28319
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'exit', fromState, newState, fromAction, oldData, newData, wasForced);
28320
+ return false;
28321
+ }
28322
+ if (_update_hook_fields(hook_args, outcome)) {
28323
+ data_changed = true;
28324
+ }
28313
28325
  }
28314
- }
28315
- // 7. edge type hook
28316
- switch (trans_type) {
28317
- // 7a. standard transition hook
28318
- case 'legal': {
28319
- const outcome = abstract_hook_step(this._standard_transition_hook, hook_args);
28326
+ // shared by steps 5 and 6: pre-commit, this._state_id is still the
28327
+ // from-state, so both probes key on the same pair; compute it once
28328
+ const pre_pair_id = pair_key(this._state_id, newStateId);
28329
+ // 5. named transition / action hook
28330
+ if (this._has_named_hooks && wasAction) {
28331
+ // Numeric pair probe, then the action id captured at dispatch (#729).
28332
+ const byPair = this._named_hooks.get(pre_pair_id);
28333
+ const nh = byPair === undefined ? undefined : byPair.get(actionId);
28334
+ const outcome = abstract_hook_step(nh, hook_args);
28320
28335
  if (!outcome.pass) {
28321
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'standard transition', fromState, newState, fromAction, oldData, newData, wasForced);
28336
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'named', fromState, newState, fromAction, oldData, newData, wasForced);
28322
28337
  return false;
28323
28338
  }
28324
28339
  if (_update_hook_fields(hook_args, outcome)) {
28325
28340
  data_changed = true;
28326
28341
  }
28327
- break;
28328
28342
  }
28329
- // 7b. main type hook
28330
- case 'main': {
28331
- const outcome = abstract_hook_step(this._main_transition_hook, hook_args);
28343
+ // 6. regular hook
28344
+ if (this._has_basic_hooks) {
28345
+ // Numeric pair probe (#729); one integer hash replaces two string maps.
28346
+ const h = this._hooks.get(pre_pair_id);
28347
+ const outcome = abstract_hook_step(h, hook_args);
28332
28348
  if (!outcome.pass) {
28333
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'main transition', fromState, newState, fromAction, oldData, newData, wasForced);
28349
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'hook', fromState, newState, fromAction, oldData, newData, wasForced);
28334
28350
  return false;
28335
28351
  }
28336
28352
  if (_update_hook_fields(hook_args, outcome)) {
28337
28353
  data_changed = true;
28338
28354
  }
28339
- break;
28340
28355
  }
28341
- // 7c. forced transition hook
28342
- case 'forced': {
28343
- const outcome = abstract_hook_step(this._forced_transition_hook, hook_args);
28356
+ // 7. edge type hook
28357
+ switch (trans_type) {
28358
+ // 7a. standard transition hook
28359
+ case 'legal': {
28360
+ const outcome = abstract_hook_step(this._standard_transition_hook, hook_args);
28361
+ if (!outcome.pass) {
28362
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'standard transition', fromState, newState, fromAction, oldData, newData, wasForced);
28363
+ return false;
28364
+ }
28365
+ if (_update_hook_fields(hook_args, outcome)) {
28366
+ data_changed = true;
28367
+ }
28368
+ break;
28369
+ }
28370
+ // 7b. main type hook
28371
+ case 'main': {
28372
+ const outcome = abstract_hook_step(this._main_transition_hook, hook_args);
28373
+ if (!outcome.pass) {
28374
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'main transition', fromState, newState, fromAction, oldData, newData, wasForced);
28375
+ return false;
28376
+ }
28377
+ if (_update_hook_fields(hook_args, outcome)) {
28378
+ data_changed = true;
28379
+ }
28380
+ break;
28381
+ }
28382
+ // 7c. forced transition hook
28383
+ case 'forced': {
28384
+ const outcome = abstract_hook_step(this._forced_transition_hook, hook_args);
28385
+ if (!outcome.pass) {
28386
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'forced transition', fromState, newState, fromAction, oldData, newData, wasForced);
28387
+ return false;
28388
+ }
28389
+ if (_update_hook_fields(hook_args, outcome)) {
28390
+ data_changed = true;
28391
+ }
28392
+ break;
28393
+ }
28394
+ }
28395
+ // 8. entry hook
28396
+ if (this._has_entry_hooks) {
28397
+ const outcome = abstract_hook_step(this._entry_hooks.get(newStateId), hook_args);
28344
28398
  if (!outcome.pass) {
28345
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'forced transition', fromState, newState, fromAction, oldData, newData, wasForced);
28399
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'entry', fromState, newState, fromAction, oldData, newData, wasForced);
28346
28400
  return false;
28347
28401
  }
28348
28402
  if (_update_hook_fields(hook_args, outcome)) {
28349
28403
  data_changed = true;
28350
28404
  }
28351
- break;
28352
28405
  }
28353
- }
28354
- // 8. entry hook
28355
- if (this._has_entry_hooks) {
28356
- const outcome = abstract_hook_step(this._entry_hooks.get(newStateId), hook_args);
28357
- if (!outcome.pass) {
28358
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'entry', fromState, newState, fromAction, oldData, newData, wasForced);
28359
- return false;
28406
+ // 9. everything hook (fires after all other pre-hooks)
28407
+ if (this._everything_hook !== undefined) {
28408
+ const outcome = abstract_everything_hook_step(this._everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'everything' }));
28409
+ if (!outcome.pass) {
28410
+ __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'everything', fromState, newState, fromAction, oldData, newData, wasForced);
28411
+ return false;
28412
+ }
28413
+ if (_update_hook_fields(hook_args, outcome)) {
28414
+ data_changed = true;
28415
+ }
28360
28416
  }
28361
- if (_update_hook_fields(hook_args, outcome)) {
28362
- data_changed = true;
28417
+ // all hooks passed! let's now establish the result
28418
+ // a hook may have redirected the destination via a complex result's
28419
+ // `state` (carried on hook_args.to). Apply it now, validating it names
28420
+ // a real state. Pre-transition hooks (including entry/exit) fired for
28421
+ // the original edge; the committed state and the post-hooks, observation
28422
+ // events, and after-timer all reflect the override. Last writer wins.
28423
+ // StoneCypher/fsl#1947
28424
+ if (hook_args.to !== newState) {
28425
+ const override_id = this._state_interner.id_of(hook_args.to);
28426
+ if (override_id === undefined) {
28427
+ throw new JssmError(this, `A hook overrode the transition destination to '${hook_args.to}', which is not a state in this machine`);
28428
+ }
28429
+ newState = hook_args.to;
28430
+ newStateId = override_id;
28363
28431
  }
28364
- }
28365
- // 9. everything hook (fires after all other pre-hooks)
28366
- if (this._everything_hook !== undefined) {
28367
- const outcome = abstract_everything_hook_step(this._everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'everything' }));
28368
- if (!outcome.pass) {
28369
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'everything', fromState, newState, fromAction, oldData, newData, wasForced);
28370
- return false;
28432
+ if (this._history_length) {
28433
+ this._history.shove([this._state, this._data]);
28371
28434
  }
28372
- if (_update_hook_fields(hook_args, outcome)) {
28373
- data_changed = true;
28435
+ this._state = newState;
28436
+ this._state_id = newStateId;
28437
+ if (data_changed) {
28438
+ this._data = hook_args.next_data;
28374
28439
  }
28375
- }
28376
- // all hooks passed! let's now establish the result
28377
- // a hook may have redirected the destination via a complex result's
28378
- // `state` (carried on hook_args.to). Apply it now, validating it names
28379
- // a real state. Pre-transition hooks (including entry/exit) fired for
28380
- // the original edge; the committed state and the post-hooks, observation
28381
- // events, and after-timer all reflect the override. Last writer wins.
28382
- // StoneCypher/fsl#1947
28383
- if (hook_args.to !== newState) {
28384
- const override_id = this._state_interner.id_of(hook_args.to);
28385
- if (override_id === undefined) {
28386
- throw new JssmError(this, `A hook overrode the transition destination to '${hook_args.to}', which is not a state in this machine`);
28440
+ else if (dataProvided) {
28441
+ this._data = newData;
28387
28442
  }
28388
- newState = hook_args.to;
28389
- newStateId = override_id;
28443
+ // success fallthrough to posthooks; intentionally no return here
28444
+ // look for "posthooks begin here"
28390
28445
  }
28391
- if (this._history_length) {
28392
- this._history.shove([this._state, this._data]);
28446
+ finally {
28447
+ // Close the pre-commit window on EVERY exit from the pipeline: normal
28448
+ // fallthrough after commit, a hook veto's `return false`, the
28449
+ // destination-override throw, or a user hook throwing. Post-hooks and
28450
+ // the boundary-action cascade run after this and may re-enter the
28451
+ // machine coherently from the committed state. #1953
28452
+ this._committing_transition = false;
28393
28453
  }
28394
- this._state = newState;
28395
- this._state_id = newStateId;
28396
- if (data_changed) {
28397
- this._data = hook_args.next_data;
28398
- }
28399
- else if (dataProvided) {
28400
- this._data = newData;
28401
- }
28402
- // success fallthrough to posthooks; intentionally no return here
28403
- // look for "posthooks begin here"
28404
28454
  // or without hooks
28405
28455
  }
28406
28456
  else {
@@ -29695,6 +29745,12 @@ _Machine_states = new WeakMap(), _Machine_edges = new WeakMap(), _Machine_edge_m
29695
29745
  }
29696
29746
  }
29697
29747
  }, _Machine_fire_hook_rejection = function _Machine_fire_hook_rejection(hook_name, fromState, newState, fromAction, oldData, newData, wasForced) {
29748
+ // Every hook veto in transition_impl's pre-commit pipeline exits through
29749
+ // here, so this is the single close point for the reentrancy guard on the
29750
+ // rejection path: clear it before firing the event so a `rejection` listener
29751
+ // may itself transition (the outer transition is abandoned, not reverted).
29752
+ // #1953
29753
+ this._committing_transition = false;
29698
29754
  __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire).call(this, 'rejection', {
29699
29755
  from: fromState,
29700
29756
  to: newState,