@vsirotin/ts-stop 1.13.1 → 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 +9 -8
- 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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sfsm-compare-json-uml-diagram
|
|
3
|
+
description: Compare an SFSM FA definition JSON file against a draw.io UML state diagram and report every discrepancy. Use when validating that a diagram matches its source JSON, or vice versa. Combines drawio-to-json and compare-compact-jsons for round-trip verification.
|
|
4
|
+
metadata:
|
|
5
|
+
author: vsirotin
|
|
6
|
+
version: "2.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Compare SFSM Compact JSON ↔ UML State Diagram (draw.io)
|
|
10
|
+
|
|
11
|
+
This skill validates the correspondence between an SFSM FA definition in compact JSON format and a draw.io UML state diagram. It combines two scripts to produce a detailed diff report without modifying either file:
|
|
12
|
+
1. **`drawio-to-json.js`** extracts JSON from the diagram (via SFSM metadata in cell styles or shape-based heuristics)
|
|
13
|
+
2. **`compare-compact-jsons.js`** performs an intelligent comparison of the original and extracted JSON
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Use this skill when you need to:
|
|
18
|
+
- **Validate** that a draw.io diagram matches its source JSON (e.g., after generating it with `sfsm-json-to-uml-diagram`)
|
|
19
|
+
- **Verify** a round-trip workflow: JSON → Diagram → JSON → Compare
|
|
20
|
+
- **Audit** a manually edited diagram to detect any data loss or drift
|
|
21
|
+
- **Catch discrepancies** early (missing transitions, target mismatches, incorrect commands, etc.)
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
The skill requests two file paths from you (unless you provide them upfront):
|
|
26
|
+
- **JSON file**: A compact-format SFSM definition
|
|
27
|
+
- **Diagram file**: A `.drawio` file (ideally generated by `json-to-drawio.js`)
|
|
28
|
+
|
|
29
|
+
It then runs:
|
|
30
|
+
1. `drawio-to-json.js <diagram.drawio> <extracted.json>` — extract JSON from the diagram
|
|
31
|
+
2. `compare-compact-jsons.js <original.json> <extracted.json>` — compare and report differences
|
|
32
|
+
|
|
33
|
+
## Output
|
|
34
|
+
|
|
35
|
+
The comparison report shows, per FA:
|
|
36
|
+
- ✅ Number of matching transitions
|
|
37
|
+
- ❌ Missing transitions (in diagram, not in JSON)
|
|
38
|
+
- ❌ Extra transitions (in diagram, not in JSON)
|
|
39
|
+
- ❌ Mismatches (same `from/signal` pair, different `to` or `command`)
|
|
40
|
+
|
|
41
|
+
If both sides match exactly, the report confirms alignment with no discrepancies.
|
|
42
|
+
|
|
43
|
+
## Input/Output
|
|
44
|
+
|
|
45
|
+
- **Input**: Compact JSON file path (requested if not provided) and diagram `.drawio` file path (requested if not provided)
|
|
46
|
+
- **Output**: A report printed to console (no files are modified)
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd ts/ts-stop
|
|
52
|
+
node scripts/drawio-to-json.js <input.drawio> <extracted.json>
|
|
53
|
+
node scripts/compare-compact-jsons.js <original.json> <extracted.json>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Important note
|
|
57
|
+
|
|
58
|
+
**No files are modified by this skill.** If the comparison reveals discrepancies and you decide which side is authoritative, use:
|
|
59
|
+
- `sfsm-json-to-uml-diagram` to regenerate the diagram from the JSON if the JSON is authoritative
|
|
60
|
+
- `sfsm-uml-diagram-to-json` to extract the diagram's corrected JSON if the diagram is authoritative
|
|
61
|
+
|
|
62
|
+
See [04-tools.md § 7.5.5](../../tutorial/04-tools.md#755-compare-compact-jsons--validate-diagram-vs-json-correspondence) for detailed documentation.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sfsm-json-to-uml-diagram
|
|
3
|
+
description: Convert an SFSM FA definition JSON file (compact or extended format) to a professional draw.io UML state diagram with hierarchical ELK.js layout. Use when asked to visualize or diagram an SFSM JSON definition. The generated diagram may require small manual repositioning for visual perfection.
|
|
4
|
+
metadata:
|
|
5
|
+
author: vsirotin
|
|
6
|
+
version: "2.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# SFSM JSON → UML State Diagram (draw.io)
|
|
10
|
+
|
|
11
|
+
This skill wraps the `ts/ts-stop/scripts/json-to-drawio.js` script to convert an SFSM FA definition into a professional, hierarchical draw.io UML state diagram.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Invoke the script directly or use this skill when you need to:
|
|
16
|
+
- **Visualize** an SFSM JSON definition as a UML state diagram
|
|
17
|
+
- **Generate** a draw.io file for inspection, sharing, or further manual editing
|
|
18
|
+
- **Export** an FA definition to a visual format
|
|
19
|
+
|
|
20
|
+
## What the script produces
|
|
21
|
+
|
|
22
|
+
- **Hierarchical layout**: Each FA rendered as a swimlane container; sub-FAs as nested light-grey boxes (100×60 px).
|
|
23
|
+
- **ELK.js automatic layout**: Professional hierarchical positioning of states within each FA using the Eclipse Layout Kernel.
|
|
24
|
+
- **Vertical stacking**: Multiple FAs stacked without overlap with 30px spacing.
|
|
25
|
+
- **Complete metadata**: All SFSM state roles, FA names, signals, and commands embedded in cell styles for round-trip conversion back to JSON.
|
|
26
|
+
- **UML state types**: Initial states (black dot), plain states (rounded rectangles), sub-FA containers (swimlanes), and exit/final states (thick-bordered circles).
|
|
27
|
+
|
|
28
|
+
## Important note
|
|
29
|
+
|
|
30
|
+
**Generated diagrams may require small manual adjustments** for visual perfection. Common improvements:
|
|
31
|
+
- Reposition state or container nodes for better spacing or clarity
|
|
32
|
+
- Adjust swimlane heights or container sizes
|
|
33
|
+
- Tweak edge routing or labels
|
|
34
|
+
|
|
35
|
+
These manual edits preserve the underlying SFSM metadata (stored in cell styles), so the diagram remains convertible back to JSON with zero data loss.
|
|
36
|
+
|
|
37
|
+
## Input/Output
|
|
38
|
+
|
|
39
|
+
- **Input**: Path to a compact or extended-format SFSM JSON file
|
|
40
|
+
- **Output**: A `.drawio` file ready for inspection in draw.io or manual editing
|
|
41
|
+
|
|
42
|
+
## Command
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd ts/ts-stop
|
|
46
|
+
npm run build # if not already built
|
|
47
|
+
node scripts/json-to-drawio.js <input.json> <output.drawio>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See [04-tools.md § 7.5.3](../../tutorial/04-tools.md#753-json-to-drawio--convert-compact-fa-json-to-uml-state-diagram) for detailed documentation.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sfsm-uml-diagram-to-json
|
|
3
|
+
description: Extract an SFSM FA definition JSON file in compact format from a draw.io UML state diagram. Use when asked to convert, export, or reverse-engineer a diagram back into SFSM/StOP JSON.
|
|
4
|
+
metadata:
|
|
5
|
+
author: vsirotin
|
|
6
|
+
version: "2.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# UML State Diagram (draw.io) → SFSM Compact JSON
|
|
10
|
+
|
|
11
|
+
This skill wraps the `ts/ts-stop/scripts/drawio-to-json.js` script to extract or reverse-convert a draw.io diagram into a compact-format SFSM FA definition ready for `Sfsm.loadFA()`.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Invoke the script directly or use this skill when you need to:
|
|
16
|
+
- **Convert** a draw.io diagram back to SFSM JSON format
|
|
17
|
+
- **Reverse-engineer** a manually drawn state diagram into a machine-readable FA definition
|
|
18
|
+
- **Extract** SFSM data from a diagram produced by the `sfsm-json-to-uml-diagram` skill (lossless round-trip)
|
|
19
|
+
- **Export** a collaborator's visual diagram as a JSON file for programmatic use
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
The script reads a `.drawio` file and extracts every state, transition, and SFSM-specific metadata:
|
|
24
|
+
- **Preferred**: Diagrams generated by `json-to-drawio.js` include custom metadata (sfsmRole, sfsmFa, sfsmKey, sfsmSignal, sfsmCommand) embedded in cell styles — these are read directly and trusted as ground truth.
|
|
25
|
+
- **Fallback**: Hand-drawn diagrams without metadata are parsed using shape-based heuristics (ellipses → states, swimlanes → sub-FAs, thick-bordered circles → exit states, etc.).
|
|
26
|
+
|
|
27
|
+
Output is a compact-format JSON object: `{ "<FaName>": [[from, signal, to], [from, signal, to, command], ...], ... }`
|
|
28
|
+
|
|
29
|
+
## Input/Output
|
|
30
|
+
|
|
31
|
+
- **Input**: A `.drawio` file (ideally generated by `json-to-drawio.js`)
|
|
32
|
+
- **Output**: A `.json` file in compact SFSM format
|
|
33
|
+
|
|
34
|
+
## Command
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd ts/ts-stop
|
|
38
|
+
node scripts/drawio-to-json.js <input.drawio> <output.json>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Validation tip
|
|
42
|
+
|
|
43
|
+
After converting a diagram to JSON, compare the result against the original JSON using the `compare-compact-jsons.js` script or the `sfsm-compare-json-uml-diagram` skill to verify the round-trip was lossless:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node scripts/compare-compact-jsons.js <original.json> <output.json>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
See [04-tools.md § 7.5.4](../../tutorial/04-tools.md#754-drawio-to-json--extract-compact-fa-json-from-uml-diagram) for detailed documentation.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for the command-receiving role of an external-world component
|
|
3
|
+
* (a "Controller" in ControllerHub terminology).
|
|
4
|
+
*
|
|
5
|
+
* Concrete subclasses declare the command names they handle via getCommandNames()
|
|
6
|
+
* (read by ControllerHub for routing and diagnostics — no need to repeat the list
|
|
7
|
+
* again at the registration call site) and implement receiveCommand() to react to
|
|
8
|
+
* commands dispatched by the SFSM.
|
|
9
|
+
*
|
|
10
|
+
* A component that both sends signals and receives commands should extend
|
|
11
|
+
* SignalSender and additionally implement this class's contract directly
|
|
12
|
+
* (getCommandNames / receiveCommand) — TypeScript does not allow extending two
|
|
13
|
+
* classes, but structurally implementing this abstract class's shape is sufficient
|
|
14
|
+
* for ControllerHub.registerCommandReceiver().
|
|
15
|
+
*/
|
|
16
|
+
export class CommandReceiver {
|
|
17
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wiring hub between the SFSM and a collection of Controllers — components that
|
|
3
|
+
* play the SignalSender and/or CommandReceiver role for the "external world".
|
|
4
|
+
*
|
|
5
|
+
* - registerSignalSender(sender) — wires a SignalSender; its signal names are
|
|
6
|
+
* read from sender.getSignalNames(), so they never need to be repeated here.
|
|
7
|
+
* - registerCommandReceiver(receiver) — wires a CommandReceiver; its command names
|
|
8
|
+
* are read from receiver.getCommandNames(). Throws if a command name is already
|
|
9
|
+
* registered by another receiver.
|
|
10
|
+
* - connectTo(sfsm) — wires everything: sets itself as the SFSM's command receiver
|
|
11
|
+
* and calls connectSignalTarget(sfsm) on every registered signal sender.
|
|
12
|
+
*
|
|
13
|
+
* A single component that plays both roles (most real devices do) extends
|
|
14
|
+
* SignalSender and additionally implements the CommandReceiver contract on the
|
|
15
|
+
* same class, then is registered once with each method:
|
|
16
|
+
*
|
|
17
|
+
* new ControllerHub()
|
|
18
|
+
* .registerSignalSender(turnstileService)
|
|
19
|
+
* .registerSignalSender(turnstileDevice)
|
|
20
|
+
* .registerCommandReceiver(turnstileDevice)
|
|
21
|
+
* .connectTo(sfsm);
|
|
22
|
+
*/
|
|
23
|
+
export class ControllerHub {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.commandRoutes = new Map();
|
|
26
|
+
this.signalSenders = [];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register a signal sender. Its signal names (getSignalNames()) are used for
|
|
30
|
+
* diagnostics via getRegisteredSignals().
|
|
31
|
+
* Fluent — returns this.
|
|
32
|
+
*/
|
|
33
|
+
registerSignalSender(sender) {
|
|
34
|
+
this.signalSenders.push(sender);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Register a command receiver for all command names it declares via getCommandNames().
|
|
39
|
+
* Throws if any of those command names is already registered.
|
|
40
|
+
* Fluent — returns this.
|
|
41
|
+
*/
|
|
42
|
+
registerCommandReceiver(receiver) {
|
|
43
|
+
for (const cmd of receiver.getCommandNames()) {
|
|
44
|
+
if (this.commandRoutes.has(cmd)) {
|
|
45
|
+
throw new Error(`ControllerHub: command "${cmd}" is already registered`);
|
|
46
|
+
}
|
|
47
|
+
this.commandRoutes.set(cmd, receiver);
|
|
48
|
+
}
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Wire this hub to the given SFSM instance:
|
|
53
|
+
* 1. Sets this hub as the SFSM's command receiver.
|
|
54
|
+
* 2. Calls connectSignalTarget(sfsm) on every registered signal sender.
|
|
55
|
+
* Fluent — returns this.
|
|
56
|
+
*/
|
|
57
|
+
connectTo(sfsm) {
|
|
58
|
+
sfsm.setCommandReceiver(this);
|
|
59
|
+
for (const sender of this.signalSenders) {
|
|
60
|
+
sender.connectSignalTarget(sfsm);
|
|
61
|
+
}
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
/** ICommandReceiver implementation — dispatches command to the registered receiver. */
|
|
65
|
+
receiveCommand(command, data) {
|
|
66
|
+
const receiver = this.commandRoutes.get(command);
|
|
67
|
+
if (!receiver) {
|
|
68
|
+
const registered = [...this.commandRoutes.keys()].join(', ');
|
|
69
|
+
throw new Error(`ControllerHub: no receiver registered for command "${command}". ` +
|
|
70
|
+
`Registered commands: [${registered}]`);
|
|
71
|
+
}
|
|
72
|
+
receiver.receiveCommand(command, data);
|
|
73
|
+
}
|
|
74
|
+
/** Returns all command names that have a registered receiver (for diagnostics / validation). */
|
|
75
|
+
getRegisteredCommands() {
|
|
76
|
+
return [...this.commandRoutes.keys()];
|
|
77
|
+
}
|
|
78
|
+
/** Returns all signal names that have a registered sender (for diagnostics / validation). */
|
|
79
|
+
getRegisteredSignals() {
|
|
80
|
+
return this.signalSenders.flatMap(s => s.getSignalNames());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -67,7 +67,8 @@ export class FaResolver {
|
|
|
67
67
|
name: rootName,
|
|
68
68
|
transitions: rootValue,
|
|
69
69
|
subFaNames: new Set(),
|
|
70
|
-
node
|
|
70
|
+
node,
|
|
71
|
+
entryState: this.resolveEntryState(rootValue)
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
else {
|
|
@@ -85,7 +86,13 @@ export class FaResolver {
|
|
|
85
86
|
.map(t => t[2])
|
|
86
87
|
.filter(target => definedFaNames.has(target)));
|
|
87
88
|
const node = { ts: transitions };
|
|
88
|
-
this.index.set(name, {
|
|
89
|
+
this.index.set(name, {
|
|
90
|
+
name,
|
|
91
|
+
transitions,
|
|
92
|
+
subFaNames,
|
|
93
|
+
node,
|
|
94
|
+
entryState: this.resolveEntryState(transitions)
|
|
95
|
+
});
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
98
|
indexNode(name, node) {
|
|
@@ -102,7 +109,8 @@ export class FaResolver {
|
|
|
102
109
|
name,
|
|
103
110
|
transitions: node.ts,
|
|
104
111
|
subFaNames,
|
|
105
|
-
node
|
|
112
|
+
node,
|
|
113
|
+
entryState: this.resolveEntryState(node.ts)
|
|
106
114
|
});
|
|
107
115
|
}
|
|
108
116
|
/** A state value is a sub-FA if it has a "ts" array property. */
|
|
@@ -112,4 +120,17 @@ export class FaResolver {
|
|
|
112
120
|
'ts' in value &&
|
|
113
121
|
Array.isArray(value.ts));
|
|
114
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Auto-detects the literal entry-state name used by a FA's own transitions:
|
|
125
|
+
* the bare `"I"`, or a namespaced `"<FaName>.I"` (e.g. `"TS.I"`). Falls back
|
|
126
|
+
* to `"I"` if the FA has no transition whose from-state matches either form.
|
|
127
|
+
*/
|
|
128
|
+
resolveEntryState(transitions) {
|
|
129
|
+
for (const t of transitions) {
|
|
130
|
+
if (t[0] === 'I' || t[0].endsWith('.I')) {
|
|
131
|
+
return t[0];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return 'I';
|
|
135
|
+
}
|
|
115
136
|
}
|
package/lib/esm/sfsm/Sfsm.js
CHANGED
|
@@ -12,10 +12,24 @@ import { FaResolver } from './FaResolver';
|
|
|
12
12
|
* processing a signal (e.g. from within a command receiver callback), the
|
|
13
13
|
* incoming signal is queued and processed after the current step completes.
|
|
14
14
|
* This prevents stack corruption and ensures deterministic ordering.
|
|
15
|
+
*
|
|
16
|
+
* Jokers: a transition may use a reserved "joker" string (default '*',
|
|
17
|
+
* configurable via SfsmOptions.jokerSignal / jokerState) in its signal or
|
|
18
|
+
* from-state slot to act as a fallback. A joker-signal transition matches
|
|
19
|
+
* any signal for its from-state; a joker-state transition matches any
|
|
20
|
+
* from-state for its signal. Exact, literal transitions always take
|
|
21
|
+
* priority over joker matches. See docs/Tutorial/Tutorial.md for examples.
|
|
22
|
+
*
|
|
23
|
+
* Entry/exit naming convention: every FA's entry state is either the bare
|
|
24
|
+
* "I", or a namespaced "<FaName>.I" (e.g. "TS.I") — auto-detected per FA from
|
|
25
|
+
* its own transitions. Every exit state either starts with "E_", or contains
|
|
26
|
+
* ".E_" (e.g. "TS.E_ok"). Both forms are recognised everywhere and may be
|
|
27
|
+
* freely mixed across FAs; see Tutorial.md §8.1 for when to use the
|
|
28
|
+
* namespaced form.
|
|
15
29
|
*/
|
|
16
30
|
export class Sfsm {
|
|
17
31
|
constructor(options = {}) {
|
|
18
|
-
var _a, _b;
|
|
32
|
+
var _a, _b, _c, _d;
|
|
19
33
|
this.resolver = null;
|
|
20
34
|
this.stack = [];
|
|
21
35
|
this.commandReceiver = null;
|
|
@@ -24,7 +38,9 @@ export class Sfsm {
|
|
|
24
38
|
this.signalQueue = [];
|
|
25
39
|
this.options = {
|
|
26
40
|
byMissingData: (_a = options.byMissingData) !== null && _a !== void 0 ? _a : 'error',
|
|
27
|
-
byMissingTransition: (_b = options.byMissingTransition) !== null && _b !== void 0 ? _b : 'error'
|
|
41
|
+
byMissingTransition: (_b = options.byMissingTransition) !== null && _b !== void 0 ? _b : 'error',
|
|
42
|
+
jokerSignal: (_c = options.jokerSignal) !== null && _c !== void 0 ? _c : '*',
|
|
43
|
+
jokerState: (_d = options.jokerState) !== null && _d !== void 0 ? _d : '*'
|
|
28
44
|
};
|
|
29
45
|
}
|
|
30
46
|
/** Register the single command receiver for this SFSM instance. */
|
|
@@ -40,7 +56,7 @@ export class Sfsm {
|
|
|
40
56
|
loadFA(definition) {
|
|
41
57
|
this.resolver = new FaResolver(definition);
|
|
42
58
|
const rootName = this.resolver.getRootName();
|
|
43
|
-
this.stack = [{ faName: rootName, currentState:
|
|
59
|
+
this.stack = [{ faName: rootName, currentState: this.resolver.get(rootName).entryState }];
|
|
44
60
|
this.log = [];
|
|
45
61
|
this.processing = false;
|
|
46
62
|
this.signalQueue = [];
|
|
@@ -92,12 +108,30 @@ export class Sfsm {
|
|
|
92
108
|
/**
|
|
93
109
|
* Rule 2: find a matching transition, searching from head down the stack.
|
|
94
110
|
* Returns { frameIndex, transition } or null if not found anywhere.
|
|
111
|
+
*
|
|
112
|
+
* Within each frame, matches are tried in priority order so that exact,
|
|
113
|
+
* literal transitions always win over joker (wildcard) ones:
|
|
114
|
+
* 1. exact from-state + exact signal
|
|
115
|
+
* 2. exact from-state + joker signal
|
|
116
|
+
* 3. joker from-state + exact signal
|
|
117
|
+
* 4. joker from-state + joker signal
|
|
95
118
|
*/
|
|
96
119
|
findTransition(signal) {
|
|
120
|
+
const jokerSignal = this.options.jokerSignal;
|
|
121
|
+
const jokerState = this.options.jokerState;
|
|
97
122
|
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
98
123
|
const frame = this.stack[i];
|
|
99
124
|
const fa = this.resolver.get(frame.faName);
|
|
100
|
-
|
|
125
|
+
let match = fa.transitions.find(t => t[0] === frame.currentState && t[1] === signal);
|
|
126
|
+
if (!match && jokerSignal !== undefined) {
|
|
127
|
+
match = fa.transitions.find(t => t[0] === frame.currentState && t[1] === jokerSignal);
|
|
128
|
+
}
|
|
129
|
+
if (!match && jokerState !== undefined) {
|
|
130
|
+
match = fa.transitions.find(t => t[0] === jokerState && t[1] === signal);
|
|
131
|
+
}
|
|
132
|
+
if (!match && jokerState !== undefined && jokerSignal !== undefined) {
|
|
133
|
+
match = fa.transitions.find(t => t[0] === jokerState && t[1] === jokerSignal);
|
|
134
|
+
}
|
|
101
135
|
if (match) {
|
|
102
136
|
return { frameIndex: i, toState: match[2], command: match[3] };
|
|
103
137
|
}
|
|
@@ -160,15 +194,15 @@ export class Sfsm {
|
|
|
160
194
|
// Rule 3: new state is itself a sub-FA — push it
|
|
161
195
|
const headFa = this.resolver.get(this.stack[frameIndex].faName);
|
|
162
196
|
if (headFa.subFaNames.has(toState)) {
|
|
163
|
-
this.stack.push({ faName: toState, currentState:
|
|
197
|
+
this.stack.push({ faName: toState, currentState: this.resolver.get(toState).entryState });
|
|
164
198
|
this.applySignal(signal, data); // forward signal to sub-FA
|
|
165
199
|
return;
|
|
166
200
|
}
|
|
167
201
|
// Rule 4: new state is an exit state
|
|
168
|
-
if (
|
|
202
|
+
if (this.isExitState(toState)) {
|
|
169
203
|
if (this.stack.length === 1) {
|
|
170
204
|
// Rule 4.1 — root FA resets to I
|
|
171
|
-
this.stack[0].currentState =
|
|
205
|
+
this.stack[0].currentState = this.resolver.get(this.stack[0].faName).entryState;
|
|
172
206
|
}
|
|
173
207
|
else {
|
|
174
208
|
// Rule 4.2 — pop current FA, forward signal to new head
|
|
@@ -178,6 +212,13 @@ export class Sfsm {
|
|
|
178
212
|
}
|
|
179
213
|
}
|
|
180
214
|
// ── Meta resolution helpers ──────────────────────────────────────────────
|
|
215
|
+
/**
|
|
216
|
+
* A state is an exit/final state if it starts with "E_" (default
|
|
217
|
+
* convention) or contains ".E_" (namespaced convention, e.g. "TS.E_ok").
|
|
218
|
+
*/
|
|
219
|
+
isExitState(state) {
|
|
220
|
+
return state.startsWith('E_') || state.includes('.E_');
|
|
221
|
+
}
|
|
181
222
|
resolveStateMeta(frameIndex, state) {
|
|
182
223
|
var _a;
|
|
183
224
|
const fa = this.resolver.get(this.stack[frameIndex].faName);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for the signal-sending role of an external-world component
|
|
3
|
+
* (a "Controller" in ControllerHub terminology).
|
|
4
|
+
*
|
|
5
|
+
* Concrete subclasses:
|
|
6
|
+
* - declare the signal names they may emit via getSignalNames()
|
|
7
|
+
* (read by ControllerHub for wiring and diagnostics — no need to repeat the
|
|
8
|
+
* list again at the registration call site);
|
|
9
|
+
* - call the protected sendSignal() to push a signal to the connected target
|
|
10
|
+
* (normally the SFSM).
|
|
11
|
+
*
|
|
12
|
+
* ControllerHub calls connectSignalTarget() automatically during connectTo().
|
|
13
|
+
*/
|
|
14
|
+
export class SignalSender {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.signalTarget = null;
|
|
17
|
+
}
|
|
18
|
+
/** Called by ControllerHub to connect the SFSM (or any receiver) as the signal target. */
|
|
19
|
+
connectSignalTarget(target) {
|
|
20
|
+
this.signalTarget = target;
|
|
21
|
+
}
|
|
22
|
+
/** Sends a signal to the connected target (normally the SFSM). */
|
|
23
|
+
sendSignal(name, data) {
|
|
24
|
+
var _a;
|
|
25
|
+
(_a = this.signalTarget) === null || _a === void 0 ? void 0 : _a.receiveSignal(name, data);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/esm/sfsm/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ControllerHub } from './ControllerHub';
|
|
2
|
+
export { SignalSender } from './SignalSender';
|
|
3
|
+
export { CommandReceiver } from './CommandReceiver';
|
|
2
4
|
export { FaResolver } from './FaResolver';
|
|
3
5
|
export { Sfsm } from './Sfsm';
|
|
4
6
|
export { reduceFA } from './FaReducer';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ICommandReceiver } from './interfaces';
|
|
2
|
+
/**
|
|
3
|
+
* Abstract base class for the command-receiving role of an external-world component
|
|
4
|
+
* (a "Controller" in ControllerHub terminology).
|
|
5
|
+
*
|
|
6
|
+
* Concrete subclasses declare the command names they handle via getCommandNames()
|
|
7
|
+
* (read by ControllerHub for routing and diagnostics — no need to repeat the list
|
|
8
|
+
* again at the registration call site) and implement receiveCommand() to react to
|
|
9
|
+
* commands dispatched by the SFSM.
|
|
10
|
+
*
|
|
11
|
+
* A component that both sends signals and receives commands should extend
|
|
12
|
+
* SignalSender and additionally implement this class's contract directly
|
|
13
|
+
* (getCommandNames / receiveCommand) — TypeScript does not allow extending two
|
|
14
|
+
* classes, but structurally implementing this abstract class's shape is sufficient
|
|
15
|
+
* for ControllerHub.registerCommandReceiver().
|
|
16
|
+
*/
|
|
17
|
+
export declare abstract class CommandReceiver implements ICommandReceiver {
|
|
18
|
+
/** Returns all command names this receiver handles. */
|
|
19
|
+
abstract getCommandNames(): readonly string[];
|
|
20
|
+
/** Called by ControllerHub when the SFSM dispatches a matching command. */
|
|
21
|
+
abstract receiveCommand(command: string, data?: unknown): void;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=CommandReceiver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandReceiver.d.ts","sourceRoot":"","sources":["../../src/sfsm/CommandReceiver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,8BAAsB,eAAgB,YAAW,gBAAgB;IAE7D,uDAAuD;IACvD,QAAQ,CAAC,eAAe,IAAI,SAAS,MAAM,EAAE;IAE7C,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI;CACjE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CommandReceiver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Abstract base class for the command-receiving role of an external-world component
|
|
6
|
+
* (a "Controller" in ControllerHub terminology).
|
|
7
|
+
*
|
|
8
|
+
* Concrete subclasses declare the command names they handle via getCommandNames()
|
|
9
|
+
* (read by ControllerHub for routing and diagnostics — no need to repeat the list
|
|
10
|
+
* again at the registration call site) and implement receiveCommand() to react to
|
|
11
|
+
* commands dispatched by the SFSM.
|
|
12
|
+
*
|
|
13
|
+
* A component that both sends signals and receives commands should extend
|
|
14
|
+
* SignalSender and additionally implement this class's contract directly
|
|
15
|
+
* (getCommandNames / receiveCommand) — TypeScript does not allow extending two
|
|
16
|
+
* classes, but structurally implementing this abstract class's shape is sufficient
|
|
17
|
+
* for ControllerHub.registerCommandReceiver().
|
|
18
|
+
*/
|
|
19
|
+
class CommandReceiver {
|
|
20
|
+
}
|
|
21
|
+
exports.CommandReceiver = CommandReceiver;
|
|
22
|
+
//# sourceMappingURL=CommandReceiver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandReceiver.js","sourceRoot":"","sources":["../../src/sfsm/CommandReceiver.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAsB,eAAe;CAOpC;AAPD,0CAOC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ICommandReceiver } from './interfaces';
|
|
2
|
+
import { Sfsm } from './Sfsm';
|
|
3
|
+
import { SignalSender } from './SignalSender';
|
|
4
|
+
import { CommandReceiver } from './CommandReceiver';
|
|
5
|
+
/**
|
|
6
|
+
* Wiring hub between the SFSM and a collection of Controllers — components that
|
|
7
|
+
* play the SignalSender and/or CommandReceiver role for the "external world".
|
|
8
|
+
*
|
|
9
|
+
* - registerSignalSender(sender) — wires a SignalSender; its signal names are
|
|
10
|
+
* read from sender.getSignalNames(), so they never need to be repeated here.
|
|
11
|
+
* - registerCommandReceiver(receiver) — wires a CommandReceiver; its command names
|
|
12
|
+
* are read from receiver.getCommandNames(). Throws if a command name is already
|
|
13
|
+
* registered by another receiver.
|
|
14
|
+
* - connectTo(sfsm) — wires everything: sets itself as the SFSM's command receiver
|
|
15
|
+
* and calls connectSignalTarget(sfsm) on every registered signal sender.
|
|
16
|
+
*
|
|
17
|
+
* A single component that plays both roles (most real devices do) extends
|
|
18
|
+
* SignalSender and additionally implements the CommandReceiver contract on the
|
|
19
|
+
* same class, then is registered once with each method:
|
|
20
|
+
*
|
|
21
|
+
* new ControllerHub()
|
|
22
|
+
* .registerSignalSender(turnstileService)
|
|
23
|
+
* .registerSignalSender(turnstileDevice)
|
|
24
|
+
* .registerCommandReceiver(turnstileDevice)
|
|
25
|
+
* .connectTo(sfsm);
|
|
26
|
+
*/
|
|
27
|
+
export declare class ControllerHub implements ICommandReceiver {
|
|
28
|
+
private commandRoutes;
|
|
29
|
+
private signalSenders;
|
|
30
|
+
/**
|
|
31
|
+
* Register a signal sender. Its signal names (getSignalNames()) are used for
|
|
32
|
+
* diagnostics via getRegisteredSignals().
|
|
33
|
+
* Fluent — returns this.
|
|
34
|
+
*/
|
|
35
|
+
registerSignalSender(sender: SignalSender): this;
|
|
36
|
+
/**
|
|
37
|
+
* Register a command receiver for all command names it declares via getCommandNames().
|
|
38
|
+
* Throws if any of those command names is already registered.
|
|
39
|
+
* Fluent — returns this.
|
|
40
|
+
*/
|
|
41
|
+
registerCommandReceiver(receiver: CommandReceiver): this;
|
|
42
|
+
/**
|
|
43
|
+
* Wire this hub to the given SFSM instance:
|
|
44
|
+
* 1. Sets this hub as the SFSM's command receiver.
|
|
45
|
+
* 2. Calls connectSignalTarget(sfsm) on every registered signal sender.
|
|
46
|
+
* Fluent — returns this.
|
|
47
|
+
*/
|
|
48
|
+
connectTo(sfsm: Sfsm): this;
|
|
49
|
+
/** ICommandReceiver implementation — dispatches command to the registered receiver. */
|
|
50
|
+
receiveCommand(command: string, data?: unknown): void;
|
|
51
|
+
/** Returns all command names that have a registered receiver (for diagnostics / validation). */
|
|
52
|
+
getRegisteredCommands(): string[];
|
|
53
|
+
/** Returns all signal names that have a registered sender (for diagnostics / validation). */
|
|
54
|
+
getRegisteredSignals(): string[];
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=ControllerHub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ControllerHub.d.ts","sourceRoot":"","sources":["../../src/sfsm/ControllerHub.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,aAAc,YAAW,gBAAgB;IAElD,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,aAAa,CAAsB;IAE3C;;;;OAIG;IACH,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAKhD;;;;OAIG;IACH,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAUxD;;;;;OAKG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAQ3B,uFAAuF;IACvF,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI;IAYrD,gGAAgG;IAChG,qBAAqB,IAAI,MAAM,EAAE;IAIjC,6FAA6F;IAC7F,oBAAoB,IAAI,MAAM,EAAE;CAGnC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ControllerHub = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Wiring hub between the SFSM and a collection of Controllers — components that
|
|
6
|
+
* play the SignalSender and/or CommandReceiver role for the "external world".
|
|
7
|
+
*
|
|
8
|
+
* - registerSignalSender(sender) — wires a SignalSender; its signal names are
|
|
9
|
+
* read from sender.getSignalNames(), so they never need to be repeated here.
|
|
10
|
+
* - registerCommandReceiver(receiver) — wires a CommandReceiver; its command names
|
|
11
|
+
* are read from receiver.getCommandNames(). Throws if a command name is already
|
|
12
|
+
* registered by another receiver.
|
|
13
|
+
* - connectTo(sfsm) — wires everything: sets itself as the SFSM's command receiver
|
|
14
|
+
* and calls connectSignalTarget(sfsm) on every registered signal sender.
|
|
15
|
+
*
|
|
16
|
+
* A single component that plays both roles (most real devices do) extends
|
|
17
|
+
* SignalSender and additionally implements the CommandReceiver contract on the
|
|
18
|
+
* same class, then is registered once with each method:
|
|
19
|
+
*
|
|
20
|
+
* new ControllerHub()
|
|
21
|
+
* .registerSignalSender(turnstileService)
|
|
22
|
+
* .registerSignalSender(turnstileDevice)
|
|
23
|
+
* .registerCommandReceiver(turnstileDevice)
|
|
24
|
+
* .connectTo(sfsm);
|
|
25
|
+
*/
|
|
26
|
+
class ControllerHub {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.commandRoutes = new Map();
|
|
29
|
+
this.signalSenders = [];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Register a signal sender. Its signal names (getSignalNames()) are used for
|
|
33
|
+
* diagnostics via getRegisteredSignals().
|
|
34
|
+
* Fluent — returns this.
|
|
35
|
+
*/
|
|
36
|
+
registerSignalSender(sender) {
|
|
37
|
+
this.signalSenders.push(sender);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Register a command receiver for all command names it declares via getCommandNames().
|
|
42
|
+
* Throws if any of those command names is already registered.
|
|
43
|
+
* Fluent — returns this.
|
|
44
|
+
*/
|
|
45
|
+
registerCommandReceiver(receiver) {
|
|
46
|
+
for (const cmd of receiver.getCommandNames()) {
|
|
47
|
+
if (this.commandRoutes.has(cmd)) {
|
|
48
|
+
throw new Error(`ControllerHub: command "${cmd}" is already registered`);
|
|
49
|
+
}
|
|
50
|
+
this.commandRoutes.set(cmd, receiver);
|
|
51
|
+
}
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Wire this hub to the given SFSM instance:
|
|
56
|
+
* 1. Sets this hub as the SFSM's command receiver.
|
|
57
|
+
* 2. Calls connectSignalTarget(sfsm) on every registered signal sender.
|
|
58
|
+
* Fluent — returns this.
|
|
59
|
+
*/
|
|
60
|
+
connectTo(sfsm) {
|
|
61
|
+
sfsm.setCommandReceiver(this);
|
|
62
|
+
for (const sender of this.signalSenders) {
|
|
63
|
+
sender.connectSignalTarget(sfsm);
|
|
64
|
+
}
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
/** ICommandReceiver implementation — dispatches command to the registered receiver. */
|
|
68
|
+
receiveCommand(command, data) {
|
|
69
|
+
const receiver = this.commandRoutes.get(command);
|
|
70
|
+
if (!receiver) {
|
|
71
|
+
const registered = [...this.commandRoutes.keys()].join(', ');
|
|
72
|
+
throw new Error(`ControllerHub: no receiver registered for command "${command}". ` +
|
|
73
|
+
`Registered commands: [${registered}]`);
|
|
74
|
+
}
|
|
75
|
+
receiver.receiveCommand(command, data);
|
|
76
|
+
}
|
|
77
|
+
/** Returns all command names that have a registered receiver (for diagnostics / validation). */
|
|
78
|
+
getRegisteredCommands() {
|
|
79
|
+
return [...this.commandRoutes.keys()];
|
|
80
|
+
}
|
|
81
|
+
/** Returns all signal names that have a registered sender (for diagnostics / validation). */
|
|
82
|
+
getRegisteredSignals() {
|
|
83
|
+
return this.signalSenders.flatMap(s => s.getSignalNames());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.ControllerHub = ControllerHub;
|
|
87
|
+
//# sourceMappingURL=ControllerHub.js.map
|