@vedmalex/statemachine 1.0.0-beta.1 → 1.0.0-beta.2
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/README.md +36 -1
- package/dist/index.cjs +434 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +434 -18
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/types/config_validator.d.ts +32 -0
- package/types/index.d.ts +10 -0
- package/types/state_machine.d.ts +101 -0
- package/types/types.d.ts +11 -2
package/dist/index.js
CHANGED
|
@@ -1805,6 +1805,9 @@ var StateMachine = class _StateMachine {
|
|
|
1805
1805
|
setContext(context) {
|
|
1806
1806
|
this.context = context;
|
|
1807
1807
|
}
|
|
1808
|
+
resolveCallbackOwner(value) {
|
|
1809
|
+
return isAdapter(value) ? value.adaptee : value;
|
|
1810
|
+
}
|
|
1808
1811
|
enqueueEvent(eventName, obj, args, type) {
|
|
1809
1812
|
if (this.externalQueue.length + this.internalQueue.length >= this.maxQueueDepth) {
|
|
1810
1813
|
const _s0 = this.getCurrentState(obj);
|
|
@@ -1913,7 +1916,8 @@ var StateMachine = class _StateMachine {
|
|
|
1913
1916
|
let transitions = event ? event.transitions.filter(
|
|
1914
1917
|
(t) => this.isTransitionPossible(t, currentState)
|
|
1915
1918
|
) : [];
|
|
1916
|
-
|
|
1919
|
+
const isEngineDoneEvent = String(eventName).startsWith("done.state.");
|
|
1920
|
+
if (!transitions.length && !isEngineDoneEvent) {
|
|
1917
1921
|
const wildcardEvent = this.events.get("*");
|
|
1918
1922
|
if (wildcardEvent) {
|
|
1919
1923
|
const wildcardTransitions = wildcardEvent.transitions.filter(
|
|
@@ -2118,6 +2122,10 @@ var StateMachine = class _StateMachine {
|
|
|
2118
2122
|
if (!currentState) return expectedState === "";
|
|
2119
2123
|
const currentParts = currentState.split("|").sort();
|
|
2120
2124
|
const expectedParts = expectedState.split("|").sort();
|
|
2125
|
+
const ancestorMatch = expectedParts.every(
|
|
2126
|
+
(expectedPart) => currentParts.some((leaf) => this.isParentState(expectedPart, leaf))
|
|
2127
|
+
);
|
|
2128
|
+
if (ancestorMatch) return true;
|
|
2121
2129
|
if (currentParts.length !== expectedParts.length) return false;
|
|
2122
2130
|
return currentParts.every((part, index) => part === expectedParts[index]);
|
|
2123
2131
|
}
|
|
@@ -2496,7 +2504,7 @@ var StateMachine = class _StateMachine {
|
|
|
2496
2504
|
const currentState = this.getCurrentState(adaptee) || "";
|
|
2497
2505
|
const currentStateMap = this.parseCompositeState(currentState);
|
|
2498
2506
|
const newStateParts = state.split("|");
|
|
2499
|
-
const isRootState = newStateParts.length === 1 && !state.includes(".");
|
|
2507
|
+
const isRootState = newStateParts.length === 1 && !state.includes(".") && !this.states.get(state)?.regions;
|
|
2500
2508
|
if (isRootState) {
|
|
2501
2509
|
currentStateMap.clear();
|
|
2502
2510
|
currentStateMap.set(state, state);
|
|
@@ -2572,12 +2580,13 @@ var StateMachine = class _StateMachine {
|
|
|
2572
2580
|
initialStates = initialState;
|
|
2573
2581
|
}
|
|
2574
2582
|
this.setCurrentState(initialStates, targetAdaptee);
|
|
2575
|
-
const
|
|
2583
|
+
const { enterStates } = this.computeEnterExitSets("", initialStates);
|
|
2584
|
+
const enterFireOrder = enterStates.length > 0 ? enterStates : [initialStates];
|
|
2576
2585
|
const context = {
|
|
2577
2586
|
state: initialStates,
|
|
2578
2587
|
phase: "enter"
|
|
2579
2588
|
};
|
|
2580
|
-
for (const statePart of
|
|
2589
|
+
for (const statePart of enterFireOrder) {
|
|
2581
2590
|
this.executeEnterActions(
|
|
2582
2591
|
targetAdaptee,
|
|
2583
2592
|
statePart,
|
|
@@ -2591,6 +2600,11 @@ var StateMachine = class _StateMachine {
|
|
|
2591
2600
|
);
|
|
2592
2601
|
});
|
|
2593
2602
|
}
|
|
2603
|
+
this.checkCompletion(
|
|
2604
|
+
targetAdaptee,
|
|
2605
|
+
"",
|
|
2606
|
+
initialStates
|
|
2607
|
+
);
|
|
2594
2608
|
}
|
|
2595
2609
|
getInitialCompositeState(initialState) {
|
|
2596
2610
|
const stateConfig = this.states.get(initialState);
|
|
@@ -2630,6 +2644,211 @@ var StateMachine = class _StateMachine {
|
|
|
2630
2644
|
}
|
|
2631
2645
|
return regionStates.join("|");
|
|
2632
2646
|
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Whether `leaf` is a UML/SCXML `<final>` atomic state of its region.
|
|
2649
|
+
*
|
|
2650
|
+
* Reads the `final` marker straight from the flattened state map populated by
|
|
2651
|
+
* processStates, so it works for any registered atomic leaf regardless of
|
|
2652
|
+
* nesting depth. Returns `false` for unregistered names and for composites
|
|
2653
|
+
* (the all-regions-final join derives doneness from atomic leaves, not from a
|
|
2654
|
+
* `final` flag on a composite parent).
|
|
2655
|
+
*/
|
|
2656
|
+
isStateFinal(leaf) {
|
|
2657
|
+
return Boolean(this.states.get(leaf)?.final);
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Whether composite `compositeId` has reached its UML/SCXML "done"
|
|
2661
|
+
* configuration: every one of its regions has its active atomic leaf in a
|
|
2662
|
+
* `final` state (recursively, for nested composites).
|
|
2663
|
+
*
|
|
2664
|
+
* D10 (mustFix): doneness is derived by scanning the active atomic `|`-leaves
|
|
2665
|
+
* against the STATIC regions tree (`this.states.get(C)?.regions`), NEVER via a
|
|
2666
|
+
* region-key Map lookup (`configMap.get`) — that map keys leaves by their
|
|
2667
|
+
* deepest region container and so cannot answer "which leaf is active in
|
|
2668
|
+
* region X of composite C". For each region we locate the active leaf via the
|
|
2669
|
+
* `C.region.` dotted prefix; the region is final iff that leaf is `final`, or
|
|
2670
|
+
* the leaf lives under a nested composite that is itself `isCompositeDone`.
|
|
2671
|
+
*
|
|
2672
|
+
* Returns `false` for a non-composite id, for a composite with no active leaf
|
|
2673
|
+
* in some region, or when no substate under it is ever `final` (cheap miss).
|
|
2674
|
+
*/
|
|
2675
|
+
isCompositeDone(compositeId, atomicLeaves) {
|
|
2676
|
+
const regions = this.states.get(compositeId)?.regions;
|
|
2677
|
+
if (!regions) return false;
|
|
2678
|
+
for (const regionName of Object.keys(regions)) {
|
|
2679
|
+
const regionPrefix = `${compositeId}.${regionName}.`;
|
|
2680
|
+
const activeLeaf = atomicLeaves.find(
|
|
2681
|
+
(leaf) => leaf.startsWith(regionPrefix)
|
|
2682
|
+
);
|
|
2683
|
+
if (!activeLeaf) return false;
|
|
2684
|
+
const nestedComposite = this.regionComposite(activeLeaf, regionPrefix);
|
|
2685
|
+
if (nestedComposite) {
|
|
2686
|
+
if (this.isCompositeDone(nestedComposite, atomicLeaves)) continue;
|
|
2687
|
+
return false;
|
|
2688
|
+
}
|
|
2689
|
+
if (this.isStateFinal(activeLeaf)) continue;
|
|
2690
|
+
return false;
|
|
2691
|
+
}
|
|
2692
|
+
return true;
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* The OUTERMOST registered composite (regions-bearing) ancestor of `leaf`
|
|
2696
|
+
* that lives strictly under `regionPrefix`, or `undefined` if the region
|
|
2697
|
+
* holds a simple atomic state directly. Used by {@link isCompositeDone} to
|
|
2698
|
+
* delegate a region's completeness to its nested composite (recursing over
|
|
2699
|
+
* every parallel branch), independent of whether any single branch leaf
|
|
2700
|
+
* happens to carry the `final` flag.
|
|
2701
|
+
*
|
|
2702
|
+
* ancestorChain is root-to-leaf, so the FIRST matching ancestor is the
|
|
2703
|
+
* region's direct composite child (e.g. `C.p.D` for leaf `C.p.D.s.s2` under
|
|
2704
|
+
* region prefix `C.p.`); isCompositeDone then recurses into its regions.
|
|
2705
|
+
*/
|
|
2706
|
+
regionComposite(leaf, regionPrefix) {
|
|
2707
|
+
for (const ancestor of this.ancestorChain(leaf)) {
|
|
2708
|
+
if (ancestor !== leaf && ancestor.startsWith(regionPrefix) && this.states.get(ancestor)?.regions) {
|
|
2709
|
+
return ancestor;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return void 0;
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Whether composite `compositeId` has reached its all-regions-final ("done")
|
|
2716
|
+
* configuration in the CURRENT active state. Public guard surface (`@stable`)
|
|
2717
|
+
* for authoring a join as `guard: () => sm.isDone('C')` instead of (or
|
|
2718
|
+
* alongside) listening on the engine `done.state.<C>` event.
|
|
2719
|
+
*
|
|
2720
|
+
* @param compositeId - The dotted id of the composite/parallel state.
|
|
2721
|
+
* @returns `true` iff every region's active atomic leaf is `final`.
|
|
2722
|
+
*/
|
|
2723
|
+
isDone(compositeId, adaptee) {
|
|
2724
|
+
const currentState = this.getCurrentState(adaptee);
|
|
2725
|
+
if (!currentState) return false;
|
|
2726
|
+
const atomicLeaves = currentState.split("|").filter(Boolean);
|
|
2727
|
+
return this.isCompositeDone(compositeId, atomicLeaves);
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* SCXML completion hook (D10/D11/D12): after a new configuration is written,
|
|
2731
|
+
* raise `done.state.<C>` for each composite that became all-regions-final.
|
|
2732
|
+
*
|
|
2733
|
+
* - Scans only composites that GAINED an active leaf (the ancestor chain of
|
|
2734
|
+
* each `|`-leaf of `newState`), so unaffected composites are not re-checked.
|
|
2735
|
+
* - Emits INNERMOST-first (a deeper composite's `done.state` precedes its
|
|
2736
|
+
* parent's), matching SCXML's inner-before-outer completion ordering, via a
|
|
2737
|
+
* per-config emitted-id Set so each id is raised at most once per call.
|
|
2738
|
+
* - Gates each emission on `this.events.has('done.state.'+C)` (D11 mustFix):
|
|
2739
|
+
* raising an undeclared event would hit the `Invalid event` throw
|
|
2740
|
+
* (executeQueuedTransition) as an unhandled microtask rejection. No declared
|
|
2741
|
+
* `done.state.<C>` event => no emission, no crash, no observable effect.
|
|
2742
|
+
* - Uses the internal queue (`raiseEvent`) + `scheduleProcessing` so the
|
|
2743
|
+
* completion event is processed before subsequent external events.
|
|
2744
|
+
*/
|
|
2745
|
+
checkCompletion(obj, oldState, newState) {
|
|
2746
|
+
const atomicLeaves = newState.split("|").filter(Boolean);
|
|
2747
|
+
if (atomicLeaves.length === 0) return;
|
|
2748
|
+
const oldLeaves = oldState.split("|").filter(Boolean);
|
|
2749
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2750
|
+
for (const leaf of atomicLeaves) {
|
|
2751
|
+
for (const ancestor of this.ancestorChain(leaf)) {
|
|
2752
|
+
if (ancestor === leaf) continue;
|
|
2753
|
+
if (seen.has(ancestor)) continue;
|
|
2754
|
+
if (!this.states.get(ancestor)?.regions) continue;
|
|
2755
|
+
seen.add(ancestor);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
const candidates = Array.from(seen).sort(
|
|
2759
|
+
(a, b) => b.split(".").length - a.split(".").length
|
|
2760
|
+
);
|
|
2761
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2762
|
+
for (const compositeId of candidates) {
|
|
2763
|
+
if (emitted.has(compositeId)) continue;
|
|
2764
|
+
if (!this.isCompositeDone(compositeId, atomicLeaves)) continue;
|
|
2765
|
+
if (this.isCompositeDone(compositeId, oldLeaves)) continue;
|
|
2766
|
+
emitted.add(compositeId);
|
|
2767
|
+
const doneEvent = `done.state.${compositeId}`;
|
|
2768
|
+
if (!this.events.has(doneEvent)) continue;
|
|
2769
|
+
this.raiseEvent(doneEvent, obj);
|
|
2770
|
+
this.scheduleProcessing();
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Build the registered ancestor chain for an atomic leaf, ordered root-to-leaf.
|
|
2775
|
+
*
|
|
2776
|
+
* Walks every dot-prefix of `leaf` and keeps only the ones that are real
|
|
2777
|
+
* registered states (`this.states.has`). Region containers are never
|
|
2778
|
+
* registered (only composite parents and atomic leaves are — see
|
|
2779
|
+
* processStates/processRegions), so they are filtered out automatically. The
|
|
2780
|
+
* result is exactly `[parent..leaf]` for a leaf inside a composite, and
|
|
2781
|
+
* `[leaf]` for a flat state.
|
|
2782
|
+
*
|
|
2783
|
+
* Example: `ancestorChain('a.r1.c1')` -> `['a', 'a.r1.c1']` (the `a.r1`
|
|
2784
|
+
* region container is excluded). Nested:
|
|
2785
|
+
* `ancestorChain('a.r1.c1.r3.x')` -> `['a', 'a.r1.c1', 'a.r1.c1.r3.x']`.
|
|
2786
|
+
*/
|
|
2787
|
+
ancestorChain(leaf) {
|
|
2788
|
+
const chain = [];
|
|
2789
|
+
let dotIndex = leaf.indexOf(".");
|
|
2790
|
+
while (dotIndex !== -1) {
|
|
2791
|
+
const prefix = leaf.substring(0, dotIndex);
|
|
2792
|
+
if (this.states.has(prefix)) {
|
|
2793
|
+
chain.push(prefix);
|
|
2794
|
+
}
|
|
2795
|
+
dotIndex = leaf.indexOf(".", dotIndex + 1);
|
|
2796
|
+
}
|
|
2797
|
+
if (this.states.has(leaf)) {
|
|
2798
|
+
chain.push(leaf);
|
|
2799
|
+
}
|
|
2800
|
+
return chain;
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Compute the ordered enter/exit sets between two composite configurations
|
|
2804
|
+
* (SCXML ancestor-first entry / descendant-first exit).
|
|
2805
|
+
*
|
|
2806
|
+
* For each `|`-separated atomic leaf in `oldComposite` and `newComposite` the
|
|
2807
|
+
* union of its {@link ancestorChain} forms the old/new active ancestry. A
|
|
2808
|
+
* state shared by both ancestries lands in NEITHER diff, so a surviving
|
|
2809
|
+
* ancestor is never re-entered nor exited (no onEnter/onExit re-fire, no
|
|
2810
|
+
* timer re-arm/leak).
|
|
2811
|
+
*
|
|
2812
|
+
* - `enterStates` = new ancestry MINUS old, sorted ascending by depth then
|
|
2813
|
+
* document (insertion) order -> root-to-leaf entry.
|
|
2814
|
+
* - `exitStates` = old ancestry MINUS new, sorted descending by depth ->
|
|
2815
|
+
* leaf-to-root exit.
|
|
2816
|
+
*
|
|
2817
|
+
* Consumed by BOTH R1 (applyTransition / setInitialState / reset) and R2.
|
|
2818
|
+
*/
|
|
2819
|
+
computeEnterExitSets(oldComposite, newComposite) {
|
|
2820
|
+
const collectAncestry = (composite) => {
|
|
2821
|
+
const ancestry = /* @__PURE__ */ new Set();
|
|
2822
|
+
if (!composite) return ancestry;
|
|
2823
|
+
for (const leaf of composite.split("|")) {
|
|
2824
|
+
if (!leaf) continue;
|
|
2825
|
+
for (const ancestor of this.ancestorChain(leaf)) {
|
|
2826
|
+
ancestry.add(ancestor);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
return ancestry;
|
|
2830
|
+
};
|
|
2831
|
+
const oldAncestry = collectAncestry(oldComposite);
|
|
2832
|
+
const newAncestry = collectAncestry(newComposite);
|
|
2833
|
+
const depthOf = (state) => {
|
|
2834
|
+
let depth = 0;
|
|
2835
|
+
for (let i = 0; i < state.length; i++) {
|
|
2836
|
+
if (state[i] === ".") depth++;
|
|
2837
|
+
}
|
|
2838
|
+
return depth;
|
|
2839
|
+
};
|
|
2840
|
+
const enterRaw = [];
|
|
2841
|
+
for (const state of newAncestry) {
|
|
2842
|
+
if (!oldAncestry.has(state)) enterRaw.push(state);
|
|
2843
|
+
}
|
|
2844
|
+
const exitRaw = [];
|
|
2845
|
+
for (const state of oldAncestry) {
|
|
2846
|
+
if (!newAncestry.has(state)) exitRaw.push(state);
|
|
2847
|
+
}
|
|
2848
|
+
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);
|
|
2849
|
+
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);
|
|
2850
|
+
return { enterStates, exitStates };
|
|
2851
|
+
}
|
|
2633
2852
|
validateCompositeState(compositeState) {
|
|
2634
2853
|
const stateParts = compositeState.split("|");
|
|
2635
2854
|
const regionKeys = /* @__PURE__ */ new Set();
|
|
@@ -2703,10 +2922,11 @@ var StateMachine = class _StateMachine {
|
|
|
2703
2922
|
return (...args) => {
|
|
2704
2923
|
const targetAdaptee = args.length >= 2 ? args[0] : adaptee;
|
|
2705
2924
|
const error = args.length >= 2 ? args[1] : args[0];
|
|
2706
|
-
return handler(targetAdaptee, error);
|
|
2925
|
+
return handler(this.resolveCallbackOwner(targetAdaptee), error);
|
|
2707
2926
|
};
|
|
2708
2927
|
}
|
|
2709
2928
|
async callAction(obj, actionName, ...args) {
|
|
2929
|
+
const targetOwner = this.resolveCallbackOwner(obj);
|
|
2710
2930
|
const _callActionState = this.getCurrentState(
|
|
2711
2931
|
obj
|
|
2712
2932
|
);
|
|
@@ -2721,16 +2941,16 @@ var StateMachine = class _StateMachine {
|
|
|
2721
2941
|
if (this.context && this.context[actionName]) {
|
|
2722
2942
|
const action = this.context[actionName];
|
|
2723
2943
|
if (typeof action === "function") {
|
|
2724
|
-
const result = action(
|
|
2944
|
+
const result = action(targetOwner, ...args);
|
|
2725
2945
|
return result instanceof Promise ? await result : result;
|
|
2726
2946
|
}
|
|
2727
2947
|
} else if (typeof actionName === "function") {
|
|
2728
|
-
const result = actionName(
|
|
2948
|
+
const result = actionName(targetOwner, ...args);
|
|
2729
2949
|
return result instanceof Promise ? await result : result;
|
|
2730
2950
|
} else if (obj.get(actionName)) {
|
|
2731
2951
|
const action = obj.get(actionName);
|
|
2732
2952
|
if (typeof action === "function") {
|
|
2733
|
-
const result = action(
|
|
2953
|
+
const result = action(targetOwner, ...args);
|
|
2734
2954
|
return result instanceof Promise ? await result : result;
|
|
2735
2955
|
}
|
|
2736
2956
|
}
|
|
@@ -2802,8 +3022,12 @@ var StateMachine = class _StateMachine {
|
|
|
2802
3022
|
if (fromState === "*") return true;
|
|
2803
3023
|
const regionKey = this.getRegionKey(fromState);
|
|
2804
3024
|
const currentStateForRegion = currentStates.get(regionKey);
|
|
2805
|
-
if (
|
|
2806
|
-
|
|
3025
|
+
if (currentStateForRegion && (currentStateForRegion === fromState || this.isParentState(currentStateForRegion, fromState))) {
|
|
3026
|
+
return true;
|
|
3027
|
+
}
|
|
3028
|
+
return Array.from(currentStates.values()).some(
|
|
3029
|
+
(leaf) => leaf === fromState || this.isParentState(fromState, leaf)
|
|
3030
|
+
);
|
|
2807
3031
|
});
|
|
2808
3032
|
}
|
|
2809
3033
|
isParentState(parentState, childState) {
|
|
@@ -2819,6 +3043,26 @@ var StateMachine = class _StateMachine {
|
|
|
2819
3043
|
const targetState = transition.to === "*" ? currentState : transition.to;
|
|
2820
3044
|
this._isTransitioning = true;
|
|
2821
3045
|
this._targetState = targetState;
|
|
3046
|
+
let newState;
|
|
3047
|
+
let enterStates;
|
|
3048
|
+
let exitStates;
|
|
3049
|
+
try {
|
|
3050
|
+
newState = this.updateState(currentState, targetState);
|
|
3051
|
+
const sets = this.computeEnterExitSets(currentState, newState);
|
|
3052
|
+
enterStates = sets.enterStates;
|
|
3053
|
+
exitStates = sets.exitStates;
|
|
3054
|
+
} catch (error) {
|
|
3055
|
+
this.logger.warn("Transition aborted: invalid target configuration", {
|
|
3056
|
+
state: currentState,
|
|
3057
|
+
target: String(targetState),
|
|
3058
|
+
error
|
|
3059
|
+
});
|
|
3060
|
+
this._isTransitioning = false;
|
|
3061
|
+
this._targetState = void 0;
|
|
3062
|
+
return void 0;
|
|
3063
|
+
}
|
|
3064
|
+
const exitFireOrder = exitStates.length > 0 ? exitStates : [transition.from];
|
|
3065
|
+
const enterFireOrder = enterStates.length > 0 ? enterStates : [transition.to];
|
|
2822
3066
|
try {
|
|
2823
3067
|
if (transition.guard) {
|
|
2824
3068
|
const allow = await this.callAction(obj, transition.guard, ...args).catch(
|
|
@@ -2844,7 +3088,9 @@ var StateMachine = class _StateMachine {
|
|
|
2844
3088
|
);
|
|
2845
3089
|
}
|
|
2846
3090
|
try {
|
|
2847
|
-
|
|
3091
|
+
for (const exitStateName of exitFireOrder) {
|
|
3092
|
+
await this.executeExitActions(obj, exitStateName, args, context);
|
|
3093
|
+
}
|
|
2848
3094
|
} catch (error) {
|
|
2849
3095
|
if (this.abortOnExitError) {
|
|
2850
3096
|
this.logger.warn("Transition aborted due to onExit error", { state: currentState, error });
|
|
@@ -2864,12 +3110,14 @@ var StateMachine = class _StateMachine {
|
|
|
2864
3110
|
);
|
|
2865
3111
|
}
|
|
2866
3112
|
try {
|
|
2867
|
-
|
|
3113
|
+
for (const enterStateName of enterFireOrder) {
|
|
3114
|
+
await this.executeEnterActions(obj, enterStateName, args, context);
|
|
3115
|
+
}
|
|
2868
3116
|
} catch (error) {
|
|
2869
3117
|
if (this.errorState) {
|
|
2870
3118
|
this.logger.error(`Failed to enter state '${targetState}'. Fallback to error state '${this.errorState}'`, { error });
|
|
2871
|
-
const
|
|
2872
|
-
this.setCurrentState(
|
|
3119
|
+
const errorNewState = this.updateState(currentState, this.errorState);
|
|
3120
|
+
this.setCurrentState(errorNewState, obj);
|
|
2873
3121
|
return this.states.get(this.errorState);
|
|
2874
3122
|
}
|
|
2875
3123
|
throw error;
|
|
@@ -2884,12 +3132,12 @@ var StateMachine = class _StateMachine {
|
|
|
2884
3132
|
event.onError,
|
|
2885
3133
|
this.onError
|
|
2886
3134
|
);
|
|
2887
|
-
errorHandler(obj, error);
|
|
3135
|
+
errorHandler(this.resolveCallbackOwner(obj), error);
|
|
2888
3136
|
}
|
|
2889
3137
|
}
|
|
2890
3138
|
const transitionStartTime = Date.now();
|
|
2891
|
-
const newState = this.updateState(currentState, targetState);
|
|
2892
3139
|
this.setCurrentState(newState, obj);
|
|
3140
|
+
this.checkCompletion(obj, currentState, newState);
|
|
2893
3141
|
const transitionTime = Date.now() - transitionStartTime;
|
|
2894
3142
|
this.monitor.recordTransition(transitionTime, true);
|
|
2895
3143
|
return this.states.get(targetState);
|
|
@@ -3065,7 +3313,7 @@ var StateMachine = class _StateMachine {
|
|
|
3065
3313
|
updateState(currentState, toState) {
|
|
3066
3314
|
const currentStateMap = this.parseCompositeState(currentState);
|
|
3067
3315
|
const toStateParts = toState.split("|");
|
|
3068
|
-
if (toStateParts.length === 1 && !toState.includes(".")) {
|
|
3316
|
+
if (toStateParts.length === 1 && !toState.includes(".") && !this.states.get(toState)?.regions) {
|
|
3069
3317
|
currentStateMap.clear();
|
|
3070
3318
|
currentStateMap.set(toState, toState);
|
|
3071
3319
|
return toState;
|
|
@@ -3447,6 +3695,7 @@ var ConfigValidator = class {
|
|
|
3447
3695
|
this.validateInitialState(smConfig);
|
|
3448
3696
|
this.validateCrossReferences(smConfig);
|
|
3449
3697
|
this.validatePerformanceConstraints(smConfig);
|
|
3698
|
+
this.validateFinalStates(smConfig);
|
|
3450
3699
|
this.runCustomRules(smConfig);
|
|
3451
3700
|
} catch (error) {
|
|
3452
3701
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -3808,6 +4057,10 @@ var ConfigValidator = class {
|
|
|
3808
4057
|
}
|
|
3809
4058
|
}
|
|
3810
4059
|
}
|
|
4060
|
+
const finalStatePaths = this.collectFinalStatePaths(smConfig.states);
|
|
4061
|
+
for (const finalPath of finalStatePaths) {
|
|
4062
|
+
reachableStates.add(finalPath);
|
|
4063
|
+
}
|
|
3811
4064
|
for (const stateName of allStateNames) {
|
|
3812
4065
|
if (!reachableStates.has(stateName)) {
|
|
3813
4066
|
this.addWarning(
|
|
@@ -3818,8 +4071,9 @@ var ConfigValidator = class {
|
|
|
3818
4071
|
}
|
|
3819
4072
|
}
|
|
3820
4073
|
for (const [eventName, event] of Object.entries(smConfig.events)) {
|
|
4074
|
+
const isDoneStateEvent = eventName.startsWith("done.state.");
|
|
3821
4075
|
const hasValidTransitions = event.transitions.some(
|
|
3822
|
-
(t) => allStateNames.has(t.from) && allStateNames.has(t.to)
|
|
4076
|
+
(t) => (allStateNames.has(t.from) || isDoneStateEvent && allStateNames.has(eventName.slice("done.state.".length))) && allStateNames.has(t.to)
|
|
3823
4077
|
);
|
|
3824
4078
|
if (!hasValidTransitions) {
|
|
3825
4079
|
this.addWarning(
|
|
@@ -3847,6 +4101,168 @@ var ConfigValidator = class {
|
|
|
3847
4101
|
}
|
|
3848
4102
|
}
|
|
3849
4103
|
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Collects every state in the tree as {dottedPath, config} entries, walking
|
|
4106
|
+
* region containers (region names are NOT registered states, so they are part
|
|
4107
|
+
* of the path but never emitted as an entry — mirroring runtime expansion).
|
|
4108
|
+
*/
|
|
4109
|
+
collectStateEntries(states, prefix, collector) {
|
|
4110
|
+
for (const [stateName, state] of Object.entries(states)) {
|
|
4111
|
+
const fullName = prefix ? `${prefix}.${stateName}` : stateName;
|
|
4112
|
+
collector.push({ path: fullName, state });
|
|
4113
|
+
if (state.regions) {
|
|
4114
|
+
for (const [regionName, regionStates] of Object.entries(
|
|
4115
|
+
state.regions
|
|
4116
|
+
)) {
|
|
4117
|
+
this.collectStateEntries(
|
|
4118
|
+
regionStates,
|
|
4119
|
+
`${fullName}.${regionName}`,
|
|
4120
|
+
collector
|
|
4121
|
+
);
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
/**
|
|
4127
|
+
* True when `composite` has at least one `final:true` atomic substate reachable
|
|
4128
|
+
* through its (possibly nested) region tree — i.e. some region of the composite
|
|
4129
|
+
* can become done. Used by REGION_NO_REACHABLE_FINAL.
|
|
4130
|
+
*/
|
|
4131
|
+
hasReachableFinal(composite) {
|
|
4132
|
+
if (!composite.regions) return false;
|
|
4133
|
+
for (const regionStates of Object.values(composite.regions)) {
|
|
4134
|
+
for (const state of Object.values(regionStates)) {
|
|
4135
|
+
if (state.final) return true;
|
|
4136
|
+
if (state.regions && this.hasReachableFinal(state)) return true;
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
return false;
|
|
4140
|
+
}
|
|
4141
|
+
/**
|
|
4142
|
+
* SCXML/UML final-state and all-regions-final (done.state.<C>) join validation.
|
|
4143
|
+
*
|
|
4144
|
+
* - FINAL_STATE_HAS_OUTGOING (error): a `final:true` state is the `from` of a
|
|
4145
|
+
* transition (a final pseudo-state cannot itself transition out).
|
|
4146
|
+
* - FINAL_ON_COMPOSITE (warning): `final:true` on a state that has regions
|
|
4147
|
+
* (final is meaningful only on an atomic leaf; ignored at runtime).
|
|
4148
|
+
* - REGION_NO_REACHABLE_FINAL (warning): a `done.state.<C>` transition exists
|
|
4149
|
+
* but composite C declares no `final` substate, so the join can never fire.
|
|
4150
|
+
* - DONE_VS_PARALLEL_EXIT_AMBIGUITY (warning): the same composite C has BOTH a
|
|
4151
|
+
* `done.state.C` join AND a plain `from:'C'` user-event parallel-exit, which
|
|
4152
|
+
* can preempt the all-final join (the user event is eligible on ANY leaf).
|
|
4153
|
+
* - REGION_MISSING_INITIAL (advisory warning, valid:true): a region omits an
|
|
4154
|
+
* explicit `initial`, so expansion relies on first-key insertion order.
|
|
4155
|
+
*/
|
|
4156
|
+
validateFinalStates(smConfig) {
|
|
4157
|
+
const entries = [];
|
|
4158
|
+
this.collectStateEntries(smConfig.states, "", entries);
|
|
4159
|
+
const finalStatePaths = /* @__PURE__ */ new Set();
|
|
4160
|
+
for (const { path, state } of entries) {
|
|
4161
|
+
if (!state.final) continue;
|
|
4162
|
+
if (state.regions) {
|
|
4163
|
+
this.addWarning(
|
|
4164
|
+
"FINAL_ON_COMPOSITE",
|
|
4165
|
+
`Final flag on composite state "${path}" is ignored; mark an atomic leaf inside a region as final instead`,
|
|
4166
|
+
`states.${path}.final`,
|
|
4167
|
+
void 0,
|
|
4168
|
+
"Remove `final: true` from this composite and set it on the atomic leaf state that represents the region's completion."
|
|
4169
|
+
);
|
|
4170
|
+
} else {
|
|
4171
|
+
finalStatePaths.add(path);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
const userFromStates = /* @__PURE__ */ new Set();
|
|
4175
|
+
const doneStateComposites = /* @__PURE__ */ new Set();
|
|
4176
|
+
for (const [eventName, event] of Object.entries(smConfig.events)) {
|
|
4177
|
+
if (!event.transitions || !Array.isArray(event.transitions)) continue;
|
|
4178
|
+
if (eventName.startsWith("done.state.")) {
|
|
4179
|
+
const compositeId = eventName.slice("done.state.".length);
|
|
4180
|
+
if (compositeId) doneStateComposites.add(compositeId);
|
|
4181
|
+
}
|
|
4182
|
+
for (const transition of event.transitions) {
|
|
4183
|
+
if (typeof transition.from !== "string") continue;
|
|
4184
|
+
for (const fromPart of transition.from.split("|")) {
|
|
4185
|
+
if (finalStatePaths.has(fromPart)) {
|
|
4186
|
+
this.addError(
|
|
4187
|
+
"FINAL_STATE_HAS_OUTGOING",
|
|
4188
|
+
`Final state "${fromPart}" cannot be the source of transition on event "${eventName}"; a final pseudo-state has no outgoing transitions`,
|
|
4189
|
+
`events.${eventName}`,
|
|
4190
|
+
void 0,
|
|
4191
|
+
`Remove the outgoing transition from "${fromPart}", or clear its \`final: true\` flag if it is not actually a final state.`
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
if (!eventName.startsWith("done.state.")) {
|
|
4195
|
+
userFromStates.add(fromPart);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
const entryByPath = new Map(entries.map((e) => [e.path, e.state]));
|
|
4201
|
+
for (const compositeId of doneStateComposites) {
|
|
4202
|
+
const composite = entryByPath.get(compositeId);
|
|
4203
|
+
if (!composite) {
|
|
4204
|
+
continue;
|
|
4205
|
+
}
|
|
4206
|
+
if (!this.hasReachableFinal(composite)) {
|
|
4207
|
+
this.addWarning(
|
|
4208
|
+
"REGION_NO_REACHABLE_FINAL",
|
|
4209
|
+
`Join event "done.state.${compositeId}" can never fire: composite "${compositeId}" has no \`final\` substate in any region`,
|
|
4210
|
+
`events.done.state.${compositeId}`,
|
|
4211
|
+
void 0,
|
|
4212
|
+
`Mark the completion leaf of each region under "${compositeId}" with \`final: true\`, or remove the done.state join.`
|
|
4213
|
+
);
|
|
4214
|
+
}
|
|
4215
|
+
if (userFromStates.has(compositeId)) {
|
|
4216
|
+
this.addWarning(
|
|
4217
|
+
"DONE_VS_PARALLEL_EXIT_AMBIGUITY",
|
|
4218
|
+
`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`,
|
|
4219
|
+
`events.done.state.${compositeId}`,
|
|
4220
|
+
void 0,
|
|
4221
|
+
`Disambiguate by using the done.state.${compositeId} join exclusively, or scope the user-event transition to a specific leaf instead of the bare composite.`
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
for (const { path, state } of entries) {
|
|
4226
|
+
if (!state.regions) continue;
|
|
4227
|
+
for (const [regionName, regionStates] of Object.entries(state.regions)) {
|
|
4228
|
+
const regionKeys = Object.keys(regionStates);
|
|
4229
|
+
if (regionKeys.length === 0) continue;
|
|
4230
|
+
const parentInitialInRegion = state.initial !== void 0 && regionKeys.includes(state.initial);
|
|
4231
|
+
if (!parentInitialInRegion) {
|
|
4232
|
+
this.addWarning(
|
|
4233
|
+
"REGION_MISSING_INITIAL",
|
|
4234
|
+
`Region "${regionName}" of composite "${path}" has no explicit initial; expansion falls back to the first key "${regionKeys[0]}" (insertion-order dependent)`,
|
|
4235
|
+
`states.${path}.regions.${regionName}`,
|
|
4236
|
+
void 0,
|
|
4237
|
+
`Set \`initial\` on composite "${path}" to a leaf of region "${regionName}", to make the starting substate explicit and order-independent.`
|
|
4238
|
+
);
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
/**
|
|
4244
|
+
* Collects the dotted paths of every `final:true` atomic leaf in the tree.
|
|
4245
|
+
*/
|
|
4246
|
+
collectFinalStatePaths(states, prefix = "", collector = /* @__PURE__ */ new Set()) {
|
|
4247
|
+
for (const [stateName, state] of Object.entries(states)) {
|
|
4248
|
+
const fullName = prefix ? `${prefix}.${stateName}` : stateName;
|
|
4249
|
+
if (state.final && !state.regions) {
|
|
4250
|
+
collector.add(fullName);
|
|
4251
|
+
}
|
|
4252
|
+
if (state.regions) {
|
|
4253
|
+
for (const [regionName, regionStates] of Object.entries(
|
|
4254
|
+
state.regions
|
|
4255
|
+
)) {
|
|
4256
|
+
this.collectFinalStatePaths(
|
|
4257
|
+
regionStates,
|
|
4258
|
+
`${fullName}.${regionName}`,
|
|
4259
|
+
collector
|
|
4260
|
+
);
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
return collector;
|
|
4265
|
+
}
|
|
3850
4266
|
validatePerformanceConstraints(smConfig) {
|
|
3851
4267
|
const stateCount = this.countStates(smConfig.states);
|
|
3852
4268
|
const eventCount = Object.keys(smConfig.events).length;
|