ihsm 0.0.19 → 0.0.20

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/README.md CHANGED
@@ -1,192 +1,333 @@
1
1
  [![CI](https://img.shields.io/github/actions/workflow/status/filasieno/ihsm/ci.yml?label=CI)](https://github.com/filasieno/ihsm/actions/workflows/ci.yml)
2
2
  [![Documentation](https://img.shields.io/github/actions/workflow/status/filasieno/ihsm/docs.yml?label=docs)](https://github.com/filasieno/ihsm/actions/workflows/docs.yml)
3
- [![Coverage](https://img.shields.io/coverallsCoverage/github/filasieno/ihsm)](https://coveralls.io/github/filasieno/ihsm)
4
3
  [![License: MIT](https://img.shields.io/github/license/filasieno/ihsm)](https://github.com/filasieno/ihsm/blob/HEAD/LICENSE)
5
4
  [![npm version](https://img.shields.io/npm/v/ihsm)](https://www.npmjs.com/package/ihsm)
6
5
  [![Node](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js)](https://github.com/filasieno/ihsm/blob/HEAD/package.json)
7
6
 
8
7
  # ihsm
9
8
 
10
- An idiomatic hierarchical state machine package for TypeScript — **Samek/QP-style** class hierarchy with
11
- **cached LCA transitions**, **zero production dependencies**, and **100% code coverage** on the runtime.
12
-
13
- ## Quality
14
-
15
- | Metric | Value |
16
- |--------|-------|
17
- | **Statements** | 100% |
18
- | **Branches** | 100% |
19
- | **Functions** | 100% |
20
- | **Lines** | 100% |
21
-
22
- CI enforces full coverage on every push (`nix flake check`).
23
-
24
- ## Features
25
-
26
- - User-defined event payloads (typed `Protocol`)
27
- - User-defined state context (`ctx`)
28
- - Hierarchically nested states (class inheritance)
29
- - Orthogonal regions (nest multiple machines; compose via `post`/`call`)
30
- - Internal transitions (handle event without calling `transition()`)
31
- - Explicit transitions with cached entry/exit sequences
32
- - Guards (inline `if` in handlers)
33
- - History (`ctx` + `restore()`)
34
- - Entry / exit actions (`onEntry`, `onExit`)
35
- - Async and sync handlers
36
- - **`call()` — typed request/response through the actor mailbox** (unique)
37
- - **`then()` — decision pseudo-states with automatic follow-up transitions**
38
- - **`postNow()` — hi-priority extended transitions within the same dispatch**
39
- - Actor-style messaging (`post`, `deferredPost`, serialized queue)
40
- - Structured errors and trace levels
9
+ **Class-based hierarchical state machines and actor mailboxes for TypeScript**typed `post`/`call`, **zero** production dependencies, **~4.6 KB gzip** in the browser. → [Documentation](https://filasieno.github.io/ihsm/)
41
10
 
42
- ## Documentation
11
+ ihsm is state management and orchestration for backends, session actors, protocol handlers, and embedded tooling: states are **classes**, events are **methods**, hierarchy is **inheritance**, and each machine is an **actor** with a serialized mailbox and run-to-completion dispatch.
43
12
 
44
- | Resource | Link |
45
- |----------|------|
46
- | **Documentation site** | [filasieno.github.io/ihsm](https://filasieno.github.io/ihsm/) — reference manual + interactive tutorials |
47
- | Reference (source) | [reference/REFERENCE.md](./reference/REFERENCE.md) |
48
- | Tutorials (source) | [tutorials/](./tutorials/) |
49
- | Examples | [`src/spec/`](./src/spec/) |
50
- | Code of conduct | [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) |
51
- | Security | [SECURITY.md](./SECURITY.md) |
52
-
53
- Each tutorial page on the documentation site combines prose, code samples, and an embedded playground
54
- (sender/message forms, live trace, reset). The same machines are verified headlessly by Mocha specs under
55
- `tutorials/*/tutorial.spec.ts`.
13
+ Requires **Node.js 22+** (or a modern browser). Class names in traces and errors come from `Class.name` — no extra registration step in a typical npm/Node project.
56
14
 
57
- ## Install
15
+ It uses event-driven programming, class-based hierarchical statecharts, and the actor model to handle complex logic in predictable, robust ways. States are **classes**, events are **methods**, hierarchy is **inheritance**, and each machine is an **actor** with a serialized mailbox with RTC guarantees.
16
+
17
+ ---
18
+
19
+ 📖 [Read the documentation](https://filasieno.github.io/ihsm/)
20
+
21
+ 📑 [API reference](https://filasieno.github.io/ihsm/api)
22
+
23
+ 📖 [Reference](https://filasieno.github.io/ihsm/reference)
24
+
25
+ 💬 [Open an issue](https://github.com/filasieno/ihsm/issues)
26
+
27
+ ---
28
+
29
+ ## Super quick start
58
30
 
59
- ```shell
60
- npm install ihsm@latest
31
+ ```bash
32
+ npm install ihsm
61
33
  ```
62
34
 
63
- ### Runtime support
35
+ ```ts
36
+ import { InitialState, makeHsm, TopState } from 'ihsm';
64
37
 
65
- ihsm ships modern **ES2022** ESM and CommonJS — no transpiled legacy (ES5/ES2015)
66
- output and no polyfills. Supported runtimes:
38
+ interface DoorCtx {
39
+ openCount: number;
40
+ }
67
41
 
68
- | Runtime | Minimum |
69
- | ------- | ------- |
70
- | Node.js | **22+** |
71
- | Chrome / Edge | **94+** |
72
- | Firefox | **93+** |
73
- | Safari (macOS / iOS) | **15.4+** |
42
+ // All possible signals are enumerated in a formal protocol
43
+ interface DoorProtocol {
44
+ open(): void;
45
+ close(): void;
46
+ }
47
+
48
+ class DoorTop extends TopState<DoorCtx, DoorProtocol> {}
74
49
 
75
- Older browsers and JavaScript engines are intentionally **not** supported. If you
76
- must target them, transpile `ihsm` yourself with your bundler (the published
77
- `browserslist` field declares this baseline for downstream tooling).
50
+ @InitialState
51
+ class Closed extends DoorTop {
52
+ open(): void {
53
+ this.ctx.openCount += 1;
54
+ this.transition(Open);
55
+ }
56
+ }
78
57
 
79
- ## Requirements
58
+ class Open extends DoorTop {
59
+ close(): void {
60
+ this.transition(Closed);
61
+ }
62
+ }
80
63
 
81
- **[Nix](https://nixos.org/download/)** with flakes enabled the only prerequisite to build and test
82
- from source.
64
+ const door = makeHsm(DoorTop, { openCount: 0 });
65
+ await door.sync(); // wait for initialization
83
66
 
84
- ## Building
67
+ door.post('open');
68
+ await door.sync();
85
69
 
86
- ```shell
87
- git clone https://github.com/filasieno/ihsm.git
88
- cd ihsm
70
+ console.log(door.currentStateName); // 'Open'
71
+ console.log(door.ctx.openCount); // 1
89
72
  ```
90
73
 
91
- ### Development environment
74
+ ---
75
+
76
+ ## Typed services with `call()`
77
+
78
+ Most state-machine libraries make you reach for snapshots, child actors, or ad hoc callbacks to ask the machine a question. ihsm treats **services** as ordinary protocol methods — the runtime injects `resolve` / `reject`, and the client gets a typed `Promise`.
92
79
 
93
- **Always use the Nix dev shell** before running npm scripts. It provides Node 22,
94
- PlantUML, Graphviz, and a store-pinned `node_modules` symlink (same lockfile as CI).
80
+ Define the service once on your `Protocol`. Implement it on a state class. Call it from anywhere that holds the `Hsm` handle.
95
81
 
96
- ```shell
97
- nix develop
98
- # or: direnv allow # .envrc → use flake; auto-enters the shell in supported terminals
82
+ ```ts
83
+ import {
84
+ InitialState,
85
+ makeHsm,
86
+ RejectCallback,
87
+ ResolveCallback,
88
+ TopState,
89
+ } from 'ihsm';
90
+
91
+ interface WalletCtx {
92
+ balance: number;
93
+ }
94
+
95
+ // note the `getBalance` and `withdraw`.
96
+ // since they have a *resolve* and *reject* the are services allowing State Machines to serve requests **AND** transition at the same time if required.
97
+ interface WalletProtocol {
98
+ deposit(amount: number): void;
99
+ getBalance(resolve: ResolveCallback<number>, reject: RejectCallback): void;
100
+ withdraw(resolve: ResolveCallback<number>, reject: RejectCallback, amount: number): void;
101
+ }
102
+
103
+ class WalletTop extends TopState<WalletCtx, WalletProtocol> implements WalletProtocol {
104
+ deposit(amount: number): void {
105
+ this.ctx.balance += amount;
106
+ }
107
+
108
+ getBalance(resolve: ResolveCallback<number>): void {
109
+ resolve(this.ctx.balance);
110
+ }
111
+
112
+ withdraw(resolve: ResolveCallback<number>, reject: RejectCallback, amount: number): void {
113
+ if (amount > this.ctx.balance) {
114
+ reject(new Error('insufficient funds'));
115
+ return;
116
+ }
117
+ this.ctx.balance -= amount;
118
+ resolve(this.ctx.balance);
119
+ }
120
+ }
121
+
122
+ @InitialState
123
+ class Open extends WalletTop {}
124
+
125
+ const wallet = makeHsm(WalletTop, { balance: 100 });
126
+ await wallet.sync();
127
+
128
+ wallet.post('deposit', 50);
129
+
130
+ const balance = await wallet.call('getBalance'); // Promise<number> — no extra sync()
131
+
132
+ try {
133
+ await wallet.call('withdraw', 200);
134
+ } catch (err) {
135
+ // reject() from the handler becomes a thrown Error here
136
+ }
137
+
138
+ const left = await wallet.call('getBalance'); // 150
99
139
  ```
100
140
 
101
- Run npm commands **inside** that shell, or prefix each one with `nix develop --command`:
141
+ **Events** (`void` handlers) → `post('deposit', 50)`. **Services** (`resolve` / `reject` handlers) → `await call('getBalance')`. Same mailbox, same serialization guarantees, full TypeScript inference on names, payloads, and return types.
142
+
143
+ See [Call services](https://filasieno.github.io/ihsm/reference#_4-messaging-post-call-sync) in the reference.
144
+
145
+ ---
146
+
147
+ ## Hierarchical (nested) state machines
148
+
149
+ Child states extend parent states. The prototype chain is the state tree; entering a composite runs `onEntry` from outer to inner initial leaf, exiting walks the lowest common ancestor path.
150
+ Hierarchical state machines are extreamly easy to write just a extend a class.
151
+ Also not that all states are stateless classes.
152
+ All state is stored in the actor context available at `this.ctx`.
153
+
154
+ ```ts
155
+ import { InitialState, makeHsm, TopState } from 'ihsm';
156
+
157
+ interface PlayerCtx {
158
+ track: string;
159
+ }
160
+
161
+ interface PlayerProtocol {
162
+ play(): void;
163
+ pause(): void;
164
+ stop(): void;
165
+ }
166
+
167
+ class PlayerTop extends TopState<PlayerCtx, PlayerProtocol> {}
168
+
169
+ class Active extends PlayerTop {
170
+ stop(): void {
171
+ this.transition(Stopped);
172
+ }
173
+ }
102
174
 
103
- ```shell
104
- nix develop --command npm test
175
+ @InitialState
176
+ class Playing extends Active {
177
+ pause(): void {
178
+ this.transition(Paused);
179
+ }
180
+ }
181
+
182
+ class Paused extends Active {
183
+ play(): void {
184
+ this.transition(Playing);
185
+ }
186
+ }
187
+
188
+ @InitialState
189
+ class Stopped extends PlayerTop {
190
+ play(): void {
191
+ this.ctx.track = 'demo.mp3';
192
+ this.transition(Playing);
193
+ }
194
+ }
195
+
196
+ const player = makeHsm(PlayerTop, { track: '' });
197
+ await player.sync();
198
+
199
+ player.post('play');
200
+ await player.sync();
201
+ // active leaf: Playing — inherits stop() from Active
105
202
  ```
106
203
 
107
- If you see `remove local node_modules/ to use Nix store deps`, delete a plain
108
- `npm install` tree: `rm -rf node_modules` and enter `nix develop` again.
204
+ See [Hierarchy & transitions](https://filasieno.github.io/ihsm/reference#_5-transitions) in the reference.
109
205
 
110
- ### Nix commands (CI parity)
206
+ ---
111
207
 
112
- | Command | Purpose |
113
- | ------- | ------- |
114
- | `nix flake check` | Full CI gate: library compile, unit + tutorial tests, lint, docs site |
115
- | `nix build` | Compile library and run tests → `result/lib/` |
116
- | `nix build .#lint` | TypeScript (full solution), ESLint, Prettier |
117
- | `nix build .#docs` | Production documentation site → `result/share/doc/ihsm/` |
208
+ ## Messaging: `post`, `sync`, and `call`
118
209
 
119
- Full check before opening a PR:
210
+ Every machine is an actor with a **single-threaded mailbox**. While a handler runs, new messages queue — no re-entrancy.
120
211
 
121
- ```shell
122
- nix flake check
212
+ | API | Role | Returns |
213
+ | --- | ---- | ------- |
214
+ | `post(event, …args)` | Fire-and-forget event | `void` (use `sync()` to wait) |
215
+ | `call(service, …args)` | Typed request/response | `Promise<T>` |
216
+ | `deferredPost(ms, event, …args)` | Timer then `post` | `void` |
217
+ | `sync()` | Drain queue up to marker | `Promise<void>` |
218
+
219
+ ```ts
220
+ door.post('open');
221
+ await door.sync(); // handler + transition finished
222
+
223
+ const id = await account.call('lookup', 'user-42'); // await the service directly
123
224
  ```
124
225
 
125
- After `nix build .#docs`, copy artifacts from `result/share/doc/ihsm/` or run
126
- `bash scripts/verify-docs-site.sh result/share/doc/ihsm`.
226
+ Inside handlers you also get `transition()`, `sleep()`, and `postNow()` for hi-priority follow-up steps within the same dispatch turn.
127
227
 
128
- ### npm scripts
228
+ See [Post & sync](https://filasieno.github.io/ihsm/reference#_4-messaging-post-call-sync) in the reference.
129
229
 
130
- All commands below assume **`nix develop`** (interactive shell) or
131
- **`nix develop --command …`** (one-shot). See [website/README.md](./website/README.md)
132
- for docs-site layout and generated output.
230
+ ---
133
231
 
134
- #### Build
232
+ ## Async handlers
135
233
 
136
- | Command | Purpose |
137
- | ------- | ------- |
138
- | `npm run build` | Compile the publishable library → `lib/cjs/` (CommonJS) and `lib/esm/` (ESM), then finalize |
139
- | `npm run build-cjs` | Compile the CommonJS tree only → `lib/cjs/` |
140
- | `npm run build-esm` | Compile the ESM tree only → `lib/esm/` |
141
- | `npm run clean` | Remove generated artifacts (`lib/`, `.tsc/`, coverage, `docs-build/`, `website/docs/`, …) |
142
- | `npm run dist` | Clean, then build library and documentation site (maintainer bundle) |
234
+ Handlers may be `async`. The runtime awaits the returned `Promise` before applying a scheduled `transition()` — so you can run an entire I/O pipeline inside one handler while staying in the same state.
235
+ This is important to minimize states and exploit RTC semantics.
143
236
 
144
- #### Test
237
+ ```ts
238
+ @InitialState
239
+ class Idle extends FileTop {
240
+ async transfer(from: string, to: string): Promise<void> {
241
+ const data = await readFile(from);
242
+ await writeFile(to, data);
243
+ this.transition(Done);
244
+ }
245
+ }
246
+ ```
145
247
 
146
- | Command | Purpose |
147
- | ------- | ------- |
148
- | `npm test` | Unit tests in Node (`src/spec/`) with NYC coverage, then the same specs minified in headless Chromium |
149
- | `npm run test:node` | Node-only unit tests (with coverage) |
150
- | `npm run test:browser` | Minified browser bundles for unit + tutorial specs (Playwright + esbuild) |
151
- | `npm run test:tutorials` | Tutorial specs in Node, then minified in the browser |
152
- | `npm run test:all` | `npm test` + `npm run test:tutorials` (both environments) |
153
- | `npm run coverage` | Print an LCOV coverage report from the last `npm run test:node` run |
248
+ See [Async handlers](https://filasieno.github.io/ihsm/reference#_9-async-handlers) in the reference.
154
249
 
155
- First-time browser setup: `npx playwright install chromium`.
250
+ ---
156
251
 
157
- #### Quality
252
+ ## Install
158
253
 
159
- | Command | Purpose |
160
- | ------- | ------- |
161
- | `npm run typecheck` | Type-check the full project graph (CJS lib, ESM lib, tutorials, website); runs `sync:docs` first |
162
- | `npm run lint` | Typecheck, then ESLint and Prettier check |
163
- | `npm run prettier` | Auto-format TypeScript sources (`src/`, `tutorials/`, `website/`) |
164
- | `npm run verify:source` | Fail if generated output (compiled `.js`/`.d.ts`, docs) appears in the source tree |
165
- | `npm run release:check` | Local release gate: `test:all`, lint, build, doc, and `verify:doc` |
254
+ Requires [Node.js](https://nodejs.org/) **22+**.
255
+
256
+ ```bash
257
+ npm install ihsm
258
+ ```
259
+
260
+ ### Runtime support
166
261
 
167
- #### Documentation
262
+ ihsm ships modern **ES2022** ESM and CommonJS. Supported runtimes:
168
263
 
169
- | Command | Purpose |
264
+ | Runtime | Minimum |
170
265
  | ------- | ------- |
171
- | `npm run sync:docs` | Generate gitignored site inputs: `website/docs/`, `website/sidebars.ts`, PlantUML SVGs |
172
- | `npm run doc` | `sync:docs`, then production Docusaurus build (website workspace) |
173
- | `npm run doc:preview` | `sync:docs`, then Docusaurus dev server at [localhost:3010/ihsm/](http://localhost:3010/ihsm/) |
174
- | `npm run doc:site` | `sync:docs`, then static site → `docs-build/` |
175
- | `npm run verify:doc` | Sanity-check `docs-build/` (links, assets, tutorial pages) |
266
+ | Node.js | **22+** |
267
+ | Chrome / Edge | **94+** |
268
+ | Firefox | **93+** |
269
+ | Safari (macOS / iOS) | **15.4+** |
270
+
271
+ ### Size and dependencies
272
+
273
+ Measured with `esbuild` bundling `lib/esm/index.js` for the browser (full runtime — mailbox, transitions, tracing, typed `call`):
274
+
275
+ | | |
276
+ | --- | --- |
277
+ | **Production dependencies** | **0** |
278
+ | **Published package** | `lib/` only (~46 KB npm tarball) |
279
+ | **Minified bundle** | **~22 KB** (21.7 KiB; single-file ESM/IIFE) |
280
+ | **Gzip** | **~4.6 KB** (typical CDN / HTTP transfer size) |
281
+ | **Tree-shaking** | `"sideEffects": false` — runtime is one cohesive module (~22 KB even when importing only `makeHsm`) |
282
+
283
+ Node loads the unminified `lib/` files directly (~18 KB entry, ~62 KB total); minify numbers apply to browser bundles.
284
+
285
+ No React, no RxJS, no interpreter plugins — just the runtime you import.
286
+
287
+ ---
288
+
289
+ ## Why?
290
+
291
+ Hierarchical statecharts are a formalism for modeling stateful, reactive systems. ihsm encodes them the **Samek/QP way**: class hierarchy, explicit transitions, cached LCA paths, and actor mailboxes — with compile-time safety from a single `Protocol` interface.
292
+
293
+ Good fit when you want:
294
+
295
+ - Typed events and services from one protocol definition
296
+ - Backend / session actors without a heavy framework
297
+ - Zero-dependency supply chain and a small browser bundle
298
+ - Class-based states that read like ordinary TypeScript
299
+
300
+ For visual editors and declarative chart JSON, libraries like [XState](https://github.com/statelyai/xstate) may fit better. See [Comparison with XState](https://filasieno.github.io/ihsm/reference#_13-comparison-with-xstate) in the reference.
301
+
302
+ Inspired by Harel statecharts and the SCXML family of notations.
303
+
304
+ ---
305
+
306
+ ## Documentation
307
+
308
+ | Resource | Link |
309
+ | -------- | ---- |
310
+ | **Documentation site** | [filasieno.github.io/ihsm](https://filasieno.github.io/ihsm/) |
311
+ | Reference (concepts + interactive examples) | [/reference](https://filasieno.github.io/ihsm/reference) |
312
+ | API reference (TSDoc) | [/api](https://filasieno.github.io/ihsm/api) |
313
+ | Source: reference | [reference/REFERENCE.md](./reference/REFERENCE.md) |
314
+ | Source: example machines | [examples/](./examples/) |
315
+
316
+ The reference page combines the full manual with embedded playgrounds; the API is generated from TSDoc.
176
317
 
177
- Release process: [RELEASING.md](./RELEASING.md).
318
+ ---
178
319
 
179
320
  ## Contributing
180
321
 
181
- Contributions are welcome — bug reports, docs, and code.
322
+ Contributions are welcome — bug reports, docs, and code. See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for the development environment, build commands, and PR guidelines.
182
323
 
183
324
  - Bug reports → [issue template](https://github.com/filasieno/ihsm/issues/new?template=bug_report.yml)
184
325
  - Features → [issue template](https://github.com/filasieno/ihsm/issues/new?template=feature_request.yml)
185
- - Security → [GitHub Security Advisories](https://github.com/filasieno/ihsm/security/advisories/new) (not public issues)
326
+ - Security → [GitHub Security Advisories](https://github.com/filasieno/ihsm/security/advisories/new)
186
327
 
187
- Please follow [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). New behavior needs tests in `src/spec/`; tutorial changes need matching specs under `tutorials/`.
328
+ Please follow [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
188
329
 
189
- **Generated output is never committed** — only sources (`src/`, `tutorials/`, `reference/`, `website/docs-src/`). CI runs `scripts/verify-no-generated-in-source.sh`. Build artifacts (`lib/`, `website/docs/`, `docs-build/`, …) are gitignored and produced by Nix/npm.
330
+ ---
190
331
 
191
332
  ## License
192
333