@flighthq/statechart 0.3.0-next.1728.26853f6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +4 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +4 -0
- package/dist/contract.js.map +1 -0
- package/dist/enableStatechartGuards.d.ts +5 -0
- package/dist/enableStatechartGuards.d.ts.map +1 -0
- package/dist/enableStatechartGuards.js +40 -0
- package/dist/enableStatechartGuards.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/statechart.d.ts +12 -0
- package/dist/statechart.d.ts.map +1 -0
- package/dist/statechart.js +369 -0
- package/dist/statechart.js.map +1 -0
- package/dist/statechartSignals.d.ts +4 -0
- package/dist/statechartSignals.d.ts.map +1 -0
- package/dist/statechartSignals.js +15 -0
- package/dist/statechartSignals.js.map +1 -0
- package/package.json +48 -0
- package/src/enableStatechartGuards.test.ts +96 -0
- package/src/statechart.test.ts +435 -0
- package/src/statechartSignals.test.ts +96 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StatechartInstance } from '@flighthq/types/contract';
|
|
2
|
+
export declare function areStatechartGuardsEnabled(instance: Readonly<StatechartInstance>): boolean;
|
|
3
|
+
export declare function disableStatechartGuards(instance: StatechartInstance): void;
|
|
4
|
+
export declare function enableStatechartGuards(instance: StatechartInstance): void;
|
|
5
|
+
//# sourceMappingURL=enableStatechartGuards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableStatechartGuards.d.ts","sourceRoot":"","sources":["../src/enableStatechartGuards.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAmC,MAAM,0BAA0B,CAAC;AAGpG,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAE1F;AAGD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAE1E;AAKD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAEzE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { logOnce } from '@flighthq/log/contract';
|
|
2
|
+
import { LogLevel } from '@flighthq/types/contract';
|
|
3
|
+
// Report whether caller-misuse diagnostics are installed for this actor.
|
|
4
|
+
export function areStatechartGuardsEnabled(instance) {
|
|
5
|
+
return instance.durationGuard === warnMissingStatechartRegionDuration;
|
|
6
|
+
}
|
|
7
|
+
// Remove the opt-in diagnostics hook without changing transition behavior.
|
|
8
|
+
export function disableStatechartGuards(instance) {
|
|
9
|
+
instance.durationGuard = null;
|
|
10
|
+
}
|
|
11
|
+
// Install state-scoped warnings for exit-time transitions whose composition layer never supplied a
|
|
12
|
+
// positive state duration. Core still coerces that gate to no requirement; this module owns the message
|
|
13
|
+
// and @flighthq/log dependency, so an unguarded count/read consumer sheds both completely.
|
|
14
|
+
export function enableStatechartGuards(instance) {
|
|
15
|
+
instance.durationGuard = warnMissingStatechartRegionDuration;
|
|
16
|
+
}
|
|
17
|
+
function warnMissingStatechartRegionDuration(instance, explanation) {
|
|
18
|
+
const chartId = getStatechartGuardChartId(instance.chart);
|
|
19
|
+
logOnce(`statechart:missing-region-duration:${chartId}:${explanation.regionIndex}:${explanation.sourceStateIndex}:${explanation.transitionIndex}`, LogLevel.Warn, {
|
|
20
|
+
exitTimeRatio: instance.chart.regions[explanation.regionIndex]?.states[explanation.sourceStateIndex]?.transitions[explanation.transitionIndex]?.exitTimeRatio ?? -1,
|
|
21
|
+
message: 'advanceStatechartInstance: exitTimeRatio requires a positive region duration; the transition was treated as having no exit-time requirement — call setStatechartRegionDuration when the region enters a state.',
|
|
22
|
+
regionDuration: instance.regionDuration[explanation.regionIndex] ?? -1,
|
|
23
|
+
regionIndex: explanation.regionIndex,
|
|
24
|
+
sourceStateIndex: explanation.sourceStateIndex,
|
|
25
|
+
status: explanation.status,
|
|
26
|
+
targetStateIndex: explanation.targetStateIndex,
|
|
27
|
+
transitionIndex: explanation.transitionIndex,
|
|
28
|
+
}, 'statechart');
|
|
29
|
+
}
|
|
30
|
+
function getStatechartGuardChartId(chart) {
|
|
31
|
+
let id = statechartGuardChartIds.get(chart);
|
|
32
|
+
if (id === undefined) {
|
|
33
|
+
id = nextStatechartGuardChartId++;
|
|
34
|
+
statechartGuardChartIds.set(chart, id);
|
|
35
|
+
}
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
const statechartGuardChartIds = new WeakMap();
|
|
39
|
+
let nextStatechartGuardChartId = 1;
|
|
40
|
+
//# sourceMappingURL=enableStatechartGuards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableStatechartGuards.js","sourceRoot":"","sources":["../src/enableStatechartGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAGpD,yEAAyE;AACzE,MAAM,UAAU,0BAA0B,CAAC,QAAsC;IAC/E,OAAO,QAAQ,CAAC,aAAa,KAAK,mCAAmC,CAAC;AACxE,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,uBAAuB,CAAC,QAA4B;IAClE,QAAQ,CAAC,aAAa,GAAG,IAAI,CAAC;AAChC,CAAC;AAED,mGAAmG;AACnG,wGAAwG;AACxG,2FAA2F;AAC3F,MAAM,UAAU,sBAAsB,CAAC,QAA4B;IACjE,QAAQ,CAAC,aAAa,GAAG,mCAAmC,CAAC;AAC/D,CAAC;AAED,SAAS,mCAAmC,CAC1C,QAAsC,EACtC,WAAsD;IAEtD,MAAM,OAAO,GAAG,yBAAyB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1D,OAAO,CACL,sCAAsC,OAAO,IAAI,WAAW,CAAC,WAAW,IAAI,WAAW,CAAC,gBAAgB,IAAI,WAAW,CAAC,eAAe,EAAE,EACzI,QAAQ,CAAC,IAAI,EACb;QACE,aAAa,EACX,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAChG,WAAW,CAAC,eAAe,CAC5B,EAAE,aAAa,IAAI,CAAC,CAAC;QACxB,OAAO,EACL,gNAAgN;QAClN,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACtE,WAAW,EAAE,WAAW,CAAC,WAAW;QACpC,gBAAgB,EAAE,WAAW,CAAC,gBAAgB;QAC9C,MAAM,EAAE,WAAW,CAAC,MAAM;QAC1B,gBAAgB,EAAE,WAAW,CAAC,gBAAgB;QAC9C,eAAe,EAAE,WAAW,CAAC,eAAe;KAC7C,EACD,YAAY,CACb,CAAC;AACJ,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAuB;IACxD,IAAI,EAAE,GAAG,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC5C,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,EAAE,GAAG,0BAA0B,EAAE,CAAC;QAClC,uBAAuB,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,uBAAuB,GAAG,IAAI,OAAO,EAAkB,CAAC;AAC9D,IAAI,0BAA0B,GAAG,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { areStatechartGuardsEnabled, disableStatechartGuards, enableStatechartGuards } from './enableStatechartGuards';
|
|
2
|
+
export { advanceStatechartInstance, createStatechartInstance, explainStatechartTransition, fireStatechartTrigger, getStatechartInputIndex, getStatechartRegionBlend, getStatechartRegionState, setStatechartBooleanInput, setStatechartNumberInput, setStatechartRegionDuration, } from './statechart';
|
|
3
|
+
export { enableStatechartSignals, getStatechartSignals } from './statechartSignals';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACvH,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,EAC3B,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { areStatechartGuardsEnabled, disableStatechartGuards, enableStatechartGuards } from './enableStatechartGuards';
|
|
2
|
+
export { advanceStatechartInstance, createStatechartInstance, explainStatechartTransition, fireStatechartTrigger, getStatechartInputIndex, getStatechartRegionBlend, getStatechartRegionState, setStatechartBooleanInput, setStatechartNumberInput, setStatechartRegionDuration, } from './statechart';
|
|
3
|
+
export { enableStatechartSignals, getStatechartSignals } from './statechartSignals';
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACvH,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,EAC3B,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Statechart, StatechartInstance, StatechartTransitionExplanation } from '@flighthq/types/contract';
|
|
2
|
+
export declare function advanceStatechartInstance(instance: StatechartInstance, deltaTime: number): number;
|
|
3
|
+
export declare function createStatechartInstance(chart: Readonly<Statechart>): StatechartInstance;
|
|
4
|
+
export declare function explainStatechartTransition(instance: Readonly<StatechartInstance>, regionIndex: number): StatechartTransitionExplanation;
|
|
5
|
+
export declare function fireStatechartTrigger(instance: StatechartInstance, inputIndex: number): void;
|
|
6
|
+
export declare function getStatechartInputIndex(chart: Readonly<Statechart>, name: string): number;
|
|
7
|
+
export declare function getStatechartRegionBlend(instance: Readonly<StatechartInstance>, regionIndex: number): number;
|
|
8
|
+
export declare function getStatechartRegionState(instance: Readonly<StatechartInstance>, regionIndex: number): number;
|
|
9
|
+
export declare function setStatechartBooleanInput(instance: StatechartInstance, inputIndex: number, value: boolean): void;
|
|
10
|
+
export declare function setStatechartNumberInput(instance: StatechartInstance, inputIndex: number, value: number): void;
|
|
11
|
+
export declare function setStatechartRegionDuration(instance: StatechartInstance, regionIndex: number, duration: number): void;
|
|
12
|
+
//# sourceMappingURL=statechart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statechart.d.ts","sourceRoot":"","sources":["../src/statechart.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EAEV,kBAAkB,EAElB,+BAA+B,EAChC,MAAM,0BAA0B,CAAC;AAMlC,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CA0BjG;AAKD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,kBAAkB,CAiCxF;AAKD,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EACtC,WAAW,EAAE,MAAM,GAClB,+BAA+B,CAuFjC;AAID,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAG5F;AAID,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKzF;AAID,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5G;AAGD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5G;AAGD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAGhH;AAID,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAG9G;AAKD,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQrH"}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { StatechartComparison, StatechartInputKind, StatechartTransitionStatus } from '@flighthq/types/contract';
|
|
2
|
+
// Advance every concurrent region by `deltaTime` milliseconds and return how many reached a new
|
|
3
|
+
// state. Each region changes at most once per call. Trigger inputs are edge latches shared by all
|
|
4
|
+
// regions for the whole pass, then cleared, so one fired trigger can independently drive several
|
|
5
|
+
// regions. Blend progress is reported as a number only; this package never samples or blends content.
|
|
6
|
+
export function advanceStatechartInstance(instance, deltaTime) {
|
|
7
|
+
const deltaTimeMs = Number.isFinite(deltaTime) && deltaTime > 0 ? deltaTime : 0;
|
|
8
|
+
let changedRegions = null;
|
|
9
|
+
let changedRegionCount = 0;
|
|
10
|
+
for (let regionIndex = 0; regionIndex < instance.chart.regions.length; regionIndex++) {
|
|
11
|
+
const previousStateIndex = instance.regionStates[regionIndex];
|
|
12
|
+
if (advanceStatechartRegion(instance, regionIndex, deltaTimeMs)) {
|
|
13
|
+
changedRegionCount++;
|
|
14
|
+
// Observer allocation is opt-in and occurs only when a state actually changes. Pairs are
|
|
15
|
+
// [regionIndex, previousStateIndex]; the new state is already in regionStates.
|
|
16
|
+
if (instance.signals !== null)
|
|
17
|
+
(changedRegions ??= []).push(regionIndex, previousStateIndex);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Clear before emitting: a listener may fire a trigger for the NEXT pass, and that edge must not be
|
|
21
|
+
// erased by cleanup from this pass. Deferring every emit also prevents a region-0 listener from
|
|
22
|
+
// mutating an input while later concurrent regions are still evaluating the shared input snapshot.
|
|
23
|
+
clearStatechartTriggers(instance);
|
|
24
|
+
if (changedRegions !== null && instance.signals !== null) {
|
|
25
|
+
for (let i = 0; i < changedRegions.length; i += 2) {
|
|
26
|
+
const regionIndex = changedRegions[i];
|
|
27
|
+
instance.signals.onStateChange.emit(regionIndex, changedRegions[i + 1], instance.regionStates[regionIndex]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return changedRegionCount;
|
|
31
|
+
}
|
|
32
|
+
// Allocate the mutable per-actor half of `chart`. The chart itself is retained by reference and is
|
|
33
|
+
// never mutated; input and region state live in independent typed arrays so one chart can drive many
|
|
34
|
+
// actors. Malformed authored indices/kinds are programmer errors and fail at this construction seam.
|
|
35
|
+
export function createStatechartInstance(chart) {
|
|
36
|
+
validateStatechart(chart);
|
|
37
|
+
const inputValues = new Float64Array(chart.inputs.length);
|
|
38
|
+
for (let inputIndex = 0; inputIndex < chart.inputs.length; inputIndex++) {
|
|
39
|
+
const input = chart.inputs[inputIndex];
|
|
40
|
+
inputValues[inputIndex] =
|
|
41
|
+
input.kind === StatechartInputKind.Boolean ? (input.initialValue === 0 ? 0 : 1) : input.initialValue;
|
|
42
|
+
}
|
|
43
|
+
const regionCount = chart.regions.length;
|
|
44
|
+
const regionBlend = new Float32Array(regionCount);
|
|
45
|
+
const regionDuration = new Float64Array(regionCount);
|
|
46
|
+
const regionElapsed = new Float64Array(regionCount);
|
|
47
|
+
const regionStates = new Int32Array(regionCount);
|
|
48
|
+
const regionTransitions = new Int32Array(regionCount);
|
|
49
|
+
regionTransitions.fill(-1);
|
|
50
|
+
for (let regionIndex = 0; regionIndex < regionCount; regionIndex++) {
|
|
51
|
+
regionStates[regionIndex] = chart.regions[regionIndex].initialStateIndex;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
chart,
|
|
55
|
+
durationGuard: null,
|
|
56
|
+
inputValues,
|
|
57
|
+
regionBlend,
|
|
58
|
+
regionDuration,
|
|
59
|
+
regionElapsed,
|
|
60
|
+
regionStates,
|
|
61
|
+
regionTransitions,
|
|
62
|
+
signals: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Explain what the next advance sees in one region. This is the shakeable diagnostic companion to
|
|
66
|
+
// sentinel/count-based runtime calls: it reports an active blend, the first ready transition, or the
|
|
67
|
+
// first authored transition's blocking condition/exit time without mutating the instance.
|
|
68
|
+
export function explainStatechartTransition(instance, regionIndex) {
|
|
69
|
+
const region = instance.chart.regions[regionIndex];
|
|
70
|
+
if (region === undefined)
|
|
71
|
+
return invalidRegionExplanation(regionIndex);
|
|
72
|
+
const sourceStateIndex = instance.regionStates[regionIndex];
|
|
73
|
+
const state = region.states[sourceStateIndex];
|
|
74
|
+
if (state === undefined)
|
|
75
|
+
return invalidRegionExplanation(regionIndex);
|
|
76
|
+
const activeTransitionIndex = instance.regionTransitions[regionIndex];
|
|
77
|
+
if (activeTransitionIndex >= 0) {
|
|
78
|
+
const activeTransition = state.transitions[activeTransitionIndex];
|
|
79
|
+
if (activeTransition === undefined)
|
|
80
|
+
return invalidRegionExplanation(regionIndex);
|
|
81
|
+
return {
|
|
82
|
+
blend: instance.regionBlend[regionIndex],
|
|
83
|
+
conditionIndex: -1,
|
|
84
|
+
regionIndex,
|
|
85
|
+
sourceStateIndex,
|
|
86
|
+
status: StatechartTransitionStatus.Transitioning,
|
|
87
|
+
targetStateIndex: activeTransition.targetStateIndex,
|
|
88
|
+
transitionIndex: activeTransitionIndex,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (state.transitions.length === 0) {
|
|
92
|
+
return {
|
|
93
|
+
blend: 0,
|
|
94
|
+
conditionIndex: -1,
|
|
95
|
+
regionIndex,
|
|
96
|
+
sourceStateIndex,
|
|
97
|
+
status: StatechartTransitionStatus.NoTransitions,
|
|
98
|
+
targetStateIndex: -1,
|
|
99
|
+
transitionIndex: -1,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
let firstBlocked = null;
|
|
103
|
+
for (let transitionIndex = 0; transitionIndex < state.transitions.length; transitionIndex++) {
|
|
104
|
+
const transition = state.transitions[transitionIndex];
|
|
105
|
+
const conditionIndex = getFirstUnmetCondition(instance, transition);
|
|
106
|
+
if (conditionIndex >= 0) {
|
|
107
|
+
firstBlocked ??= {
|
|
108
|
+
blend: 0,
|
|
109
|
+
conditionIndex,
|
|
110
|
+
regionIndex,
|
|
111
|
+
sourceStateIndex,
|
|
112
|
+
status: StatechartTransitionStatus.ConditionsUnmet,
|
|
113
|
+
targetStateIndex: transition.targetStateIndex,
|
|
114
|
+
transitionIndex,
|
|
115
|
+
};
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const exitStatus = getStatechartExitStatus(instance, regionIndex, transition);
|
|
119
|
+
if (exitStatus === StatechartTransitionStatus.MissingRegionDuration) {
|
|
120
|
+
return {
|
|
121
|
+
blend: 0,
|
|
122
|
+
conditionIndex: -1,
|
|
123
|
+
regionIndex,
|
|
124
|
+
sourceStateIndex,
|
|
125
|
+
status: StatechartTransitionStatus.MissingRegionDuration,
|
|
126
|
+
targetStateIndex: transition.targetStateIndex,
|
|
127
|
+
transitionIndex,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (exitStatus === StatechartTransitionStatus.ExitTimePending) {
|
|
131
|
+
firstBlocked ??= {
|
|
132
|
+
blend: 0,
|
|
133
|
+
conditionIndex: -1,
|
|
134
|
+
regionIndex,
|
|
135
|
+
sourceStateIndex,
|
|
136
|
+
status: StatechartTransitionStatus.ExitTimePending,
|
|
137
|
+
targetStateIndex: transition.targetStateIndex,
|
|
138
|
+
transitionIndex,
|
|
139
|
+
};
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
blend: 0,
|
|
144
|
+
conditionIndex: -1,
|
|
145
|
+
regionIndex,
|
|
146
|
+
sourceStateIndex,
|
|
147
|
+
status: StatechartTransitionStatus.Ready,
|
|
148
|
+
targetStateIndex: transition.targetStateIndex,
|
|
149
|
+
transitionIndex,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return firstBlocked;
|
|
153
|
+
}
|
|
154
|
+
// Latch a Trigger input for the next whole-region advance. Trigger conditions ignore their authored
|
|
155
|
+
// comparison/value and test only this latch. Invalid indices and kind mismatches are programmer errors.
|
|
156
|
+
export function fireStatechartTrigger(instance, inputIndex) {
|
|
157
|
+
assertStatechartInputKind(instance, inputIndex, StatechartInputKind.Trigger);
|
|
158
|
+
instance.inputValues[inputIndex] = 1;
|
|
159
|
+
}
|
|
160
|
+
// Return the first input with `name`, or -1 when it is absent. Authored duplicate names deliberately
|
|
161
|
+
// preserve array/serialization order rather than building hidden lookup state.
|
|
162
|
+
export function getStatechartInputIndex(chart, name) {
|
|
163
|
+
for (let inputIndex = 0; inputIndex < chart.inputs.length; inputIndex++) {
|
|
164
|
+
if (chart.inputs[inputIndex].name === name)
|
|
165
|
+
return inputIndex;
|
|
166
|
+
}
|
|
167
|
+
return -1;
|
|
168
|
+
}
|
|
169
|
+
// Return one concurrent region's target-state blend weight in [0, 1], or -1 for an invalid region.
|
|
170
|
+
// The composition layer decides what the plain number means; statechart performs no animation work.
|
|
171
|
+
export function getStatechartRegionBlend(instance, regionIndex) {
|
|
172
|
+
return regionIndex >= 0 && regionIndex < instance.regionBlend.length ? instance.regionBlend[regionIndex] : -1;
|
|
173
|
+
}
|
|
174
|
+
// Return one concurrent region's current source-state index, or -1 for an invalid region.
|
|
175
|
+
export function getStatechartRegionState(instance, regionIndex) {
|
|
176
|
+
return regionIndex >= 0 && regionIndex < instance.regionStates.length ? instance.regionStates[regionIndex] : -1;
|
|
177
|
+
}
|
|
178
|
+
// Write a Boolean input as the numeric 0/1 vocabulary used by data-only transition conditions.
|
|
179
|
+
export function setStatechartBooleanInput(instance, inputIndex, value) {
|
|
180
|
+
assertStatechartInputKind(instance, inputIndex, StatechartInputKind.Boolean);
|
|
181
|
+
instance.inputValues[inputIndex] = value ? 1 : 0;
|
|
182
|
+
}
|
|
183
|
+
// Write a Number input. Numbers remain exact authored/runtime values; comparisons happen only while
|
|
184
|
+
// selecting a transition.
|
|
185
|
+
export function setStatechartNumberInput(instance, inputIndex, value) {
|
|
186
|
+
assertStatechartInputKind(instance, inputIndex, StatechartInputKind.Number);
|
|
187
|
+
instance.inputValues[inputIndex] = value;
|
|
188
|
+
}
|
|
189
|
+
// Supply the current state's duration for exit-time evaluation, in the same millisecond unit as
|
|
190
|
+
// advanceStatechartInstance. The composition layer owns what the duration means and where it came from;
|
|
191
|
+
// statechart only divides elapsed time by this plain number. Zero explicitly marks duration unavailable.
|
|
192
|
+
export function setStatechartRegionDuration(instance, regionIndex, duration) {
|
|
193
|
+
if (!Number.isInteger(regionIndex) || regionIndex < 0 || regionIndex >= instance.regionDuration.length) {
|
|
194
|
+
throw new RangeError(`Statechart region index ${regionIndex} is out of range`);
|
|
195
|
+
}
|
|
196
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
197
|
+
throw new RangeError('Statechart region duration must be finite and non-negative');
|
|
198
|
+
}
|
|
199
|
+
instance.regionDuration[regionIndex] = duration;
|
|
200
|
+
}
|
|
201
|
+
function advanceStatechartRegion(instance, regionIndex, deltaTimeMs) {
|
|
202
|
+
const region = instance.chart.regions[regionIndex];
|
|
203
|
+
const sourceStateIndex = instance.regionStates[regionIndex];
|
|
204
|
+
const sourceState = region.states[sourceStateIndex];
|
|
205
|
+
if (sourceState === undefined)
|
|
206
|
+
throw new RangeError(`Statechart region ${regionIndex} has an invalid active state`);
|
|
207
|
+
instance.regionElapsed[regionIndex] += deltaTimeMs;
|
|
208
|
+
let transitionIndex = instance.regionTransitions[regionIndex];
|
|
209
|
+
let transition;
|
|
210
|
+
if (transitionIndex >= 0) {
|
|
211
|
+
transition = sourceState.transitions[transitionIndex];
|
|
212
|
+
if (transition === undefined) {
|
|
213
|
+
throw new RangeError(`Statechart region ${regionIndex} has an invalid active transition`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
transitionIndex = getReadyTransitionIndex(instance, regionIndex);
|
|
218
|
+
if (transitionIndex < 0)
|
|
219
|
+
return false;
|
|
220
|
+
transition = sourceState.transitions[transitionIndex];
|
|
221
|
+
instance.regionTransitions[regionIndex] = transitionIndex;
|
|
222
|
+
instance.regionBlend[regionIndex] = 0;
|
|
223
|
+
}
|
|
224
|
+
const durationMs = Number.isFinite(transition.durationMs) && transition.durationMs > 0 ? transition.durationMs : 0;
|
|
225
|
+
const blend = durationMs === 0 ? 1 : Math.min(1, instance.regionBlend[regionIndex] + deltaTimeMs / durationMs);
|
|
226
|
+
instance.regionBlend[regionIndex] = blend;
|
|
227
|
+
if (blend < 1)
|
|
228
|
+
return false;
|
|
229
|
+
instance.regionStates[regionIndex] = transition.targetStateIndex;
|
|
230
|
+
instance.regionTransitions[regionIndex] = -1;
|
|
231
|
+
instance.regionBlend[regionIndex] = 0;
|
|
232
|
+
instance.regionDuration[regionIndex] = 0;
|
|
233
|
+
instance.regionElapsed[regionIndex] = 0;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
function assertStatechartInputKind(instance, inputIndex, expectedKind) {
|
|
237
|
+
const input = instance.chart.inputs[inputIndex];
|
|
238
|
+
if (input === undefined)
|
|
239
|
+
throw new RangeError(`Statechart input index ${inputIndex} is out of range`);
|
|
240
|
+
if (input.kind !== expectedKind) {
|
|
241
|
+
throw new TypeError(`Statechart input ${inputIndex} is ${input.kind}, not ${expectedKind}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function clearStatechartTriggers(instance) {
|
|
245
|
+
for (let inputIndex = 0; inputIndex < instance.chart.inputs.length; inputIndex++) {
|
|
246
|
+
if (instance.chart.inputs[inputIndex].kind === StatechartInputKind.Trigger)
|
|
247
|
+
instance.inputValues[inputIndex] = 0;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function compareStatechartCondition(value, condition) {
|
|
251
|
+
switch (condition.comparison) {
|
|
252
|
+
case StatechartComparison.Equal:
|
|
253
|
+
return value === condition.value;
|
|
254
|
+
case StatechartComparison.GreaterThan:
|
|
255
|
+
return value > condition.value;
|
|
256
|
+
case StatechartComparison.GreaterThanOrEqual:
|
|
257
|
+
return value >= condition.value;
|
|
258
|
+
case StatechartComparison.LessThan:
|
|
259
|
+
return value < condition.value;
|
|
260
|
+
case StatechartComparison.LessThanOrEqual:
|
|
261
|
+
return value <= condition.value;
|
|
262
|
+
case StatechartComparison.NotEqual:
|
|
263
|
+
return value !== condition.value;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function getFirstUnmetCondition(instance, transition) {
|
|
267
|
+
for (let conditionIndex = 0; conditionIndex < transition.conditions.length; conditionIndex++) {
|
|
268
|
+
const condition = transition.conditions[conditionIndex];
|
|
269
|
+
const input = instance.chart.inputs[condition.inputIndex];
|
|
270
|
+
if (input === undefined)
|
|
271
|
+
return conditionIndex;
|
|
272
|
+
const inputValue = instance.inputValues[condition.inputIndex];
|
|
273
|
+
const met = input.kind === StatechartInputKind.Trigger ? inputValue !== 0 : compareStatechartCondition(inputValue, condition);
|
|
274
|
+
if (!met)
|
|
275
|
+
return conditionIndex;
|
|
276
|
+
}
|
|
277
|
+
return -1;
|
|
278
|
+
}
|
|
279
|
+
function getReadyTransitionIndex(instance, regionIndex) {
|
|
280
|
+
const region = instance.chart.regions[regionIndex];
|
|
281
|
+
const state = region.states[instance.regionStates[regionIndex]];
|
|
282
|
+
for (let transitionIndex = 0; transitionIndex < state.transitions.length; transitionIndex++) {
|
|
283
|
+
const transition = state.transitions[transitionIndex];
|
|
284
|
+
if (getFirstUnmetCondition(instance, transition) >= 0)
|
|
285
|
+
continue;
|
|
286
|
+
const exitStatus = getStatechartExitStatus(instance, regionIndex, transition);
|
|
287
|
+
if (exitStatus === StatechartTransitionStatus.ExitTimePending)
|
|
288
|
+
continue;
|
|
289
|
+
if (exitStatus === StatechartTransitionStatus.MissingRegionDuration) {
|
|
290
|
+
instance.durationGuard?.(instance, {
|
|
291
|
+
blend: 0,
|
|
292
|
+
conditionIndex: -1,
|
|
293
|
+
regionIndex,
|
|
294
|
+
sourceStateIndex: instance.regionStates[regionIndex],
|
|
295
|
+
status: StatechartTransitionStatus.MissingRegionDuration,
|
|
296
|
+
targetStateIndex: transition.targetStateIndex,
|
|
297
|
+
transitionIndex,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return transitionIndex;
|
|
301
|
+
}
|
|
302
|
+
return -1;
|
|
303
|
+
}
|
|
304
|
+
function getStatechartExitStatus(instance, regionIndex, transition) {
|
|
305
|
+
if (transition.exitTimeRatio < 0)
|
|
306
|
+
return StatechartTransitionStatus.Ready;
|
|
307
|
+
const duration = instance.regionDuration[regionIndex];
|
|
308
|
+
if (duration <= 0)
|
|
309
|
+
return StatechartTransitionStatus.MissingRegionDuration;
|
|
310
|
+
return instance.regionElapsed[regionIndex] / duration >= transition.exitTimeRatio
|
|
311
|
+
? StatechartTransitionStatus.Ready
|
|
312
|
+
: StatechartTransitionStatus.ExitTimePending;
|
|
313
|
+
}
|
|
314
|
+
function invalidRegionExplanation(regionIndex) {
|
|
315
|
+
return {
|
|
316
|
+
blend: -1,
|
|
317
|
+
conditionIndex: -1,
|
|
318
|
+
regionIndex,
|
|
319
|
+
sourceStateIndex: -1,
|
|
320
|
+
status: StatechartTransitionStatus.InvalidRegion,
|
|
321
|
+
targetStateIndex: -1,
|
|
322
|
+
transitionIndex: -1,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function validateStatechart(chart) {
|
|
326
|
+
for (let inputIndex = 0; inputIndex < chart.inputs.length; inputIndex++) {
|
|
327
|
+
const kind = chart.inputs[inputIndex].kind;
|
|
328
|
+
if (kind !== StatechartInputKind.Boolean &&
|
|
329
|
+
kind !== StatechartInputKind.Number &&
|
|
330
|
+
kind !== StatechartInputKind.Trigger) {
|
|
331
|
+
throw new TypeError(`Statechart input ${inputIndex} has unknown kind ${String(kind)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (let regionIndex = 0; regionIndex < chart.regions.length; regionIndex++) {
|
|
335
|
+
const region = chart.regions[regionIndex];
|
|
336
|
+
if (region.initialStateIndex < 0 || region.initialStateIndex >= region.states.length) {
|
|
337
|
+
throw new RangeError(`Statechart region ${regionIndex} has an invalid initialStateIndex`);
|
|
338
|
+
}
|
|
339
|
+
for (let stateIndex = 0; stateIndex < region.states.length; stateIndex++) {
|
|
340
|
+
const state = region.states[stateIndex];
|
|
341
|
+
if (typeof state.kind !== 'string' || state.kind.length === 0) {
|
|
342
|
+
throw new TypeError(`Statechart region ${regionIndex} state ${stateIndex} has an invalid kind`);
|
|
343
|
+
}
|
|
344
|
+
for (let transitionIndex = 0; transitionIndex < state.transitions.length; transitionIndex++) {
|
|
345
|
+
const transition = state.transitions[transitionIndex];
|
|
346
|
+
if (!Number.isFinite(transition.durationMs) || transition.durationMs < 0) {
|
|
347
|
+
throw new RangeError(`Statechart region ${regionIndex} state ${stateIndex} transition ${transitionIndex} has an invalid duration`);
|
|
348
|
+
}
|
|
349
|
+
if (!Number.isFinite(transition.exitTimeRatio) || transition.exitTimeRatio < -1) {
|
|
350
|
+
throw new RangeError(`Statechart region ${regionIndex} state ${stateIndex} transition ${transitionIndex} has an invalid exit time`);
|
|
351
|
+
}
|
|
352
|
+
if (transition.targetStateIndex < 0 || transition.targetStateIndex >= region.states.length) {
|
|
353
|
+
throw new RangeError(`Statechart region ${regionIndex} state ${stateIndex} transition ${transitionIndex} has an invalid target`);
|
|
354
|
+
}
|
|
355
|
+
for (let conditionIndex = 0; conditionIndex < transition.conditions.length; conditionIndex++) {
|
|
356
|
+
const condition = transition.conditions[conditionIndex];
|
|
357
|
+
const inputIndex = condition.inputIndex;
|
|
358
|
+
if (!Number.isInteger(inputIndex) || inputIndex < 0 || inputIndex >= chart.inputs.length) {
|
|
359
|
+
throw new RangeError(`Statechart region ${regionIndex} state ${stateIndex} transition ${transitionIndex} has an invalid input`);
|
|
360
|
+
}
|
|
361
|
+
if (!Object.values(StatechartComparison).includes(condition.comparison)) {
|
|
362
|
+
throw new TypeError(`Statechart region ${regionIndex} state ${stateIndex} transition ${transitionIndex} has an invalid comparison`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
//# sourceMappingURL=statechart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statechart.js","sourceRoot":"","sources":["../src/statechart.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { StatechartInstance, StatechartSignals } from '@flighthq/types/contract';
|
|
2
|
+
export declare function enableStatechartSignals(instance: StatechartInstance): StatechartSignals;
|
|
3
|
+
export declare function getStatechartSignals(instance: Readonly<StatechartInstance>): StatechartSignals | null;
|
|
4
|
+
//# sourceMappingURL=statechartSignals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statechartSignals.d.ts","sourceRoot":"","sources":["../src/statechartSignals.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAKtF,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,iBAAiB,CAIvF;AAID,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,iBAAiB,GAAG,IAAI,CAErG"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createSignal } from '@flighthq/signals/contract';
|
|
2
|
+
// Allocate the optional observer group on first use. The nullable slot lives on the mutable instance,
|
|
3
|
+
// never on the immutable chart, so authored/imported state remains closure-free and serializable. This
|
|
4
|
+
// module is separately tree-shakeable: count/read users do not pay for the signals runtime.
|
|
5
|
+
export function enableStatechartSignals(instance) {
|
|
6
|
+
return (instance.signals ??= {
|
|
7
|
+
onStateChange: createSignal(),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
// Read the optional observer group without allocating it. Returns null until enableStatechartSignals
|
|
11
|
+
// has been called for this actor.
|
|
12
|
+
export function getStatechartSignals(instance) {
|
|
13
|
+
return instance.signals;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=statechartSignals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statechartSignals.js","sourceRoot":"","sources":["../src/statechartSignals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1D,sGAAsG;AACtG,uGAAuG;AACvG,4FAA4F;AAC5F,MAAM,UAAU,uBAAuB,CAAC,QAA4B;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,KAAK;QAC3B,aAAa,EAAE,YAAY,EAAE;KAC9B,CAAC,CAAC;AACL,CAAC;AAED,qGAAqG;AACrG,kCAAkC;AAClC,MAAM,UAAU,oBAAoB,CAAC,QAAsC;IACzE,OAAO,QAAQ,CAAC,OAAO,CAAC;AAC1B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/statechart",
|
|
3
|
+
"version": "0.3.0-next.1728.26853f6",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/flighthq/flight.git",
|
|
7
|
+
"directory": "packages/statechart"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./contract": {
|
|
18
|
+
"types": "./dist/contract.d.ts",
|
|
19
|
+
"default": "./dist/contract.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src/**/*.test.ts",
|
|
25
|
+
"!dist/**/*.test.js",
|
|
26
|
+
"!dist/**/*.test.d.ts",
|
|
27
|
+
"!dist/**/*.test.js.map",
|
|
28
|
+
"!dist/**/*.test.d.ts.map"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -b",
|
|
32
|
+
"clean": "tsc -b --clean",
|
|
33
|
+
"test": "vitest run --config vitest.config.ts",
|
|
34
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
35
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
36
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@flighthq/log": "0.3.0-next.1728.26853f6",
|
|
40
|
+
"@flighthq/signals": "0.3.0-next.1728.26853f6",
|
|
41
|
+
"@flighthq/types": "0.3.0-next.1728.26853f6"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.3.0"
|
|
45
|
+
},
|
|
46
|
+
"description": "Plain-data concurrent statecharts with typed inputs, guarded transitions, and reported blend progress",
|
|
47
|
+
"sideEffects": false
|
|
48
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import { StatechartAtomicStateKind, StatechartTransitionStatus } from '@flighthq/types/contract';
|
|
3
|
+
import type { LogEntry, Statechart } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import { areStatechartGuardsEnabled, disableStatechartGuards, enableStatechartGuards } from './enableStatechartGuards';
|
|
6
|
+
import { advanceStatechartInstance, createStatechartInstance, setStatechartRegionDuration } from './statechart';
|
|
7
|
+
|
|
8
|
+
describe('areStatechartGuardsEnabled', () => {
|
|
9
|
+
it('reports guard state per actor', () => {
|
|
10
|
+
const guarded = createStatechartInstance(createExitTimeChart());
|
|
11
|
+
const unguarded = createStatechartInstance(createExitTimeChart());
|
|
12
|
+
enableStatechartGuards(guarded);
|
|
13
|
+
expect(areStatechartGuardsEnabled(guarded)).toBe(true);
|
|
14
|
+
expect(areStatechartGuardsEnabled(unguarded)).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('disableStatechartGuards', () => {
|
|
19
|
+
it('restores silent coercion without changing transition behavior', () => {
|
|
20
|
+
const instance = createStatechartInstance(createExitTimeChart());
|
|
21
|
+
enableStatechartGuards(instance);
|
|
22
|
+
disableStatechartGuards(instance);
|
|
23
|
+
const entries = captureLog(() => {
|
|
24
|
+
expect(advanceStatechartInstance(instance, 0)).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
expect(entries).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('enableStatechartGuards', () => {
|
|
31
|
+
it('warns once and names the duration setter while preserving no-requirement coercion', () => {
|
|
32
|
+
const instance = createStatechartInstance(createExitTimeChart());
|
|
33
|
+
enableStatechartGuards(instance);
|
|
34
|
+
const entries = captureLog(() => {
|
|
35
|
+
expect(advanceStatechartInstance(instance, 0)).toBe(1);
|
|
36
|
+
});
|
|
37
|
+
expect(entries).toHaveLength(1);
|
|
38
|
+
expect(entries[0]).toMatchObject({ channel: 'statechart', level: 2 });
|
|
39
|
+
expect(entries[0].data).toMatchObject({
|
|
40
|
+
exitTimeRatio: 0.5,
|
|
41
|
+
regionDuration: 0,
|
|
42
|
+
regionIndex: 0,
|
|
43
|
+
status: StatechartTransitionStatus.MissingRegionDuration,
|
|
44
|
+
transitionIndex: 0,
|
|
45
|
+
});
|
|
46
|
+
expect(String((entries[0].data as Record<string, unknown>).message)).toContain('setStatechartRegionDuration');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('stays silent when a positive duration makes the exit-time ratio well-defined', () => {
|
|
50
|
+
const instance = createStatechartInstance(createExitTimeChart());
|
|
51
|
+
enableStatechartGuards(instance);
|
|
52
|
+
setStatechartRegionDuration(instance, 0, 100);
|
|
53
|
+
const entries = captureLog(() => {
|
|
54
|
+
expect(advanceStatechartInstance(instance, 49)).toBe(0);
|
|
55
|
+
expect(advanceStatechartInstance(instance, 1)).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
expect(entries).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('keeps the production-default coercion silent when guards were never enabled', () => {
|
|
61
|
+
const instance = createStatechartInstance(createExitTimeChart());
|
|
62
|
+
const entries = captureLog(() => {
|
|
63
|
+
expect(advanceStatechartInstance(instance, 0)).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
expect(entries).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function captureLog(run: () => void): readonly LogEntry[] {
|
|
70
|
+
const sink = createMemoryLogSink(8);
|
|
71
|
+
addLogSink(sink.sink);
|
|
72
|
+
try {
|
|
73
|
+
run();
|
|
74
|
+
return getMemoryLogSinkEntries(sink);
|
|
75
|
+
} finally {
|
|
76
|
+
removeLogSink(sink.sink);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createExitTimeChart(): Statechart {
|
|
81
|
+
return {
|
|
82
|
+
inputs: [],
|
|
83
|
+
regions: [
|
|
84
|
+
{
|
|
85
|
+
initialStateIndex: 0,
|
|
86
|
+
states: [
|
|
87
|
+
{
|
|
88
|
+
kind: StatechartAtomicStateKind,
|
|
89
|
+
transitions: [{ conditions: [], durationMs: 0, exitTimeRatio: 0.5, targetStateIndex: 1 }],
|
|
90
|
+
},
|
|
91
|
+
{ kind: StatechartAtomicStateKind, transitions: [] },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StatechartAtomicStateKind,
|
|
3
|
+
StatechartComparison,
|
|
4
|
+
StatechartInputKind,
|
|
5
|
+
StatechartNestedStateKind,
|
|
6
|
+
StatechartTransitionStatus,
|
|
7
|
+
} from '@flighthq/types/contract';
|
|
8
|
+
import type { Statechart, StatechartCondition, StatechartTransition } from '@flighthq/types/contract';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
advanceStatechartInstance,
|
|
12
|
+
createStatechartInstance,
|
|
13
|
+
explainStatechartTransition,
|
|
14
|
+
fireStatechartTrigger,
|
|
15
|
+
getStatechartInputIndex,
|
|
16
|
+
getStatechartRegionBlend,
|
|
17
|
+
getStatechartRegionState,
|
|
18
|
+
setStatechartBooleanInput,
|
|
19
|
+
setStatechartNumberInput,
|
|
20
|
+
setStatechartRegionDuration,
|
|
21
|
+
} from './statechart';
|
|
22
|
+
|
|
23
|
+
describe('advanceStatechartInstance', () => {
|
|
24
|
+
it('advances every concurrent region before clearing a shared trigger', () => {
|
|
25
|
+
const chart = createTwoRegionTriggerChart();
|
|
26
|
+
const instance = createStatechartInstance(chart);
|
|
27
|
+
|
|
28
|
+
fireStatechartTrigger(instance, 0);
|
|
29
|
+
expect(advanceStatechartInstance(instance, 0)).toBe(2);
|
|
30
|
+
expect([...instance.regionStates]).toEqual([1, 1]);
|
|
31
|
+
expect(instance.inputValues[0]).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('requires all data-only conditions and selects the first eligible transition', () => {
|
|
35
|
+
const chart: Statechart = {
|
|
36
|
+
inputs: [
|
|
37
|
+
{ initialValue: 0, kind: StatechartInputKind.Boolean, name: 'armed' },
|
|
38
|
+
{ initialValue: 2, kind: StatechartInputKind.Number, name: 'speed' },
|
|
39
|
+
],
|
|
40
|
+
regions: [
|
|
41
|
+
{
|
|
42
|
+
initialStateIndex: 0,
|
|
43
|
+
states: [
|
|
44
|
+
{
|
|
45
|
+
kind: StatechartAtomicStateKind,
|
|
46
|
+
transitions: [
|
|
47
|
+
transition(1, [condition(0, StatechartComparison.Equal, 1)]),
|
|
48
|
+
transition(2, [
|
|
49
|
+
condition(0, StatechartComparison.Equal, 0),
|
|
50
|
+
condition(1, StatechartComparison.GreaterThan, 4),
|
|
51
|
+
]),
|
|
52
|
+
transition(1, [
|
|
53
|
+
condition(0, StatechartComparison.Equal, 0),
|
|
54
|
+
condition(1, StatechartComparison.LessThanOrEqual, 2),
|
|
55
|
+
]),
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{ kind: StatechartAtomicStateKind, transitions: [] },
|
|
59
|
+
{ kind: StatechartAtomicStateKind, transitions: [] },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
const instance = createStatechartInstance(chart);
|
|
65
|
+
|
|
66
|
+
expect(advanceStatechartInstance(instance, 0)).toBe(1);
|
|
67
|
+
expect(instance.regionStates[0]).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it.each([
|
|
71
|
+
[StatechartComparison.Equal, 3, 3, true],
|
|
72
|
+
[StatechartComparison.GreaterThan, 3, 2, true],
|
|
73
|
+
[StatechartComparison.GreaterThanOrEqual, 3, 3, true],
|
|
74
|
+
[StatechartComparison.LessThan, 3, 4, true],
|
|
75
|
+
[StatechartComparison.LessThanOrEqual, 3, 3, true],
|
|
76
|
+
[StatechartComparison.NotEqual, 3, 4, true],
|
|
77
|
+
])('evaluates the %s numeric comparison', (comparison, inputValue, expectedValue, expected) => {
|
|
78
|
+
const chart = createSingleTransitionChart(
|
|
79
|
+
[{ initialValue: inputValue, kind: StatechartInputKind.Number }],
|
|
80
|
+
[condition(0, comparison, expectedValue)],
|
|
81
|
+
);
|
|
82
|
+
expect(advanceStatechartInstance(createStatechartInstance(chart), 0) === 1).toBe(expected);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('reports blend progress without performing composition and changes state at completion', () => {
|
|
86
|
+
const chart = createSingleTransitionChart([], [], { durationMs: 100 });
|
|
87
|
+
const instance = createStatechartInstance(chart);
|
|
88
|
+
|
|
89
|
+
expect(advanceStatechartInstance(instance, 40)).toBe(0);
|
|
90
|
+
expect(instance.regionStates[0]).toBe(0);
|
|
91
|
+
expect(getStatechartRegionBlend(instance, 0)).toBeCloseTo(0.4);
|
|
92
|
+
expect(advanceStatechartInstance(instance, 60)).toBe(1);
|
|
93
|
+
expect(instance.regionStates[0]).toBe(1);
|
|
94
|
+
expect(getStatechartRegionBlend(instance, 0)).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('accumulates elapsed time and evaluates exitTimeRatio against the caller-supplied duration', () => {
|
|
98
|
+
const chart = createSingleTransitionChart([], [], { exitTimeRatio: 0.5 });
|
|
99
|
+
const instance = createStatechartInstance(chart);
|
|
100
|
+
setStatechartRegionDuration(instance, 0, 100);
|
|
101
|
+
|
|
102
|
+
expect(advanceStatechartInstance(instance, 49)).toBe(0);
|
|
103
|
+
expect(instance.regionElapsed[0]).toBe(49);
|
|
104
|
+
expect(advanceStatechartInstance(instance, 1)).toBe(1);
|
|
105
|
+
expect(instance.regionStates[0]).toBe(1);
|
|
106
|
+
expect(instance.regionElapsed[0]).toBe(0);
|
|
107
|
+
expect(instance.regionDuration[0]).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('changes a region at most once per call even when the target has an immediate transition', () => {
|
|
111
|
+
const chart: Statechart = {
|
|
112
|
+
inputs: [],
|
|
113
|
+
regions: [
|
|
114
|
+
{
|
|
115
|
+
initialStateIndex: 0,
|
|
116
|
+
states: [
|
|
117
|
+
{ kind: StatechartAtomicStateKind, transitions: [transition(1)] },
|
|
118
|
+
{ kind: StatechartAtomicStateKind, transitions: [transition(2)] },
|
|
119
|
+
{ kind: StatechartAtomicStateKind, transitions: [] },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
const instance = createStatechartInstance(chart);
|
|
125
|
+
|
|
126
|
+
expect(advanceStatechartInstance(instance, 0)).toBe(1);
|
|
127
|
+
expect(instance.regionStates[0]).toBe(1);
|
|
128
|
+
expect(advanceStatechartInstance(instance, 0)).toBe(1);
|
|
129
|
+
expect(instance.regionStates[0]).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('treats non-positive and non-finite time as no blend progress', () => {
|
|
133
|
+
const chart = createSingleTransitionChart([], [], { durationMs: 100 });
|
|
134
|
+
const instance = createStatechartInstance(chart);
|
|
135
|
+
|
|
136
|
+
expect(advanceStatechartInstance(instance, -1)).toBe(0);
|
|
137
|
+
expect(advanceStatechartInstance(instance, Number.NaN)).toBe(0);
|
|
138
|
+
expect(instance.regionBlend[0]).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('createStatechartInstance', () => {
|
|
143
|
+
it('creates independent typed-array actor state while retaining the immutable chart', () => {
|
|
144
|
+
const chart: Statechart = {
|
|
145
|
+
inputs: [
|
|
146
|
+
{ initialValue: -2, kind: StatechartInputKind.Boolean },
|
|
147
|
+
{ initialValue: 3.5, kind: StatechartInputKind.Number },
|
|
148
|
+
{ initialValue: 0, kind: StatechartInputKind.Trigger },
|
|
149
|
+
],
|
|
150
|
+
name: 'shared',
|
|
151
|
+
regions: [
|
|
152
|
+
{
|
|
153
|
+
initialStateIndex: 1,
|
|
154
|
+
states: [
|
|
155
|
+
{ kind: StatechartNestedStateKind, transitions: [] },
|
|
156
|
+
{ kind: 'acme.CustomState', transitions: [] },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const first = createStatechartInstance(chart);
|
|
163
|
+
const second = createStatechartInstance(chart);
|
|
164
|
+
expect(first.chart).toBe(chart);
|
|
165
|
+
expect(first.chart).toBe(second.chart);
|
|
166
|
+
expect(first.inputValues).toBeInstanceOf(Float64Array);
|
|
167
|
+
expect([...first.inputValues]).toEqual([1, 3.5, 0]);
|
|
168
|
+
expect(first.regionBlend).toBeInstanceOf(Float32Array);
|
|
169
|
+
expect(first.regionDuration).toBeInstanceOf(Float64Array);
|
|
170
|
+
expect(first.regionElapsed).toBeInstanceOf(Float64Array);
|
|
171
|
+
expect(first.regionStates).toBeInstanceOf(Int32Array);
|
|
172
|
+
expect(first.regionTransitions).toBeInstanceOf(Int32Array);
|
|
173
|
+
expect([...first.regionStates]).toEqual([1]);
|
|
174
|
+
expect([...first.regionTransitions]).toEqual([-1]);
|
|
175
|
+
expect(first.durationGuard).toBeNull();
|
|
176
|
+
expect(first.signals).toBeNull();
|
|
177
|
+
|
|
178
|
+
first.inputValues[1] = 9;
|
|
179
|
+
expect(second.inputValues[1]).toBe(3.5);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('rejects malformed authored indices as programmer errors', () => {
|
|
183
|
+
const chart: Statechart = {
|
|
184
|
+
inputs: [],
|
|
185
|
+
regions: [{ initialStateIndex: 1, states: [{ kind: StatechartAtomicStateKind, transitions: [] }] }],
|
|
186
|
+
};
|
|
187
|
+
expect(() => createStatechartInstance(chart)).toThrow(RangeError);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('accepts nested and vendor-prefixed state kinds without closed dispatch', () => {
|
|
191
|
+
const chart: Statechart = {
|
|
192
|
+
inputs: [],
|
|
193
|
+
regions: [
|
|
194
|
+
{
|
|
195
|
+
initialStateIndex: 0,
|
|
196
|
+
states: [
|
|
197
|
+
{ kind: StatechartNestedStateKind, transitions: [] },
|
|
198
|
+
{ kind: 'vendor.Special', transitions: [] },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
expect(() => createStatechartInstance(chart)).not.toThrow();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('explainStatechartTransition', () => {
|
|
208
|
+
it('returns an invalid-region diagnostic for the silent getter sentinel', () => {
|
|
209
|
+
const explanation = explainStatechartTransition(createStatechartInstance(createEmptyChart()), 4);
|
|
210
|
+
expect(explanation).toEqual({
|
|
211
|
+
blend: -1,
|
|
212
|
+
conditionIndex: -1,
|
|
213
|
+
regionIndex: 4,
|
|
214
|
+
sourceStateIndex: -1,
|
|
215
|
+
status: StatechartTransitionStatus.InvalidRegion,
|
|
216
|
+
targetStateIndex: -1,
|
|
217
|
+
transitionIndex: -1,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('reports a state with no transitions', () => {
|
|
222
|
+
const explanation = explainStatechartTransition(createStatechartInstance(createEmptyChart()), 0);
|
|
223
|
+
expect(explanation.status).toBe(StatechartTransitionStatus.NoTransitions);
|
|
224
|
+
expect(explanation.transitionIndex).toBe(-1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('identifies the first unmet condition', () => {
|
|
228
|
+
const chart = createSingleTransitionChart(
|
|
229
|
+
[{ initialValue: 0, kind: StatechartInputKind.Number }],
|
|
230
|
+
[condition(0, StatechartComparison.GreaterThan, 2)],
|
|
231
|
+
);
|
|
232
|
+
const explanation = explainStatechartTransition(createStatechartInstance(chart), 0);
|
|
233
|
+
expect(explanation.status).toBe(StatechartTransitionStatus.ConditionsUnmet);
|
|
234
|
+
expect(explanation.conditionIndex).toBe(0);
|
|
235
|
+
expect(explanation.targetStateIndex).toBe(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('reports ready and transitioning states without mutation', () => {
|
|
239
|
+
const chart = createSingleTransitionChart([], [], { durationMs: 100 });
|
|
240
|
+
const instance = createStatechartInstance(chart);
|
|
241
|
+
expect(explainStatechartTransition(instance, 0).status).toBe(StatechartTransitionStatus.Ready);
|
|
242
|
+
advanceStatechartInstance(instance, 25);
|
|
243
|
+
const explanation = explainStatechartTransition(instance, 0);
|
|
244
|
+
expect(explanation.status).toBe(StatechartTransitionStatus.Transitioning);
|
|
245
|
+
expect(explanation.blend).toBeCloseTo(0.25);
|
|
246
|
+
expect(explanation.transitionIndex).toBe(0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('reports a satisfied guard waiting on exit time', () => {
|
|
250
|
+
const chart = createSingleTransitionChart([], [], { exitTimeRatio: 0.5 });
|
|
251
|
+
const instance = createStatechartInstance(chart);
|
|
252
|
+
setStatechartRegionDuration(instance, 0, 100);
|
|
253
|
+
const explanation = explainStatechartTransition(instance, 0);
|
|
254
|
+
expect(explanation.status).toBe(StatechartTransitionStatus.ExitTimePending);
|
|
255
|
+
expect(explanation.conditionIndex).toBe(-1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('reports a missing duration that runtime coerces to no exit-time requirement', () => {
|
|
259
|
+
const chart = createSingleTransitionChart([], [], { exitTimeRatio: 0.5 });
|
|
260
|
+
const explanation = explainStatechartTransition(createStatechartInstance(chart), 0);
|
|
261
|
+
expect(explanation.status).toBe(StatechartTransitionStatus.MissingRegionDuration);
|
|
262
|
+
expect(explanation.transitionIndex).toBe(0);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('fireStatechartTrigger', () => {
|
|
267
|
+
it('latches a trigger until all regions see the next advance and ignores condition value', () => {
|
|
268
|
+
const instance = createStatechartInstance(createTwoRegionTriggerChart());
|
|
269
|
+
fireStatechartTrigger(instance, 0);
|
|
270
|
+
expect(instance.inputValues[0]).toBe(1);
|
|
271
|
+
expect(advanceStatechartInstance(instance, 0)).toBe(2);
|
|
272
|
+
expect(instance.inputValues[0]).toBe(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('throws for an invalid index or non-trigger input', () => {
|
|
276
|
+
const instance = createStatechartInstance(
|
|
277
|
+
createSingleTransitionChart([{ initialValue: 0, kind: StatechartInputKind.Number }]),
|
|
278
|
+
);
|
|
279
|
+
expect(() => fireStatechartTrigger(instance, -1)).toThrow(RangeError);
|
|
280
|
+
expect(() => fireStatechartTrigger(instance, 0)).toThrow(TypeError);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('getStatechartInputIndex', () => {
|
|
285
|
+
it('returns the first matching input index and -1 when absent', () => {
|
|
286
|
+
const chart: Statechart = {
|
|
287
|
+
inputs: [
|
|
288
|
+
{ initialValue: 0, kind: StatechartInputKind.Number, name: 'speed' },
|
|
289
|
+
{ initialValue: 1, kind: StatechartInputKind.Number, name: 'speed' },
|
|
290
|
+
],
|
|
291
|
+
regions: [],
|
|
292
|
+
};
|
|
293
|
+
expect(getStatechartInputIndex(chart, 'speed')).toBe(0);
|
|
294
|
+
expect(getStatechartInputIndex(chart, 'missing')).toBe(-1);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('getStatechartRegionBlend', () => {
|
|
299
|
+
it('returns a region blend and -1 for invalid regions', () => {
|
|
300
|
+
const instance = createStatechartInstance(createEmptyChart());
|
|
301
|
+
instance.regionBlend[0] = 0.4;
|
|
302
|
+
expect(getStatechartRegionBlend(instance, 0)).toBeCloseTo(0.4);
|
|
303
|
+
expect(getStatechartRegionBlend(instance, -1)).toBe(-1);
|
|
304
|
+
expect(getStatechartRegionBlend(instance, 1)).toBe(-1);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('getStatechartRegionState', () => {
|
|
309
|
+
it('returns a region state and -1 for invalid regions', () => {
|
|
310
|
+
const instance = createStatechartInstance(createEmptyChart());
|
|
311
|
+
expect(getStatechartRegionState(instance, 0)).toBe(0);
|
|
312
|
+
expect(getStatechartRegionState(instance, -1)).toBe(-1);
|
|
313
|
+
expect(getStatechartRegionState(instance, 1)).toBe(-1);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('setStatechartBooleanInput', () => {
|
|
318
|
+
it('writes the Boolean input vocabulary as numeric 0/1', () => {
|
|
319
|
+
const instance = createStatechartInstance(
|
|
320
|
+
createSingleTransitionChart([{ initialValue: 0, kind: StatechartInputKind.Boolean }]),
|
|
321
|
+
);
|
|
322
|
+
setStatechartBooleanInput(instance, 0, true);
|
|
323
|
+
expect(instance.inputValues[0]).toBe(1);
|
|
324
|
+
setStatechartBooleanInput(instance, 0, false);
|
|
325
|
+
expect(instance.inputValues[0]).toBe(0);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('throws for an invalid index or non-Boolean input', () => {
|
|
329
|
+
const instance = createStatechartInstance(
|
|
330
|
+
createSingleTransitionChart([{ initialValue: 0, kind: StatechartInputKind.Number }]),
|
|
331
|
+
);
|
|
332
|
+
expect(() => setStatechartBooleanInput(instance, 1, true)).toThrow(RangeError);
|
|
333
|
+
expect(() => setStatechartBooleanInput(instance, 0, true)).toThrow(TypeError);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('setStatechartNumberInput', () => {
|
|
338
|
+
it('writes an exact numeric value', () => {
|
|
339
|
+
const instance = createStatechartInstance(
|
|
340
|
+
createSingleTransitionChart([{ initialValue: 0, kind: StatechartInputKind.Number }]),
|
|
341
|
+
);
|
|
342
|
+
setStatechartNumberInput(instance, 0, -3.25);
|
|
343
|
+
expect(instance.inputValues[0]).toBe(-3.25);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('throws for an invalid index or non-Number input', () => {
|
|
347
|
+
const instance = createStatechartInstance(
|
|
348
|
+
createSingleTransitionChart([{ initialValue: 0, kind: StatechartInputKind.Boolean }]),
|
|
349
|
+
);
|
|
350
|
+
expect(() => setStatechartNumberInput(instance, 1, 2)).toThrow(RangeError);
|
|
351
|
+
expect(() => setStatechartNumberInput(instance, 0, 2)).toThrow(TypeError);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('setStatechartRegionDuration', () => {
|
|
356
|
+
it('sets the plain per-region exit-time denominator', () => {
|
|
357
|
+
const instance = createStatechartInstance(createEmptyChart());
|
|
358
|
+
setStatechartRegionDuration(instance, 0, 250);
|
|
359
|
+
expect(instance.regionDuration[0]).toBe(250);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('accepts zero as an explicit unavailable duration', () => {
|
|
363
|
+
const instance = createStatechartInstance(createEmptyChart());
|
|
364
|
+
setStatechartRegionDuration(instance, 0, 0);
|
|
365
|
+
expect(instance.regionDuration[0]).toBe(0);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('throws for an invalid region or invalid duration', () => {
|
|
369
|
+
const instance = createStatechartInstance(createEmptyChart());
|
|
370
|
+
expect(() => setStatechartRegionDuration(instance, 1, 1)).toThrow(RangeError);
|
|
371
|
+
expect(() => setStatechartRegionDuration(instance, 0, -1)).toThrow(RangeError);
|
|
372
|
+
expect(() => setStatechartRegionDuration(instance, 0, Number.NaN)).toThrow(RangeError);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
function condition(inputIndex: number, comparison: StatechartComparison, value: number): StatechartCondition {
|
|
377
|
+
return { comparison, inputIndex, value };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function createEmptyChart(): Statechart {
|
|
381
|
+
return {
|
|
382
|
+
inputs: [],
|
|
383
|
+
regions: [{ initialStateIndex: 0, states: [{ kind: StatechartAtomicStateKind, transitions: [] }] }],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function createSingleTransitionChart(
|
|
388
|
+
inputs: Statechart['inputs'],
|
|
389
|
+
conditions: readonly StatechartCondition[] = [],
|
|
390
|
+
options: Partial<Pick<StatechartTransition, 'durationMs' | 'exitTimeRatio'>> = {},
|
|
391
|
+
): Statechart {
|
|
392
|
+
return {
|
|
393
|
+
inputs,
|
|
394
|
+
regions: [
|
|
395
|
+
{
|
|
396
|
+
initialStateIndex: 0,
|
|
397
|
+
states: [
|
|
398
|
+
{
|
|
399
|
+
kind: StatechartAtomicStateKind,
|
|
400
|
+
transitions: [transition(1, conditions, options)],
|
|
401
|
+
},
|
|
402
|
+
{ kind: StatechartAtomicStateKind, transitions: [] },
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function createTwoRegionTriggerChart(): Statechart {
|
|
410
|
+
const triggerTransition = transition(1, [condition(0, StatechartComparison.GreaterThan, 999)]);
|
|
411
|
+
const region = () => ({
|
|
412
|
+
initialStateIndex: 0,
|
|
413
|
+
states: [
|
|
414
|
+
{ kind: StatechartAtomicStateKind, transitions: [triggerTransition] },
|
|
415
|
+
{ kind: StatechartAtomicStateKind, transitions: [] },
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
inputs: [{ initialValue: 0, kind: StatechartInputKind.Trigger, name: 'go' }],
|
|
420
|
+
regions: [region(), region()],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function transition(
|
|
425
|
+
targetStateIndex: number,
|
|
426
|
+
conditions: readonly StatechartCondition[] = [],
|
|
427
|
+
options: Partial<Pick<StatechartTransition, 'durationMs' | 'exitTimeRatio'>> = {},
|
|
428
|
+
): StatechartTransition {
|
|
429
|
+
return {
|
|
430
|
+
conditions,
|
|
431
|
+
durationMs: options.durationMs ?? 0,
|
|
432
|
+
exitTimeRatio: options.exitTimeRatio ?? -1,
|
|
433
|
+
targetStateIndex,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
|
+
import { StatechartAtomicStateKind } from '@flighthq/types/contract';
|
|
3
|
+
import type { Statechart } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
advanceStatechartInstance,
|
|
7
|
+
createStatechartInstance,
|
|
8
|
+
fireStatechartTrigger,
|
|
9
|
+
setStatechartRegionDuration,
|
|
10
|
+
} from './statechart';
|
|
11
|
+
import { enableStatechartSignals, getStatechartSignals } from './statechartSignals';
|
|
12
|
+
|
|
13
|
+
describe('enableStatechartSignals', () => {
|
|
14
|
+
it('lazily attaches one standard signal group to the mutable instance', () => {
|
|
15
|
+
const instance = createStatechartInstance(createImmediateChart());
|
|
16
|
+
const first = enableStatechartSignals(instance);
|
|
17
|
+
const second = enableStatechartSignals(instance);
|
|
18
|
+
expect(first).toBe(second);
|
|
19
|
+
expect(instance.signals).toBe(first);
|
|
20
|
+
expect(first.onStateChange.data).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('observes completed entry and exit without putting a closure on authored state data', () => {
|
|
24
|
+
const chart = createImmediateChart();
|
|
25
|
+
const instance = createStatechartInstance(chart);
|
|
26
|
+
const calls: number[][] = [];
|
|
27
|
+
connectSignal(enableStatechartSignals(instance).onStateChange, (regionIndex, previousState, state) => {
|
|
28
|
+
calls.push([regionIndex, previousState, state]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(JSON.stringify(chart)).not.toContain('onEnter');
|
|
32
|
+
advanceStatechartInstance(instance, 0);
|
|
33
|
+
expect(calls).toEqual([[0, 0, 1]]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('emits after trigger cleanup so a listener can latch an edge for the next pass', () => {
|
|
37
|
+
const chart: Statechart = {
|
|
38
|
+
inputs: [{ initialValue: 0, kind: 'Trigger' }],
|
|
39
|
+
regions: [
|
|
40
|
+
{
|
|
41
|
+
initialStateIndex: 0,
|
|
42
|
+
states: [
|
|
43
|
+
{
|
|
44
|
+
kind: StatechartAtomicStateKind,
|
|
45
|
+
transitions: [{ conditions: [], durationMs: 0, exitTimeRatio: -1, targetStateIndex: 1 }],
|
|
46
|
+
},
|
|
47
|
+
{ kind: StatechartAtomicStateKind, transitions: [] },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const instance = createStatechartInstance(chart);
|
|
53
|
+
connectSignal(enableStatechartSignals(instance).onStateChange, () => {
|
|
54
|
+
fireStatechartTrigger(instance, 0);
|
|
55
|
+
});
|
|
56
|
+
advanceStatechartInstance(instance, 0);
|
|
57
|
+
expect(instance.inputValues[0]).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('lets composition supply the entered state duration after core resets the old one', () => {
|
|
61
|
+
const instance = createStatechartInstance(createImmediateChart());
|
|
62
|
+
setStatechartRegionDuration(instance, 0, 100);
|
|
63
|
+
connectSignal(enableStatechartSignals(instance).onStateChange, (regionIndex) => {
|
|
64
|
+
setStatechartRegionDuration(instance, regionIndex, 250);
|
|
65
|
+
});
|
|
66
|
+
advanceStatechartInstance(instance, 0);
|
|
67
|
+
expect(instance.regionDuration[0]).toBe(250);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getStatechartSignals', () => {
|
|
72
|
+
it('returns null before enablement and the group afterward', () => {
|
|
73
|
+
const instance = createStatechartInstance(createImmediateChart());
|
|
74
|
+
expect(getStatechartSignals(instance)).toBeNull();
|
|
75
|
+
const signals = enableStatechartSignals(instance);
|
|
76
|
+
expect(getStatechartSignals(instance)).toBe(signals);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function createImmediateChart(): Statechart {
|
|
81
|
+
return {
|
|
82
|
+
inputs: [],
|
|
83
|
+
regions: [
|
|
84
|
+
{
|
|
85
|
+
initialStateIndex: 0,
|
|
86
|
+
states: [
|
|
87
|
+
{
|
|
88
|
+
kind: StatechartAtomicStateKind,
|
|
89
|
+
transitions: [{ conditions: [], durationMs: 0, exitTimeRatio: -1, targetStateIndex: 1 }],
|
|
90
|
+
},
|
|
91
|
+
{ kind: StatechartAtomicStateKind, transitions: [] },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|