@vsirotin/ts-stop 1.13.0 → 2.4.1
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/ai/skills/sfsm-compare-json-uml-diagram/SKILL.md +62 -0
- package/ai/skills/sfsm-json-to-uml-diagram/SKILL.md +50 -0
- package/ai/skills/sfsm-uml-diagram-to-json/SKILL.md +49 -0
- package/lib/esm/sfsm/CommandReceiver.js +17 -0
- package/lib/esm/sfsm/ControllerHub.js +82 -0
- package/lib/esm/sfsm/FaResolver.js +24 -3
- package/lib/esm/sfsm/Sfsm.js +48 -7
- package/lib/esm/sfsm/SignalSender.js +27 -0
- package/lib/esm/sfsm/index.js +3 -1
- package/lib/sfsm/CommandReceiver.d.ts +23 -0
- package/lib/sfsm/CommandReceiver.d.ts.map +1 -0
- package/lib/sfsm/CommandReceiver.js +22 -0
- package/lib/sfsm/CommandReceiver.js.map +1 -0
- package/lib/sfsm/ControllerHub.d.ts +56 -0
- package/lib/sfsm/ControllerHub.d.ts.map +1 -0
- package/lib/sfsm/ControllerHub.js +87 -0
- package/lib/sfsm/ControllerHub.js.map +1 -0
- package/lib/sfsm/FaResolver.d.ts +14 -0
- package/lib/sfsm/FaResolver.d.ts.map +1 -1
- package/lib/sfsm/FaResolver.js +24 -3
- package/lib/sfsm/FaResolver.js.map +1 -1
- package/lib/sfsm/Sfsm.d.ts +26 -0
- package/lib/sfsm/Sfsm.d.ts.map +1 -1
- package/lib/sfsm/Sfsm.js +48 -7
- package/lib/sfsm/Sfsm.js.map +1 -1
- package/lib/sfsm/SignalSender.d.ts +24 -0
- package/lib/sfsm/SignalSender.d.ts.map +1 -0
- package/lib/sfsm/SignalSender.js +32 -0
- package/lib/sfsm/SignalSender.js.map +1 -0
- package/lib/sfsm/index.d.ts +3 -1
- package/lib/sfsm/index.d.ts.map +1 -1
- package/lib/sfsm/index.js +7 -3
- package/lib/sfsm/index.js.map +1 -1
- package/lib/sfsm/interfaces.d.ts +1 -1
- package/lib/sfsm/types.d.ts +21 -0
- package/lib/sfsm/types.d.ts.map +1 -1
- package/package.json +8 -5
- package/scripts/compare-compact-jsons.js +146 -0
- package/scripts/drawio-to-json.js +149 -0
- package/scripts/json-to-drawio-old.js +311 -0
- package/scripts/json-to-drawio.js +309 -0
- package/tutorial/01-finite-state-machine.md +223 -0
- package/tutorial/02-stacked-finite-state-machine.md +32 -0
- package/tutorial/03-advanced-themes.md +110 -0
- package/tutorial/04-tools.md +164 -0
- package/tutorial/images/Turnstile-img.png +0 -0
- package/tutorial/images/TurnstileBaseFA.png +0 -0
- package/LICENSE +0 -201
- package/LICENSE-COMMERCIAL.md +0 -41
- package/README.md +0 -19
- package/lib/DefaultState.d.ts +0 -9
- package/lib/DefaultState.d.ts.map +0 -1
- package/lib/DefaultState.js +0 -15
- package/lib/DefaultState.js.map +0 -1
- package/lib/FiniteStateMachine.d.ts +0 -227
- package/lib/FiniteStateMachine.d.ts.map +0 -1
- package/lib/FiniteStateMachine.js +0 -410
- package/lib/FiniteStateMachine.js.map +0 -1
- package/lib/IStateWithActions.d.ts +0 -18
- package/lib/IStateWithActions.d.ts.map +0 -1
- package/lib/IStateWithActions.js +0 -3
- package/lib/IStateWithActions.js.map +0 -1
- package/lib/IStateWithOutputSignal.d.ts +0 -4
- package/lib/IStateWithOutputSignal.d.ts.map +0 -1
- package/lib/IStateWithOutputSignal.js +0 -3
- package/lib/IStateWithOutputSignal.js.map +0 -1
- package/lib/MatrixBasedStateMachine.d.ts +0 -108
- package/lib/MatrixBasedStateMachine.d.ts.map +0 -1
- package/lib/MatrixBasedStateMachine.js +0 -132
- package/lib/MatrixBasedStateMachine.js.map +0 -1
- package/lib/TransitionMatrix.d.ts +0 -61
- package/lib/TransitionMatrix.d.ts.map +0 -1
- package/lib/TransitionMatrix.js +0 -104
- package/lib/TransitionMatrix.js.map +0 -1
- package/lib/esm/DefaultState.js +0 -10
- package/lib/esm/FiniteStateMachine.js +0 -405
- package/lib/esm/IStateWithActions.js +0 -1
- package/lib/esm/IStateWithOutputSignal.js +0 -1
- package/lib/esm/MatrixBasedStateMachine.js +0 -127
- package/lib/esm/TransitionMatrix.js +0 -98
- package/lib/esm/fa/DefaultState.js +0 -10
- package/lib/esm/fa/FiniteStateMachine.js +0 -405
- package/lib/esm/fa/IStateWithActions.js +0 -1
- package/lib/esm/fa/IStateWithOutputSignal.js +0 -1
- package/lib/esm/fa/MatrixBasedStateMachine.js +0 -127
- package/lib/esm/fa/TransitionMatrix.js +0 -98
- package/lib/esm/index.js +0 -9
- package/lib/esm/sfsm/ExternalWorldHub.js +0 -80
- package/lib/esm/sfsm/index.browser.js +0 -5
- package/lib/fa/DefaultState.d.ts +0 -9
- package/lib/fa/DefaultState.d.ts.map +0 -1
- package/lib/fa/DefaultState.js +0 -15
- package/lib/fa/DefaultState.js.map +0 -1
- package/lib/fa/FiniteStateMachine.d.ts +0 -227
- package/lib/fa/FiniteStateMachine.d.ts.map +0 -1
- package/lib/fa/FiniteStateMachine.js +0 -410
- package/lib/fa/FiniteStateMachine.js.map +0 -1
- package/lib/fa/IStateWithActions.d.ts +0 -18
- package/lib/fa/IStateWithActions.d.ts.map +0 -1
- package/lib/fa/IStateWithActions.js +0 -3
- package/lib/fa/IStateWithActions.js.map +0 -1
- package/lib/fa/IStateWithOutputSignal.d.ts +0 -4
- package/lib/fa/IStateWithOutputSignal.d.ts.map +0 -1
- package/lib/fa/IStateWithOutputSignal.js +0 -3
- package/lib/fa/IStateWithOutputSignal.js.map +0 -1
- package/lib/fa/MatrixBasedStateMachine.d.ts +0 -108
- package/lib/fa/MatrixBasedStateMachine.d.ts.map +0 -1
- package/lib/fa/MatrixBasedStateMachine.js +0 -132
- package/lib/fa/MatrixBasedStateMachine.js.map +0 -1
- package/lib/fa/TransitionMatrix.d.ts +0 -61
- package/lib/fa/TransitionMatrix.d.ts.map +0 -1
- package/lib/fa/TransitionMatrix.js +0 -104
- package/lib/fa/TransitionMatrix.js.map +0 -1
- package/lib/index.d.ts +0 -10
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -26
- package/lib/index.js.map +0 -1
- package/lib/sfsm/ExternalWorldHub.d.ts +0 -52
- package/lib/sfsm/ExternalWorldHub.d.ts.map +0 -1
- package/lib/sfsm/ExternalWorldHub.js +0 -85
- package/lib/sfsm/ExternalWorldHub.js.map +0 -1
- package/lib/sfsm/index.browser.d.ts +0 -7
- package/scripts/publish-local.sh +0 -15
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
import { DefaultState } from './DefaultState';
|
|
2
|
-
/**
|
|
3
|
-
* Concrete implementation of a finite state machine with optional state actions.
|
|
4
|
-
*
|
|
5
|
-
* This class supports states that can optionally implement action interfaces:
|
|
6
|
-
* - IStateWithAfterEntryAction: Execute action when entering the state
|
|
7
|
-
* - IStateWithBeforeExitAction: Execute action when exiting the state
|
|
8
|
-
* - Both interfaces: Execute both entry and exit actions
|
|
9
|
-
* - Neither interface: Traditional state machine behavior (no actions)
|
|
10
|
-
*
|
|
11
|
-
* @template STATE - The type representing possible states
|
|
12
|
-
* @template SIGNAL - The type representing signals/events
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* // Basic string-based turnstile (traditional approach)
|
|
17
|
-
* class Turnstile extends FiniteStateMachine<string, string> {
|
|
18
|
-
* constructor() {
|
|
19
|
-
* super(
|
|
20
|
-
* ['locked', 'unlocked'],
|
|
21
|
-
* ['coin', 'push'],
|
|
22
|
-
* [
|
|
23
|
-
* { from: 'locked', signal: 'coin', to: 'unlocked' },
|
|
24
|
-
* { from: 'unlocked', signal: 'push', to: 'locked' }
|
|
25
|
-
* ],
|
|
26
|
-
* 'locked'
|
|
27
|
-
* );
|
|
28
|
-
* }
|
|
29
|
-
*
|
|
30
|
-
* ...
|
|
31
|
-
* }
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
* class TurnstileWithActions extends FiniteStateMachine<ActionState, string> {
|
|
36
|
-
* private locked = new ActionState('locked', 'Payment required');
|
|
37
|
-
* private unlocked = new ActionState('unlocked', 'Please proceed');
|
|
38
|
-
*
|
|
39
|
-
* constructor() {
|
|
40
|
-
* super(
|
|
41
|
-
* [this.locked, this.unlocked],
|
|
42
|
-
* ['coin', 'push'],
|
|
43
|
-
* [
|
|
44
|
-
* { from: this.locked, signal: 'coin', to: this.unlocked },
|
|
45
|
-
* { from: this.unlocked, signal: 'push', to: this.locked }
|
|
46
|
-
* ],
|
|
47
|
-
* this.locked
|
|
48
|
-
* );
|
|
49
|
-
* }
|
|
50
|
-
*
|
|
51
|
-
* insertCoin(): ActionState { return this.sendSignal('coin'); }
|
|
52
|
-
* pushThrough(): ActionState { return this.sendSignal('push'); }
|
|
53
|
-
* }
|
|
54
|
-
*
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export class FiniteStateMachine {
|
|
58
|
-
/**
|
|
59
|
-
* Creates a new finite state machine.
|
|
60
|
-
*
|
|
61
|
-
* @param states - Array of all possible states
|
|
62
|
-
* @param signals - Array of all possible signals/events
|
|
63
|
-
* @param transitions - Array of transition rules defining how signals move between states
|
|
64
|
-
* @param startState - The initial state of the machine
|
|
65
|
-
* @param skipValidation - When true, all structural validations are skipped (default: false)
|
|
66
|
-
*/
|
|
67
|
-
constructor(states, signals, transitions, startState, skipValidation = false) {
|
|
68
|
-
this.states = states;
|
|
69
|
-
this.signals = signals;
|
|
70
|
-
this.transitions = transitions;
|
|
71
|
-
this.startState = startState;
|
|
72
|
-
/**
|
|
73
|
-
* The default state that handles invalid signals.
|
|
74
|
-
* Set to null if no state implements IDefaultState.
|
|
75
|
-
*/
|
|
76
|
-
this.defaultState = null;
|
|
77
|
-
// Find states that implement IDefaultState
|
|
78
|
-
const defaultStates = states.filter(state => this.isDefaultState(state));
|
|
79
|
-
if (!skipValidation) {
|
|
80
|
-
// Validate default state count
|
|
81
|
-
if (defaultStates.length > 1) {
|
|
82
|
-
throw new Error(`ERROR-STOP-01: Multiple states implement IDefaultState interface. ` +
|
|
83
|
-
`Only one state can handle invalid signals. ` +
|
|
84
|
-
`Found ${defaultStates.length} states with IDefaultState.`);
|
|
85
|
-
}
|
|
86
|
-
// Validate that all transition and start-state references are defined
|
|
87
|
-
this.validateDanglingReferences();
|
|
88
|
-
// Validate that all output signal states have at least one outgoing transition
|
|
89
|
-
this.validateOutputSignalStates();
|
|
90
|
-
// Validate that output-signal states do not form cycles
|
|
91
|
-
this.validateOutputSignalCycles();
|
|
92
|
-
// Validate that all states are reachable from the start state
|
|
93
|
-
this.validateUnreachableStates();
|
|
94
|
-
}
|
|
95
|
-
// Set default state if exactly one found
|
|
96
|
-
if (defaultStates.length === 1) {
|
|
97
|
-
this.defaultState = defaultStates[0];
|
|
98
|
-
}
|
|
99
|
-
// Initialize the machine to its starting state
|
|
100
|
-
this.currentState = startState;
|
|
101
|
-
// Check if a current state has output signal and process it
|
|
102
|
-
this.tryProcessStateWithOutputSignal();
|
|
103
|
-
// Execute initial state entry action if supported
|
|
104
|
-
this.executeEntryAction(this.currentState);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Gets the current state of the state machine.
|
|
108
|
-
*
|
|
109
|
-
* @returns The current state
|
|
110
|
-
*/
|
|
111
|
-
getCurrentState() {
|
|
112
|
-
return this.currentState;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Processes a signal and potentially transitions to a new state with action execution.
|
|
116
|
-
*
|
|
117
|
-
* This method:
|
|
118
|
-
* 1. Finds a matching transition for the current state and signal
|
|
119
|
-
* 2. If found and the target state is different:
|
|
120
|
-
* a. Executes beforeExitAction on current state (if implemented)
|
|
121
|
-
* b. Changes to the new state
|
|
122
|
-
* c. Executes afterEntryAction on new state (if implemented)
|
|
123
|
-
* 3. Returns the resulting state
|
|
124
|
-
*
|
|
125
|
-
* @param signal - The signal/event to process
|
|
126
|
-
* @returns The resulting state after processing the signal
|
|
127
|
-
*/
|
|
128
|
-
sendSignal(signal) {
|
|
129
|
-
// Find a transition that matches current state and signal
|
|
130
|
-
const transition = this.transitions.find(t => t.from === this.currentState && t.signal === signal);
|
|
131
|
-
// If valid transition found, handle state change with actions
|
|
132
|
-
if (transition) {
|
|
133
|
-
this.executeStateTransition(transition);
|
|
134
|
-
}
|
|
135
|
-
else if (this.defaultState) {
|
|
136
|
-
// No valid transition found, but default state exists
|
|
137
|
-
// Execute default state's actions (entry/exit if implemented)
|
|
138
|
-
this.executeStateActions(this.defaultState);
|
|
139
|
-
// Current state remains unchanged
|
|
140
|
-
}
|
|
141
|
-
this.tryProcessStateWithOutputSignal();
|
|
142
|
-
return this.currentState;
|
|
143
|
-
}
|
|
144
|
-
tryProcessStateWithOutputSignal() {
|
|
145
|
-
if (this.isStateWithOutputSignal(this.currentState)) {
|
|
146
|
-
// Processing of behaviour for state with inside calculated output signal
|
|
147
|
-
this.executeStateActions(this.currentState);
|
|
148
|
-
const stateWithOutputSignal = this.currentState;
|
|
149
|
-
const outputSignal = stateWithOutputSignal.getOutputSignal();
|
|
150
|
-
// Add null/undefined check to prevent errors
|
|
151
|
-
if (outputSignal !== null && outputSignal !== undefined) {
|
|
152
|
-
this.sendSignal(outputSignal);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
validateUnreachableStates() {
|
|
157
|
-
const reachable = new Set();
|
|
158
|
-
const queue = [this.startState];
|
|
159
|
-
reachable.add(this.startState);
|
|
160
|
-
while (queue.length > 0) {
|
|
161
|
-
const current = queue.shift();
|
|
162
|
-
for (const t of this.transitions) {
|
|
163
|
-
if (t.from === current && !reachable.has(t.to)) {
|
|
164
|
-
reachable.add(t.to);
|
|
165
|
-
queue.push(t.to);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
for (const state of this.states) {
|
|
170
|
-
if (this.isDefaultState(state))
|
|
171
|
-
continue; // DefaultState handles invalid signals — unreachable by design
|
|
172
|
-
if (!reachable.has(state)) {
|
|
173
|
-
throw new Error(`ERROR-STOP-08: State '${state}' is unreachable from start state '${this.startState}'. ` +
|
|
174
|
-
`All non-default states must be reachable from the start state via the transition graph.`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
validateDanglingReferences() {
|
|
179
|
-
const stateSet = new Set(this.states);
|
|
180
|
-
const signalSet = new Set(this.signals);
|
|
181
|
-
if (!stateSet.has(this.startState)) {
|
|
182
|
-
throw new Error(`ERROR-STOP-03: Start state '${this.startState}' is not in the states array. ` +
|
|
183
|
-
`The start state must be one of the provided states.`);
|
|
184
|
-
}
|
|
185
|
-
for (const t of this.transitions) {
|
|
186
|
-
if (!stateSet.has(t.from)) {
|
|
187
|
-
throw new Error(`ERROR-STOP-04: Transition references unknown state '${t.from}' as 'from'. ` +
|
|
188
|
-
`All transition states must be in the states array.`);
|
|
189
|
-
}
|
|
190
|
-
if (!stateSet.has(t.to)) {
|
|
191
|
-
throw new Error(`ERROR-STOP-05: Transition references unknown state '${t.to}' as 'to'. ` +
|
|
192
|
-
`All transition states must be in the states array.`);
|
|
193
|
-
}
|
|
194
|
-
if (!signalSet.has(t.signal)) {
|
|
195
|
-
throw new Error(`ERROR-STOP-06: Transition references unknown signal '${String(t.signal)}'. ` +
|
|
196
|
-
`All transition signals must be in the signals array.`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
validateOutputSignalStates() {
|
|
201
|
-
for (const state of this.states) {
|
|
202
|
-
if (this.isStateWithOutputSignal(state)) {
|
|
203
|
-
const hasOutgoing = this.transitions.some(t => t.from === state);
|
|
204
|
-
if (!hasOutgoing) {
|
|
205
|
-
throw new Error(`ERROR-STOP-02: Configuration error: State '${state}' implements IStateWithOutputSignal ` +
|
|
206
|
-
`but has no outgoing transitions. States with output signals must have ` +
|
|
207
|
-
`at least one outgoing transition.`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
validateOutputSignalCycles() {
|
|
213
|
-
const outputStateSet = new Set(this.states.filter(s => this.isStateWithOutputSignal(s)));
|
|
214
|
-
if (outputStateSet.size < 2)
|
|
215
|
-
return;
|
|
216
|
-
// Build adjacency: output state → output states reachable via one transition
|
|
217
|
-
const adj = new Map();
|
|
218
|
-
for (const state of outputStateSet) {
|
|
219
|
-
adj.set(state, this.transitions
|
|
220
|
-
.filter(t => t.from === state && outputStateSet.has(t.to))
|
|
221
|
-
.map(t => t.to));
|
|
222
|
-
}
|
|
223
|
-
// DFS-based cycle detection (colour: 0=unvisited, 1=in-stack, 2=done)
|
|
224
|
-
const visited = new Set();
|
|
225
|
-
const onStack = new Set();
|
|
226
|
-
const stackPath = [];
|
|
227
|
-
const dfs = (state) => {
|
|
228
|
-
var _a;
|
|
229
|
-
visited.add(state);
|
|
230
|
-
onStack.add(state);
|
|
231
|
-
stackPath.push(state);
|
|
232
|
-
for (const neighbor of ((_a = adj.get(state)) !== null && _a !== void 0 ? _a : [])) {
|
|
233
|
-
if (onStack.has(neighbor)) {
|
|
234
|
-
const cycleStart = stackPath.indexOf(neighbor);
|
|
235
|
-
const cyclePath = [...stackPath.slice(cycleStart), neighbor];
|
|
236
|
-
throw new Error(`ERROR-STOP-07: Circular dependency detected in output signals: ` +
|
|
237
|
-
`${cyclePath.map(s => String(s)).join(' -> ')}. ` +
|
|
238
|
-
`States with output signals cannot form cycles as this creates infinite loops.`);
|
|
239
|
-
}
|
|
240
|
-
if (!visited.has(neighbor)) {
|
|
241
|
-
dfs(neighbor);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
onStack.delete(state);
|
|
245
|
-
stackPath.pop();
|
|
246
|
-
};
|
|
247
|
-
for (const state of outputStateSet) {
|
|
248
|
-
if (!visited.has(state)) {
|
|
249
|
-
dfs(state);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
isStateWithOutputSignal(state) {
|
|
254
|
-
const obj = state;
|
|
255
|
-
return (typeof obj === 'object' &&
|
|
256
|
-
obj !== null &&
|
|
257
|
-
typeof obj.getOutputSignal === 'function');
|
|
258
|
-
}
|
|
259
|
-
executeStateTransition(transition) {
|
|
260
|
-
const oldState = this.currentState;
|
|
261
|
-
const newState = transition.to;
|
|
262
|
-
// Only execute actions if there's an actual state change
|
|
263
|
-
if (oldState !== newState) {
|
|
264
|
-
// Execute before exit action on current state
|
|
265
|
-
this.executeExitAction(oldState);
|
|
266
|
-
// Change state
|
|
267
|
-
this.currentState = newState;
|
|
268
|
-
// Execute after entry action on new state
|
|
269
|
-
this.executeEntryAction(newState);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
executeStateActions(state) {
|
|
273
|
-
this.executeEntryAction(state);
|
|
274
|
-
this.executeExitAction(state);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Type guard to check if a state is an instance of DefaultState class.
|
|
278
|
-
*
|
|
279
|
-
* @param state - The state to check
|
|
280
|
-
* @returns true if the state is an instance of DefaultState
|
|
281
|
-
*/
|
|
282
|
-
isDefaultState(state) {
|
|
283
|
-
return state instanceof DefaultState;
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Type guard to check if a state implements IStateWithAfterEntryAction.
|
|
287
|
-
*
|
|
288
|
-
* @param state - The state to check
|
|
289
|
-
* @returns true if the state implements afterEntryAction method
|
|
290
|
-
*/
|
|
291
|
-
hasAfterEntryAction(state) {
|
|
292
|
-
return typeof state === 'object' &&
|
|
293
|
-
state !== null &&
|
|
294
|
-
'afterEntryAction' in state &&
|
|
295
|
-
typeof state.afterEntryAction === 'function';
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Type guard to check if a state implements IStateWithBeforeExitAction.
|
|
299
|
-
*
|
|
300
|
-
* @param state - The state to check
|
|
301
|
-
* @returns true if the state implements beforeExitAction method
|
|
302
|
-
*/
|
|
303
|
-
hasBeforeExitAction(state) {
|
|
304
|
-
return typeof state === 'object' &&
|
|
305
|
-
state !== null &&
|
|
306
|
-
'beforeExitAction' in state &&
|
|
307
|
-
typeof state.beforeExitAction === 'function';
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Executes the afterEntryAction on a state if it implements IStateWithAfterEntryAction.
|
|
311
|
-
*
|
|
312
|
-
* @param state - The state to execute the entry action on
|
|
313
|
-
*/
|
|
314
|
-
executeEntryAction(state) {
|
|
315
|
-
if (this.hasAfterEntryAction(state)) {
|
|
316
|
-
state.afterEntryAction();
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Executes the beforeExitAction on a state if it implements IStateWithBeforeExitAction.
|
|
321
|
-
*
|
|
322
|
-
* @param state - The state to execute the exit action on
|
|
323
|
-
*/
|
|
324
|
-
executeExitAction(state) {
|
|
325
|
-
if (this.hasBeforeExitAction(state)) {
|
|
326
|
-
state.beforeExitAction();
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Gets all possible states.
|
|
331
|
-
*
|
|
332
|
-
* @returns Array of all states
|
|
333
|
-
*/
|
|
334
|
-
getAllStates() {
|
|
335
|
-
return [...this.states];
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Gets all possible signals.
|
|
339
|
-
*
|
|
340
|
-
* @returns Array of all signals
|
|
341
|
-
*/
|
|
342
|
-
getAllSignals() {
|
|
343
|
-
return [...this.signals];
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Checks if a given signal is valid from the current state.
|
|
347
|
-
*
|
|
348
|
-
* @param signal - The signal to check
|
|
349
|
-
* @returns true if the signal can trigger a transition from the current state
|
|
350
|
-
*/
|
|
351
|
-
isValidSignalFromCurrentState(signal) {
|
|
352
|
-
return this.transitions.some(t => t.from === this.currentState && t.signal === signal);
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Gets all valid signals from the current state.
|
|
356
|
-
*
|
|
357
|
-
* @returns Array of signals that can trigger transitions from the current state
|
|
358
|
-
*/
|
|
359
|
-
getValidSignalsFromCurrentState() {
|
|
360
|
-
return this.transitions
|
|
361
|
-
.filter(t => t.from === this.currentState)
|
|
362
|
-
.map(t => t.signal);
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Gets all transitions.
|
|
366
|
-
*
|
|
367
|
-
* @returns Array of all transitions
|
|
368
|
-
*/
|
|
369
|
-
getTransitions() {
|
|
370
|
-
return [...this.transitions];
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Checks if a signal is valid in the state machine.
|
|
374
|
-
* @param signal - The signal to check
|
|
375
|
-
* @returns True if the signal is valid, false otherwise.
|
|
376
|
-
*/
|
|
377
|
-
hasSignal(signal) {
|
|
378
|
-
return this.getAllSignals().includes(signal);
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Checks if a given state exists in the state machine.
|
|
382
|
-
*
|
|
383
|
-
* @param state - The state to check
|
|
384
|
-
* @returns true if the state exists, false otherwise
|
|
385
|
-
*/
|
|
386
|
-
hasState(state) {
|
|
387
|
-
return this.getAllStates().includes(state);
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Gets the default state that handles invalid signals.
|
|
391
|
-
*
|
|
392
|
-
* @returns The default state or null if none exists
|
|
393
|
-
*/
|
|
394
|
-
getDefaultState() {
|
|
395
|
-
return this.defaultState;
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Checks if the state machine has a default state.
|
|
399
|
-
*
|
|
400
|
-
* @returns true if a default state exists
|
|
401
|
-
*/
|
|
402
|
-
hasDefaultState() {
|
|
403
|
-
return this.defaultState !== null;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { FiniteStateMachine } from './FiniteStateMachine';
|
|
2
|
-
/**
|
|
3
|
-
* Abstract base class for finite state machines that use transition matrices.
|
|
4
|
-
*
|
|
5
|
-
* This class extends FiniteStateMachine to provide a more intuitive way to define
|
|
6
|
-
* state transitions using a 2D matrix representation instead of explicit transition arrays.
|
|
7
|
-
*
|
|
8
|
-
* The matrix format allows for visual representation of state transitions:
|
|
9
|
-
* - Columns represent states
|
|
10
|
-
* - Rows represent signals
|
|
11
|
-
* - Cell values represent target states
|
|
12
|
-
* - Empty cells represent invalid/ignored transitions
|
|
13
|
-
*
|
|
14
|
-
* @template STATE - The type representing possible states
|
|
15
|
-
* @template SIGNAL - The type representing signals/events
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* // Example 1: Using explicit start state
|
|
20
|
-
* class TurnstileMatrix extends MatrixBasedStateMachine<string, string> {
|
|
21
|
-
* constructor() {
|
|
22
|
-
* const matrix = transitionMatrix([
|
|
23
|
-
* [ , "locked" , "unlocked" ],
|
|
24
|
-
* [ "coin" , "unlocked" , ],
|
|
25
|
-
* [ "push" , , "locked" ]
|
|
26
|
-
* ]);
|
|
27
|
-
* super(matrix, "locked"); // Explicit start state
|
|
28
|
-
* }
|
|
29
|
-
* }
|
|
30
|
-
*
|
|
31
|
-
* // Example 2: Using first state as start state (auto)
|
|
32
|
-
* class TurnstileMatrixAuto extends MatrixBasedStateMachine<string, string> {
|
|
33
|
-
* constructor() {
|
|
34
|
-
* const matrix = transitionMatrix([
|
|
35
|
-
* [ , "locked" , "unlocked" ], // "locked" becomes start state
|
|
36
|
-
* [ "coin" , "unlocked" , ],
|
|
37
|
-
* [ "push" , , "locked" ]
|
|
38
|
-
* ]);
|
|
39
|
-
* super(matrix); // Uses first state ("locked") as start state
|
|
40
|
-
* }
|
|
41
|
-
* }
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export class MatrixBasedStateMachine extends FiniteStateMachine {
|
|
45
|
-
/**
|
|
46
|
-
* Creates a new matrix-based finite state machine.
|
|
47
|
-
*
|
|
48
|
-
* @param matrix - The transition matrix defining all states, signals, and transitions
|
|
49
|
-
* @param startState - Optional. The initial state of the machine. If not provided,
|
|
50
|
-
* the first state from the matrix (first column in header row) will be used.
|
|
51
|
-
* If provided, must be one of the states in the matrix.
|
|
52
|
-
*
|
|
53
|
-
* @throws {Error} If matrix has no states
|
|
54
|
-
* @throws {Error} If startState is provided but not found in the matrix states
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* ```typescript
|
|
58
|
-
* // Constructor with explicit start state
|
|
59
|
-
* const matrix = transitionMatrix([
|
|
60
|
-
* [ , "locked" , "unlocked" ],
|
|
61
|
-
* [ "coin" , "unlocked" , ],
|
|
62
|
-
* [ "push" , , "locked" ]
|
|
63
|
-
* ]);
|
|
64
|
-
* super(matrix, "unlocked"); // Start in unlocked state
|
|
65
|
-
*
|
|
66
|
-
* // Constructor with auto start state (uses first state)
|
|
67
|
-
* super(matrix); // Automatically starts in "locked" state
|
|
68
|
-
* ```
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* ```typescript
|
|
72
|
-
* // TurnstileMatrix implementation from test/Turnstile/TurnstileMatrix.ts
|
|
73
|
-
* class TurnstileMatrix extends MatrixBasedStateMachine<string, string> {
|
|
74
|
-
* constructor() {
|
|
75
|
-
* const matrix = transitionMatrix([
|
|
76
|
-
* [ , "locked" , "unlocked" ],
|
|
77
|
-
* [ "coin" , "unlocked" , ],
|
|
78
|
-
* [ "push" , , "locked" ]
|
|
79
|
-
* ]);
|
|
80
|
-
*
|
|
81
|
-
* // Two ways to call the constructor:
|
|
82
|
-
* super(matrix, "locked"); // Option 1: Explicit start state
|
|
83
|
-
* // OR
|
|
84
|
-
* super(matrix); // Option 2: Auto start state (uses "locked")
|
|
85
|
-
* }
|
|
86
|
-
*
|
|
87
|
-
* insertCoin(): string { return this.sendSignal('coin'); }
|
|
88
|
-
* pushThrough(): string { return this.sendSignal('push'); }
|
|
89
|
-
* isLocked(): boolean { return this.getCurrentState() === 'locked'; }
|
|
90
|
-
* isUnlocked(): boolean { return this.getCurrentState() === 'unlocked'; }
|
|
91
|
-
* }
|
|
92
|
-
* ```
|
|
93
|
-
*/
|
|
94
|
-
constructor(matrix, startState) {
|
|
95
|
-
// Get states from matrix
|
|
96
|
-
const states = matrix.getStates();
|
|
97
|
-
// Validate matrix has states
|
|
98
|
-
if (states.length === 0) {
|
|
99
|
-
throw new Error('Matrix must contain at least one state');
|
|
100
|
-
}
|
|
101
|
-
// Determine the actual start state
|
|
102
|
-
let actualStartState;
|
|
103
|
-
if (startState !== undefined) {
|
|
104
|
-
// Explicit start state provided - validate it exists
|
|
105
|
-
if (!states.includes(startState)) {
|
|
106
|
-
throw new Error(`Start state '${String(startState)}' not found in matrix states: [${states.map(s => String(s)).join(', ')}]`);
|
|
107
|
-
}
|
|
108
|
-
actualStartState = startState;
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
// No start state provided - use first state from matrix
|
|
112
|
-
actualStartState = states[0];
|
|
113
|
-
}
|
|
114
|
-
// Call parent constructor with matrix-derived data
|
|
115
|
-
super(matrix.getStates(), matrix.getSignals(), matrix.getTransitions(), actualStartState);
|
|
116
|
-
// Store the matrix for potential future use
|
|
117
|
-
this.matrix = matrix;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Gets the transition matrix used by this state machine.
|
|
121
|
-
*
|
|
122
|
-
* @returns The transition matrix
|
|
123
|
-
*/
|
|
124
|
-
getMatrix() {
|
|
125
|
-
return this.matrix;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates transitions from a 2D matrix representation.
|
|
3
|
-
*
|
|
4
|
-
* Matrix format:
|
|
5
|
-
* - First row: [undefined, ...states] - column headers
|
|
6
|
-
* - Subsequent rows: [signal, ...transitions] - signal + target states
|
|
7
|
-
* - Empty cells: undefined, null, "", or omitted
|
|
8
|
-
*
|
|
9
|
-
* @template STATE - Type for states
|
|
10
|
-
* @template SIGNAL - Type for signals
|
|
11
|
-
*/
|
|
12
|
-
export class TransitionMatrix {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.states = [];
|
|
15
|
-
this.signals = [];
|
|
16
|
-
this.transitions = [];
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Creates a transition matrix from a 2D array.
|
|
20
|
-
*
|
|
21
|
-
* @param matrix - 2D array where:
|
|
22
|
-
* - matrix[0] = [undefined, ...states] (header row)
|
|
23
|
-
* - matrix[i] = [signal, target1, target2, ...] (transition rows)
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```typescript
|
|
27
|
-
* const matrix = [
|
|
28
|
-
* [ , "locked" , "unlocked" ], // States header
|
|
29
|
-
* [ "coin" , "unlocked" , ], // coin transitions
|
|
30
|
-
* [ "push" , , "locked" ] // push transitions
|
|
31
|
-
* ];
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
static fromArray(matrix) {
|
|
35
|
-
const instance = new TransitionMatrix();
|
|
36
|
-
if (matrix.length === 0) {
|
|
37
|
-
throw new Error('Matrix cannot be empty');
|
|
38
|
-
}
|
|
39
|
-
// Extract states from header row (skip first empty cell)
|
|
40
|
-
const headerRow = matrix[0];
|
|
41
|
-
const stateValues = headerRow.slice(1).filter(cell => cell !== undefined && cell !== null && cell !== "");
|
|
42
|
-
// Type assertion: assume header row contains only states
|
|
43
|
-
instance.states = stateValues;
|
|
44
|
-
// Process each signal row
|
|
45
|
-
for (let rowIndex = 1; rowIndex < matrix.length; rowIndex++) {
|
|
46
|
-
const row = matrix[rowIndex];
|
|
47
|
-
const signal = row[0];
|
|
48
|
-
// Skip rows without valid signal
|
|
49
|
-
if (!signal || signal === "" || signal === null || signal === undefined) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
// Type assertion: assume first column contains only signals
|
|
53
|
-
instance.signals.push(signal);
|
|
54
|
-
// Process each state column
|
|
55
|
-
for (let colIndex = 1; colIndex < row.length; colIndex++) {
|
|
56
|
-
const targetState = row[colIndex];
|
|
57
|
-
const fromState = instance.states[colIndex - 1];
|
|
58
|
-
// If cell has a value, create transition
|
|
59
|
-
if (targetState && targetState !== "" && targetState !== null && targetState !== undefined && fromState) {
|
|
60
|
-
// Type assertion: assume cell values are states
|
|
61
|
-
instance.transitions.push({
|
|
62
|
-
from: fromState,
|
|
63
|
-
to: targetState,
|
|
64
|
-
signal: signal
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return instance;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Get all states from the matrix.
|
|
73
|
-
*/
|
|
74
|
-
getStates() {
|
|
75
|
-
return [...this.states];
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Get all signals from the matrix.
|
|
79
|
-
*/
|
|
80
|
-
getSignals() {
|
|
81
|
-
return [...this.signals];
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Get all transitions from the matrix.
|
|
85
|
-
*/
|
|
86
|
-
getTransitions() {
|
|
87
|
-
return [...this.transitions];
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Helper function to create transition matrix from 2D array.
|
|
92
|
-
*
|
|
93
|
-
* @template S - State type
|
|
94
|
-
* @template G - Signal type
|
|
95
|
-
*/
|
|
96
|
-
export function transitionMatrix(matrix) {
|
|
97
|
-
return TransitionMatrix.fromArray(matrix);
|
|
98
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Marker base class for states that should be used processed by invalid signals and
|
|
3
|
-
* in situation, then no transitions set for pair (current signal, current state).
|
|
4
|
-
* Only one state in a state machine can have this role.
|
|
5
|
-
*/
|
|
6
|
-
export class DefaultState {
|
|
7
|
-
isDefaultState() {
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
}
|