ihsm 0.0.14 → 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.
Files changed (50) hide show
  1. package/README.md +330 -5
  2. package/lib/cjs/index.d.ts +934 -0
  3. package/lib/cjs/index.js +493 -0
  4. package/lib/cjs/index.js.map +1 -0
  5. package/lib/cjs/internal/defs.private.d.ts +31 -0
  6. package/lib/cjs/internal/defs.private.js +3 -0
  7. package/lib/cjs/internal/defs.private.js.map +1 -0
  8. package/lib/cjs/internal/dispatch.debug.d.ts +4 -0
  9. package/lib/cjs/internal/dispatch.debug.js +330 -0
  10. package/lib/cjs/internal/dispatch.debug.js.map +1 -0
  11. package/lib/cjs/internal/dispatch.production.d.ts +6 -0
  12. package/lib/cjs/internal/dispatch.production.js +239 -0
  13. package/lib/cjs/internal/dispatch.production.js.map +1 -0
  14. package/lib/cjs/internal/dispatch.trace.d.ts +4 -0
  15. package/lib/cjs/internal/dispatch.trace.js +410 -0
  16. package/lib/cjs/internal/dispatch.trace.js.map +1 -0
  17. package/lib/cjs/internal/hsm.d.ts +54 -0
  18. package/lib/cjs/internal/hsm.js +182 -0
  19. package/lib/cjs/internal/hsm.js.map +1 -0
  20. package/lib/cjs/internal/utils.d.ts +18 -0
  21. package/lib/cjs/internal/utils.js +51 -0
  22. package/lib/cjs/internal/utils.js.map +1 -0
  23. package/lib/cjs/package.json +3 -0
  24. package/lib/esm/index.d.ts +934 -0
  25. package/lib/esm/index.js +476 -0
  26. package/lib/esm/index.js.map +1 -0
  27. package/lib/esm/internal/defs.private.d.ts +31 -0
  28. package/lib/esm/internal/defs.private.js +2 -0
  29. package/lib/esm/internal/defs.private.js.map +1 -0
  30. package/lib/esm/internal/dispatch.debug.d.ts +4 -0
  31. package/lib/esm/internal/dispatch.debug.js +326 -0
  32. package/lib/esm/internal/dispatch.debug.js.map +1 -0
  33. package/lib/esm/internal/dispatch.production.d.ts +6 -0
  34. package/lib/esm/internal/dispatch.production.js +235 -0
  35. package/lib/esm/internal/dispatch.production.js.map +1 -0
  36. package/lib/esm/internal/dispatch.trace.d.ts +4 -0
  37. package/lib/esm/internal/dispatch.trace.js +406 -0
  38. package/lib/esm/internal/dispatch.trace.js.map +1 -0
  39. package/lib/esm/internal/hsm.d.ts +54 -0
  40. package/lib/esm/internal/hsm.js +178 -0
  41. package/lib/esm/internal/hsm.js.map +1 -0
  42. package/lib/esm/internal/utils.d.ts +18 -0
  43. package/lib/esm/internal/utils.js +41 -0
  44. package/lib/esm/internal/utils.js.map +1 -0
  45. package/lib/esm/package.json +3 -0
  46. package/package.json +116 -66
  47. package/lib/index.browser.js +0 -523
  48. package/lib/index.d.ts +0 -104
  49. package/lib/index.js +0 -374
  50. package/tsconfig.json +0 -72
package/README.md CHANGED
@@ -1,9 +1,334 @@
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
+ [![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
+ [![License: MIT](https://img.shields.io/github/license/filasieno/ihsm)](https://github.com/filasieno/ihsm/blob/HEAD/LICENSE)
4
+ [![npm version](https://img.shields.io/npm/v/ihsm)](https://www.npmjs.com/package/ihsm)
5
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js)](https://github.com/filasieno/ihsm/blob/HEAD/package.json)
6
+
7
+ # ihsm
8
+
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/)
10
+
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.
12
+
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.
14
+
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
30
+
31
+ ```bash
32
+ npm install ihsm
33
+ ```
34
+
35
+ ```ts
36
+ import { InitialState, makeHsm, TopState } from 'ihsm';
37
+
38
+ interface DoorCtx {
39
+ openCount: number;
40
+ }
41
+
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> {}
49
+
50
+ @InitialState
51
+ class Closed extends DoorTop {
52
+ open(): void {
53
+ this.ctx.openCount += 1;
54
+ this.transition(Open);
55
+ }
56
+ }
57
+
58
+ class Open extends DoorTop {
59
+ close(): void {
60
+ this.transition(Closed);
61
+ }
62
+ }
63
+
64
+ const door = makeHsm(DoorTop, { openCount: 0 });
65
+ await door.sync(); // wait for initialization
66
+
67
+ door.post('open');
68
+ await door.sync();
69
+
70
+ console.log(door.currentStateName); // 'Open'
71
+ console.log(door.ctx.openCount); // 1
72
+ ```
73
+
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`.
79
+
80
+ Define the service once on your `Protocol`. Implement it on a state class. Call it from anywhere that holds the `Hsm` handle.
81
+
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
139
+ ```
140
+
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
+ }
174
+
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
202
+ ```
203
+
204
+ See [Hierarchy & transitions](https://filasieno.github.io/ihsm/reference#_5-transitions) in the reference.
205
+
206
+ ---
207
+
208
+ ## Messaging: `post`, `sync`, and `call`
209
+
210
+ Every machine is an actor with a **single-threaded mailbox**. While a handler runs, new messages queue — no re-entrancy.
211
+
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
+
1
219
  ```ts
2
- /* (_) | |__ ___ _ __ ___ */
3
- /* | | | '_ \ / __| | '_ ` _ \ */
4
- /* | | | | | | \__ \ | | | | | | */
5
- /* |_| |_| |_| |___/ |_| |_| |_| */
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
6
224
  ```
7
225
 
8
- # Idiomatic hierarchical state machine
226
+ Inside handlers you also get `transition()`, `sleep()`, and `postNow()` for hi-priority follow-up steps within the same dispatch turn.
227
+
228
+ See [Post & sync](https://filasieno.github.io/ihsm/reference#_4-messaging-post-call-sync) in the reference.
229
+
230
+ ---
231
+
232
+ ## Async handlers
233
+
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.
236
+
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
+ ```
247
+
248
+ See [Async handlers](https://filasieno.github.io/ihsm/reference#_9-async-handlers) in the reference.
249
+
250
+ ---
251
+
252
+ ## Install
253
+
254
+ Requires [Node.js](https://nodejs.org/) **22+**.
255
+
256
+ ```bash
257
+ npm install ihsm
258
+ ```
259
+
260
+ ### Runtime support
261
+
262
+ ihsm ships modern **ES2022** ESM and CommonJS. Supported runtimes:
263
+
264
+ | Runtime | Minimum |
265
+ | ------- | ------- |
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.
317
+
318
+ ---
319
+
320
+ ## Contributing
321
+
322
+ Contributions are welcome — bug reports, docs, and code. See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for the development environment, build commands, and PR guidelines.
323
+
324
+ - Bug reports → [issue template](https://github.com/filasieno/ihsm/issues/new?template=bug_report.yml)
325
+ - Features → [issue template](https://github.com/filasieno/ihsm/issues/new?template=feature_request.yml)
326
+ - Security → [GitHub Security Advisories](https://github.com/filasieno/ihsm/security/advisories/new)
327
+
328
+ Please follow [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
329
+
330
+ ---
331
+
332
+ ## License
9
333
 
334
+ [MIT](./LICENSE) © Fabio N. Filasieno, Roberto Boati