@vedmalex/statemachine 1.0.0-beta.1 → 1.0.0-beta.3

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/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  StateMachineError: () => StateMachineError,
36
36
  createEnhancedError: () => createEnhancedError,
37
37
  createMachine: () => createMachine,
38
+ createVirtualScheduler: () => createVirtualScheduler,
38
39
  isAdapter: () => isAdapter,
39
40
  isRecoverableError: () => isRecoverableError,
40
41
  isValidConfig: () => isValidConfig,
@@ -53,7 +54,9 @@ var TimerScheduler = class {
53
54
  intervalId = null;
54
55
  pollingInterval = 100;
55
56
  // ms
56
- constructor() {
57
+ clock;
58
+ constructor(clock = Date.now) {
59
+ this.clock = clock;
57
60
  }
58
61
  /**
59
62
  * Настройка режима опроса
@@ -77,7 +80,7 @@ var TimerScheduler = class {
77
80
  */
78
81
  start() {
79
82
  if (this.intervalId) return;
80
- this.intervalId = setInterval(() => this.process(), this.pollingInterval);
83
+ this.intervalId = setInterval(() => this.process(this.clock()), this.pollingInterval);
81
84
  }
82
85
  /**
83
86
  * Остановка единого таймера
@@ -96,7 +99,7 @@ var TimerScheduler = class {
96
99
  */
97
100
  schedule(delay, callback) {
98
101
  const token = {};
99
- const executeAt = Date.now() + delay;
102
+ const executeAt = this.clock() + delay;
100
103
  const task = { token, executeAt, callback };
101
104
  this.activeTokens.add(token);
102
105
  this.insert(task);
@@ -112,7 +115,7 @@ var TimerScheduler = class {
112
115
  /**
113
116
  * Обработать очередь (вызывается таймером или вручную)
114
117
  */
115
- process(now = Date.now()) {
118
+ process(now = this.clock()) {
116
119
  while (this.heap.length > 0) {
117
120
  const task = this.heap[0];
118
121
  if (task === void 0) break;
@@ -203,6 +206,23 @@ var TimerScheduler = class {
203
206
  function createDefaultScheduler() {
204
207
  return new TimerScheduler();
205
208
  }
209
+ function createVirtualScheduler(clock) {
210
+ const inner = new TimerScheduler(clock);
211
+ return {
212
+ isActive() {
213
+ return true;
214
+ },
215
+ schedule(delay, callback) {
216
+ return inner.schedule(delay, callback);
217
+ },
218
+ cancel(token) {
219
+ inner.cancel(token);
220
+ },
221
+ process(now) {
222
+ inner.process(now ?? clock());
223
+ }
224
+ };
225
+ }
206
226
 
207
227
  // src/logger.ts
208
228
  var LogLevel = {
@@ -1749,6 +1769,8 @@ var StateMachine = class _StateMachine {
1749
1769
  monitor;
1750
1770
  scheduler;
1751
1771
  errorHandler;
1772
+ clock;
1773
+ schedulerProvided;
1752
1774
  // Свойства
1753
1775
  states;
1754
1776
  events;
@@ -1799,7 +1821,9 @@ var StateMachine = class _StateMachine {
1799
1821
  this.initialState = config.initialState;
1800
1822
  this.logger = options?.logger ?? ConsoleLogger;
1801
1823
  this.monitor = options?.monitor ?? createDefaultMonitor();
1824
+ this.schedulerProvided = options?.scheduler !== void 0;
1802
1825
  this.scheduler = options?.scheduler ?? createDefaultScheduler();
1826
+ this.clock = options?.clock ?? Date.now;
1803
1827
  this.errorHandler = options?.errorHandler ?? createDefaultErrorHandler();
1804
1828
  if (adaptee) {
1805
1829
  if (!isAdapter(adaptee)) {
@@ -1850,6 +1874,9 @@ var StateMachine = class _StateMachine {
1850
1874
  setContext(context) {
1851
1875
  this.context = context;
1852
1876
  }
1877
+ resolveCallbackOwner(value) {
1878
+ return isAdapter(value) ? value.adaptee : value;
1879
+ }
1853
1880
  enqueueEvent(eventName, obj, args, type) {
1854
1881
  if (this.externalQueue.length + this.internalQueue.length >= this.maxQueueDepth) {
1855
1882
  const _s0 = this.getCurrentState(obj);
@@ -1870,7 +1897,7 @@ var StateMachine = class _StateMachine {
1870
1897
  args,
1871
1898
  resolve,
1872
1899
  reject,
1873
- timestamp: Date.now(),
1900
+ timestamp: this.clock(),
1874
1901
  type: "external"
1875
1902
  });
1876
1903
  this.scheduleProcessing();
@@ -1881,7 +1908,7 @@ var StateMachine = class _StateMachine {
1881
1908
  eventName,
1882
1909
  obj,
1883
1910
  args,
1884
- timestamp: Date.now(),
1911
+ timestamp: this.clock(),
1885
1912
  type: "internal"
1886
1913
  });
1887
1914
  return Promise.resolve(true);
@@ -1892,7 +1919,7 @@ var StateMachine = class _StateMachine {
1892
1919
  eventName,
1893
1920
  obj,
1894
1921
  args,
1895
- timestamp: Date.now(),
1922
+ timestamp: this.clock(),
1896
1923
  type: "internal"
1897
1924
  });
1898
1925
  }
@@ -1958,7 +1985,8 @@ var StateMachine = class _StateMachine {
1958
1985
  let transitions = event ? event.transitions.filter(
1959
1986
  (t) => this.isTransitionPossible(t, currentState)
1960
1987
  ) : [];
1961
- if (!transitions.length) {
1988
+ const isEngineDoneEvent = String(eventName).startsWith("done.state.");
1989
+ if (!transitions.length && !isEngineDoneEvent) {
1962
1990
  const wildcardEvent = this.events.get("*");
1963
1991
  if (wildcardEvent) {
1964
1992
  const wildcardTransitions = wildcardEvent.transitions.filter(
@@ -2052,7 +2080,7 @@ var StateMachine = class _StateMachine {
2052
2080
  };
2053
2081
  }
2054
2082
  getQueuedEvents() {
2055
- const now = Date.now();
2083
+ const now = this.clock();
2056
2084
  const mapEvent = (evt) => ({
2057
2085
  id: evt.id,
2058
2086
  event: evt.eventName,
@@ -2163,57 +2191,59 @@ var StateMachine = class _StateMachine {
2163
2191
  if (!currentState) return expectedState === "";
2164
2192
  const currentParts = currentState.split("|").sort();
2165
2193
  const expectedParts = expectedState.split("|").sort();
2194
+ const ancestorMatch = expectedParts.every(
2195
+ (expectedPart) => currentParts.some((leaf) => this.isParentState(expectedPart, leaf))
2196
+ );
2197
+ if (ancestorMatch) return true;
2166
2198
  if (currentParts.length !== expectedParts.length) return false;
2167
2199
  return currentParts.every((part, index) => part === expectedParts[index]);
2168
2200
  }
2169
2201
  attachToObject(object, eventMap) {
2170
- for (const objectEventName in eventMap) {
2171
- if (eventMap.hasOwnProperty(objectEventName)) {
2172
- const stateMachineEventName = eventMap[objectEventName];
2173
- if (stateMachineEventName === void 0) continue;
2174
- if (typeof object.addEventListener === "function") {
2175
- object.addEventListener(objectEventName, (...args) => {
2176
- this.fireEvent(stateMachineEventName, object, ...args).catch(
2177
- (e) => this.logger.error(
2178
- "Error firing event",
2179
- {
2180
- objectEventName,
2181
- stateMachineEventName
2182
- },
2183
- /* c8 ignore next */
2184
- e instanceof Error ? e : new Error(String(e))
2185
- )
2186
- );
2187
- });
2188
- } else if (typeof object.on === "function") {
2189
- object.on(objectEventName, (...args) => {
2190
- this.fireEvent(stateMachineEventName, object, ...args).catch(
2191
- (e) => this.logger.error(
2192
- "Error firing event",
2193
- {
2194
- objectEventName,
2195
- stateMachineEventName
2196
- },
2197
- /* c8 ignore next */
2198
- e instanceof Error ? e : new Error(String(e))
2199
- )
2200
- );
2201
- });
2202
- } else {
2203
- object[`on${objectEventName}`] = async (...args) => {
2204
- return this.fireEvent(stateMachineEventName, object, ...args).catch(
2205
- (e) => this.logger.error(
2206
- "Error firing event",
2207
- {
2208
- objectEventName,
2209
- stateMachineEventName
2210
- },
2211
- /* c8 ignore next */
2212
- e instanceof Error ? e : new Error(String(e))
2213
- )
2214
- );
2215
- };
2216
- }
2202
+ for (const objectEventName of Object.keys(eventMap)) {
2203
+ const stateMachineEventName = eventMap[objectEventName];
2204
+ if (stateMachineEventName === void 0) continue;
2205
+ if (typeof object.addEventListener === "function") {
2206
+ object.addEventListener(objectEventName, (...args) => {
2207
+ this.fireEvent(stateMachineEventName, object, ...args).catch(
2208
+ (e) => this.logger.error(
2209
+ "Error firing event",
2210
+ {
2211
+ objectEventName,
2212
+ stateMachineEventName
2213
+ },
2214
+ /* c8 ignore next */
2215
+ e instanceof Error ? e : new Error(String(e))
2216
+ )
2217
+ );
2218
+ });
2219
+ } else if (typeof object.on === "function") {
2220
+ object.on(objectEventName, (...args) => {
2221
+ this.fireEvent(stateMachineEventName, object, ...args).catch(
2222
+ (e) => this.logger.error(
2223
+ "Error firing event",
2224
+ {
2225
+ objectEventName,
2226
+ stateMachineEventName
2227
+ },
2228
+ /* c8 ignore next */
2229
+ e instanceof Error ? e : new Error(String(e))
2230
+ )
2231
+ );
2232
+ });
2233
+ } else {
2234
+ object[`on${objectEventName}`] = async (...args) => {
2235
+ return this.fireEvent(stateMachineEventName, object, ...args).catch(
2236
+ (e) => this.logger.error(
2237
+ "Error firing event",
2238
+ {
2239
+ objectEventName,
2240
+ stateMachineEventName
2241
+ },
2242
+ /* c8 ignore next */
2243
+ e instanceof Error ? e : new Error(String(e))
2244
+ )
2245
+ );
2246
+ };
2217
2247
  }
2218
2248
  }
2219
2249
  }
@@ -2541,7 +2571,7 @@ var StateMachine = class _StateMachine {
2541
2571
  const currentState = this.getCurrentState(adaptee) || "";
2542
2572
  const currentStateMap = this.parseCompositeState(currentState);
2543
2573
  const newStateParts = state.split("|");
2544
- const isRootState = newStateParts.length === 1 && !state.includes(".");
2574
+ const isRootState = newStateParts.length === 1 && !state.includes(".") && !this.states.get(state)?.regions;
2545
2575
  if (isRootState) {
2546
2576
  currentStateMap.clear();
2547
2577
  currentStateMap.set(state, state);
@@ -2617,12 +2647,13 @@ var StateMachine = class _StateMachine {
2617
2647
  initialStates = initialState;
2618
2648
  }
2619
2649
  this.setCurrentState(initialStates, targetAdaptee);
2620
- const stateParts = initialStates.split("|");
2650
+ const { enterStates } = this.computeEnterExitSets("", initialStates);
2651
+ const enterFireOrder = enterStates.length > 0 ? enterStates : [initialStates];
2621
2652
  const context = {
2622
2653
  state: initialStates,
2623
2654
  phase: "enter"
2624
2655
  };
2625
- for (const statePart of stateParts) {
2656
+ for (const statePart of enterFireOrder) {
2626
2657
  this.executeEnterActions(
2627
2658
  targetAdaptee,
2628
2659
  statePart,
@@ -2636,6 +2667,11 @@ var StateMachine = class _StateMachine {
2636
2667
  );
2637
2668
  });
2638
2669
  }
2670
+ this.checkCompletion(
2671
+ targetAdaptee,
2672
+ "",
2673
+ initialStates
2674
+ );
2639
2675
  }
2640
2676
  getInitialCompositeState(initialState) {
2641
2677
  const stateConfig = this.states.get(initialState);
@@ -2675,6 +2711,211 @@ var StateMachine = class _StateMachine {
2675
2711
  }
2676
2712
  return regionStates.join("|");
2677
2713
  }
2714
+ /**
2715
+ * Whether `leaf` is a UML/SCXML `<final>` atomic state of its region.
2716
+ *
2717
+ * Reads the `final` marker straight from the flattened state map populated by
2718
+ * processStates, so it works for any registered atomic leaf regardless of
2719
+ * nesting depth. Returns `false` for unregistered names and for composites
2720
+ * (the all-regions-final join derives doneness from atomic leaves, not from a
2721
+ * `final` flag on a composite parent).
2722
+ */
2723
+ isStateFinal(leaf) {
2724
+ return Boolean(this.states.get(leaf)?.final);
2725
+ }
2726
+ /**
2727
+ * Whether composite `compositeId` has reached its UML/SCXML "done"
2728
+ * configuration: every one of its regions has its active atomic leaf in a
2729
+ * `final` state (recursively, for nested composites).
2730
+ *
2731
+ * D10 (mustFix): doneness is derived by scanning the active atomic `|`-leaves
2732
+ * against the STATIC regions tree (`this.states.get(C)?.regions`), NEVER via a
2733
+ * region-key Map lookup (`configMap.get`) — that map keys leaves by their
2734
+ * deepest region container and so cannot answer "which leaf is active in
2735
+ * region X of composite C". For each region we locate the active leaf via the
2736
+ * `C.region.` dotted prefix; the region is final iff that leaf is `final`, or
2737
+ * the leaf lives under a nested composite that is itself `isCompositeDone`.
2738
+ *
2739
+ * Returns `false` for a non-composite id, for a composite with no active leaf
2740
+ * in some region, or when no substate under it is ever `final` (cheap miss).
2741
+ */
2742
+ isCompositeDone(compositeId, atomicLeaves) {
2743
+ const regions = this.states.get(compositeId)?.regions;
2744
+ if (!regions) return false;
2745
+ for (const regionName of Object.keys(regions)) {
2746
+ const regionPrefix = `${compositeId}.${regionName}.`;
2747
+ const activeLeaf = atomicLeaves.find(
2748
+ (leaf) => leaf.startsWith(regionPrefix)
2749
+ );
2750
+ if (!activeLeaf) return false;
2751
+ const nestedComposite = this.regionComposite(activeLeaf, regionPrefix);
2752
+ if (nestedComposite) {
2753
+ if (this.isCompositeDone(nestedComposite, atomicLeaves)) continue;
2754
+ return false;
2755
+ }
2756
+ if (this.isStateFinal(activeLeaf)) continue;
2757
+ return false;
2758
+ }
2759
+ return true;
2760
+ }
2761
+ /**
2762
+ * The OUTERMOST registered composite (regions-bearing) ancestor of `leaf`
2763
+ * that lives strictly under `regionPrefix`, or `undefined` if the region
2764
+ * holds a simple atomic state directly. Used by {@link isCompositeDone} to
2765
+ * delegate a region's completeness to its nested composite (recursing over
2766
+ * every parallel branch), independent of whether any single branch leaf
2767
+ * happens to carry the `final` flag.
2768
+ *
2769
+ * ancestorChain is root-to-leaf, so the FIRST matching ancestor is the
2770
+ * region's direct composite child (e.g. `C.p.D` for leaf `C.p.D.s.s2` under
2771
+ * region prefix `C.p.`); isCompositeDone then recurses into its regions.
2772
+ */
2773
+ regionComposite(leaf, regionPrefix) {
2774
+ for (const ancestor of this.ancestorChain(leaf)) {
2775
+ if (ancestor !== leaf && ancestor.startsWith(regionPrefix) && this.states.get(ancestor)?.regions) {
2776
+ return ancestor;
2777
+ }
2778
+ }
2779
+ return void 0;
2780
+ }
2781
+ /**
2782
+ * Whether composite `compositeId` has reached its all-regions-final ("done")
2783
+ * configuration in the CURRENT active state. Public guard surface (`@stable`)
2784
+ * for authoring a join as `guard: () => sm.isDone('C')` instead of (or
2785
+ * alongside) listening on the engine `done.state.<C>` event.
2786
+ *
2787
+ * @param compositeId - The dotted id of the composite/parallel state.
2788
+ * @returns `true` iff every region's active atomic leaf is `final`.
2789
+ */
2790
+ isDone(compositeId, adaptee) {
2791
+ const currentState = this.getCurrentState(adaptee);
2792
+ if (!currentState) return false;
2793
+ const atomicLeaves = currentState.split("|").filter(Boolean);
2794
+ return this.isCompositeDone(compositeId, atomicLeaves);
2795
+ }
2796
+ /**
2797
+ * SCXML completion hook (D10/D11/D12): after a new configuration is written,
2798
+ * raise `done.state.<C>` for each composite that became all-regions-final.
2799
+ *
2800
+ * - Scans only composites that GAINED an active leaf (the ancestor chain of
2801
+ * each `|`-leaf of `newState`), so unaffected composites are not re-checked.
2802
+ * - Emits INNERMOST-first (a deeper composite's `done.state` precedes its
2803
+ * parent's), matching SCXML's inner-before-outer completion ordering, via a
2804
+ * per-config emitted-id Set so each id is raised at most once per call.
2805
+ * - Gates each emission on `this.events.has('done.state.'+C)` (D11 mustFix):
2806
+ * raising an undeclared event would hit the `Invalid event` throw
2807
+ * (executeQueuedTransition) as an unhandled microtask rejection. No declared
2808
+ * `done.state.<C>` event => no emission, no crash, no observable effect.
2809
+ * - Uses the internal queue (`raiseEvent`) + `scheduleProcessing` so the
2810
+ * completion event is processed before subsequent external events.
2811
+ */
2812
+ checkCompletion(obj, oldState, newState) {
2813
+ const atomicLeaves = newState.split("|").filter(Boolean);
2814
+ if (atomicLeaves.length === 0) return;
2815
+ const oldLeaves = oldState.split("|").filter(Boolean);
2816
+ const seen = /* @__PURE__ */ new Set();
2817
+ for (const leaf of atomicLeaves) {
2818
+ for (const ancestor of this.ancestorChain(leaf)) {
2819
+ if (ancestor === leaf) continue;
2820
+ if (seen.has(ancestor)) continue;
2821
+ if (!this.states.get(ancestor)?.regions) continue;
2822
+ seen.add(ancestor);
2823
+ }
2824
+ }
2825
+ const candidates = Array.from(seen).sort(
2826
+ (a, b) => b.split(".").length - a.split(".").length
2827
+ );
2828
+ const emitted = /* @__PURE__ */ new Set();
2829
+ for (const compositeId of candidates) {
2830
+ if (emitted.has(compositeId)) continue;
2831
+ if (!this.isCompositeDone(compositeId, atomicLeaves)) continue;
2832
+ if (this.isCompositeDone(compositeId, oldLeaves)) continue;
2833
+ emitted.add(compositeId);
2834
+ const doneEvent = `done.state.${compositeId}`;
2835
+ if (!this.events.has(doneEvent)) continue;
2836
+ this.raiseEvent(doneEvent, obj);
2837
+ this.scheduleProcessing();
2838
+ }
2839
+ }
2840
+ /**
2841
+ * Build the registered ancestor chain for an atomic leaf, ordered root-to-leaf.
2842
+ *
2843
+ * Walks every dot-prefix of `leaf` and keeps only the ones that are real
2844
+ * registered states (`this.states.has`). Region containers are never
2845
+ * registered (only composite parents and atomic leaves are — see
2846
+ * processStates/processRegions), so they are filtered out automatically. The
2847
+ * result is exactly `[parent..leaf]` for a leaf inside a composite, and
2848
+ * `[leaf]` for a flat state.
2849
+ *
2850
+ * Example: `ancestorChain('a.r1.c1')` -> `['a', 'a.r1.c1']` (the `a.r1`
2851
+ * region container is excluded). Nested:
2852
+ * `ancestorChain('a.r1.c1.r3.x')` -> `['a', 'a.r1.c1', 'a.r1.c1.r3.x']`.
2853
+ */
2854
+ ancestorChain(leaf) {
2855
+ const chain = [];
2856
+ let dotIndex = leaf.indexOf(".");
2857
+ while (dotIndex !== -1) {
2858
+ const prefix = leaf.substring(0, dotIndex);
2859
+ if (this.states.has(prefix)) {
2860
+ chain.push(prefix);
2861
+ }
2862
+ dotIndex = leaf.indexOf(".", dotIndex + 1);
2863
+ }
2864
+ if (this.states.has(leaf)) {
2865
+ chain.push(leaf);
2866
+ }
2867
+ return chain;
2868
+ }
2869
+ /**
2870
+ * Compute the ordered enter/exit sets between two composite configurations
2871
+ * (SCXML ancestor-first entry / descendant-first exit).
2872
+ *
2873
+ * For each `|`-separated atomic leaf in `oldComposite` and `newComposite` the
2874
+ * union of its {@link ancestorChain} forms the old/new active ancestry. A
2875
+ * state shared by both ancestries lands in NEITHER diff, so a surviving
2876
+ * ancestor is never re-entered nor exited (no onEnter/onExit re-fire, no
2877
+ * timer re-arm/leak).
2878
+ *
2879
+ * - `enterStates` = new ancestry MINUS old, sorted ascending by depth then
2880
+ * document (insertion) order -> root-to-leaf entry.
2881
+ * - `exitStates` = old ancestry MINUS new, sorted descending by depth ->
2882
+ * leaf-to-root exit.
2883
+ *
2884
+ * Consumed by BOTH R1 (applyTransition / setInitialState / reset) and R2.
2885
+ */
2886
+ computeEnterExitSets(oldComposite, newComposite) {
2887
+ const collectAncestry = (composite) => {
2888
+ const ancestry = /* @__PURE__ */ new Set();
2889
+ if (!composite) return ancestry;
2890
+ for (const leaf of composite.split("|")) {
2891
+ if (!leaf) continue;
2892
+ for (const ancestor of this.ancestorChain(leaf)) {
2893
+ ancestry.add(ancestor);
2894
+ }
2895
+ }
2896
+ return ancestry;
2897
+ };
2898
+ const oldAncestry = collectAncestry(oldComposite);
2899
+ const newAncestry = collectAncestry(newComposite);
2900
+ const depthOf = (state) => {
2901
+ let depth = 0;
2902
+ for (let i = 0; i < state.length; i++) {
2903
+ if (state[i] === ".") depth++;
2904
+ }
2905
+ return depth;
2906
+ };
2907
+ const enterRaw = [];
2908
+ for (const state of newAncestry) {
2909
+ if (!oldAncestry.has(state)) enterRaw.push(state);
2910
+ }
2911
+ const exitRaw = [];
2912
+ for (const state of oldAncestry) {
2913
+ if (!newAncestry.has(state)) exitRaw.push(state);
2914
+ }
2915
+ const enterStates = enterRaw.map((state, index) => ({ state, index, depth: depthOf(state) })).sort((a, b) => a.depth - b.depth || a.index - b.index).map((entry) => entry.state);
2916
+ const exitStates = exitRaw.map((state, index) => ({ state, index, depth: depthOf(state) })).sort((a, b) => b.depth - a.depth || a.index - b.index).map((entry) => entry.state);
2917
+ return { enterStates, exitStates };
2918
+ }
2678
2919
  validateCompositeState(compositeState) {
2679
2920
  const stateParts = compositeState.split("|");
2680
2921
  const regionKeys = /* @__PURE__ */ new Set();
@@ -2748,10 +2989,11 @@ var StateMachine = class _StateMachine {
2748
2989
  return (...args) => {
2749
2990
  const targetAdaptee = args.length >= 2 ? args[0] : adaptee;
2750
2991
  const error = args.length >= 2 ? args[1] : args[0];
2751
- return handler(targetAdaptee, error);
2992
+ return handler(this.resolveCallbackOwner(targetAdaptee), error);
2752
2993
  };
2753
2994
  }
2754
2995
  async callAction(obj, actionName, ...args) {
2996
+ const targetOwner = this.resolveCallbackOwner(obj);
2755
2997
  const _callActionState = this.getCurrentState(
2756
2998
  obj
2757
2999
  );
@@ -2766,16 +3008,16 @@ var StateMachine = class _StateMachine {
2766
3008
  if (this.context && this.context[actionName]) {
2767
3009
  const action = this.context[actionName];
2768
3010
  if (typeof action === "function") {
2769
- const result = action(this.context, ...args);
3011
+ const result = action(targetOwner, ...args);
2770
3012
  return result instanceof Promise ? await result : result;
2771
3013
  }
2772
3014
  } else if (typeof actionName === "function") {
2773
- const result = actionName(obj, ...args);
3015
+ const result = actionName(targetOwner, ...args);
2774
3016
  return result instanceof Promise ? await result : result;
2775
3017
  } else if (obj.get(actionName)) {
2776
3018
  const action = obj.get(actionName);
2777
3019
  if (typeof action === "function") {
2778
- const result = action(obj, ...args);
3020
+ const result = action(targetOwner, ...args);
2779
3021
  return result instanceof Promise ? await result : result;
2780
3022
  }
2781
3023
  }
@@ -2791,19 +3033,22 @@ var StateMachine = class _StateMachine {
2791
3033
  }
2792
3034
  };
2793
3035
  if (this.transitionTimeout && this.transitionTimeout > 0) {
2794
- return Promise.race([
2795
- executeAction(),
2796
- new Promise(
2797
- (_, reject) => setTimeout(
2798
- () => reject(new StateMachineError("Transition timeout", {
2799
- /* c8 ignore next */
2800
- action: typeof actionName === "string" ? actionName : "anonymous",
2801
- phase: "action"
2802
- })),
2803
- this.transitionTimeout
2804
- )
2805
- )
2806
- ]);
3036
+ const timeoutMs = this.transitionTimeout;
3037
+ let timeoutHandle;
3038
+ const timeoutPromise = new Promise((_, reject) => {
3039
+ const fire = () => reject(new StateMachineError("Transition timeout", {
3040
+ /* c8 ignore next */
3041
+ action: typeof actionName === "string" ? actionName : "anonymous",
3042
+ phase: "action"
3043
+ }));
3044
+ timeoutHandle = this.setTimer(fire, timeoutMs);
3045
+ });
3046
+ if (this.schedulerProvided) {
3047
+ return Promise.race([executeAction(), timeoutPromise]).finally(() => {
3048
+ this.clearTimer(timeoutHandle);
3049
+ });
3050
+ }
3051
+ return Promise.race([executeAction(), timeoutPromise]);
2807
3052
  }
2808
3053
  return executeAction();
2809
3054
  }
@@ -2847,8 +3092,12 @@ var StateMachine = class _StateMachine {
2847
3092
  if (fromState === "*") return true;
2848
3093
  const regionKey = this.getRegionKey(fromState);
2849
3094
  const currentStateForRegion = currentStates.get(regionKey);
2850
- if (!currentStateForRegion) return false;
2851
- return currentStateForRegion === fromState || this.isParentState(currentStateForRegion, fromState);
3095
+ if (currentStateForRegion && (currentStateForRegion === fromState || this.isParentState(currentStateForRegion, fromState))) {
3096
+ return true;
3097
+ }
3098
+ return Array.from(currentStates.values()).some(
3099
+ (leaf) => leaf === fromState || this.isParentState(fromState, leaf)
3100
+ );
2852
3101
  });
2853
3102
  }
2854
3103
  isParentState(parentState, childState) {
@@ -2864,6 +3113,26 @@ var StateMachine = class _StateMachine {
2864
3113
  const targetState = transition.to === "*" ? currentState : transition.to;
2865
3114
  this._isTransitioning = true;
2866
3115
  this._targetState = targetState;
3116
+ let newState;
3117
+ let enterStates;
3118
+ let exitStates;
3119
+ try {
3120
+ newState = this.updateState(currentState, targetState);
3121
+ const sets = this.computeEnterExitSets(currentState, newState);
3122
+ enterStates = sets.enterStates;
3123
+ exitStates = sets.exitStates;
3124
+ } catch (error) {
3125
+ this.logger.warn("Transition aborted: invalid target configuration", {
3126
+ state: currentState,
3127
+ target: String(targetState),
3128
+ error
3129
+ });
3130
+ this._isTransitioning = false;
3131
+ this._targetState = void 0;
3132
+ return void 0;
3133
+ }
3134
+ const exitFireOrder = exitStates.length > 0 ? exitStates : [transition.from];
3135
+ const enterFireOrder = enterStates.length > 0 ? enterStates : [transition.to];
2867
3136
  try {
2868
3137
  if (transition.guard) {
2869
3138
  const allow = await this.callAction(obj, transition.guard, ...args).catch(
@@ -2889,7 +3158,9 @@ var StateMachine = class _StateMachine {
2889
3158
  );
2890
3159
  }
2891
3160
  try {
2892
- await this.executeExitActions(obj, transition.from, args, context);
3161
+ for (const exitStateName of exitFireOrder) {
3162
+ await this.executeExitActions(obj, exitStateName, args, context);
3163
+ }
2893
3164
  } catch (error) {
2894
3165
  if (this.abortOnExitError) {
2895
3166
  this.logger.warn("Transition aborted due to onExit error", { state: currentState, error });
@@ -2909,12 +3180,14 @@ var StateMachine = class _StateMachine {
2909
3180
  );
2910
3181
  }
2911
3182
  try {
2912
- await this.executeEnterActions(obj, transition.to, args, context);
3183
+ for (const enterStateName of enterFireOrder) {
3184
+ await this.executeEnterActions(obj, enterStateName, args, context);
3185
+ }
2913
3186
  } catch (error) {
2914
3187
  if (this.errorState) {
2915
3188
  this.logger.error(`Failed to enter state '${targetState}'. Fallback to error state '${this.errorState}'`, { error });
2916
- const newState2 = this.updateState(currentState, this.errorState);
2917
- this.setCurrentState(newState2, obj);
3189
+ const errorNewState = this.updateState(currentState, this.errorState);
3190
+ this.setCurrentState(errorNewState, obj);
2918
3191
  return this.states.get(this.errorState);
2919
3192
  }
2920
3193
  throw error;
@@ -2929,12 +3202,12 @@ var StateMachine = class _StateMachine {
2929
3202
  event.onError,
2930
3203
  this.onError
2931
3204
  );
2932
- errorHandler(obj, error);
3205
+ errorHandler(this.resolveCallbackOwner(obj), error);
2933
3206
  }
2934
3207
  }
2935
3208
  const transitionStartTime = Date.now();
2936
- const newState = this.updateState(currentState, targetState);
2937
3209
  this.setCurrentState(newState, obj);
3210
+ this.checkCompletion(obj, currentState, newState);
2938
3211
  const transitionTime = Date.now() - transitionStartTime;
2939
3212
  this.monitor.recordTransition(transitionTime, true);
2940
3213
  return this.states.get(targetState);
@@ -2992,7 +3265,7 @@ var StateMachine = class _StateMachine {
2992
3265
  }
2993
3266
  if (toState.invoke && toState.invoke.length > 0) {
2994
3267
  if (!this.stateEntryTimes.has(toStateName)) {
2995
- this.stateEntryTimes.set(toStateName, Date.now());
3268
+ this.stateEntryTimes.set(toStateName, this.clock());
2996
3269
  }
2997
3270
  const timers = [];
2998
3271
  for (const invocation of toState.invoke) {
@@ -3038,6 +3311,9 @@ var StateMachine = class _StateMachine {
3038
3311
  */
3039
3312
  setTimer(callback, delay) {
3040
3313
  const scheduler = this.scheduler;
3314
+ if (this.schedulerProvided) {
3315
+ return scheduler.schedule(delay, callback);
3316
+ }
3041
3317
  if (scheduler.isActive()) {
3042
3318
  return scheduler.schedule(delay, callback);
3043
3319
  }
@@ -3048,6 +3324,10 @@ var StateMachine = class _StateMachine {
3048
3324
  */
3049
3325
  clearTimer(timerId) {
3050
3326
  const scheduler = this.scheduler;
3327
+ if (this.schedulerProvided) {
3328
+ if (timerId !== void 0) scheduler.cancel(timerId);
3329
+ return;
3330
+ }
3051
3331
  if (scheduler.isActive() && typeof timerId === "object" && timerId !== null && !("ref" in timerId)) {
3052
3332
  scheduler.cancel(timerId);
3053
3333
  } else {
@@ -3110,7 +3390,7 @@ var StateMachine = class _StateMachine {
3110
3390
  updateState(currentState, toState) {
3111
3391
  const currentStateMap = this.parseCompositeState(currentState);
3112
3392
  const toStateParts = toState.split("|");
3113
- if (toStateParts.length === 1 && !toState.includes(".")) {
3393
+ if (toStateParts.length === 1 && !toState.includes(".") && !this.states.get(toState)?.regions) {
3114
3394
  currentStateMap.clear();
3115
3395
  currentStateMap.set(toState, toState);
3116
3396
  return toState;
@@ -3205,13 +3485,13 @@ var StateMachine = class _StateMachine {
3205
3485
  this.stateEntryTimes.delete(stateName);
3206
3486
  }
3207
3487
  }
3208
- const now = Date.now();
3488
+ const now = this.clock();
3209
3489
  for (const stateName of activeStates) {
3210
3490
  const state = this.states.get(stateName);
3211
3491
  if (!state || !state.invoke || state.invoke.length === 0) continue;
3212
3492
  const entryTime = this.stateEntryTimes.get(stateName);
3213
- const startTime = entryTime || now;
3214
- if (!entryTime) {
3493
+ const startTime = entryTime ?? now;
3494
+ if (entryTime === void 0) {
3215
3495
  this.stateEntryTimes.set(stateName, startTime);
3216
3496
  }
3217
3497
  const elapsed = now - startTime;
@@ -3492,6 +3772,7 @@ var ConfigValidator = class {
3492
3772
  this.validateInitialState(smConfig);
3493
3773
  this.validateCrossReferences(smConfig);
3494
3774
  this.validatePerformanceConstraints(smConfig);
3775
+ this.validateFinalStates(smConfig);
3495
3776
  this.runCustomRules(smConfig);
3496
3777
  } catch (error) {
3497
3778
  const msg = error instanceof Error ? error.message : String(error);
@@ -3853,6 +4134,10 @@ var ConfigValidator = class {
3853
4134
  }
3854
4135
  }
3855
4136
  }
4137
+ const finalStatePaths = this.collectFinalStatePaths(smConfig.states);
4138
+ for (const finalPath of finalStatePaths) {
4139
+ reachableStates.add(finalPath);
4140
+ }
3856
4141
  for (const stateName of allStateNames) {
3857
4142
  if (!reachableStates.has(stateName)) {
3858
4143
  this.addWarning(
@@ -3863,8 +4148,9 @@ var ConfigValidator = class {
3863
4148
  }
3864
4149
  }
3865
4150
  for (const [eventName, event] of Object.entries(smConfig.events)) {
4151
+ const isDoneStateEvent = eventName.startsWith("done.state.");
3866
4152
  const hasValidTransitions = event.transitions.some(
3867
- (t) => allStateNames.has(t.from) && allStateNames.has(t.to)
4153
+ (t) => (allStateNames.has(t.from) || isDoneStateEvent && allStateNames.has(eventName.slice("done.state.".length))) && allStateNames.has(t.to)
3868
4154
  );
3869
4155
  if (!hasValidTransitions) {
3870
4156
  this.addWarning(
@@ -3892,6 +4178,168 @@ var ConfigValidator = class {
3892
4178
  }
3893
4179
  }
3894
4180
  }
4181
+ /**
4182
+ * Collects every state in the tree as {dottedPath, config} entries, walking
4183
+ * region containers (region names are NOT registered states, so they are part
4184
+ * of the path but never emitted as an entry — mirroring runtime expansion).
4185
+ */
4186
+ collectStateEntries(states, prefix, collector) {
4187
+ for (const [stateName, state] of Object.entries(states)) {
4188
+ const fullName = prefix ? `${prefix}.${stateName}` : stateName;
4189
+ collector.push({ path: fullName, state });
4190
+ if (state.regions) {
4191
+ for (const [regionName, regionStates] of Object.entries(
4192
+ state.regions
4193
+ )) {
4194
+ this.collectStateEntries(
4195
+ regionStates,
4196
+ `${fullName}.${regionName}`,
4197
+ collector
4198
+ );
4199
+ }
4200
+ }
4201
+ }
4202
+ }
4203
+ /**
4204
+ * True when `composite` has at least one `final:true` atomic substate reachable
4205
+ * through its (possibly nested) region tree — i.e. some region of the composite
4206
+ * can become done. Used by REGION_NO_REACHABLE_FINAL.
4207
+ */
4208
+ hasReachableFinal(composite) {
4209
+ if (!composite.regions) return false;
4210
+ for (const regionStates of Object.values(composite.regions)) {
4211
+ for (const state of Object.values(regionStates)) {
4212
+ if (state.final) return true;
4213
+ if (state.regions && this.hasReachableFinal(state)) return true;
4214
+ }
4215
+ }
4216
+ return false;
4217
+ }
4218
+ /**
4219
+ * SCXML/UML final-state and all-regions-final (done.state.<C>) join validation.
4220
+ *
4221
+ * - FINAL_STATE_HAS_OUTGOING (error): a `final:true` state is the `from` of a
4222
+ * transition (a final pseudo-state cannot itself transition out).
4223
+ * - FINAL_ON_COMPOSITE (warning): `final:true` on a state that has regions
4224
+ * (final is meaningful only on an atomic leaf; ignored at runtime).
4225
+ * - REGION_NO_REACHABLE_FINAL (warning): a `done.state.<C>` transition exists
4226
+ * but composite C declares no `final` substate, so the join can never fire.
4227
+ * - DONE_VS_PARALLEL_EXIT_AMBIGUITY (warning): the same composite C has BOTH a
4228
+ * `done.state.C` join AND a plain `from:'C'` user-event parallel-exit, which
4229
+ * can preempt the all-final join (the user event is eligible on ANY leaf).
4230
+ * - REGION_MISSING_INITIAL (advisory warning, valid:true): a region omits an
4231
+ * explicit `initial`, so expansion relies on first-key insertion order.
4232
+ */
4233
+ validateFinalStates(smConfig) {
4234
+ const entries = [];
4235
+ this.collectStateEntries(smConfig.states, "", entries);
4236
+ const finalStatePaths = /* @__PURE__ */ new Set();
4237
+ for (const { path, state } of entries) {
4238
+ if (!state.final) continue;
4239
+ if (state.regions) {
4240
+ this.addWarning(
4241
+ "FINAL_ON_COMPOSITE",
4242
+ `Final flag on composite state "${path}" is ignored; mark an atomic leaf inside a region as final instead`,
4243
+ `states.${path}.final`,
4244
+ void 0,
4245
+ "Remove `final: true` from this composite and set it on the atomic leaf state that represents the region's completion."
4246
+ );
4247
+ } else {
4248
+ finalStatePaths.add(path);
4249
+ }
4250
+ }
4251
+ const userFromStates = /* @__PURE__ */ new Set();
4252
+ const doneStateComposites = /* @__PURE__ */ new Set();
4253
+ for (const [eventName, event] of Object.entries(smConfig.events)) {
4254
+ if (!event.transitions || !Array.isArray(event.transitions)) continue;
4255
+ if (eventName.startsWith("done.state.")) {
4256
+ const compositeId = eventName.slice("done.state.".length);
4257
+ if (compositeId) doneStateComposites.add(compositeId);
4258
+ }
4259
+ for (const transition of event.transitions) {
4260
+ if (typeof transition.from !== "string") continue;
4261
+ for (const fromPart of transition.from.split("|")) {
4262
+ if (finalStatePaths.has(fromPart)) {
4263
+ this.addError(
4264
+ "FINAL_STATE_HAS_OUTGOING",
4265
+ `Final state "${fromPart}" cannot be the source of transition on event "${eventName}"; a final pseudo-state has no outgoing transitions`,
4266
+ `events.${eventName}`,
4267
+ void 0,
4268
+ `Remove the outgoing transition from "${fromPart}", or clear its \`final: true\` flag if it is not actually a final state.`
4269
+ );
4270
+ }
4271
+ if (!eventName.startsWith("done.state.")) {
4272
+ userFromStates.add(fromPart);
4273
+ }
4274
+ }
4275
+ }
4276
+ }
4277
+ const entryByPath = new Map(entries.map((e) => [e.path, e.state]));
4278
+ for (const compositeId of doneStateComposites) {
4279
+ const composite = entryByPath.get(compositeId);
4280
+ if (!composite) {
4281
+ continue;
4282
+ }
4283
+ if (!this.hasReachableFinal(composite)) {
4284
+ this.addWarning(
4285
+ "REGION_NO_REACHABLE_FINAL",
4286
+ `Join event "done.state.${compositeId}" can never fire: composite "${compositeId}" has no \`final\` substate in any region`,
4287
+ `events.done.state.${compositeId}`,
4288
+ void 0,
4289
+ `Mark the completion leaf of each region under "${compositeId}" with \`final: true\`, or remove the done.state join.`
4290
+ );
4291
+ }
4292
+ if (userFromStates.has(compositeId)) {
4293
+ this.addWarning(
4294
+ "DONE_VS_PARALLEL_EXIT_AMBIGUITY",
4295
+ `Composite "${compositeId}" has both a done.state join and a user-event transition with from:"${compositeId}"; the user event can preempt the all-final join because it is eligible on any active leaf`,
4296
+ `events.done.state.${compositeId}`,
4297
+ void 0,
4298
+ `Disambiguate by using the done.state.${compositeId} join exclusively, or scope the user-event transition to a specific leaf instead of the bare composite.`
4299
+ );
4300
+ }
4301
+ }
4302
+ for (const { path, state } of entries) {
4303
+ if (!state.regions) continue;
4304
+ for (const [regionName, regionStates] of Object.entries(state.regions)) {
4305
+ const regionKeys = Object.keys(regionStates);
4306
+ if (regionKeys.length === 0) continue;
4307
+ const parentInitialInRegion = state.initial !== void 0 && regionKeys.includes(state.initial);
4308
+ if (!parentInitialInRegion) {
4309
+ this.addWarning(
4310
+ "REGION_MISSING_INITIAL",
4311
+ `Region "${regionName}" of composite "${path}" has no explicit initial; expansion falls back to the first key "${regionKeys[0]}" (insertion-order dependent)`,
4312
+ `states.${path}.regions.${regionName}`,
4313
+ void 0,
4314
+ `Set \`initial\` on composite "${path}" to a leaf of region "${regionName}", to make the starting substate explicit and order-independent.`
4315
+ );
4316
+ }
4317
+ }
4318
+ }
4319
+ }
4320
+ /**
4321
+ * Collects the dotted paths of every `final:true` atomic leaf in the tree.
4322
+ */
4323
+ collectFinalStatePaths(states, prefix = "", collector = /* @__PURE__ */ new Set()) {
4324
+ for (const [stateName, state] of Object.entries(states)) {
4325
+ const fullName = prefix ? `${prefix}.${stateName}` : stateName;
4326
+ if (state.final && !state.regions) {
4327
+ collector.add(fullName);
4328
+ }
4329
+ if (state.regions) {
4330
+ for (const [regionName, regionStates] of Object.entries(
4331
+ state.regions
4332
+ )) {
4333
+ this.collectFinalStatePaths(
4334
+ regionStates,
4335
+ `${fullName}.${regionName}`,
4336
+ collector
4337
+ );
4338
+ }
4339
+ }
4340
+ }
4341
+ return collector;
4342
+ }
3895
4343
  validatePerformanceConstraints(smConfig) {
3896
4344
  const stateCount = this.countStates(smConfig.states);
3897
4345
  const eventCount = Object.keys(smConfig.events).length;
@@ -3980,6 +4428,7 @@ function isValidConfig(config) {
3980
4428
  StateMachineError,
3981
4429
  createEnhancedError,
3982
4430
  createMachine,
4431
+ createVirtualScheduler,
3983
4432
  isAdapter,
3984
4433
  isRecoverableError,
3985
4434
  isValidConfig,