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.
- package/README.md +330 -5
- package/lib/cjs/index.d.ts +934 -0
- package/lib/cjs/index.js +493 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/internal/defs.private.d.ts +31 -0
- package/lib/cjs/internal/defs.private.js +3 -0
- package/lib/cjs/internal/defs.private.js.map +1 -0
- package/lib/cjs/internal/dispatch.debug.d.ts +4 -0
- package/lib/cjs/internal/dispatch.debug.js +330 -0
- package/lib/cjs/internal/dispatch.debug.js.map +1 -0
- package/lib/cjs/internal/dispatch.production.d.ts +6 -0
- package/lib/cjs/internal/dispatch.production.js +239 -0
- package/lib/cjs/internal/dispatch.production.js.map +1 -0
- package/lib/cjs/internal/dispatch.trace.d.ts +4 -0
- package/lib/cjs/internal/dispatch.trace.js +410 -0
- package/lib/cjs/internal/dispatch.trace.js.map +1 -0
- package/lib/cjs/internal/hsm.d.ts +54 -0
- package/lib/cjs/internal/hsm.js +182 -0
- package/lib/cjs/internal/hsm.js.map +1 -0
- package/lib/cjs/internal/utils.d.ts +18 -0
- package/lib/cjs/internal/utils.js +51 -0
- package/lib/cjs/internal/utils.js.map +1 -0
- package/lib/cjs/package.json +3 -0
- package/lib/esm/index.d.ts +934 -0
- package/lib/esm/index.js +476 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/internal/defs.private.d.ts +31 -0
- package/lib/esm/internal/defs.private.js +2 -0
- package/lib/esm/internal/defs.private.js.map +1 -0
- package/lib/esm/internal/dispatch.debug.d.ts +4 -0
- package/lib/esm/internal/dispatch.debug.js +326 -0
- package/lib/esm/internal/dispatch.debug.js.map +1 -0
- package/lib/esm/internal/dispatch.production.d.ts +6 -0
- package/lib/esm/internal/dispatch.production.js +235 -0
- package/lib/esm/internal/dispatch.production.js.map +1 -0
- package/lib/esm/internal/dispatch.trace.d.ts +4 -0
- package/lib/esm/internal/dispatch.trace.js +406 -0
- package/lib/esm/internal/dispatch.trace.js.map +1 -0
- package/lib/esm/internal/hsm.d.ts +54 -0
- package/lib/esm/internal/hsm.js +178 -0
- package/lib/esm/internal/hsm.js.map +1 -0
- package/lib/esm/internal/utils.d.ts +18 -0
- package/lib/esm/internal/utils.js +41 -0
- package/lib/esm/internal/utils.js.map +1 -0
- package/lib/esm/package.json +3 -0
- package/package.json +116 -66
- package/lib/index.browser.js +0 -523
- package/lib/index.d.ts +0 -104
- package/lib/index.js +0 -374
- package/tsconfig.json +0 -72
package/README.md
CHANGED
|
@@ -1,9 +1,334 @@
|
|
|
1
|
+
[](https://github.com/filasieno/ihsm/actions/workflows/ci.yml)
|
|
2
|
+
[](https://github.com/filasieno/ihsm/actions/workflows/docs.yml)
|
|
3
|
+
[](https://github.com/filasieno/ihsm/blob/HEAD/LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/ihsm)
|
|
5
|
+
[](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
|
-
|
|
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
|