jssm 5.162.27 → 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.27";
24457
+ const version = "5.162.28";
24458
24458
 
24459
24459
  /**
24460
24460
  * The FSL Markdown fence convention parser — pure, host-agnostic logic that
@@ -25188,6 +25188,7 @@ class Machine {
25188
25188
  this._event_handlers = new Map();
25189
25189
  this._event_listener_count = 0;
25190
25190
  this._firing_error = false;
25191
+ this._committing_transition = false;
25191
25192
  // Boundary-hook action cascade guard. Limit defaults to 100 but is
25192
25193
  // configurable via the `boundary_depth_limit` constructor option so tests
25193
25194
  // can tighten the cap and deep pipelines can raise it.
@@ -28126,10 +28127,25 @@ class Machine {
28126
28127
  * @returns `true` if the transition was valid and every hook passed;
28127
28128
  * `false` if the transition was invalid or any hook rejected.
28128
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
+ *
28129
28137
  * @internal
28130
28138
  *
28131
28139
  */
28132
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
+ }
28133
28149
  let valid = false,
28134
28150
  // deliberately `string`, not `JssmArrowKind`, though only arrow kinds are
28135
28151
  // ever assigned: declaring this local as the 4-member union makes tsc's
@@ -28238,185 +28254,203 @@ class Machine {
28238
28254
  const oldData = this._data;
28239
28255
  if (valid) {
28240
28256
  if (this._has_hooks) {
28241
- let data_changed = false;
28242
- // 0. pre everything hook (fires before all other pre-hooks)
28243
- if (this._pre_everything_hook !== undefined) {
28244
- const outcome = abstract_everything_hook_step(this._pre_everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'pre everything' }));
28245
- if (!outcome.pass) {
28246
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'pre everything', fromState, newState, fromAction, oldData, newData, wasForced);
28247
- return false;
28248
- }
28249
- if (_update_hook_fields(hook_args, outcome)) {
28250
- data_changed = true;
28251
- }
28252
- }
28253
- if (wasAction) {
28254
- // 1a. any action hook
28255
- const outcome = abstract_hook_step(this._any_action_hook, hook_args);
28256
- if (!outcome.pass) {
28257
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'any action', fromState, newState, fromAction, oldData, newData, wasForced);
28258
- return false;
28259
- }
28260
- if (_update_hook_fields(hook_args, outcome)) {
28261
- data_changed = true;
28262
- }
28263
- // 1b. global specific action hook
28264
- const outcome2 = abstract_hook_step(this._global_action_hooks.get(actionId), hook_args);
28265
- if (!outcome2.pass) {
28266
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'global action', fromState, newState, fromAction, oldData, newData, wasForced);
28267
- return false;
28268
- }
28269
- if (_update_hook_fields(hook_args, outcome2)) {
28270
- data_changed = true;
28271
- }
28272
- }
28273
- // 2. (removed) After hooks do NOT fire on dispatch. They are the
28274
- // `after`-timer's companion (fsl#698: "delay over!") and fire only from
28275
- // the state-timeout path. Through v5.143.28 a probe here keyed on
28276
- // newStateOrAction spuriously fired them on entering the hooked state —
28277
- // or on a same-named action — making one timer elapse read as two
28278
- // handler calls (StoneCypher/fsl#1327).
28279
- // 3. any transition hook
28280
- if (this._any_transition_hook !== undefined) {
28281
- const outcome = abstract_hook_step(this._any_transition_hook, hook_args);
28282
- if (!outcome.pass) {
28283
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'any transition', fromState, newState, fromAction, oldData, newData, wasForced);
28284
- return false;
28285
- }
28286
- if (_update_hook_fields(hook_args, outcome)) {
28287
- data_changed = true;
28288
- }
28289
- }
28290
- // 4. exit hook
28291
- if (this._has_exit_hooks) {
28292
- const outcome = abstract_hook_step(this._exit_hooks.get(this._state_id), hook_args);
28293
- if (!outcome.pass) {
28294
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'exit', fromState, newState, fromAction, oldData, newData, wasForced);
28295
- return false;
28296
- }
28297
- if (_update_hook_fields(hook_args, outcome)) {
28298
- data_changed = true;
28299
- }
28300
- }
28301
- // shared by steps 5 and 6: pre-commit, this._state_id is still the
28302
- // from-state, so both probes key on the same pair; compute it once
28303
- const pre_pair_id = pair_key(this._state_id, newStateId);
28304
- // 5. named transition / action hook
28305
- if (this._has_named_hooks && wasAction) {
28306
- // Numeric pair probe, then the action id captured at dispatch (#729).
28307
- const byPair = this._named_hooks.get(pre_pair_id);
28308
- const nh = byPair === undefined ? undefined : byPair.get(actionId);
28309
- const outcome = abstract_hook_step(nh, hook_args);
28310
- if (!outcome.pass) {
28311
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'named', fromState, newState, fromAction, oldData, newData, wasForced);
28312
- 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
+ }
28313
28277
  }
28314
- if (_update_hook_fields(hook_args, outcome)) {
28315
- 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
+ }
28316
28297
  }
28317
- }
28318
- // 6. regular hook
28319
- if (this._has_basic_hooks) {
28320
- // Numeric pair probe (#729); one integer hash replaces two string maps.
28321
- const h = this._hooks.get(pre_pair_id);
28322
- const outcome = abstract_hook_step(h, hook_args);
28323
- if (!outcome.pass) {
28324
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'hook', fromState, newState, fromAction, oldData, newData, wasForced);
28325
- 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
+ }
28326
28314
  }
28327
- if (_update_hook_fields(hook_args, outcome)) {
28328
- 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
+ }
28329
28325
  }
28330
- }
28331
- // 7. edge type hook
28332
- switch (trans_type) {
28333
- // 7a. standard transition hook
28334
- case 'legal': {
28335
- 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);
28336
28335
  if (!outcome.pass) {
28337
- __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);
28338
28337
  return false;
28339
28338
  }
28340
28339
  if (_update_hook_fields(hook_args, outcome)) {
28341
28340
  data_changed = true;
28342
28341
  }
28343
- break;
28344
28342
  }
28345
- // 7b. main type hook
28346
- case 'main': {
28347
- 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);
28348
28348
  if (!outcome.pass) {
28349
- __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);
28350
28350
  return false;
28351
28351
  }
28352
28352
  if (_update_hook_fields(hook_args, outcome)) {
28353
28353
  data_changed = true;
28354
28354
  }
28355
- break;
28356
28355
  }
28357
- // 7c. forced transition hook
28358
- case 'forced': {
28359
- 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);
28360
28398
  if (!outcome.pass) {
28361
- __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);
28362
28400
  return false;
28363
28401
  }
28364
28402
  if (_update_hook_fields(hook_args, outcome)) {
28365
28403
  data_changed = true;
28366
28404
  }
28367
- break;
28368
28405
  }
28369
- }
28370
- // 8. entry hook
28371
- if (this._has_entry_hooks) {
28372
- const outcome = abstract_hook_step(this._entry_hooks.get(newStateId), hook_args);
28373
- if (!outcome.pass) {
28374
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'entry', fromState, newState, fromAction, oldData, newData, wasForced);
28375
- 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
+ }
28376
28416
  }
28377
- if (_update_hook_fields(hook_args, outcome)) {
28378
- 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;
28379
28431
  }
28380
- }
28381
- // 9. everything hook (fires after all other pre-hooks)
28382
- if (this._everything_hook !== undefined) {
28383
- const outcome = abstract_everything_hook_step(this._everything_hook, Object.assign(Object.assign({}, hook_args), { hook_name: 'everything' }));
28384
- if (!outcome.pass) {
28385
- __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire_hook_rejection).call(this, 'everything', fromState, newState, fromAction, oldData, newData, wasForced);
28386
- return false;
28432
+ if (this._history_length) {
28433
+ this._history.shove([this._state, this._data]);
28387
28434
  }
28388
- if (_update_hook_fields(hook_args, outcome)) {
28389
- data_changed = true;
28435
+ this._state = newState;
28436
+ this._state_id = newStateId;
28437
+ if (data_changed) {
28438
+ this._data = hook_args.next_data;
28390
28439
  }
28391
- }
28392
- // all hooks passed! let's now establish the result
28393
- // a hook may have redirected the destination via a complex result's
28394
- // `state` (carried on hook_args.to). Apply it now, validating it names
28395
- // a real state. Pre-transition hooks (including entry/exit) fired for
28396
- // the original edge; the committed state and the post-hooks, observation
28397
- // events, and after-timer all reflect the override. Last writer wins.
28398
- // StoneCypher/fsl#1947
28399
- if (hook_args.to !== newState) {
28400
- const override_id = this._state_interner.id_of(hook_args.to);
28401
- if (override_id === undefined) {
28402
- 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;
28403
28442
  }
28404
- newState = hook_args.to;
28405
- newStateId = override_id;
28443
+ // success fallthrough to posthooks; intentionally no return here
28444
+ // look for "posthooks begin here"
28406
28445
  }
28407
- if (this._history_length) {
28408
- 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;
28409
28453
  }
28410
- this._state = newState;
28411
- this._state_id = newStateId;
28412
- if (data_changed) {
28413
- this._data = hook_args.next_data;
28414
- }
28415
- else if (dataProvided) {
28416
- this._data = newData;
28417
- }
28418
- // success fallthrough to posthooks; intentionally no return here
28419
- // look for "posthooks begin here"
28420
28454
  // or without hooks
28421
28455
  }
28422
28456
  else {
@@ -29711,6 +29745,12 @@ _Machine_states = new WeakMap(), _Machine_edges = new WeakMap(), _Machine_edge_m
29711
29745
  }
29712
29746
  }
29713
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;
29714
29754
  __classPrivateFieldGet(this, _Machine_instances, "m", _Machine_fire).call(this, 'rejection', {
29715
29755
  from: fromState,
29716
29756
  to: newState,