@billdaddy/hsmkit 0.1.0
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/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/index.cjs +236 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +206 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 trananhtung
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# hsmkit
|
|
2
|
+
|
|
3
|
+
[](#contributors-)
|
|
4
|
+
|
|
5
|
+
> Zero-dependency TypeScript hierarchical state machine (HSM/statecharts). Compound states, entry/exit actions, guards, shallow history, internal transitions. Port of Python `pytransitions` / C# `Stateless` / Ruby `AASM` — lighter than XState.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@billdaddy/hsmkit)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @billdaddy/hsmkit
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createHSM } from "@billdaddy/hsmkit";
|
|
20
|
+
|
|
21
|
+
const machine = createHSM({
|
|
22
|
+
initial: "idle",
|
|
23
|
+
context: { count: 0 },
|
|
24
|
+
states: {
|
|
25
|
+
idle: {
|
|
26
|
+
on: { START: "active" },
|
|
27
|
+
},
|
|
28
|
+
active: {
|
|
29
|
+
initial: "running", // compound state
|
|
30
|
+
entry: (ctx) => { console.log("entered active"); return ctx; },
|
|
31
|
+
exit: (ctx) => { console.log("exited active"); return ctx; },
|
|
32
|
+
states: {
|
|
33
|
+
running: {
|
|
34
|
+
on: {
|
|
35
|
+
PAUSE: "active.paused", // transition within compound state
|
|
36
|
+
STOP: "idle", // exit compound state
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
paused: {
|
|
40
|
+
on: {
|
|
41
|
+
RESUME: "active.running",
|
|
42
|
+
STOP: "idle",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const service = machine.start();
|
|
51
|
+
service.state; // "idle"
|
|
52
|
+
service.send("START");
|
|
53
|
+
service.state; // "active.running"
|
|
54
|
+
service.send("PAUSE");
|
|
55
|
+
service.state; // "active.paused"
|
|
56
|
+
service.matches("active"); // true — prefix match
|
|
57
|
+
service.send("STOP");
|
|
58
|
+
service.state; // "idle"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Why hsmkit?
|
|
62
|
+
|
|
63
|
+
| Library | Download/week | Last updated | Hierarchical? | Zero-dep? |
|
|
64
|
+
|---|---|---|---|---|
|
|
65
|
+
| XState | ~3M | Active | ✅ statecharts | ❌ (actor model) |
|
|
66
|
+
| `@steelbreeze/state` | ~344 | **March 2022** | ✅ | ✅ |
|
|
67
|
+
| `javascript-state-machine` | ~1.8M | **2021** | ❌ flat only | ✅ |
|
|
68
|
+
| **hsmkit** | — | **Active** | ✅ | ✅ |
|
|
69
|
+
|
|
70
|
+
XState is excellent but its v5 abstraction is the **actor model** — the right tool for distributed systems, not for a simple UI component or protocol parser. `hsmkit` gives you the core of Harel statecharts (the part that matters for most uses) in 0 dependencies.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **Compound states** — states that contain child states (hierarchical nesting)
|
|
75
|
+
- **Entry/exit actions** — called at LCA-correct depth on every transition
|
|
76
|
+
- **Guards** — conditional transitions with fallthrough to next candidate
|
|
77
|
+
- **Shallow history** — remember last active substate across interruptions
|
|
78
|
+
- **Internal transitions** — actions without exit/entry (via `target: undefined`)
|
|
79
|
+
- **Event payload** — pass data with `service.send("EVT", { value: 42 })`
|
|
80
|
+
- **Mutable context** — return new context from actions
|
|
81
|
+
- **subscribe()** — listen to every state change; returns an unsubscribe function
|
|
82
|
+
|
|
83
|
+
## API
|
|
84
|
+
|
|
85
|
+
### `createHSM(config)`
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const machine = createHSM({
|
|
89
|
+
initial: "idle", // required: initial state (dot-notation for nested)
|
|
90
|
+
context: { count: 0 }, // optional: initial context
|
|
91
|
+
states: { // state definitions
|
|
92
|
+
idle: { ... },
|
|
93
|
+
active: { ... },
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `StateConfig`
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
interface StateConfig<Ctx> {
|
|
102
|
+
initial?: string; // required for compound states
|
|
103
|
+
states?: Record<string, StateConfig<Ctx>>; // child states
|
|
104
|
+
history?: boolean; // shallow history (default: false)
|
|
105
|
+
entry?: ActionFn<Ctx> | ActionFn<Ctx>[];
|
|
106
|
+
exit?: ActionFn<Ctx> | ActionFn<Ctx>[];
|
|
107
|
+
on?: Record<string, TransitionTarget>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Transition targets:
|
|
111
|
+
"stateName" // simple target
|
|
112
|
+
"parent.child" // nested target (dot-notation)
|
|
113
|
+
{ target: "stateName", guard, actions } // with guard/actions
|
|
114
|
+
[{ target, guard }, { target }] // multiple candidates (fallthrough)
|
|
115
|
+
{ target: undefined, actions: [...] } // internal (no exit/entry)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `HSMService`
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
service.state // string — e.g. "active.running"
|
|
122
|
+
service.stateValue // string[] — e.g. ["active", "running"]
|
|
123
|
+
service.context // current context
|
|
124
|
+
service.send(type, payload?) // fire an event, returns this
|
|
125
|
+
service.matches(state) // true if state is prefix of current path
|
|
126
|
+
service.subscribe(fn) // returns unsub function
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Examples
|
|
130
|
+
|
|
131
|
+
### Guards with fallthrough
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const machine = createHSM({
|
|
135
|
+
initial: "idle",
|
|
136
|
+
context: { role: "guest" },
|
|
137
|
+
states: {
|
|
138
|
+
idle: {
|
|
139
|
+
on: {
|
|
140
|
+
ENTER: [
|
|
141
|
+
{ target: "admin", guard: (ctx) => ctx.role === "admin" },
|
|
142
|
+
{ target: "user", guard: (ctx) => ctx.role === "user" },
|
|
143
|
+
{ target: "guest" }, // default — no guard
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
admin: {}, user: {}, guest: {},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Shallow history — audio player
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const player = createHSM({
|
|
156
|
+
initial: "idle",
|
|
157
|
+
states: {
|
|
158
|
+
idle: { on: { PLAY: "playing" } },
|
|
159
|
+
playing: {
|
|
160
|
+
history: true, // remember last substate
|
|
161
|
+
initial: "normal",
|
|
162
|
+
on: { PAUSE_ALL: "idle" },
|
|
163
|
+
states: {
|
|
164
|
+
normal: { on: { SHUFFLE: "playing.shuffle" } },
|
|
165
|
+
shuffle: { on: { NORMAL: "playing.normal" } },
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const s = player.start();
|
|
172
|
+
s.send("PLAY").send("SHUFFLE"); // playing.shuffle
|
|
173
|
+
s.send("PAUSE_ALL").send("PLAY"); // returns to playing.shuffle (history!)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Internal transitions (no exit/entry)
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const machine = createHSM({
|
|
180
|
+
initial: "active",
|
|
181
|
+
context: { ticks: 0 },
|
|
182
|
+
states: {
|
|
183
|
+
active: {
|
|
184
|
+
on: {
|
|
185
|
+
TICK: [{
|
|
186
|
+
// target: undefined — internal transition
|
|
187
|
+
actions: [(ctx) => ({ ...ctx, ticks: ctx.ticks + 1 })],
|
|
188
|
+
}],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Traffic light
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const light = createHSM({
|
|
199
|
+
initial: "green",
|
|
200
|
+
states: {
|
|
201
|
+
green: { on: { NEXT: "yellow" } },
|
|
202
|
+
yellow: { on: { NEXT: "red" } },
|
|
203
|
+
red: { on: { NEXT: "green" } },
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const s = light.start();
|
|
207
|
+
s.send("NEXT").send("NEXT").send("NEXT");
|
|
208
|
+
s.state; // "green"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Comparison
|
|
212
|
+
|
|
213
|
+
| Feature | `pytransitions` | C# `Stateless` | XState v5 | `hsmkit` |
|
|
214
|
+
|---|---|---|---|---|
|
|
215
|
+
| Compound states | ✅ | ✅ | ✅ | ✅ |
|
|
216
|
+
| Entry/exit actions | ✅ | ✅ | ✅ | ✅ |
|
|
217
|
+
| Guards | ✅ | ✅ | ✅ | ✅ |
|
|
218
|
+
| History | ✅ | ✅ | ✅ | ✅ (shallow) |
|
|
219
|
+
| Parallel regions | ✅ | ✅ | ✅ | ❌ (future) |
|
|
220
|
+
| Actor model | ❌ | ❌ | ✅ (v5) | ❌ |
|
|
221
|
+
| Zero dependencies | ✅ | ✅ | ❌ | ✅ |
|
|
222
|
+
|
|
223
|
+
## Contributors ✨
|
|
224
|
+
|
|
225
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the [emoji key](https://allcontributors.org/docs/en/emoji-key) for how each contribution is recognized, and open a PR or issue to get involved.
|
|
226
|
+
|
|
227
|
+
Thanks goes to these wonderful people:
|
|
228
|
+
|
|
229
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
230
|
+
<!-- prettier-ignore-start -->
|
|
231
|
+
<!-- markdownlint-disable -->
|
|
232
|
+
<table>
|
|
233
|
+
<tbody>
|
|
234
|
+
<tr>
|
|
235
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/trananhtung"><img src="https://avatars.githubusercontent.com/u/30992229?v=4?s=100" width="100px;" alt="Tung Tran"/><br /><sub><b>Tung Tran</b></sub></a><br /><a href="https://github.com/trananhtung/hsmkit/commits?author=trananhtung" title="Code">💻</a> <a href="#maintenance-trananhtung" title="Maintenance">🚧</a></td>
|
|
236
|
+
</tr>
|
|
237
|
+
</tbody>
|
|
238
|
+
</table>
|
|
239
|
+
|
|
240
|
+
<!-- markdownlint-restore -->
|
|
241
|
+
<!-- prettier-ignore-end -->
|
|
242
|
+
|
|
243
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT © [trananhtung](https://github.com/trananhtung)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
HSM: () => HSM,
|
|
24
|
+
HSMError: () => HSMError,
|
|
25
|
+
HSMService: () => HSMService,
|
|
26
|
+
createHSM: () => createHSM
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/hsm.ts
|
|
31
|
+
function buildNode(id, config, parent) {
|
|
32
|
+
const node = {
|
|
33
|
+
id,
|
|
34
|
+
initial: config.initial,
|
|
35
|
+
children: /* @__PURE__ */ new Map(),
|
|
36
|
+
parent,
|
|
37
|
+
transitions: /* @__PURE__ */ new Map(),
|
|
38
|
+
entry: config.entry ? Array.isArray(config.entry) ? config.entry : [config.entry] : [],
|
|
39
|
+
exit: config.exit ? Array.isArray(config.exit) ? config.exit : [config.exit] : [],
|
|
40
|
+
history: config.history ?? false
|
|
41
|
+
};
|
|
42
|
+
if (config.states) {
|
|
43
|
+
for (const [childId, childConfig] of Object.entries(config.states)) {
|
|
44
|
+
node.children.set(childId, buildNode(childId, childConfig, node));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (config.on) {
|
|
48
|
+
for (const [eventType, transConfig] of Object.entries(config.on)) {
|
|
49
|
+
const defs = parseTransitions(transConfig);
|
|
50
|
+
node.transitions.set(eventType, defs);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return node;
|
|
54
|
+
}
|
|
55
|
+
function parseTransitions(raw) {
|
|
56
|
+
if (typeof raw === "string") {
|
|
57
|
+
return [{ target: raw, actions: [] }];
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(raw)) {
|
|
60
|
+
return raw.map((t) => ({
|
|
61
|
+
target: t.target,
|
|
62
|
+
guard: t.guard,
|
|
63
|
+
actions: t.actions ?? []
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
return [{ target: raw.target, guard: raw.guard, actions: raw.actions ?? [] }];
|
|
67
|
+
}
|
|
68
|
+
function getNodeAtPath(root, path) {
|
|
69
|
+
let node = root;
|
|
70
|
+
for (const id of path) {
|
|
71
|
+
const child = node.children.get(id);
|
|
72
|
+
if (!child) throw new HSMError(`State "${id}" not found`);
|
|
73
|
+
node = child;
|
|
74
|
+
}
|
|
75
|
+
return node;
|
|
76
|
+
}
|
|
77
|
+
function resolvePath(target) {
|
|
78
|
+
return target.split(".");
|
|
79
|
+
}
|
|
80
|
+
function lcaDepth(a, b) {
|
|
81
|
+
let i = 0;
|
|
82
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
83
|
+
return i;
|
|
84
|
+
}
|
|
85
|
+
function expandToLeaf(root, path, ctx, event) {
|
|
86
|
+
let cur = getNodeAtPath(root, path);
|
|
87
|
+
let p = [...path];
|
|
88
|
+
while (cur.children.size > 0) {
|
|
89
|
+
if (!cur.initial) throw new HSMError(`Compound state "${p.join(".")}" has no initial substate`);
|
|
90
|
+
const childId = cur.history && cur.historyValue ? cur.historyValue : cur.initial;
|
|
91
|
+
cur = cur.children.get(childId);
|
|
92
|
+
for (const fn of cur.entry) ctx = fn(ctx, event) ?? ctx;
|
|
93
|
+
p.push(childId);
|
|
94
|
+
}
|
|
95
|
+
return { path: p, ctx };
|
|
96
|
+
}
|
|
97
|
+
var HSMError = class extends Error {
|
|
98
|
+
constructor(message) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "HSMError";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var HSMService = class {
|
|
104
|
+
constructor(root, initialPath, ctx) {
|
|
105
|
+
this._listeners = /* @__PURE__ */ new Set();
|
|
106
|
+
this._root = root;
|
|
107
|
+
this._path = initialPath;
|
|
108
|
+
this._ctx = ctx;
|
|
109
|
+
}
|
|
110
|
+
/** Current state as dot-notation string, e.g. `"active.running"`. */
|
|
111
|
+
get state() {
|
|
112
|
+
return this._path.join(".");
|
|
113
|
+
}
|
|
114
|
+
/** Current context. */
|
|
115
|
+
get context() {
|
|
116
|
+
return this._ctx;
|
|
117
|
+
}
|
|
118
|
+
/** Current state as array path, e.g. `["active", "running"]`. */
|
|
119
|
+
get stateValue() {
|
|
120
|
+
return [...this._path];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Returns true if the current state matches the given prefix.
|
|
124
|
+
* `matches("active")` returns true for `"active.running"`.
|
|
125
|
+
*/
|
|
126
|
+
matches(state) {
|
|
127
|
+
const parts = state.split(".");
|
|
128
|
+
return parts.every((part, i) => this._path[i] === part);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Send an event to the machine.
|
|
132
|
+
* Returns this for chaining.
|
|
133
|
+
*/
|
|
134
|
+
send(eventType, payload = {}) {
|
|
135
|
+
const event = { type: eventType, ...payload };
|
|
136
|
+
for (let depth = this._path.length; depth >= 0; depth--) {
|
|
137
|
+
const nodePath = this._path.slice(0, depth);
|
|
138
|
+
const node = getNodeAtPath(this._root, nodePath);
|
|
139
|
+
const defs = node.transitions.get(eventType);
|
|
140
|
+
if (!defs) continue;
|
|
141
|
+
for (const def of defs) {
|
|
142
|
+
if (def.guard && !def.guard(this._ctx, event)) continue;
|
|
143
|
+
if (def.target === void 0) {
|
|
144
|
+
let ctx = this._ctx;
|
|
145
|
+
for (const fn of def.actions) ctx = fn(ctx, event) ?? ctx;
|
|
146
|
+
this._ctx = ctx;
|
|
147
|
+
} else {
|
|
148
|
+
const targetPath = resolvePath(def.target);
|
|
149
|
+
this._doTransition(this._path, nodePath, targetPath, def.actions, event);
|
|
150
|
+
}
|
|
151
|
+
this._notify(event);
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
158
|
+
subscribe(listener) {
|
|
159
|
+
this._listeners.add(listener);
|
|
160
|
+
return () => this._listeners.delete(listener);
|
|
161
|
+
}
|
|
162
|
+
_notify(event) {
|
|
163
|
+
for (const fn of this._listeners) fn(this.state, this._ctx, event);
|
|
164
|
+
}
|
|
165
|
+
_doTransition(currentPath, sourcePath, targetPath, transActions, event) {
|
|
166
|
+
const lca = lcaDepth(currentPath, targetPath);
|
|
167
|
+
let ctx = this._ctx;
|
|
168
|
+
for (let i = currentPath.length - 1; i >= lca; i--) {
|
|
169
|
+
const node = getNodeAtPath(this._root, currentPath.slice(0, i + 1));
|
|
170
|
+
for (const fn of node.exit) ctx = fn(ctx, event) ?? ctx;
|
|
171
|
+
if (i > 0) {
|
|
172
|
+
const parent = getNodeAtPath(this._root, currentPath.slice(0, i));
|
|
173
|
+
if (parent.history) parent.historyValue = currentPath[i];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const fn of transActions) ctx = fn(ctx, event) ?? ctx;
|
|
177
|
+
for (let i = lca; i < targetPath.length; i++) {
|
|
178
|
+
const node = getNodeAtPath(this._root, targetPath.slice(0, i + 1));
|
|
179
|
+
for (const fn of node.entry) ctx = fn(ctx, event) ?? ctx;
|
|
180
|
+
}
|
|
181
|
+
this._ctx = ctx;
|
|
182
|
+
const { path, ctx: finalCtx } = expandToLeaf(this._root, targetPath, ctx, event);
|
|
183
|
+
this._path = path;
|
|
184
|
+
this._ctx = finalCtx;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var HSM = class {
|
|
188
|
+
constructor(config) {
|
|
189
|
+
this._root = buildNode("__root__", { states: config.states });
|
|
190
|
+
this._initialCtx = config.context ?? {};
|
|
191
|
+
this._initial = config.initial;
|
|
192
|
+
}
|
|
193
|
+
_resolveInitialPath(root, initial) {
|
|
194
|
+
const basePath = resolvePath(initial);
|
|
195
|
+
let p = [];
|
|
196
|
+
let node = root;
|
|
197
|
+
for (const id of basePath) {
|
|
198
|
+
const child = node.children.get(id);
|
|
199
|
+
if (!child) throw new HSMError(`Initial state "${id}" not found`);
|
|
200
|
+
node = child;
|
|
201
|
+
p.push(id);
|
|
202
|
+
}
|
|
203
|
+
while (node.children.size > 0) {
|
|
204
|
+
if (!node.initial) throw new HSMError(`Compound state "${p.join(".")}" has no initial substate`);
|
|
205
|
+
const childId = node.initial;
|
|
206
|
+
node = node.children.get(childId);
|
|
207
|
+
p.push(childId);
|
|
208
|
+
}
|
|
209
|
+
return p;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Start the machine and run entry actions for the initial state path.
|
|
213
|
+
* Returns the running service.
|
|
214
|
+
*/
|
|
215
|
+
start() {
|
|
216
|
+
const initialPath = this._resolveInitialPath(this._root, this._initial);
|
|
217
|
+
let ctx = this._initialCtx;
|
|
218
|
+
const event = { type: "__init__" };
|
|
219
|
+
for (let i = 0; i < initialPath.length; i++) {
|
|
220
|
+
const node = getNodeAtPath(this._root, initialPath.slice(0, i + 1));
|
|
221
|
+
for (const fn of node.entry) ctx = fn(ctx, event) ?? ctx;
|
|
222
|
+
}
|
|
223
|
+
return new HSMService(this._root, [...initialPath], ctx);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function createHSM(config) {
|
|
227
|
+
return new HSM(config);
|
|
228
|
+
}
|
|
229
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
230
|
+
0 && (module.exports = {
|
|
231
|
+
HSM,
|
|
232
|
+
HSMError,
|
|
233
|
+
HSMService,
|
|
234
|
+
createHSM
|
|
235
|
+
});
|
|
236
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/hsm.ts"],"sourcesContent":["export type {\n EventType,\n StateId,\n HSMEvent,\n ActionFn,\n GuardFn,\n TransitionConfig,\n StateConfig,\n HSMConfig,\n StateListener,\n} from \"./types.js\";\n\nexport { HSM, HSMService, HSMError, createHSM } from \"./hsm.js\";\n","import type {\n ActionFn,\n GuardFn,\n HSMConfig,\n HSMEvent,\n StateConfig,\n StateId,\n StateListener,\n TransitionConfig,\n} from \"./types.js\";\n\n// ── Internal state node ──────────────────────────────────────────────────────\n\ninterface StateNode<Ctx> {\n id: string;\n initial?: string;\n children: Map<string, StateNode<Ctx>>;\n parent?: StateNode<Ctx>;\n transitions: Map<string, TransitionDef<Ctx>[]>;\n entry: ActionFn<Ctx>[];\n exit: ActionFn<Ctx>[];\n history: boolean;\n historyValue?: string;\n}\n\ninterface TransitionDef<Ctx> {\n target?: string;\n guard?: GuardFn<Ctx>;\n actions: ActionFn<Ctx>[];\n}\n\n// ── Builder ──────────────────────────────────────────────────────────────────\n\nfunction buildNode<Ctx>(\n id: string,\n config: StateConfig<Ctx>,\n parent?: StateNode<Ctx>,\n): StateNode<Ctx> {\n const node: StateNode<Ctx> = {\n id,\n initial: config.initial,\n children: new Map(),\n parent,\n transitions: new Map(),\n entry: config.entry\n ? Array.isArray(config.entry) ? config.entry : [config.entry]\n : [],\n exit: config.exit\n ? Array.isArray(config.exit) ? config.exit : [config.exit]\n : [],\n history: config.history ?? false,\n };\n\n // Build child states\n if (config.states) {\n for (const [childId, childConfig] of Object.entries(config.states)) {\n node.children.set(childId, buildNode(childId, childConfig, node));\n }\n }\n\n // Parse transitions\n if (config.on) {\n for (const [eventType, transConfig] of Object.entries(config.on)) {\n const defs = parseTransitions(transConfig as string | TransitionConfig<Ctx> | Array<TransitionConfig<Ctx>>);\n node.transitions.set(eventType, defs);\n }\n }\n\n return node;\n}\n\nfunction parseTransitions<Ctx>(\n raw: string | TransitionConfig<Ctx> | Array<TransitionConfig<Ctx>>,\n): TransitionDef<Ctx>[] {\n if (typeof raw === \"string\") {\n return [{ target: raw, actions: [] }];\n }\n if (Array.isArray(raw)) {\n return raw.map((t) => ({\n target: t.target,\n guard: t.guard,\n actions: t.actions ?? [],\n }));\n }\n return [{ target: raw.target, guard: raw.guard, actions: raw.actions ?? [] }];\n}\n\n// ── Path utilities ───────────────────────────────────────────────────────────\n\nfunction getNodeAtPath<Ctx>(root: StateNode<Ctx>, path: string[]): StateNode<Ctx> {\n let node = root;\n for (const id of path) {\n const child = node.children.get(id);\n if (!child) throw new HSMError(`State \"${id}\" not found`);\n node = child;\n }\n return node;\n}\n\n/** Resolve a dotted target string to a path array from the virtual root. */\nfunction resolvePath(target: string): string[] {\n return target.split(\".\");\n}\n\n/** LCA index: length of longest common prefix of two paths. */\nfunction lcaDepth(a: string[], b: string[]): number {\n let i = 0;\n while (i < a.length && i < b.length && a[i] === b[i]) i++;\n return i;\n}\n\n/** Enter compound state to leaf following initial/history chain. */\nfunction expandToLeaf<Ctx>(\n root: StateNode<Ctx>,\n path: string[],\n ctx: Ctx,\n event: HSMEvent,\n): { path: string[]; ctx: Ctx } {\n let cur = getNodeAtPath(root, path);\n let p = [...path];\n\n while (cur.children.size > 0) {\n if (!cur.initial) throw new HSMError(`Compound state \"${p.join(\".\")}\" has no initial substate`);\n const childId = cur.history && cur.historyValue ? cur.historyValue : cur.initial;\n cur = cur.children.get(childId)!;\n for (const fn of cur.entry) ctx = fn(ctx, event) ?? ctx;\n p.push(childId);\n }\n\n return { path: p, ctx };\n}\n\n// ── HSMService ───────────────────────────────────────────────────────────────\n\nexport class HSMError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"HSMError\";\n }\n}\n\nexport class HSMService<Ctx> {\n private _path: string[];\n private _ctx: Ctx;\n private readonly _root: StateNode<Ctx>;\n private readonly _listeners = new Set<StateListener<Ctx>>();\n\n constructor(root: StateNode<Ctx>, initialPath: string[], ctx: Ctx) {\n this._root = root;\n this._path = initialPath;\n this._ctx = ctx;\n }\n\n /** Current state as dot-notation string, e.g. `\"active.running\"`. */\n get state(): string {\n return this._path.join(\".\");\n }\n\n /** Current context. */\n get context(): Ctx {\n return this._ctx;\n }\n\n /** Current state as array path, e.g. `[\"active\", \"running\"]`. */\n get stateValue(): string[] {\n return [...this._path];\n }\n\n /**\n * Returns true if the current state matches the given prefix.\n * `matches(\"active\")` returns true for `\"active.running\"`.\n */\n matches(state: string): boolean {\n const parts = state.split(\".\");\n return parts.every((part, i) => this._path[i] === part);\n }\n\n /**\n * Send an event to the machine.\n * Returns this for chaining.\n */\n send(eventType: string, payload: Record<string, unknown> = {}): this {\n const event: HSMEvent = { type: eventType, ...payload };\n\n // Walk up from current leaf to root looking for applicable transition\n for (let depth = this._path.length; depth >= 0; depth--) {\n const nodePath = this._path.slice(0, depth);\n const node = getNodeAtPath(this._root, nodePath);\n const defs = node.transitions.get(eventType);\n if (!defs) continue;\n\n for (const def of defs) {\n if (def.guard && !def.guard(this._ctx, event)) continue;\n\n if (def.target === undefined) {\n // Internal transition — run actions, no exit/entry\n let ctx = this._ctx;\n for (const fn of def.actions) ctx = fn(ctx, event) ?? ctx;\n this._ctx = ctx;\n } else {\n const targetPath = resolvePath(def.target);\n this._doTransition(this._path, nodePath, targetPath, def.actions, event);\n }\n\n this._notify(event);\n return this;\n }\n }\n\n return this;\n }\n\n /** Subscribe to state changes. Returns an unsubscribe function. */\n subscribe(listener: StateListener<Ctx>): () => void {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener);\n }\n\n private _notify(event: HSMEvent): void {\n for (const fn of this._listeners) fn(this.state, this._ctx, event);\n }\n\n private _doTransition(\n currentPath: string[],\n sourcePath: string[],\n targetPath: string[],\n transActions: ActionFn<Ctx>[],\n event: HSMEvent,\n ): void {\n const lca = lcaDepth(currentPath, targetPath);\n let ctx = this._ctx;\n\n // Exit from current leaf up to (not including) LCA\n for (let i = currentPath.length - 1; i >= lca; i--) {\n const node = getNodeAtPath(this._root, currentPath.slice(0, i + 1));\n for (const fn of node.exit) ctx = fn(ctx, event) ?? ctx;\n\n // Record history in parent\n if (i > 0) {\n const parent = getNodeAtPath(this._root, currentPath.slice(0, i));\n if (parent.history) parent.historyValue = currentPath[i];\n }\n }\n\n // Transition actions\n for (const fn of transActions) ctx = fn(ctx, event) ?? ctx;\n\n // Entry from LCA down to target\n for (let i = lca; i < targetPath.length; i++) {\n const node = getNodeAtPath(this._root, targetPath.slice(0, i + 1));\n for (const fn of node.entry) ctx = fn(ctx, event) ?? ctx;\n }\n\n this._ctx = ctx;\n\n // Expand target compound state to leaf\n const { path, ctx: finalCtx } = expandToLeaf(this._root, targetPath, ctx, event);\n this._path = path;\n this._ctx = finalCtx;\n }\n}\n\n// ── HSM factory ───────────────────────────────────────────────────────────────\n\nexport class HSM<Ctx> {\n private readonly _root: StateNode<Ctx>;\n private readonly _initial: string;\n private readonly _initialCtx: Ctx;\n\n constructor(config: HSMConfig<Ctx>) {\n this._root = buildNode<Ctx>(\"__root__\", { states: config.states } as StateConfig<Ctx>);\n this._initialCtx = (config.context ?? ({} as unknown as Ctx));\n this._initial = config.initial;\n }\n\n private _resolveInitialPath(root: StateNode<Ctx>, initial: string): string[] {\n const basePath = resolvePath(initial);\n let p: string[] = [];\n let node = root;\n for (const id of basePath) {\n const child = node.children.get(id);\n if (!child) throw new HSMError(`Initial state \"${id}\" not found`);\n node = child;\n p.push(id);\n }\n // Expand to leaf via initial chain\n while (node.children.size > 0) {\n if (!node.initial) throw new HSMError(`Compound state \"${p.join(\".\")}\" has no initial substate`);\n const childId = node.initial;\n node = node.children.get(childId)!;\n p.push(childId);\n }\n return p;\n }\n\n /**\n * Start the machine and run entry actions for the initial state path.\n * Returns the running service.\n */\n start(): HSMService<Ctx> {\n const initialPath = this._resolveInitialPath(this._root, this._initial);\n let ctx = this._initialCtx;\n const event: HSMEvent = { type: \"__init__\" };\n\n // Run entry actions down to initial leaf\n for (let i = 0; i < initialPath.length; i++) {\n const node = getNodeAtPath(this._root, initialPath.slice(0, i + 1));\n for (const fn of node.entry) ctx = fn(ctx, event) ?? ctx;\n }\n\n return new HSMService<Ctx>(this._root, [...initialPath], ctx);\n }\n}\n\n/**\n * Create a hierarchical state machine.\n *\n * @example\n * const machine = createHSM({\n * initial: 'idle',\n * context: { count: 0 },\n * states: {\n * idle: { on: { START: 'active' } },\n * active: {\n * initial: 'running',\n * states: {\n * running: { on: { PAUSE: 'active.paused', STOP: 'idle' } },\n * paused: { on: { RESUME: 'active.running', STOP: 'idle' } },\n * },\n * },\n * },\n * });\n * const service = machine.start();\n */\nexport function createHSM<Ctx = Record<string, unknown>>(config: HSMConfig<Ctx>): HSM<Ctx> {\n return new HSM(config);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiCA,SAAS,UACP,IACA,QACA,QACgB;AAChB,QAAM,OAAuB;AAAA,IAC3B;AAAA,IACA,SAAS,OAAO;AAAA,IAChB,UAAU,oBAAI,IAAI;AAAA,IAClB;AAAA,IACA,aAAa,oBAAI,IAAI;AAAA,IACrB,OAAO,OAAO,QACV,MAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,IAC1D,CAAC;AAAA,IACL,MAAM,OAAO,OACT,MAAM,QAAQ,OAAO,IAAI,IAAI,OAAO,OAAO,CAAC,OAAO,IAAI,IACvD,CAAC;AAAA,IACL,SAAS,OAAO,WAAW;AAAA,EAC7B;AAGA,MAAI,OAAO,QAAQ;AACjB,eAAW,CAAC,SAAS,WAAW,KAAK,OAAO,QAAQ,OAAO,MAAM,GAAG;AAClE,WAAK,SAAS,IAAI,SAAS,UAAU,SAAS,aAAa,IAAI,CAAC;AAAA,IAClE;AAAA,EACF;AAGA,MAAI,OAAO,IAAI;AACb,eAAW,CAAC,WAAW,WAAW,KAAK,OAAO,QAAQ,OAAO,EAAE,GAAG;AAChE,YAAM,OAAO,iBAAiB,WAA4E;AAC1G,WAAK,YAAY,IAAI,WAAW,IAAI;AAAA,IACtC;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,iBACP,KACsB;AACtB,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO,CAAC,EAAE,QAAQ,KAAK,SAAS,CAAC,EAAE,CAAC;AAAA,EACtC;AACA,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,WAAO,IAAI,IAAI,CAAC,OAAO;AAAA,MACrB,QAAQ,EAAE;AAAA,MACV,OAAO,EAAE;AAAA,MACT,SAAS,EAAE,WAAW,CAAC;AAAA,IACzB,EAAE;AAAA,EACJ;AACA,SAAO,CAAC,EAAE,QAAQ,IAAI,QAAQ,OAAO,IAAI,OAAO,SAAS,IAAI,WAAW,CAAC,EAAE,CAAC;AAC9E;AAIA,SAAS,cAAmB,MAAsB,MAAgC;AAChF,MAAI,OAAO;AACX,aAAW,MAAM,MAAM;AACrB,UAAM,QAAQ,KAAK,SAAS,IAAI,EAAE;AAClC,QAAI,CAAC,MAAO,OAAM,IAAI,SAAS,UAAU,EAAE,aAAa;AACxD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGA,SAAS,YAAY,QAA0B;AAC7C,SAAO,OAAO,MAAM,GAAG;AACzB;AAGA,SAAS,SAAS,GAAa,GAAqB;AAClD,MAAI,IAAI;AACR,SAAO,IAAI,EAAE,UAAU,IAAI,EAAE,UAAU,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG;AACtD,SAAO;AACT;AAGA,SAAS,aACP,MACA,MACA,KACA,OAC8B;AAC9B,MAAI,MAAM,cAAc,MAAM,IAAI;AAClC,MAAI,IAAI,CAAC,GAAG,IAAI;AAEhB,SAAO,IAAI,SAAS,OAAO,GAAG;AAC5B,QAAI,CAAC,IAAI,QAAS,OAAM,IAAI,SAAS,mBAAmB,EAAE,KAAK,GAAG,CAAC,2BAA2B;AAC9F,UAAM,UAAU,IAAI,WAAW,IAAI,eAAe,IAAI,eAAe,IAAI;AACzE,UAAM,IAAI,SAAS,IAAI,OAAO;AAC9B,eAAW,MAAM,IAAI,MAAO,OAAM,GAAG,KAAK,KAAK,KAAK;AACpD,MAAE,KAAK,OAAO;AAAA,EAChB;AAEA,SAAO,EAAE,MAAM,GAAG,IAAI;AACxB;AAIO,IAAM,WAAN,cAAuB,MAAM;AAAA,EAClC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,aAAN,MAAsB;AAAA,EAM3B,YAAY,MAAsB,aAAuB,KAAU;AAFnE,SAAiB,aAAa,oBAAI,IAAwB;AAGxD,SAAK,QAAQ;AACb,SAAK,QAAQ;AACb,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,MAAM,KAAK,GAAG;AAAA,EAC5B;AAAA;AAAA,EAGA,IAAI,UAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,aAAuB;AACzB,WAAO,CAAC,GAAG,KAAK,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAQ,OAAwB;AAC9B,UAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,WAAO,MAAM,MAAM,CAAC,MAAM,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAK,WAAmB,UAAmC,CAAC,GAAS;AACnE,UAAM,QAAkB,EAAE,MAAM,WAAW,GAAG,QAAQ;AAGtD,aAAS,QAAQ,KAAK,MAAM,QAAQ,SAAS,GAAG,SAAS;AACvD,YAAM,WAAW,KAAK,MAAM,MAAM,GAAG,KAAK;AAC1C,YAAM,OAAO,cAAc,KAAK,OAAO,QAAQ;AAC/C,YAAM,OAAO,KAAK,YAAY,IAAI,SAAS;AAC3C,UAAI,CAAC,KAAM;AAEX,iBAAW,OAAO,MAAM;AACtB,YAAI,IAAI,SAAS,CAAC,IAAI,MAAM,KAAK,MAAM,KAAK,EAAG;AAE/C,YAAI,IAAI,WAAW,QAAW;AAE5B,cAAI,MAAM,KAAK;AACf,qBAAW,MAAM,IAAI,QAAS,OAAM,GAAG,KAAK,KAAK,KAAK;AACtD,eAAK,OAAO;AAAA,QACd,OAAO;AACL,gBAAM,aAAa,YAAY,IAAI,MAAM;AACzC,eAAK,cAAc,KAAK,OAAO,UAAU,YAAY,IAAI,SAAS,KAAK;AAAA,QACzE;AAEA,aAAK,QAAQ,KAAK;AAClB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAU,UAA0C;AAClD,SAAK,WAAW,IAAI,QAAQ;AAC5B,WAAO,MAAM,KAAK,WAAW,OAAO,QAAQ;AAAA,EAC9C;AAAA,EAEQ,QAAQ,OAAuB;AACrC,eAAW,MAAM,KAAK,WAAY,IAAG,KAAK,OAAO,KAAK,MAAM,KAAK;AAAA,EACnE;AAAA,EAEQ,cACN,aACA,YACA,YACA,cACA,OACM;AACN,UAAM,MAAM,SAAS,aAAa,UAAU;AAC5C,QAAI,MAAM,KAAK;AAGf,aAAS,IAAI,YAAY,SAAS,GAAG,KAAK,KAAK,KAAK;AAClD,YAAM,OAAO,cAAc,KAAK,OAAO,YAAY,MAAM,GAAG,IAAI,CAAC,CAAC;AAClE,iBAAW,MAAM,KAAK,KAAM,OAAM,GAAG,KAAK,KAAK,KAAK;AAGpD,UAAI,IAAI,GAAG;AACT,cAAM,SAAS,cAAc,KAAK,OAAO,YAAY,MAAM,GAAG,CAAC,CAAC;AAChE,YAAI,OAAO,QAAS,QAAO,eAAe,YAAY,CAAC;AAAA,MACzD;AAAA,IACF;AAGA,eAAW,MAAM,aAAc,OAAM,GAAG,KAAK,KAAK,KAAK;AAGvD,aAAS,IAAI,KAAK,IAAI,WAAW,QAAQ,KAAK;AAC5C,YAAM,OAAO,cAAc,KAAK,OAAO,WAAW,MAAM,GAAG,IAAI,CAAC,CAAC;AACjE,iBAAW,MAAM,KAAK,MAAO,OAAM,GAAG,KAAK,KAAK,KAAK;AAAA,IACvD;AAEA,SAAK,OAAO;AAGZ,UAAM,EAAE,MAAM,KAAK,SAAS,IAAI,aAAa,KAAK,OAAO,YAAY,KAAK,KAAK;AAC/E,SAAK,QAAQ;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAIO,IAAM,MAAN,MAAe;AAAA,EAKpB,YAAY,QAAwB;AAClC,SAAK,QAAQ,UAAe,YAAY,EAAE,QAAQ,OAAO,OAAO,CAAqB;AACrF,SAAK,cAAe,OAAO,WAAY,CAAC;AACxC,SAAK,WAAW,OAAO;AAAA,EACzB;AAAA,EAEQ,oBAAoB,MAAsB,SAA2B;AAC3E,UAAM,WAAW,YAAY,OAAO;AACpC,QAAI,IAAc,CAAC;AACnB,QAAI,OAAO;AACX,eAAW,MAAM,UAAU;AACzB,YAAM,QAAQ,KAAK,SAAS,IAAI,EAAE;AAClC,UAAI,CAAC,MAAO,OAAM,IAAI,SAAS,kBAAkB,EAAE,aAAa;AAChE,aAAO;AACP,QAAE,KAAK,EAAE;AAAA,IACX;AAEA,WAAO,KAAK,SAAS,OAAO,GAAG;AAC7B,UAAI,CAAC,KAAK,QAAS,OAAM,IAAI,SAAS,mBAAmB,EAAE,KAAK,GAAG,CAAC,2BAA2B;AAC/F,YAAM,UAAU,KAAK;AACrB,aAAO,KAAK,SAAS,IAAI,OAAO;AAChC,QAAE,KAAK,OAAO;AAAA,IAChB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAyB;AACvB,UAAM,cAAc,KAAK,oBAAoB,KAAK,OAAO,KAAK,QAAQ;AACtE,QAAI,MAAM,KAAK;AACf,UAAM,QAAkB,EAAE,MAAM,WAAW;AAG3C,aAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,YAAM,OAAO,cAAc,KAAK,OAAO,YAAY,MAAM,GAAG,IAAI,CAAC,CAAC;AAClE,iBAAW,MAAM,KAAK,MAAO,OAAM,GAAG,KAAK,KAAK,KAAK;AAAA,IACvD;AAEA,WAAO,IAAI,WAAgB,KAAK,OAAO,CAAC,GAAG,WAAW,GAAG,GAAG;AAAA,EAC9D;AACF;AAsBO,SAAS,UAAyC,QAAkC;AACzF,SAAO,IAAI,IAAI,MAAM;AACvB;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
type EventType = string;
|
|
2
|
+
type StateId = string;
|
|
3
|
+
interface HSMEvent {
|
|
4
|
+
type: EventType;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
type ActionFn<Ctx> = (ctx: Ctx, event: HSMEvent) => Ctx | void;
|
|
8
|
+
type GuardFn<Ctx> = (ctx: Ctx, event: HSMEvent) => boolean;
|
|
9
|
+
interface TransitionConfig<Ctx> {
|
|
10
|
+
target?: string;
|
|
11
|
+
guard?: GuardFn<Ctx>;
|
|
12
|
+
actions?: ActionFn<Ctx>[];
|
|
13
|
+
}
|
|
14
|
+
interface StateConfig<Ctx> {
|
|
15
|
+
initial?: string;
|
|
16
|
+
type?: "compound" | "atomic";
|
|
17
|
+
states?: Record<string, StateConfig<Ctx>>;
|
|
18
|
+
on?: Record<string, string | TransitionConfig<Ctx> | Array<TransitionConfig<Ctx>>>;
|
|
19
|
+
entry?: ActionFn<Ctx> | ActionFn<Ctx>[];
|
|
20
|
+
exit?: ActionFn<Ctx> | ActionFn<Ctx>[];
|
|
21
|
+
history?: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface HSMConfig<Ctx> {
|
|
24
|
+
initial: string;
|
|
25
|
+
context?: Ctx;
|
|
26
|
+
states: Record<string, StateConfig<Ctx>>;
|
|
27
|
+
}
|
|
28
|
+
type StateListener<Ctx> = (state: string, ctx: Ctx, event: HSMEvent) => void;
|
|
29
|
+
|
|
30
|
+
interface StateNode<Ctx> {
|
|
31
|
+
id: string;
|
|
32
|
+
initial?: string;
|
|
33
|
+
children: Map<string, StateNode<Ctx>>;
|
|
34
|
+
parent?: StateNode<Ctx>;
|
|
35
|
+
transitions: Map<string, TransitionDef<Ctx>[]>;
|
|
36
|
+
entry: ActionFn<Ctx>[];
|
|
37
|
+
exit: ActionFn<Ctx>[];
|
|
38
|
+
history: boolean;
|
|
39
|
+
historyValue?: string;
|
|
40
|
+
}
|
|
41
|
+
interface TransitionDef<Ctx> {
|
|
42
|
+
target?: string;
|
|
43
|
+
guard?: GuardFn<Ctx>;
|
|
44
|
+
actions: ActionFn<Ctx>[];
|
|
45
|
+
}
|
|
46
|
+
declare class HSMError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
declare class HSMService<Ctx> {
|
|
50
|
+
private _path;
|
|
51
|
+
private _ctx;
|
|
52
|
+
private readonly _root;
|
|
53
|
+
private readonly _listeners;
|
|
54
|
+
constructor(root: StateNode<Ctx>, initialPath: string[], ctx: Ctx);
|
|
55
|
+
/** Current state as dot-notation string, e.g. `"active.running"`. */
|
|
56
|
+
get state(): string;
|
|
57
|
+
/** Current context. */
|
|
58
|
+
get context(): Ctx;
|
|
59
|
+
/** Current state as array path, e.g. `["active", "running"]`. */
|
|
60
|
+
get stateValue(): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Returns true if the current state matches the given prefix.
|
|
63
|
+
* `matches("active")` returns true for `"active.running"`.
|
|
64
|
+
*/
|
|
65
|
+
matches(state: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Send an event to the machine.
|
|
68
|
+
* Returns this for chaining.
|
|
69
|
+
*/
|
|
70
|
+
send(eventType: string, payload?: Record<string, unknown>): this;
|
|
71
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
72
|
+
subscribe(listener: StateListener<Ctx>): () => void;
|
|
73
|
+
private _notify;
|
|
74
|
+
private _doTransition;
|
|
75
|
+
}
|
|
76
|
+
declare class HSM<Ctx> {
|
|
77
|
+
private readonly _root;
|
|
78
|
+
private readonly _initial;
|
|
79
|
+
private readonly _initialCtx;
|
|
80
|
+
constructor(config: HSMConfig<Ctx>);
|
|
81
|
+
private _resolveInitialPath;
|
|
82
|
+
/**
|
|
83
|
+
* Start the machine and run entry actions for the initial state path.
|
|
84
|
+
* Returns the running service.
|
|
85
|
+
*/
|
|
86
|
+
start(): HSMService<Ctx>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a hierarchical state machine.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const machine = createHSM({
|
|
93
|
+
* initial: 'idle',
|
|
94
|
+
* context: { count: 0 },
|
|
95
|
+
* states: {
|
|
96
|
+
* idle: { on: { START: 'active' } },
|
|
97
|
+
* active: {
|
|
98
|
+
* initial: 'running',
|
|
99
|
+
* states: {
|
|
100
|
+
* running: { on: { PAUSE: 'active.paused', STOP: 'idle' } },
|
|
101
|
+
* paused: { on: { RESUME: 'active.running', STOP: 'idle' } },
|
|
102
|
+
* },
|
|
103
|
+
* },
|
|
104
|
+
* },
|
|
105
|
+
* });
|
|
106
|
+
* const service = machine.start();
|
|
107
|
+
*/
|
|
108
|
+
declare function createHSM<Ctx = Record<string, unknown>>(config: HSMConfig<Ctx>): HSM<Ctx>;
|
|
109
|
+
|
|
110
|
+
export { type ActionFn, type EventType, type GuardFn, HSM, type HSMConfig, HSMError, type HSMEvent, HSMService, type StateConfig, type StateId, type StateListener, type TransitionConfig, createHSM };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
type EventType = string;
|
|
2
|
+
type StateId = string;
|
|
3
|
+
interface HSMEvent {
|
|
4
|
+
type: EventType;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
type ActionFn<Ctx> = (ctx: Ctx, event: HSMEvent) => Ctx | void;
|
|
8
|
+
type GuardFn<Ctx> = (ctx: Ctx, event: HSMEvent) => boolean;
|
|
9
|
+
interface TransitionConfig<Ctx> {
|
|
10
|
+
target?: string;
|
|
11
|
+
guard?: GuardFn<Ctx>;
|
|
12
|
+
actions?: ActionFn<Ctx>[];
|
|
13
|
+
}
|
|
14
|
+
interface StateConfig<Ctx> {
|
|
15
|
+
initial?: string;
|
|
16
|
+
type?: "compound" | "atomic";
|
|
17
|
+
states?: Record<string, StateConfig<Ctx>>;
|
|
18
|
+
on?: Record<string, string | TransitionConfig<Ctx> | Array<TransitionConfig<Ctx>>>;
|
|
19
|
+
entry?: ActionFn<Ctx> | ActionFn<Ctx>[];
|
|
20
|
+
exit?: ActionFn<Ctx> | ActionFn<Ctx>[];
|
|
21
|
+
history?: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface HSMConfig<Ctx> {
|
|
24
|
+
initial: string;
|
|
25
|
+
context?: Ctx;
|
|
26
|
+
states: Record<string, StateConfig<Ctx>>;
|
|
27
|
+
}
|
|
28
|
+
type StateListener<Ctx> = (state: string, ctx: Ctx, event: HSMEvent) => void;
|
|
29
|
+
|
|
30
|
+
interface StateNode<Ctx> {
|
|
31
|
+
id: string;
|
|
32
|
+
initial?: string;
|
|
33
|
+
children: Map<string, StateNode<Ctx>>;
|
|
34
|
+
parent?: StateNode<Ctx>;
|
|
35
|
+
transitions: Map<string, TransitionDef<Ctx>[]>;
|
|
36
|
+
entry: ActionFn<Ctx>[];
|
|
37
|
+
exit: ActionFn<Ctx>[];
|
|
38
|
+
history: boolean;
|
|
39
|
+
historyValue?: string;
|
|
40
|
+
}
|
|
41
|
+
interface TransitionDef<Ctx> {
|
|
42
|
+
target?: string;
|
|
43
|
+
guard?: GuardFn<Ctx>;
|
|
44
|
+
actions: ActionFn<Ctx>[];
|
|
45
|
+
}
|
|
46
|
+
declare class HSMError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
declare class HSMService<Ctx> {
|
|
50
|
+
private _path;
|
|
51
|
+
private _ctx;
|
|
52
|
+
private readonly _root;
|
|
53
|
+
private readonly _listeners;
|
|
54
|
+
constructor(root: StateNode<Ctx>, initialPath: string[], ctx: Ctx);
|
|
55
|
+
/** Current state as dot-notation string, e.g. `"active.running"`. */
|
|
56
|
+
get state(): string;
|
|
57
|
+
/** Current context. */
|
|
58
|
+
get context(): Ctx;
|
|
59
|
+
/** Current state as array path, e.g. `["active", "running"]`. */
|
|
60
|
+
get stateValue(): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Returns true if the current state matches the given prefix.
|
|
63
|
+
* `matches("active")` returns true for `"active.running"`.
|
|
64
|
+
*/
|
|
65
|
+
matches(state: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Send an event to the machine.
|
|
68
|
+
* Returns this for chaining.
|
|
69
|
+
*/
|
|
70
|
+
send(eventType: string, payload?: Record<string, unknown>): this;
|
|
71
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
72
|
+
subscribe(listener: StateListener<Ctx>): () => void;
|
|
73
|
+
private _notify;
|
|
74
|
+
private _doTransition;
|
|
75
|
+
}
|
|
76
|
+
declare class HSM<Ctx> {
|
|
77
|
+
private readonly _root;
|
|
78
|
+
private readonly _initial;
|
|
79
|
+
private readonly _initialCtx;
|
|
80
|
+
constructor(config: HSMConfig<Ctx>);
|
|
81
|
+
private _resolveInitialPath;
|
|
82
|
+
/**
|
|
83
|
+
* Start the machine and run entry actions for the initial state path.
|
|
84
|
+
* Returns the running service.
|
|
85
|
+
*/
|
|
86
|
+
start(): HSMService<Ctx>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a hierarchical state machine.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const machine = createHSM({
|
|
93
|
+
* initial: 'idle',
|
|
94
|
+
* context: { count: 0 },
|
|
95
|
+
* states: {
|
|
96
|
+
* idle: { on: { START: 'active' } },
|
|
97
|
+
* active: {
|
|
98
|
+
* initial: 'running',
|
|
99
|
+
* states: {
|
|
100
|
+
* running: { on: { PAUSE: 'active.paused', STOP: 'idle' } },
|
|
101
|
+
* paused: { on: { RESUME: 'active.running', STOP: 'idle' } },
|
|
102
|
+
* },
|
|
103
|
+
* },
|
|
104
|
+
* },
|
|
105
|
+
* });
|
|
106
|
+
* const service = machine.start();
|
|
107
|
+
*/
|
|
108
|
+
declare function createHSM<Ctx = Record<string, unknown>>(config: HSMConfig<Ctx>): HSM<Ctx>;
|
|
109
|
+
|
|
110
|
+
export { type ActionFn, type EventType, type GuardFn, HSM, type HSMConfig, HSMError, type HSMEvent, HSMService, type StateConfig, type StateId, type StateListener, type TransitionConfig, createHSM };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/hsm.ts
|
|
2
|
+
function buildNode(id, config, parent) {
|
|
3
|
+
const node = {
|
|
4
|
+
id,
|
|
5
|
+
initial: config.initial,
|
|
6
|
+
children: /* @__PURE__ */ new Map(),
|
|
7
|
+
parent,
|
|
8
|
+
transitions: /* @__PURE__ */ new Map(),
|
|
9
|
+
entry: config.entry ? Array.isArray(config.entry) ? config.entry : [config.entry] : [],
|
|
10
|
+
exit: config.exit ? Array.isArray(config.exit) ? config.exit : [config.exit] : [],
|
|
11
|
+
history: config.history ?? false
|
|
12
|
+
};
|
|
13
|
+
if (config.states) {
|
|
14
|
+
for (const [childId, childConfig] of Object.entries(config.states)) {
|
|
15
|
+
node.children.set(childId, buildNode(childId, childConfig, node));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (config.on) {
|
|
19
|
+
for (const [eventType, transConfig] of Object.entries(config.on)) {
|
|
20
|
+
const defs = parseTransitions(transConfig);
|
|
21
|
+
node.transitions.set(eventType, defs);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
function parseTransitions(raw) {
|
|
27
|
+
if (typeof raw === "string") {
|
|
28
|
+
return [{ target: raw, actions: [] }];
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(raw)) {
|
|
31
|
+
return raw.map((t) => ({
|
|
32
|
+
target: t.target,
|
|
33
|
+
guard: t.guard,
|
|
34
|
+
actions: t.actions ?? []
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
return [{ target: raw.target, guard: raw.guard, actions: raw.actions ?? [] }];
|
|
38
|
+
}
|
|
39
|
+
function getNodeAtPath(root, path) {
|
|
40
|
+
let node = root;
|
|
41
|
+
for (const id of path) {
|
|
42
|
+
const child = node.children.get(id);
|
|
43
|
+
if (!child) throw new HSMError(`State "${id}" not found`);
|
|
44
|
+
node = child;
|
|
45
|
+
}
|
|
46
|
+
return node;
|
|
47
|
+
}
|
|
48
|
+
function resolvePath(target) {
|
|
49
|
+
return target.split(".");
|
|
50
|
+
}
|
|
51
|
+
function lcaDepth(a, b) {
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
54
|
+
return i;
|
|
55
|
+
}
|
|
56
|
+
function expandToLeaf(root, path, ctx, event) {
|
|
57
|
+
let cur = getNodeAtPath(root, path);
|
|
58
|
+
let p = [...path];
|
|
59
|
+
while (cur.children.size > 0) {
|
|
60
|
+
if (!cur.initial) throw new HSMError(`Compound state "${p.join(".")}" has no initial substate`);
|
|
61
|
+
const childId = cur.history && cur.historyValue ? cur.historyValue : cur.initial;
|
|
62
|
+
cur = cur.children.get(childId);
|
|
63
|
+
for (const fn of cur.entry) ctx = fn(ctx, event) ?? ctx;
|
|
64
|
+
p.push(childId);
|
|
65
|
+
}
|
|
66
|
+
return { path: p, ctx };
|
|
67
|
+
}
|
|
68
|
+
var HSMError = class extends Error {
|
|
69
|
+
constructor(message) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "HSMError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var HSMService = class {
|
|
75
|
+
constructor(root, initialPath, ctx) {
|
|
76
|
+
this._listeners = /* @__PURE__ */ new Set();
|
|
77
|
+
this._root = root;
|
|
78
|
+
this._path = initialPath;
|
|
79
|
+
this._ctx = ctx;
|
|
80
|
+
}
|
|
81
|
+
/** Current state as dot-notation string, e.g. `"active.running"`. */
|
|
82
|
+
get state() {
|
|
83
|
+
return this._path.join(".");
|
|
84
|
+
}
|
|
85
|
+
/** Current context. */
|
|
86
|
+
get context() {
|
|
87
|
+
return this._ctx;
|
|
88
|
+
}
|
|
89
|
+
/** Current state as array path, e.g. `["active", "running"]`. */
|
|
90
|
+
get stateValue() {
|
|
91
|
+
return [...this._path];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns true if the current state matches the given prefix.
|
|
95
|
+
* `matches("active")` returns true for `"active.running"`.
|
|
96
|
+
*/
|
|
97
|
+
matches(state) {
|
|
98
|
+
const parts = state.split(".");
|
|
99
|
+
return parts.every((part, i) => this._path[i] === part);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Send an event to the machine.
|
|
103
|
+
* Returns this for chaining.
|
|
104
|
+
*/
|
|
105
|
+
send(eventType, payload = {}) {
|
|
106
|
+
const event = { type: eventType, ...payload };
|
|
107
|
+
for (let depth = this._path.length; depth >= 0; depth--) {
|
|
108
|
+
const nodePath = this._path.slice(0, depth);
|
|
109
|
+
const node = getNodeAtPath(this._root, nodePath);
|
|
110
|
+
const defs = node.transitions.get(eventType);
|
|
111
|
+
if (!defs) continue;
|
|
112
|
+
for (const def of defs) {
|
|
113
|
+
if (def.guard && !def.guard(this._ctx, event)) continue;
|
|
114
|
+
if (def.target === void 0) {
|
|
115
|
+
let ctx = this._ctx;
|
|
116
|
+
for (const fn of def.actions) ctx = fn(ctx, event) ?? ctx;
|
|
117
|
+
this._ctx = ctx;
|
|
118
|
+
} else {
|
|
119
|
+
const targetPath = resolvePath(def.target);
|
|
120
|
+
this._doTransition(this._path, nodePath, targetPath, def.actions, event);
|
|
121
|
+
}
|
|
122
|
+
this._notify(event);
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
129
|
+
subscribe(listener) {
|
|
130
|
+
this._listeners.add(listener);
|
|
131
|
+
return () => this._listeners.delete(listener);
|
|
132
|
+
}
|
|
133
|
+
_notify(event) {
|
|
134
|
+
for (const fn of this._listeners) fn(this.state, this._ctx, event);
|
|
135
|
+
}
|
|
136
|
+
_doTransition(currentPath, sourcePath, targetPath, transActions, event) {
|
|
137
|
+
const lca = lcaDepth(currentPath, targetPath);
|
|
138
|
+
let ctx = this._ctx;
|
|
139
|
+
for (let i = currentPath.length - 1; i >= lca; i--) {
|
|
140
|
+
const node = getNodeAtPath(this._root, currentPath.slice(0, i + 1));
|
|
141
|
+
for (const fn of node.exit) ctx = fn(ctx, event) ?? ctx;
|
|
142
|
+
if (i > 0) {
|
|
143
|
+
const parent = getNodeAtPath(this._root, currentPath.slice(0, i));
|
|
144
|
+
if (parent.history) parent.historyValue = currentPath[i];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const fn of transActions) ctx = fn(ctx, event) ?? ctx;
|
|
148
|
+
for (let i = lca; i < targetPath.length; i++) {
|
|
149
|
+
const node = getNodeAtPath(this._root, targetPath.slice(0, i + 1));
|
|
150
|
+
for (const fn of node.entry) ctx = fn(ctx, event) ?? ctx;
|
|
151
|
+
}
|
|
152
|
+
this._ctx = ctx;
|
|
153
|
+
const { path, ctx: finalCtx } = expandToLeaf(this._root, targetPath, ctx, event);
|
|
154
|
+
this._path = path;
|
|
155
|
+
this._ctx = finalCtx;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var HSM = class {
|
|
159
|
+
constructor(config) {
|
|
160
|
+
this._root = buildNode("__root__", { states: config.states });
|
|
161
|
+
this._initialCtx = config.context ?? {};
|
|
162
|
+
this._initial = config.initial;
|
|
163
|
+
}
|
|
164
|
+
_resolveInitialPath(root, initial) {
|
|
165
|
+
const basePath = resolvePath(initial);
|
|
166
|
+
let p = [];
|
|
167
|
+
let node = root;
|
|
168
|
+
for (const id of basePath) {
|
|
169
|
+
const child = node.children.get(id);
|
|
170
|
+
if (!child) throw new HSMError(`Initial state "${id}" not found`);
|
|
171
|
+
node = child;
|
|
172
|
+
p.push(id);
|
|
173
|
+
}
|
|
174
|
+
while (node.children.size > 0) {
|
|
175
|
+
if (!node.initial) throw new HSMError(`Compound state "${p.join(".")}" has no initial substate`);
|
|
176
|
+
const childId = node.initial;
|
|
177
|
+
node = node.children.get(childId);
|
|
178
|
+
p.push(childId);
|
|
179
|
+
}
|
|
180
|
+
return p;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Start the machine and run entry actions for the initial state path.
|
|
184
|
+
* Returns the running service.
|
|
185
|
+
*/
|
|
186
|
+
start() {
|
|
187
|
+
const initialPath = this._resolveInitialPath(this._root, this._initial);
|
|
188
|
+
let ctx = this._initialCtx;
|
|
189
|
+
const event = { type: "__init__" };
|
|
190
|
+
for (let i = 0; i < initialPath.length; i++) {
|
|
191
|
+
const node = getNodeAtPath(this._root, initialPath.slice(0, i + 1));
|
|
192
|
+
for (const fn of node.entry) ctx = fn(ctx, event) ?? ctx;
|
|
193
|
+
}
|
|
194
|
+
return new HSMService(this._root, [...initialPath], ctx);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
function createHSM(config) {
|
|
198
|
+
return new HSM(config);
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
HSM,
|
|
202
|
+
HSMError,
|
|
203
|
+
HSMService,
|
|
204
|
+
createHSM
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hsm.ts"],"sourcesContent":["import type {\n ActionFn,\n GuardFn,\n HSMConfig,\n HSMEvent,\n StateConfig,\n StateId,\n StateListener,\n TransitionConfig,\n} from \"./types.js\";\n\n// ── Internal state node ──────────────────────────────────────────────────────\n\ninterface StateNode<Ctx> {\n id: string;\n initial?: string;\n children: Map<string, StateNode<Ctx>>;\n parent?: StateNode<Ctx>;\n transitions: Map<string, TransitionDef<Ctx>[]>;\n entry: ActionFn<Ctx>[];\n exit: ActionFn<Ctx>[];\n history: boolean;\n historyValue?: string;\n}\n\ninterface TransitionDef<Ctx> {\n target?: string;\n guard?: GuardFn<Ctx>;\n actions: ActionFn<Ctx>[];\n}\n\n// ── Builder ──────────────────────────────────────────────────────────────────\n\nfunction buildNode<Ctx>(\n id: string,\n config: StateConfig<Ctx>,\n parent?: StateNode<Ctx>,\n): StateNode<Ctx> {\n const node: StateNode<Ctx> = {\n id,\n initial: config.initial,\n children: new Map(),\n parent,\n transitions: new Map(),\n entry: config.entry\n ? Array.isArray(config.entry) ? config.entry : [config.entry]\n : [],\n exit: config.exit\n ? Array.isArray(config.exit) ? config.exit : [config.exit]\n : [],\n history: config.history ?? false,\n };\n\n // Build child states\n if (config.states) {\n for (const [childId, childConfig] of Object.entries(config.states)) {\n node.children.set(childId, buildNode(childId, childConfig, node));\n }\n }\n\n // Parse transitions\n if (config.on) {\n for (const [eventType, transConfig] of Object.entries(config.on)) {\n const defs = parseTransitions(transConfig as string | TransitionConfig<Ctx> | Array<TransitionConfig<Ctx>>);\n node.transitions.set(eventType, defs);\n }\n }\n\n return node;\n}\n\nfunction parseTransitions<Ctx>(\n raw: string | TransitionConfig<Ctx> | Array<TransitionConfig<Ctx>>,\n): TransitionDef<Ctx>[] {\n if (typeof raw === \"string\") {\n return [{ target: raw, actions: [] }];\n }\n if (Array.isArray(raw)) {\n return raw.map((t) => ({\n target: t.target,\n guard: t.guard,\n actions: t.actions ?? [],\n }));\n }\n return [{ target: raw.target, guard: raw.guard, actions: raw.actions ?? [] }];\n}\n\n// ── Path utilities ───────────────────────────────────────────────────────────\n\nfunction getNodeAtPath<Ctx>(root: StateNode<Ctx>, path: string[]): StateNode<Ctx> {\n let node = root;\n for (const id of path) {\n const child = node.children.get(id);\n if (!child) throw new HSMError(`State \"${id}\" not found`);\n node = child;\n }\n return node;\n}\n\n/** Resolve a dotted target string to a path array from the virtual root. */\nfunction resolvePath(target: string): string[] {\n return target.split(\".\");\n}\n\n/** LCA index: length of longest common prefix of two paths. */\nfunction lcaDepth(a: string[], b: string[]): number {\n let i = 0;\n while (i < a.length && i < b.length && a[i] === b[i]) i++;\n return i;\n}\n\n/** Enter compound state to leaf following initial/history chain. */\nfunction expandToLeaf<Ctx>(\n root: StateNode<Ctx>,\n path: string[],\n ctx: Ctx,\n event: HSMEvent,\n): { path: string[]; ctx: Ctx } {\n let cur = getNodeAtPath(root, path);\n let p = [...path];\n\n while (cur.children.size > 0) {\n if (!cur.initial) throw new HSMError(`Compound state \"${p.join(\".\")}\" has no initial substate`);\n const childId = cur.history && cur.historyValue ? cur.historyValue : cur.initial;\n cur = cur.children.get(childId)!;\n for (const fn of cur.entry) ctx = fn(ctx, event) ?? ctx;\n p.push(childId);\n }\n\n return { path: p, ctx };\n}\n\n// ── HSMService ───────────────────────────────────────────────────────────────\n\nexport class HSMError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"HSMError\";\n }\n}\n\nexport class HSMService<Ctx> {\n private _path: string[];\n private _ctx: Ctx;\n private readonly _root: StateNode<Ctx>;\n private readonly _listeners = new Set<StateListener<Ctx>>();\n\n constructor(root: StateNode<Ctx>, initialPath: string[], ctx: Ctx) {\n this._root = root;\n this._path = initialPath;\n this._ctx = ctx;\n }\n\n /** Current state as dot-notation string, e.g. `\"active.running\"`. */\n get state(): string {\n return this._path.join(\".\");\n }\n\n /** Current context. */\n get context(): Ctx {\n return this._ctx;\n }\n\n /** Current state as array path, e.g. `[\"active\", \"running\"]`. */\n get stateValue(): string[] {\n return [...this._path];\n }\n\n /**\n * Returns true if the current state matches the given prefix.\n * `matches(\"active\")` returns true for `\"active.running\"`.\n */\n matches(state: string): boolean {\n const parts = state.split(\".\");\n return parts.every((part, i) => this._path[i] === part);\n }\n\n /**\n * Send an event to the machine.\n * Returns this for chaining.\n */\n send(eventType: string, payload: Record<string, unknown> = {}): this {\n const event: HSMEvent = { type: eventType, ...payload };\n\n // Walk up from current leaf to root looking for applicable transition\n for (let depth = this._path.length; depth >= 0; depth--) {\n const nodePath = this._path.slice(0, depth);\n const node = getNodeAtPath(this._root, nodePath);\n const defs = node.transitions.get(eventType);\n if (!defs) continue;\n\n for (const def of defs) {\n if (def.guard && !def.guard(this._ctx, event)) continue;\n\n if (def.target === undefined) {\n // Internal transition — run actions, no exit/entry\n let ctx = this._ctx;\n for (const fn of def.actions) ctx = fn(ctx, event) ?? ctx;\n this._ctx = ctx;\n } else {\n const targetPath = resolvePath(def.target);\n this._doTransition(this._path, nodePath, targetPath, def.actions, event);\n }\n\n this._notify(event);\n return this;\n }\n }\n\n return this;\n }\n\n /** Subscribe to state changes. Returns an unsubscribe function. */\n subscribe(listener: StateListener<Ctx>): () => void {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener);\n }\n\n private _notify(event: HSMEvent): void {\n for (const fn of this._listeners) fn(this.state, this._ctx, event);\n }\n\n private _doTransition(\n currentPath: string[],\n sourcePath: string[],\n targetPath: string[],\n transActions: ActionFn<Ctx>[],\n event: HSMEvent,\n ): void {\n const lca = lcaDepth(currentPath, targetPath);\n let ctx = this._ctx;\n\n // Exit from current leaf up to (not including) LCA\n for (let i = currentPath.length - 1; i >= lca; i--) {\n const node = getNodeAtPath(this._root, currentPath.slice(0, i + 1));\n for (const fn of node.exit) ctx = fn(ctx, event) ?? ctx;\n\n // Record history in parent\n if (i > 0) {\n const parent = getNodeAtPath(this._root, currentPath.slice(0, i));\n if (parent.history) parent.historyValue = currentPath[i];\n }\n }\n\n // Transition actions\n for (const fn of transActions) ctx = fn(ctx, event) ?? ctx;\n\n // Entry from LCA down to target\n for (let i = lca; i < targetPath.length; i++) {\n const node = getNodeAtPath(this._root, targetPath.slice(0, i + 1));\n for (const fn of node.entry) ctx = fn(ctx, event) ?? ctx;\n }\n\n this._ctx = ctx;\n\n // Expand target compound state to leaf\n const { path, ctx: finalCtx } = expandToLeaf(this._root, targetPath, ctx, event);\n this._path = path;\n this._ctx = finalCtx;\n }\n}\n\n// ── HSM factory ───────────────────────────────────────────────────────────────\n\nexport class HSM<Ctx> {\n private readonly _root: StateNode<Ctx>;\n private readonly _initial: string;\n private readonly _initialCtx: Ctx;\n\n constructor(config: HSMConfig<Ctx>) {\n this._root = buildNode<Ctx>(\"__root__\", { states: config.states } as StateConfig<Ctx>);\n this._initialCtx = (config.context ?? ({} as unknown as Ctx));\n this._initial = config.initial;\n }\n\n private _resolveInitialPath(root: StateNode<Ctx>, initial: string): string[] {\n const basePath = resolvePath(initial);\n let p: string[] = [];\n let node = root;\n for (const id of basePath) {\n const child = node.children.get(id);\n if (!child) throw new HSMError(`Initial state \"${id}\" not found`);\n node = child;\n p.push(id);\n }\n // Expand to leaf via initial chain\n while (node.children.size > 0) {\n if (!node.initial) throw new HSMError(`Compound state \"${p.join(\".\")}\" has no initial substate`);\n const childId = node.initial;\n node = node.children.get(childId)!;\n p.push(childId);\n }\n return p;\n }\n\n /**\n * Start the machine and run entry actions for the initial state path.\n * Returns the running service.\n */\n start(): HSMService<Ctx> {\n const initialPath = this._resolveInitialPath(this._root, this._initial);\n let ctx = this._initialCtx;\n const event: HSMEvent = { type: \"__init__\" };\n\n // Run entry actions down to initial leaf\n for (let i = 0; i < initialPath.length; i++) {\n const node = getNodeAtPath(this._root, initialPath.slice(0, i + 1));\n for (const fn of node.entry) ctx = fn(ctx, event) ?? ctx;\n }\n\n return new HSMService<Ctx>(this._root, [...initialPath], ctx);\n }\n}\n\n/**\n * Create a hierarchical state machine.\n *\n * @example\n * const machine = createHSM({\n * initial: 'idle',\n * context: { count: 0 },\n * states: {\n * idle: { on: { START: 'active' } },\n * active: {\n * initial: 'running',\n * states: {\n * running: { on: { PAUSE: 'active.paused', STOP: 'idle' } },\n * paused: { on: { RESUME: 'active.running', STOP: 'idle' } },\n * },\n * },\n * },\n * });\n * const service = machine.start();\n */\nexport function createHSM<Ctx = Record<string, unknown>>(config: HSMConfig<Ctx>): HSM<Ctx> {\n return new HSM(config);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@billdaddy/hsmkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency TypeScript hierarchical state machine (statecharts): compound states, entry/exit actions, guards, shallow history, internal transitions. Like Python pytransitions / C# Stateless / Ruby AASM.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest --forceExit",
|
|
25
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"state-machine",
|
|
29
|
+
"hsm",
|
|
30
|
+
"statechart",
|
|
31
|
+
"hierarchical",
|
|
32
|
+
"compound-state",
|
|
33
|
+
"history-state",
|
|
34
|
+
"entry-exit",
|
|
35
|
+
"guards",
|
|
36
|
+
"typescript",
|
|
37
|
+
"zero-dependencies"
|
|
38
|
+
],
|
|
39
|
+
"author": "trananhtung",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/trananhtung/hsmkit.git"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/jest": "^30.0.0",
|
|
47
|
+
"jest": "^30.4.2",
|
|
48
|
+
"ts-jest": "^29.4.11",
|
|
49
|
+
"tsup": "^8.5.1",
|
|
50
|
+
"typescript": "^6.0.3"
|
|
51
|
+
}
|
|
52
|
+
}
|