@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/README.md +139 -4
- package/dist/index.cjs +539 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +538 -90
- 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 +33 -0
- package/types/scheduler.d.ts +31 -1
- package/types/state_machine.d.ts +103 -0
- package/types/types.d.ts +24 -2
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ var TimerScheduler = class {
|
|
|
8
8
|
intervalId = null;
|
|
9
9
|
pollingInterval = 100;
|
|
10
10
|
// ms
|
|
11
|
-
|
|
11
|
+
clock;
|
|
12
|
+
constructor(clock = Date.now) {
|
|
13
|
+
this.clock = clock;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Настройка режима опроса
|
|
@@ -32,7 +34,7 @@ var TimerScheduler = class {
|
|
|
32
34
|
*/
|
|
33
35
|
start() {
|
|
34
36
|
if (this.intervalId) return;
|
|
35
|
-
this.intervalId = setInterval(() => this.process(), this.pollingInterval);
|
|
37
|
+
this.intervalId = setInterval(() => this.process(this.clock()), this.pollingInterval);
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Остановка единого таймера
|
|
@@ -51,7 +53,7 @@ var TimerScheduler = class {
|
|
|
51
53
|
*/
|
|
52
54
|
schedule(delay, callback) {
|
|
53
55
|
const token = {};
|
|
54
|
-
const executeAt =
|
|
56
|
+
const executeAt = this.clock() + delay;
|
|
55
57
|
const task = { token, executeAt, callback };
|
|
56
58
|
this.activeTokens.add(token);
|
|
57
59
|
this.insert(task);
|
|
@@ -67,7 +69,7 @@ var TimerScheduler = class {
|
|
|
67
69
|
/**
|
|
68
70
|
* Обработать очередь (вызывается таймером или вручную)
|
|
69
71
|
*/
|
|
70
|
-
process(now =
|
|
72
|
+
process(now = this.clock()) {
|
|
71
73
|
while (this.heap.length > 0) {
|
|
72
74
|
const task = this.heap[0];
|
|
73
75
|
if (task === void 0) break;
|
|
@@ -158,6 +160,23 @@ var TimerScheduler = class {
|
|
|
158
160
|
function createDefaultScheduler() {
|
|
159
161
|
return new TimerScheduler();
|
|
160
162
|
}
|
|
163
|
+
function createVirtualScheduler(clock) {
|
|
164
|
+
const inner = new TimerScheduler(clock);
|
|
165
|
+
return {
|
|
166
|
+
isActive() {
|
|
167
|
+
return true;
|
|
168
|
+
},
|
|
169
|
+
schedule(delay, callback) {
|
|
170
|
+
return inner.schedule(delay, callback);
|
|
171
|
+
},
|
|
172
|
+
cancel(token) {
|
|
173
|
+
inner.cancel(token);
|
|
174
|
+
},
|
|
175
|
+
process(now) {
|
|
176
|
+
inner.process(now ?? clock());
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
161
180
|
|
|
162
181
|
// src/logger.ts
|
|
163
182
|
var LogLevel = {
|
|
@@ -1704,6 +1723,8 @@ var StateMachine = class _StateMachine {
|
|
|
1704
1723
|
monitor;
|
|
1705
1724
|
scheduler;
|
|
1706
1725
|
errorHandler;
|
|
1726
|
+
clock;
|
|
1727
|
+
schedulerProvided;
|
|
1707
1728
|
// Свойства
|
|
1708
1729
|
states;
|
|
1709
1730
|
events;
|
|
@@ -1754,7 +1775,9 @@ var StateMachine = class _StateMachine {
|
|
|
1754
1775
|
this.initialState = config.initialState;
|
|
1755
1776
|
this.logger = options?.logger ?? ConsoleLogger;
|
|
1756
1777
|
this.monitor = options?.monitor ?? createDefaultMonitor();
|
|
1778
|
+
this.schedulerProvided = options?.scheduler !== void 0;
|
|
1757
1779
|
this.scheduler = options?.scheduler ?? createDefaultScheduler();
|
|
1780
|
+
this.clock = options?.clock ?? Date.now;
|
|
1758
1781
|
this.errorHandler = options?.errorHandler ?? createDefaultErrorHandler();
|
|
1759
1782
|
if (adaptee) {
|
|
1760
1783
|
if (!isAdapter(adaptee)) {
|
|
@@ -1805,6 +1828,9 @@ var StateMachine = class _StateMachine {
|
|
|
1805
1828
|
setContext(context) {
|
|
1806
1829
|
this.context = context;
|
|
1807
1830
|
}
|
|
1831
|
+
resolveCallbackOwner(value) {
|
|
1832
|
+
return isAdapter(value) ? value.adaptee : value;
|
|
1833
|
+
}
|
|
1808
1834
|
enqueueEvent(eventName, obj, args, type) {
|
|
1809
1835
|
if (this.externalQueue.length + this.internalQueue.length >= this.maxQueueDepth) {
|
|
1810
1836
|
const _s0 = this.getCurrentState(obj);
|
|
@@ -1825,7 +1851,7 @@ var StateMachine = class _StateMachine {
|
|
|
1825
1851
|
args,
|
|
1826
1852
|
resolve,
|
|
1827
1853
|
reject,
|
|
1828
|
-
timestamp:
|
|
1854
|
+
timestamp: this.clock(),
|
|
1829
1855
|
type: "external"
|
|
1830
1856
|
});
|
|
1831
1857
|
this.scheduleProcessing();
|
|
@@ -1836,7 +1862,7 @@ var StateMachine = class _StateMachine {
|
|
|
1836
1862
|
eventName,
|
|
1837
1863
|
obj,
|
|
1838
1864
|
args,
|
|
1839
|
-
timestamp:
|
|
1865
|
+
timestamp: this.clock(),
|
|
1840
1866
|
type: "internal"
|
|
1841
1867
|
});
|
|
1842
1868
|
return Promise.resolve(true);
|
|
@@ -1847,7 +1873,7 @@ var StateMachine = class _StateMachine {
|
|
|
1847
1873
|
eventName,
|
|
1848
1874
|
obj,
|
|
1849
1875
|
args,
|
|
1850
|
-
timestamp:
|
|
1876
|
+
timestamp: this.clock(),
|
|
1851
1877
|
type: "internal"
|
|
1852
1878
|
});
|
|
1853
1879
|
}
|
|
@@ -1913,7 +1939,8 @@ var StateMachine = class _StateMachine {
|
|
|
1913
1939
|
let transitions = event ? event.transitions.filter(
|
|
1914
1940
|
(t) => this.isTransitionPossible(t, currentState)
|
|
1915
1941
|
) : [];
|
|
1916
|
-
|
|
1942
|
+
const isEngineDoneEvent = String(eventName).startsWith("done.state.");
|
|
1943
|
+
if (!transitions.length && !isEngineDoneEvent) {
|
|
1917
1944
|
const wildcardEvent = this.events.get("*");
|
|
1918
1945
|
if (wildcardEvent) {
|
|
1919
1946
|
const wildcardTransitions = wildcardEvent.transitions.filter(
|
|
@@ -2007,7 +2034,7 @@ var StateMachine = class _StateMachine {
|
|
|
2007
2034
|
};
|
|
2008
2035
|
}
|
|
2009
2036
|
getQueuedEvents() {
|
|
2010
|
-
const now =
|
|
2037
|
+
const now = this.clock();
|
|
2011
2038
|
const mapEvent = (evt) => ({
|
|
2012
2039
|
id: evt.id,
|
|
2013
2040
|
event: evt.eventName,
|
|
@@ -2118,57 +2145,59 @@ var StateMachine = class _StateMachine {
|
|
|
2118
2145
|
if (!currentState) return expectedState === "";
|
|
2119
2146
|
const currentParts = currentState.split("|").sort();
|
|
2120
2147
|
const expectedParts = expectedState.split("|").sort();
|
|
2148
|
+
const ancestorMatch = expectedParts.every(
|
|
2149
|
+
(expectedPart) => currentParts.some((leaf) => this.isParentState(expectedPart, leaf))
|
|
2150
|
+
);
|
|
2151
|
+
if (ancestorMatch) return true;
|
|
2121
2152
|
if (currentParts.length !== expectedParts.length) return false;
|
|
2122
2153
|
return currentParts.every((part, index) => part === expectedParts[index]);
|
|
2123
2154
|
}
|
|
2124
2155
|
attachToObject(object, eventMap) {
|
|
2125
|
-
for (const objectEventName
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
};
|
|
2171
|
-
}
|
|
2156
|
+
for (const objectEventName of Object.keys(eventMap)) {
|
|
2157
|
+
const stateMachineEventName = eventMap[objectEventName];
|
|
2158
|
+
if (stateMachineEventName === void 0) continue;
|
|
2159
|
+
if (typeof object.addEventListener === "function") {
|
|
2160
|
+
object.addEventListener(objectEventName, (...args) => {
|
|
2161
|
+
this.fireEvent(stateMachineEventName, object, ...args).catch(
|
|
2162
|
+
(e) => this.logger.error(
|
|
2163
|
+
"Error firing event",
|
|
2164
|
+
{
|
|
2165
|
+
objectEventName,
|
|
2166
|
+
stateMachineEventName
|
|
2167
|
+
},
|
|
2168
|
+
/* c8 ignore next */
|
|
2169
|
+
e instanceof Error ? e : new Error(String(e))
|
|
2170
|
+
)
|
|
2171
|
+
);
|
|
2172
|
+
});
|
|
2173
|
+
} else if (typeof object.on === "function") {
|
|
2174
|
+
object.on(objectEventName, (...args) => {
|
|
2175
|
+
this.fireEvent(stateMachineEventName, object, ...args).catch(
|
|
2176
|
+
(e) => this.logger.error(
|
|
2177
|
+
"Error firing event",
|
|
2178
|
+
{
|
|
2179
|
+
objectEventName,
|
|
2180
|
+
stateMachineEventName
|
|
2181
|
+
},
|
|
2182
|
+
/* c8 ignore next */
|
|
2183
|
+
e instanceof Error ? e : new Error(String(e))
|
|
2184
|
+
)
|
|
2185
|
+
);
|
|
2186
|
+
});
|
|
2187
|
+
} else {
|
|
2188
|
+
object[`on${objectEventName}`] = async (...args) => {
|
|
2189
|
+
return this.fireEvent(stateMachineEventName, object, ...args).catch(
|
|
2190
|
+
(e) => this.logger.error(
|
|
2191
|
+
"Error firing event",
|
|
2192
|
+
{
|
|
2193
|
+
objectEventName,
|
|
2194
|
+
stateMachineEventName
|
|
2195
|
+
},
|
|
2196
|
+
/* c8 ignore next */
|
|
2197
|
+
e instanceof Error ? e : new Error(String(e))
|
|
2198
|
+
)
|
|
2199
|
+
);
|
|
2200
|
+
};
|
|
2172
2201
|
}
|
|
2173
2202
|
}
|
|
2174
2203
|
}
|
|
@@ -2496,7 +2525,7 @@ var StateMachine = class _StateMachine {
|
|
|
2496
2525
|
const currentState = this.getCurrentState(adaptee) || "";
|
|
2497
2526
|
const currentStateMap = this.parseCompositeState(currentState);
|
|
2498
2527
|
const newStateParts = state.split("|");
|
|
2499
|
-
const isRootState = newStateParts.length === 1 && !state.includes(".");
|
|
2528
|
+
const isRootState = newStateParts.length === 1 && !state.includes(".") && !this.states.get(state)?.regions;
|
|
2500
2529
|
if (isRootState) {
|
|
2501
2530
|
currentStateMap.clear();
|
|
2502
2531
|
currentStateMap.set(state, state);
|
|
@@ -2572,12 +2601,13 @@ var StateMachine = class _StateMachine {
|
|
|
2572
2601
|
initialStates = initialState;
|
|
2573
2602
|
}
|
|
2574
2603
|
this.setCurrentState(initialStates, targetAdaptee);
|
|
2575
|
-
const
|
|
2604
|
+
const { enterStates } = this.computeEnterExitSets("", initialStates);
|
|
2605
|
+
const enterFireOrder = enterStates.length > 0 ? enterStates : [initialStates];
|
|
2576
2606
|
const context = {
|
|
2577
2607
|
state: initialStates,
|
|
2578
2608
|
phase: "enter"
|
|
2579
2609
|
};
|
|
2580
|
-
for (const statePart of
|
|
2610
|
+
for (const statePart of enterFireOrder) {
|
|
2581
2611
|
this.executeEnterActions(
|
|
2582
2612
|
targetAdaptee,
|
|
2583
2613
|
statePart,
|
|
@@ -2591,6 +2621,11 @@ var StateMachine = class _StateMachine {
|
|
|
2591
2621
|
);
|
|
2592
2622
|
});
|
|
2593
2623
|
}
|
|
2624
|
+
this.checkCompletion(
|
|
2625
|
+
targetAdaptee,
|
|
2626
|
+
"",
|
|
2627
|
+
initialStates
|
|
2628
|
+
);
|
|
2594
2629
|
}
|
|
2595
2630
|
getInitialCompositeState(initialState) {
|
|
2596
2631
|
const stateConfig = this.states.get(initialState);
|
|
@@ -2630,6 +2665,211 @@ var StateMachine = class _StateMachine {
|
|
|
2630
2665
|
}
|
|
2631
2666
|
return regionStates.join("|");
|
|
2632
2667
|
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Whether `leaf` is a UML/SCXML `<final>` atomic state of its region.
|
|
2670
|
+
*
|
|
2671
|
+
* Reads the `final` marker straight from the flattened state map populated by
|
|
2672
|
+
* processStates, so it works for any registered atomic leaf regardless of
|
|
2673
|
+
* nesting depth. Returns `false` for unregistered names and for composites
|
|
2674
|
+
* (the all-regions-final join derives doneness from atomic leaves, not from a
|
|
2675
|
+
* `final` flag on a composite parent).
|
|
2676
|
+
*/
|
|
2677
|
+
isStateFinal(leaf) {
|
|
2678
|
+
return Boolean(this.states.get(leaf)?.final);
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Whether composite `compositeId` has reached its UML/SCXML "done"
|
|
2682
|
+
* configuration: every one of its regions has its active atomic leaf in a
|
|
2683
|
+
* `final` state (recursively, for nested composites).
|
|
2684
|
+
*
|
|
2685
|
+
* D10 (mustFix): doneness is derived by scanning the active atomic `|`-leaves
|
|
2686
|
+
* against the STATIC regions tree (`this.states.get(C)?.regions`), NEVER via a
|
|
2687
|
+
* region-key Map lookup (`configMap.get`) — that map keys leaves by their
|
|
2688
|
+
* deepest region container and so cannot answer "which leaf is active in
|
|
2689
|
+
* region X of composite C". For each region we locate the active leaf via the
|
|
2690
|
+
* `C.region.` dotted prefix; the region is final iff that leaf is `final`, or
|
|
2691
|
+
* the leaf lives under a nested composite that is itself `isCompositeDone`.
|
|
2692
|
+
*
|
|
2693
|
+
* Returns `false` for a non-composite id, for a composite with no active leaf
|
|
2694
|
+
* in some region, or when no substate under it is ever `final` (cheap miss).
|
|
2695
|
+
*/
|
|
2696
|
+
isCompositeDone(compositeId, atomicLeaves) {
|
|
2697
|
+
const regions = this.states.get(compositeId)?.regions;
|
|
2698
|
+
if (!regions) return false;
|
|
2699
|
+
for (const regionName of Object.keys(regions)) {
|
|
2700
|
+
const regionPrefix = `${compositeId}.${regionName}.`;
|
|
2701
|
+
const activeLeaf = atomicLeaves.find(
|
|
2702
|
+
(leaf) => leaf.startsWith(regionPrefix)
|
|
2703
|
+
);
|
|
2704
|
+
if (!activeLeaf) return false;
|
|
2705
|
+
const nestedComposite = this.regionComposite(activeLeaf, regionPrefix);
|
|
2706
|
+
if (nestedComposite) {
|
|
2707
|
+
if (this.isCompositeDone(nestedComposite, atomicLeaves)) continue;
|
|
2708
|
+
return false;
|
|
2709
|
+
}
|
|
2710
|
+
if (this.isStateFinal(activeLeaf)) continue;
|
|
2711
|
+
return false;
|
|
2712
|
+
}
|
|
2713
|
+
return true;
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* The OUTERMOST registered composite (regions-bearing) ancestor of `leaf`
|
|
2717
|
+
* that lives strictly under `regionPrefix`, or `undefined` if the region
|
|
2718
|
+
* holds a simple atomic state directly. Used by {@link isCompositeDone} to
|
|
2719
|
+
* delegate a region's completeness to its nested composite (recursing over
|
|
2720
|
+
* every parallel branch), independent of whether any single branch leaf
|
|
2721
|
+
* happens to carry the `final` flag.
|
|
2722
|
+
*
|
|
2723
|
+
* ancestorChain is root-to-leaf, so the FIRST matching ancestor is the
|
|
2724
|
+
* region's direct composite child (e.g. `C.p.D` for leaf `C.p.D.s.s2` under
|
|
2725
|
+
* region prefix `C.p.`); isCompositeDone then recurses into its regions.
|
|
2726
|
+
*/
|
|
2727
|
+
regionComposite(leaf, regionPrefix) {
|
|
2728
|
+
for (const ancestor of this.ancestorChain(leaf)) {
|
|
2729
|
+
if (ancestor !== leaf && ancestor.startsWith(regionPrefix) && this.states.get(ancestor)?.regions) {
|
|
2730
|
+
return ancestor;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
return void 0;
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Whether composite `compositeId` has reached its all-regions-final ("done")
|
|
2737
|
+
* configuration in the CURRENT active state. Public guard surface (`@stable`)
|
|
2738
|
+
* for authoring a join as `guard: () => sm.isDone('C')` instead of (or
|
|
2739
|
+
* alongside) listening on the engine `done.state.<C>` event.
|
|
2740
|
+
*
|
|
2741
|
+
* @param compositeId - The dotted id of the composite/parallel state.
|
|
2742
|
+
* @returns `true` iff every region's active atomic leaf is `final`.
|
|
2743
|
+
*/
|
|
2744
|
+
isDone(compositeId, adaptee) {
|
|
2745
|
+
const currentState = this.getCurrentState(adaptee);
|
|
2746
|
+
if (!currentState) return false;
|
|
2747
|
+
const atomicLeaves = currentState.split("|").filter(Boolean);
|
|
2748
|
+
return this.isCompositeDone(compositeId, atomicLeaves);
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* SCXML completion hook (D10/D11/D12): after a new configuration is written,
|
|
2752
|
+
* raise `done.state.<C>` for each composite that became all-regions-final.
|
|
2753
|
+
*
|
|
2754
|
+
* - Scans only composites that GAINED an active leaf (the ancestor chain of
|
|
2755
|
+
* each `|`-leaf of `newState`), so unaffected composites are not re-checked.
|
|
2756
|
+
* - Emits INNERMOST-first (a deeper composite's `done.state` precedes its
|
|
2757
|
+
* parent's), matching SCXML's inner-before-outer completion ordering, via a
|
|
2758
|
+
* per-config emitted-id Set so each id is raised at most once per call.
|
|
2759
|
+
* - Gates each emission on `this.events.has('done.state.'+C)` (D11 mustFix):
|
|
2760
|
+
* raising an undeclared event would hit the `Invalid event` throw
|
|
2761
|
+
* (executeQueuedTransition) as an unhandled microtask rejection. No declared
|
|
2762
|
+
* `done.state.<C>` event => no emission, no crash, no observable effect.
|
|
2763
|
+
* - Uses the internal queue (`raiseEvent`) + `scheduleProcessing` so the
|
|
2764
|
+
* completion event is processed before subsequent external events.
|
|
2765
|
+
*/
|
|
2766
|
+
checkCompletion(obj, oldState, newState) {
|
|
2767
|
+
const atomicLeaves = newState.split("|").filter(Boolean);
|
|
2768
|
+
if (atomicLeaves.length === 0) return;
|
|
2769
|
+
const oldLeaves = oldState.split("|").filter(Boolean);
|
|
2770
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2771
|
+
for (const leaf of atomicLeaves) {
|
|
2772
|
+
for (const ancestor of this.ancestorChain(leaf)) {
|
|
2773
|
+
if (ancestor === leaf) continue;
|
|
2774
|
+
if (seen.has(ancestor)) continue;
|
|
2775
|
+
if (!this.states.get(ancestor)?.regions) continue;
|
|
2776
|
+
seen.add(ancestor);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
const candidates = Array.from(seen).sort(
|
|
2780
|
+
(a, b) => b.split(".").length - a.split(".").length
|
|
2781
|
+
);
|
|
2782
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2783
|
+
for (const compositeId of candidates) {
|
|
2784
|
+
if (emitted.has(compositeId)) continue;
|
|
2785
|
+
if (!this.isCompositeDone(compositeId, atomicLeaves)) continue;
|
|
2786
|
+
if (this.isCompositeDone(compositeId, oldLeaves)) continue;
|
|
2787
|
+
emitted.add(compositeId);
|
|
2788
|
+
const doneEvent = `done.state.${compositeId}`;
|
|
2789
|
+
if (!this.events.has(doneEvent)) continue;
|
|
2790
|
+
this.raiseEvent(doneEvent, obj);
|
|
2791
|
+
this.scheduleProcessing();
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Build the registered ancestor chain for an atomic leaf, ordered root-to-leaf.
|
|
2796
|
+
*
|
|
2797
|
+
* Walks every dot-prefix of `leaf` and keeps only the ones that are real
|
|
2798
|
+
* registered states (`this.states.has`). Region containers are never
|
|
2799
|
+
* registered (only composite parents and atomic leaves are — see
|
|
2800
|
+
* processStates/processRegions), so they are filtered out automatically. The
|
|
2801
|
+
* result is exactly `[parent..leaf]` for a leaf inside a composite, and
|
|
2802
|
+
* `[leaf]` for a flat state.
|
|
2803
|
+
*
|
|
2804
|
+
* Example: `ancestorChain('a.r1.c1')` -> `['a', 'a.r1.c1']` (the `a.r1`
|
|
2805
|
+
* region container is excluded). Nested:
|
|
2806
|
+
* `ancestorChain('a.r1.c1.r3.x')` -> `['a', 'a.r1.c1', 'a.r1.c1.r3.x']`.
|
|
2807
|
+
*/
|
|
2808
|
+
ancestorChain(leaf) {
|
|
2809
|
+
const chain = [];
|
|
2810
|
+
let dotIndex = leaf.indexOf(".");
|
|
2811
|
+
while (dotIndex !== -1) {
|
|
2812
|
+
const prefix = leaf.substring(0, dotIndex);
|
|
2813
|
+
if (this.states.has(prefix)) {
|
|
2814
|
+
chain.push(prefix);
|
|
2815
|
+
}
|
|
2816
|
+
dotIndex = leaf.indexOf(".", dotIndex + 1);
|
|
2817
|
+
}
|
|
2818
|
+
if (this.states.has(leaf)) {
|
|
2819
|
+
chain.push(leaf);
|
|
2820
|
+
}
|
|
2821
|
+
return chain;
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Compute the ordered enter/exit sets between two composite configurations
|
|
2825
|
+
* (SCXML ancestor-first entry / descendant-first exit).
|
|
2826
|
+
*
|
|
2827
|
+
* For each `|`-separated atomic leaf in `oldComposite` and `newComposite` the
|
|
2828
|
+
* union of its {@link ancestorChain} forms the old/new active ancestry. A
|
|
2829
|
+
* state shared by both ancestries lands in NEITHER diff, so a surviving
|
|
2830
|
+
* ancestor is never re-entered nor exited (no onEnter/onExit re-fire, no
|
|
2831
|
+
* timer re-arm/leak).
|
|
2832
|
+
*
|
|
2833
|
+
* - `enterStates` = new ancestry MINUS old, sorted ascending by depth then
|
|
2834
|
+
* document (insertion) order -> root-to-leaf entry.
|
|
2835
|
+
* - `exitStates` = old ancestry MINUS new, sorted descending by depth ->
|
|
2836
|
+
* leaf-to-root exit.
|
|
2837
|
+
*
|
|
2838
|
+
* Consumed by BOTH R1 (applyTransition / setInitialState / reset) and R2.
|
|
2839
|
+
*/
|
|
2840
|
+
computeEnterExitSets(oldComposite, newComposite) {
|
|
2841
|
+
const collectAncestry = (composite) => {
|
|
2842
|
+
const ancestry = /* @__PURE__ */ new Set();
|
|
2843
|
+
if (!composite) return ancestry;
|
|
2844
|
+
for (const leaf of composite.split("|")) {
|
|
2845
|
+
if (!leaf) continue;
|
|
2846
|
+
for (const ancestor of this.ancestorChain(leaf)) {
|
|
2847
|
+
ancestry.add(ancestor);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
return ancestry;
|
|
2851
|
+
};
|
|
2852
|
+
const oldAncestry = collectAncestry(oldComposite);
|
|
2853
|
+
const newAncestry = collectAncestry(newComposite);
|
|
2854
|
+
const depthOf = (state) => {
|
|
2855
|
+
let depth = 0;
|
|
2856
|
+
for (let i = 0; i < state.length; i++) {
|
|
2857
|
+
if (state[i] === ".") depth++;
|
|
2858
|
+
}
|
|
2859
|
+
return depth;
|
|
2860
|
+
};
|
|
2861
|
+
const enterRaw = [];
|
|
2862
|
+
for (const state of newAncestry) {
|
|
2863
|
+
if (!oldAncestry.has(state)) enterRaw.push(state);
|
|
2864
|
+
}
|
|
2865
|
+
const exitRaw = [];
|
|
2866
|
+
for (const state of oldAncestry) {
|
|
2867
|
+
if (!newAncestry.has(state)) exitRaw.push(state);
|
|
2868
|
+
}
|
|
2869
|
+
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);
|
|
2870
|
+
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);
|
|
2871
|
+
return { enterStates, exitStates };
|
|
2872
|
+
}
|
|
2633
2873
|
validateCompositeState(compositeState) {
|
|
2634
2874
|
const stateParts = compositeState.split("|");
|
|
2635
2875
|
const regionKeys = /* @__PURE__ */ new Set();
|
|
@@ -2703,10 +2943,11 @@ var StateMachine = class _StateMachine {
|
|
|
2703
2943
|
return (...args) => {
|
|
2704
2944
|
const targetAdaptee = args.length >= 2 ? args[0] : adaptee;
|
|
2705
2945
|
const error = args.length >= 2 ? args[1] : args[0];
|
|
2706
|
-
return handler(targetAdaptee, error);
|
|
2946
|
+
return handler(this.resolveCallbackOwner(targetAdaptee), error);
|
|
2707
2947
|
};
|
|
2708
2948
|
}
|
|
2709
2949
|
async callAction(obj, actionName, ...args) {
|
|
2950
|
+
const targetOwner = this.resolveCallbackOwner(obj);
|
|
2710
2951
|
const _callActionState = this.getCurrentState(
|
|
2711
2952
|
obj
|
|
2712
2953
|
);
|
|
@@ -2721,16 +2962,16 @@ var StateMachine = class _StateMachine {
|
|
|
2721
2962
|
if (this.context && this.context[actionName]) {
|
|
2722
2963
|
const action = this.context[actionName];
|
|
2723
2964
|
if (typeof action === "function") {
|
|
2724
|
-
const result = action(
|
|
2965
|
+
const result = action(targetOwner, ...args);
|
|
2725
2966
|
return result instanceof Promise ? await result : result;
|
|
2726
2967
|
}
|
|
2727
2968
|
} else if (typeof actionName === "function") {
|
|
2728
|
-
const result = actionName(
|
|
2969
|
+
const result = actionName(targetOwner, ...args);
|
|
2729
2970
|
return result instanceof Promise ? await result : result;
|
|
2730
2971
|
} else if (obj.get(actionName)) {
|
|
2731
2972
|
const action = obj.get(actionName);
|
|
2732
2973
|
if (typeof action === "function") {
|
|
2733
|
-
const result = action(
|
|
2974
|
+
const result = action(targetOwner, ...args);
|
|
2734
2975
|
return result instanceof Promise ? await result : result;
|
|
2735
2976
|
}
|
|
2736
2977
|
}
|
|
@@ -2746,19 +2987,22 @@ var StateMachine = class _StateMachine {
|
|
|
2746
2987
|
}
|
|
2747
2988
|
};
|
|
2748
2989
|
if (this.transitionTimeout && this.transitionTimeout > 0) {
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
)
|
|
2761
|
-
|
|
2990
|
+
const timeoutMs = this.transitionTimeout;
|
|
2991
|
+
let timeoutHandle;
|
|
2992
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2993
|
+
const fire = () => reject(new StateMachineError("Transition timeout", {
|
|
2994
|
+
/* c8 ignore next */
|
|
2995
|
+
action: typeof actionName === "string" ? actionName : "anonymous",
|
|
2996
|
+
phase: "action"
|
|
2997
|
+
}));
|
|
2998
|
+
timeoutHandle = this.setTimer(fire, timeoutMs);
|
|
2999
|
+
});
|
|
3000
|
+
if (this.schedulerProvided) {
|
|
3001
|
+
return Promise.race([executeAction(), timeoutPromise]).finally(() => {
|
|
3002
|
+
this.clearTimer(timeoutHandle);
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
return Promise.race([executeAction(), timeoutPromise]);
|
|
2762
3006
|
}
|
|
2763
3007
|
return executeAction();
|
|
2764
3008
|
}
|
|
@@ -2802,8 +3046,12 @@ var StateMachine = class _StateMachine {
|
|
|
2802
3046
|
if (fromState === "*") return true;
|
|
2803
3047
|
const regionKey = this.getRegionKey(fromState);
|
|
2804
3048
|
const currentStateForRegion = currentStates.get(regionKey);
|
|
2805
|
-
if (
|
|
2806
|
-
|
|
3049
|
+
if (currentStateForRegion && (currentStateForRegion === fromState || this.isParentState(currentStateForRegion, fromState))) {
|
|
3050
|
+
return true;
|
|
3051
|
+
}
|
|
3052
|
+
return Array.from(currentStates.values()).some(
|
|
3053
|
+
(leaf) => leaf === fromState || this.isParentState(fromState, leaf)
|
|
3054
|
+
);
|
|
2807
3055
|
});
|
|
2808
3056
|
}
|
|
2809
3057
|
isParentState(parentState, childState) {
|
|
@@ -2819,6 +3067,26 @@ var StateMachine = class _StateMachine {
|
|
|
2819
3067
|
const targetState = transition.to === "*" ? currentState : transition.to;
|
|
2820
3068
|
this._isTransitioning = true;
|
|
2821
3069
|
this._targetState = targetState;
|
|
3070
|
+
let newState;
|
|
3071
|
+
let enterStates;
|
|
3072
|
+
let exitStates;
|
|
3073
|
+
try {
|
|
3074
|
+
newState = this.updateState(currentState, targetState);
|
|
3075
|
+
const sets = this.computeEnterExitSets(currentState, newState);
|
|
3076
|
+
enterStates = sets.enterStates;
|
|
3077
|
+
exitStates = sets.exitStates;
|
|
3078
|
+
} catch (error) {
|
|
3079
|
+
this.logger.warn("Transition aborted: invalid target configuration", {
|
|
3080
|
+
state: currentState,
|
|
3081
|
+
target: String(targetState),
|
|
3082
|
+
error
|
|
3083
|
+
});
|
|
3084
|
+
this._isTransitioning = false;
|
|
3085
|
+
this._targetState = void 0;
|
|
3086
|
+
return void 0;
|
|
3087
|
+
}
|
|
3088
|
+
const exitFireOrder = exitStates.length > 0 ? exitStates : [transition.from];
|
|
3089
|
+
const enterFireOrder = enterStates.length > 0 ? enterStates : [transition.to];
|
|
2822
3090
|
try {
|
|
2823
3091
|
if (transition.guard) {
|
|
2824
3092
|
const allow = await this.callAction(obj, transition.guard, ...args).catch(
|
|
@@ -2844,7 +3112,9 @@ var StateMachine = class _StateMachine {
|
|
|
2844
3112
|
);
|
|
2845
3113
|
}
|
|
2846
3114
|
try {
|
|
2847
|
-
|
|
3115
|
+
for (const exitStateName of exitFireOrder) {
|
|
3116
|
+
await this.executeExitActions(obj, exitStateName, args, context);
|
|
3117
|
+
}
|
|
2848
3118
|
} catch (error) {
|
|
2849
3119
|
if (this.abortOnExitError) {
|
|
2850
3120
|
this.logger.warn("Transition aborted due to onExit error", { state: currentState, error });
|
|
@@ -2864,12 +3134,14 @@ var StateMachine = class _StateMachine {
|
|
|
2864
3134
|
);
|
|
2865
3135
|
}
|
|
2866
3136
|
try {
|
|
2867
|
-
|
|
3137
|
+
for (const enterStateName of enterFireOrder) {
|
|
3138
|
+
await this.executeEnterActions(obj, enterStateName, args, context);
|
|
3139
|
+
}
|
|
2868
3140
|
} catch (error) {
|
|
2869
3141
|
if (this.errorState) {
|
|
2870
3142
|
this.logger.error(`Failed to enter state '${targetState}'. Fallback to error state '${this.errorState}'`, { error });
|
|
2871
|
-
const
|
|
2872
|
-
this.setCurrentState(
|
|
3143
|
+
const errorNewState = this.updateState(currentState, this.errorState);
|
|
3144
|
+
this.setCurrentState(errorNewState, obj);
|
|
2873
3145
|
return this.states.get(this.errorState);
|
|
2874
3146
|
}
|
|
2875
3147
|
throw error;
|
|
@@ -2884,12 +3156,12 @@ var StateMachine = class _StateMachine {
|
|
|
2884
3156
|
event.onError,
|
|
2885
3157
|
this.onError
|
|
2886
3158
|
);
|
|
2887
|
-
errorHandler(obj, error);
|
|
3159
|
+
errorHandler(this.resolveCallbackOwner(obj), error);
|
|
2888
3160
|
}
|
|
2889
3161
|
}
|
|
2890
3162
|
const transitionStartTime = Date.now();
|
|
2891
|
-
const newState = this.updateState(currentState, targetState);
|
|
2892
3163
|
this.setCurrentState(newState, obj);
|
|
3164
|
+
this.checkCompletion(obj, currentState, newState);
|
|
2893
3165
|
const transitionTime = Date.now() - transitionStartTime;
|
|
2894
3166
|
this.monitor.recordTransition(transitionTime, true);
|
|
2895
3167
|
return this.states.get(targetState);
|
|
@@ -2947,7 +3219,7 @@ var StateMachine = class _StateMachine {
|
|
|
2947
3219
|
}
|
|
2948
3220
|
if (toState.invoke && toState.invoke.length > 0) {
|
|
2949
3221
|
if (!this.stateEntryTimes.has(toStateName)) {
|
|
2950
|
-
this.stateEntryTimes.set(toStateName,
|
|
3222
|
+
this.stateEntryTimes.set(toStateName, this.clock());
|
|
2951
3223
|
}
|
|
2952
3224
|
const timers = [];
|
|
2953
3225
|
for (const invocation of toState.invoke) {
|
|
@@ -2993,6 +3265,9 @@ var StateMachine = class _StateMachine {
|
|
|
2993
3265
|
*/
|
|
2994
3266
|
setTimer(callback, delay) {
|
|
2995
3267
|
const scheduler = this.scheduler;
|
|
3268
|
+
if (this.schedulerProvided) {
|
|
3269
|
+
return scheduler.schedule(delay, callback);
|
|
3270
|
+
}
|
|
2996
3271
|
if (scheduler.isActive()) {
|
|
2997
3272
|
return scheduler.schedule(delay, callback);
|
|
2998
3273
|
}
|
|
@@ -3003,6 +3278,10 @@ var StateMachine = class _StateMachine {
|
|
|
3003
3278
|
*/
|
|
3004
3279
|
clearTimer(timerId) {
|
|
3005
3280
|
const scheduler = this.scheduler;
|
|
3281
|
+
if (this.schedulerProvided) {
|
|
3282
|
+
if (timerId !== void 0) scheduler.cancel(timerId);
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3006
3285
|
if (scheduler.isActive() && typeof timerId === "object" && timerId !== null && !("ref" in timerId)) {
|
|
3007
3286
|
scheduler.cancel(timerId);
|
|
3008
3287
|
} else {
|
|
@@ -3065,7 +3344,7 @@ var StateMachine = class _StateMachine {
|
|
|
3065
3344
|
updateState(currentState, toState) {
|
|
3066
3345
|
const currentStateMap = this.parseCompositeState(currentState);
|
|
3067
3346
|
const toStateParts = toState.split("|");
|
|
3068
|
-
if (toStateParts.length === 1 && !toState.includes(".")) {
|
|
3347
|
+
if (toStateParts.length === 1 && !toState.includes(".") && !this.states.get(toState)?.regions) {
|
|
3069
3348
|
currentStateMap.clear();
|
|
3070
3349
|
currentStateMap.set(toState, toState);
|
|
3071
3350
|
return toState;
|
|
@@ -3160,13 +3439,13 @@ var StateMachine = class _StateMachine {
|
|
|
3160
3439
|
this.stateEntryTimes.delete(stateName);
|
|
3161
3440
|
}
|
|
3162
3441
|
}
|
|
3163
|
-
const now =
|
|
3442
|
+
const now = this.clock();
|
|
3164
3443
|
for (const stateName of activeStates) {
|
|
3165
3444
|
const state = this.states.get(stateName);
|
|
3166
3445
|
if (!state || !state.invoke || state.invoke.length === 0) continue;
|
|
3167
3446
|
const entryTime = this.stateEntryTimes.get(stateName);
|
|
3168
|
-
const startTime = entryTime
|
|
3169
|
-
if (
|
|
3447
|
+
const startTime = entryTime ?? now;
|
|
3448
|
+
if (entryTime === void 0) {
|
|
3170
3449
|
this.stateEntryTimes.set(stateName, startTime);
|
|
3171
3450
|
}
|
|
3172
3451
|
const elapsed = now - startTime;
|
|
@@ -3447,6 +3726,7 @@ var ConfigValidator = class {
|
|
|
3447
3726
|
this.validateInitialState(smConfig);
|
|
3448
3727
|
this.validateCrossReferences(smConfig);
|
|
3449
3728
|
this.validatePerformanceConstraints(smConfig);
|
|
3729
|
+
this.validateFinalStates(smConfig);
|
|
3450
3730
|
this.runCustomRules(smConfig);
|
|
3451
3731
|
} catch (error) {
|
|
3452
3732
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -3808,6 +4088,10 @@ var ConfigValidator = class {
|
|
|
3808
4088
|
}
|
|
3809
4089
|
}
|
|
3810
4090
|
}
|
|
4091
|
+
const finalStatePaths = this.collectFinalStatePaths(smConfig.states);
|
|
4092
|
+
for (const finalPath of finalStatePaths) {
|
|
4093
|
+
reachableStates.add(finalPath);
|
|
4094
|
+
}
|
|
3811
4095
|
for (const stateName of allStateNames) {
|
|
3812
4096
|
if (!reachableStates.has(stateName)) {
|
|
3813
4097
|
this.addWarning(
|
|
@@ -3818,8 +4102,9 @@ var ConfigValidator = class {
|
|
|
3818
4102
|
}
|
|
3819
4103
|
}
|
|
3820
4104
|
for (const [eventName, event] of Object.entries(smConfig.events)) {
|
|
4105
|
+
const isDoneStateEvent = eventName.startsWith("done.state.");
|
|
3821
4106
|
const hasValidTransitions = event.transitions.some(
|
|
3822
|
-
(t) => allStateNames.has(t.from) && allStateNames.has(t.to)
|
|
4107
|
+
(t) => (allStateNames.has(t.from) || isDoneStateEvent && allStateNames.has(eventName.slice("done.state.".length))) && allStateNames.has(t.to)
|
|
3823
4108
|
);
|
|
3824
4109
|
if (!hasValidTransitions) {
|
|
3825
4110
|
this.addWarning(
|
|
@@ -3847,6 +4132,168 @@ var ConfigValidator = class {
|
|
|
3847
4132
|
}
|
|
3848
4133
|
}
|
|
3849
4134
|
}
|
|
4135
|
+
/**
|
|
4136
|
+
* Collects every state in the tree as {dottedPath, config} entries, walking
|
|
4137
|
+
* region containers (region names are NOT registered states, so they are part
|
|
4138
|
+
* of the path but never emitted as an entry — mirroring runtime expansion).
|
|
4139
|
+
*/
|
|
4140
|
+
collectStateEntries(states, prefix, collector) {
|
|
4141
|
+
for (const [stateName, state] of Object.entries(states)) {
|
|
4142
|
+
const fullName = prefix ? `${prefix}.${stateName}` : stateName;
|
|
4143
|
+
collector.push({ path: fullName, state });
|
|
4144
|
+
if (state.regions) {
|
|
4145
|
+
for (const [regionName, regionStates] of Object.entries(
|
|
4146
|
+
state.regions
|
|
4147
|
+
)) {
|
|
4148
|
+
this.collectStateEntries(
|
|
4149
|
+
regionStates,
|
|
4150
|
+
`${fullName}.${regionName}`,
|
|
4151
|
+
collector
|
|
4152
|
+
);
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
/**
|
|
4158
|
+
* True when `composite` has at least one `final:true` atomic substate reachable
|
|
4159
|
+
* through its (possibly nested) region tree — i.e. some region of the composite
|
|
4160
|
+
* can become done. Used by REGION_NO_REACHABLE_FINAL.
|
|
4161
|
+
*/
|
|
4162
|
+
hasReachableFinal(composite) {
|
|
4163
|
+
if (!composite.regions) return false;
|
|
4164
|
+
for (const regionStates of Object.values(composite.regions)) {
|
|
4165
|
+
for (const state of Object.values(regionStates)) {
|
|
4166
|
+
if (state.final) return true;
|
|
4167
|
+
if (state.regions && this.hasReachableFinal(state)) return true;
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
return false;
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* SCXML/UML final-state and all-regions-final (done.state.<C>) join validation.
|
|
4174
|
+
*
|
|
4175
|
+
* - FINAL_STATE_HAS_OUTGOING (error): a `final:true` state is the `from` of a
|
|
4176
|
+
* transition (a final pseudo-state cannot itself transition out).
|
|
4177
|
+
* - FINAL_ON_COMPOSITE (warning): `final:true` on a state that has regions
|
|
4178
|
+
* (final is meaningful only on an atomic leaf; ignored at runtime).
|
|
4179
|
+
* - REGION_NO_REACHABLE_FINAL (warning): a `done.state.<C>` transition exists
|
|
4180
|
+
* but composite C declares no `final` substate, so the join can never fire.
|
|
4181
|
+
* - DONE_VS_PARALLEL_EXIT_AMBIGUITY (warning): the same composite C has BOTH a
|
|
4182
|
+
* `done.state.C` join AND a plain `from:'C'` user-event parallel-exit, which
|
|
4183
|
+
* can preempt the all-final join (the user event is eligible on ANY leaf).
|
|
4184
|
+
* - REGION_MISSING_INITIAL (advisory warning, valid:true): a region omits an
|
|
4185
|
+
* explicit `initial`, so expansion relies on first-key insertion order.
|
|
4186
|
+
*/
|
|
4187
|
+
validateFinalStates(smConfig) {
|
|
4188
|
+
const entries = [];
|
|
4189
|
+
this.collectStateEntries(smConfig.states, "", entries);
|
|
4190
|
+
const finalStatePaths = /* @__PURE__ */ new Set();
|
|
4191
|
+
for (const { path, state } of entries) {
|
|
4192
|
+
if (!state.final) continue;
|
|
4193
|
+
if (state.regions) {
|
|
4194
|
+
this.addWarning(
|
|
4195
|
+
"FINAL_ON_COMPOSITE",
|
|
4196
|
+
`Final flag on composite state "${path}" is ignored; mark an atomic leaf inside a region as final instead`,
|
|
4197
|
+
`states.${path}.final`,
|
|
4198
|
+
void 0,
|
|
4199
|
+
"Remove `final: true` from this composite and set it on the atomic leaf state that represents the region's completion."
|
|
4200
|
+
);
|
|
4201
|
+
} else {
|
|
4202
|
+
finalStatePaths.add(path);
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
const userFromStates = /* @__PURE__ */ new Set();
|
|
4206
|
+
const doneStateComposites = /* @__PURE__ */ new Set();
|
|
4207
|
+
for (const [eventName, event] of Object.entries(smConfig.events)) {
|
|
4208
|
+
if (!event.transitions || !Array.isArray(event.transitions)) continue;
|
|
4209
|
+
if (eventName.startsWith("done.state.")) {
|
|
4210
|
+
const compositeId = eventName.slice("done.state.".length);
|
|
4211
|
+
if (compositeId) doneStateComposites.add(compositeId);
|
|
4212
|
+
}
|
|
4213
|
+
for (const transition of event.transitions) {
|
|
4214
|
+
if (typeof transition.from !== "string") continue;
|
|
4215
|
+
for (const fromPart of transition.from.split("|")) {
|
|
4216
|
+
if (finalStatePaths.has(fromPart)) {
|
|
4217
|
+
this.addError(
|
|
4218
|
+
"FINAL_STATE_HAS_OUTGOING",
|
|
4219
|
+
`Final state "${fromPart}" cannot be the source of transition on event "${eventName}"; a final pseudo-state has no outgoing transitions`,
|
|
4220
|
+
`events.${eventName}`,
|
|
4221
|
+
void 0,
|
|
4222
|
+
`Remove the outgoing transition from "${fromPart}", or clear its \`final: true\` flag if it is not actually a final state.`
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
4225
|
+
if (!eventName.startsWith("done.state.")) {
|
|
4226
|
+
userFromStates.add(fromPart);
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
const entryByPath = new Map(entries.map((e) => [e.path, e.state]));
|
|
4232
|
+
for (const compositeId of doneStateComposites) {
|
|
4233
|
+
const composite = entryByPath.get(compositeId);
|
|
4234
|
+
if (!composite) {
|
|
4235
|
+
continue;
|
|
4236
|
+
}
|
|
4237
|
+
if (!this.hasReachableFinal(composite)) {
|
|
4238
|
+
this.addWarning(
|
|
4239
|
+
"REGION_NO_REACHABLE_FINAL",
|
|
4240
|
+
`Join event "done.state.${compositeId}" can never fire: composite "${compositeId}" has no \`final\` substate in any region`,
|
|
4241
|
+
`events.done.state.${compositeId}`,
|
|
4242
|
+
void 0,
|
|
4243
|
+
`Mark the completion leaf of each region under "${compositeId}" with \`final: true\`, or remove the done.state join.`
|
|
4244
|
+
);
|
|
4245
|
+
}
|
|
4246
|
+
if (userFromStates.has(compositeId)) {
|
|
4247
|
+
this.addWarning(
|
|
4248
|
+
"DONE_VS_PARALLEL_EXIT_AMBIGUITY",
|
|
4249
|
+
`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`,
|
|
4250
|
+
`events.done.state.${compositeId}`,
|
|
4251
|
+
void 0,
|
|
4252
|
+
`Disambiguate by using the done.state.${compositeId} join exclusively, or scope the user-event transition to a specific leaf instead of the bare composite.`
|
|
4253
|
+
);
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
for (const { path, state } of entries) {
|
|
4257
|
+
if (!state.regions) continue;
|
|
4258
|
+
for (const [regionName, regionStates] of Object.entries(state.regions)) {
|
|
4259
|
+
const regionKeys = Object.keys(regionStates);
|
|
4260
|
+
if (regionKeys.length === 0) continue;
|
|
4261
|
+
const parentInitialInRegion = state.initial !== void 0 && regionKeys.includes(state.initial);
|
|
4262
|
+
if (!parentInitialInRegion) {
|
|
4263
|
+
this.addWarning(
|
|
4264
|
+
"REGION_MISSING_INITIAL",
|
|
4265
|
+
`Region "${regionName}" of composite "${path}" has no explicit initial; expansion falls back to the first key "${regionKeys[0]}" (insertion-order dependent)`,
|
|
4266
|
+
`states.${path}.regions.${regionName}`,
|
|
4267
|
+
void 0,
|
|
4268
|
+
`Set \`initial\` on composite "${path}" to a leaf of region "${regionName}", to make the starting substate explicit and order-independent.`
|
|
4269
|
+
);
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
/**
|
|
4275
|
+
* Collects the dotted paths of every `final:true` atomic leaf in the tree.
|
|
4276
|
+
*/
|
|
4277
|
+
collectFinalStatePaths(states, prefix = "", collector = /* @__PURE__ */ new Set()) {
|
|
4278
|
+
for (const [stateName, state] of Object.entries(states)) {
|
|
4279
|
+
const fullName = prefix ? `${prefix}.${stateName}` : stateName;
|
|
4280
|
+
if (state.final && !state.regions) {
|
|
4281
|
+
collector.add(fullName);
|
|
4282
|
+
}
|
|
4283
|
+
if (state.regions) {
|
|
4284
|
+
for (const [regionName, regionStates] of Object.entries(
|
|
4285
|
+
state.regions
|
|
4286
|
+
)) {
|
|
4287
|
+
this.collectFinalStatePaths(
|
|
4288
|
+
regionStates,
|
|
4289
|
+
`${fullName}.${regionName}`,
|
|
4290
|
+
collector
|
|
4291
|
+
);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
return collector;
|
|
4296
|
+
}
|
|
3850
4297
|
validatePerformanceConstraints(smConfig) {
|
|
3851
4298
|
const stateCount = this.countStates(smConfig.states);
|
|
3852
4299
|
const eventCount = Object.keys(smConfig.events).length;
|
|
@@ -3934,6 +4381,7 @@ export {
|
|
|
3934
4381
|
StateMachineError,
|
|
3935
4382
|
createEnhancedError,
|
|
3936
4383
|
createMachine,
|
|
4384
|
+
createVirtualScheduler,
|
|
3937
4385
|
isAdapter,
|
|
3938
4386
|
isRecoverableError,
|
|
3939
4387
|
isValidConfig,
|