@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 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
+ [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#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
+ [![npm](https://img.shields.io/npm/v/@billdaddy/hsmkit)](https://www.npmjs.com/package/@billdaddy/hsmkit)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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":[]}
@@ -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 };
@@ -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
+ }