@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.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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
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 (
|
|
2851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2917
|
-
this.setCurrentState(
|
|
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,
|
|
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 =
|
|
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
|
|
3214
|
-
if (
|
|
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,
|