ihsm 0.0.21 → 0.0.23
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 +161 -8
- package/lib/cjs/index.d.ts +521 -58
- package/lib/cjs/index.js +279 -5
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/internal/defs.private.d.ts +10 -0
- package/lib/cjs/internal/dispatch.debug.js +4 -2
- package/lib/cjs/internal/dispatch.debug.js.map +1 -1
- package/lib/cjs/internal/dispatch.production.js +4 -2
- package/lib/cjs/internal/dispatch.production.js.map +1 -1
- package/lib/cjs/internal/dispatch.trace.js +10 -2
- package/lib/cjs/internal/dispatch.trace.js.map +1 -1
- package/lib/cjs/internal/hsm.d.ts +7 -1
- package/lib/cjs/internal/hsm.js +36 -3
- package/lib/cjs/internal/hsm.js.map +1 -1
- package/lib/cjs/internal/lookup.d.ts +15 -0
- package/lib/cjs/internal/lookup.js +32 -0
- package/lib/cjs/internal/lookup.js.map +1 -0
- package/lib/cjs/internal/utils.d.ts +8 -0
- package/lib/cjs/internal/utils.js +12 -0
- package/lib/cjs/internal/utils.js.map +1 -1
- package/lib/cjs/testing.d.ts +279 -0
- package/lib/cjs/testing.js +392 -0
- package/lib/cjs/testing.js.map +1 -0
- package/lib/esm/index.d.ts +521 -58
- package/lib/esm/index.js +275 -5
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/internal/defs.private.d.ts +10 -0
- package/lib/esm/internal/dispatch.debug.js +5 -3
- package/lib/esm/internal/dispatch.debug.js.map +1 -1
- package/lib/esm/internal/dispatch.production.js +5 -3
- package/lib/esm/internal/dispatch.production.js.map +1 -1
- package/lib/esm/internal/dispatch.trace.js +11 -3
- package/lib/esm/internal/dispatch.trace.js.map +1 -1
- package/lib/esm/internal/hsm.d.ts +7 -1
- package/lib/esm/internal/hsm.js +36 -3
- package/lib/esm/internal/hsm.js.map +1 -1
- package/lib/esm/internal/lookup.d.ts +15 -0
- package/lib/esm/internal/lookup.js +29 -0
- package/lib/esm/internal/lookup.js.map +1 -0
- package/lib/esm/internal/utils.d.ts +8 -0
- package/lib/esm/internal/utils.js +11 -0
- package/lib/esm/internal/utils.js.map +1 -1
- package/lib/esm/testing.d.ts +279 -0
- package/lib/esm/testing.js +370 -0
- package/lib/esm/testing.js.map +1 -0
- package/package.json +20 -4
package/README.md
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
# ihsm
|
|
8
8
|
|
|
9
|
-
**Class-based hierarchical state machines and
|
|
9
|
+
**Class-based hierarchical state machines and run-to-completion actors for TypeScript, explicitly designed for [Deterministic Simulation Testing](https://filasieno.github.io/ihsm/testing) (DST)** — typed `post`/`call`, **zero** production dependencies, **~4.6 KB gzip** in the browser. → [Documentation](https://filasieno.github.io/ihsm/)
|
|
10
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
|
|
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 serialized, run-to-completion dispatch.
|
|
12
|
+
|
|
13
|
+
> **Built for Deterministic Simulation Testing.** Determinism is not an add-on here — it is the design center. Every source of nondeterminism is pushed behind one seam: **serialized run-to-completion dispatch** (each handler runs to completion and never interleaves; `await sync()` drains to a barrier), a single **`Port`** boundary for *all* I/O (sockets, clocks, the filesystem), and a compiler-enforced **public/internal protocol split**. Swap the port for a mock, replace the clock with one you advance by hand, and the same inputs always produce the same outputs — so a failure **replays exactly**. The dedicated [`ihsm/testing`](#entry-points) entry point ships `makeTestActor`, `@mock`/`makeTestPort`, and a `TestPort` virtual clock for this, and never bloats your production bundle. See the [Deterministic Testing chapter](https://filasieno.github.io/ihsm/testing).
|
|
12
14
|
|
|
13
15
|
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
16
|
|
|
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
|
|
17
|
+
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 serialized, run-to-completion (RTC) dispatch.
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
@@ -22,14 +24,32 @@ It uses event-driven programming, class-based hierarchical statecharts, and the
|
|
|
22
24
|
|
|
23
25
|
📖 [Reference](https://filasieno.github.io/ihsm/reference)
|
|
24
26
|
|
|
27
|
+
🧪 [Deterministic Testing chapter](https://filasieno.github.io/ihsm/testing)
|
|
28
|
+
|
|
25
29
|
💬 [Open an issue](https://github.com/filasieno/ihsm/issues)
|
|
26
30
|
|
|
27
31
|
---
|
|
28
32
|
|
|
33
|
+
## Development (from source)
|
|
34
|
+
|
|
35
|
+
All build and test commands run in **`packages/ihsm/`** (this directory):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
nix develop # Node 22, Chromium, store-pinned node_modules
|
|
39
|
+
npm install # only if not using the Nix shell symlink
|
|
40
|
+
npm run build # → lib/cjs + lib/esm
|
|
41
|
+
npm run test:all
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
From the **repo root**, `nix develop` / direnv auto-`cd` here; `nix flake check` runs the same gates as CI.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
29
48
|
## Super quick start
|
|
30
49
|
|
|
31
50
|
```bash
|
|
32
51
|
npm install ihsm
|
|
52
|
+
# scoped alias (same runtime, published in lockstep): npm install @ihsm/core
|
|
33
53
|
```
|
|
34
54
|
|
|
35
55
|
```ts
|
|
@@ -100,7 +120,7 @@ interface WalletProtocol {
|
|
|
100
120
|
withdraw(resolve: ResolveCallback<number>, reject: RejectCallback, amount: number): void;
|
|
101
121
|
}
|
|
102
122
|
|
|
103
|
-
class WalletTop extends TopState<WalletCtx, WalletProtocol>
|
|
123
|
+
class WalletTop extends TopState<WalletCtx, WalletProtocol> {
|
|
104
124
|
deposit(amount: number): void {
|
|
105
125
|
this.ctx.balance += amount;
|
|
106
126
|
}
|
|
@@ -138,7 +158,9 @@ try {
|
|
|
138
158
|
const left = await wallet.call('getBalance'); // 150
|
|
139
159
|
```
|
|
140
160
|
|
|
141
|
-
**Events** (`void` handlers) → `post('deposit', 50)`. **Services** (`resolve` / `reject` handlers) → `await call('getBalance')`. Same
|
|
161
|
+
**Events** (`void` handlers) → `post('deposit', 50)`. **Services** (`resolve` / `reject` handlers) → `await call('getBalance')`. Same run-to-completion dispatch, same serialization guarantees, full TypeScript inference on names, payloads, and return types.
|
|
162
|
+
|
|
163
|
+
The split is enforced **at compile time**: the protocol is partitioned into event keys and service keys, so `post('getBalance')` (a service) and `call('deposit', 50)` (an event) are both type errors — you can only `post` events and `call` services.
|
|
142
164
|
|
|
143
165
|
See [Call services](https://filasieno.github.io/ihsm/reference#_4-messaging-post-call-sync) in the reference.
|
|
144
166
|
|
|
@@ -207,7 +229,7 @@ See [Hierarchy & transitions](https://filasieno.github.io/ihsm/reference#_5-tran
|
|
|
207
229
|
|
|
208
230
|
## Messaging: `post`, `sync`, and `call`
|
|
209
231
|
|
|
210
|
-
Every machine is an actor with
|
|
232
|
+
Every machine is an actor with **single-threaded, run-to-completion dispatch**. While a handler runs to completion, new messages queue — no re-entrancy.
|
|
211
233
|
|
|
212
234
|
| API | Role | Returns |
|
|
213
235
|
| --- | ---- | ------- |
|
|
@@ -249,6 +271,115 @@ See [Async handlers](https://filasieno.github.io/ihsm/reference#_9-async-handler
|
|
|
249
271
|
|
|
250
272
|
---
|
|
251
273
|
|
|
274
|
+
## Deterministic Simulation Testing (DST)
|
|
275
|
+
|
|
276
|
+
Production code imports `ihsm`; tests import `ihsm/testing`. Every source of nondeterminism lives behind a **`Port`** — sockets, clocks, randomness, the filesystem. Tests swap in a **`TestPort`** (virtual clock, scripted random, recorded message log) or an **`@mock`** port stub, then drive the machine with **`makeTestActor`** (merged public + internal protocol, `subscribe()` for golden traces).
|
|
277
|
+
|
|
278
|
+
Two rules: **never perform I/O outside a port**, and **never `sleep()` on wall-clock time in a test** — advance virtual time and `await sync()` instead.
|
|
279
|
+
|
|
280
|
+
### Virtual clock — simulate days of timers in microseconds
|
|
281
|
+
|
|
282
|
+
`deferredPost` arms timers through the port. Replace the real clock with `TestPort` and call `advance(ms)` by hand:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { InitialState, TopState } from 'ihsm';
|
|
286
|
+
import { makeTestActor, TestPort } from 'ihsm/testing';
|
|
287
|
+
|
|
288
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
289
|
+
|
|
290
|
+
class HeartbeatCtx {
|
|
291
|
+
ticks = 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface HeartbeatPublic {
|
|
295
|
+
start(): void;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface HeartbeatInternal {
|
|
299
|
+
onTick(): void;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
class HeartbeatTop extends TopState<HeartbeatCtx, HeartbeatPublic, HeartbeatInternal> {}
|
|
303
|
+
|
|
304
|
+
@InitialState
|
|
305
|
+
class Running extends HeartbeatTop {
|
|
306
|
+
start(): void {
|
|
307
|
+
this.deferredPost(HOUR_MS, 'onTick');
|
|
308
|
+
}
|
|
309
|
+
onTick(): void {
|
|
310
|
+
this.ctx.ticks += 1;
|
|
311
|
+
this.deferredPost(HOUR_MS, 'onTick');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const clock = new TestPort<HeartbeatTop>();
|
|
316
|
+
const test = makeTestActor(HeartbeatTop, new HeartbeatCtx(), clock);
|
|
317
|
+
await test.sync();
|
|
318
|
+
|
|
319
|
+
test.post('start');
|
|
320
|
+
await test.sync();
|
|
321
|
+
|
|
322
|
+
for (let hour = 0; hour < 48; hour++) {
|
|
323
|
+
clock.advance(HOUR_MS); // fire the due tick — no real waiting
|
|
324
|
+
await test.sync();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// test.ctx.ticks === 48
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Or post the internal `onTick` directly — `makeTestActor` exposes the merged protocol, so no timer is required when you only care about handler logic.
|
|
331
|
+
|
|
332
|
+
### Mock port — control *what* the network returns and *when*
|
|
333
|
+
|
|
334
|
+
Put `fetch()` behind a port. The mock records outbound calls but does **not** auto-deliver responses; the test settles them with `port.send(...)` when ready:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { mock, makeTestActor, makeTestPort, TestPort } from 'ihsm/testing';
|
|
338
|
+
|
|
339
|
+
@mock
|
|
340
|
+
abstract class MockFetchPort extends TestPort<FetchTop> {
|
|
341
|
+
abstract request(url: string): { value: number; subscription: { dispose(): void } };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const port = makeTestPort(MockFetchPort);
|
|
345
|
+
port.request.default(() => ({
|
|
346
|
+
value: 1,
|
|
347
|
+
subscription: { dispose: () => port.record('abort', 1) },
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
const fetcher = makeTestActor(FetchTop, freshCtx(), port);
|
|
351
|
+
await fetcher.sync();
|
|
352
|
+
|
|
353
|
+
fetcher.post('fetch', 'https://example.com');
|
|
354
|
+
await fetcher.sync();
|
|
355
|
+
// fetcher.currentState === Fetching — in-flight, still timer-free
|
|
356
|
+
|
|
357
|
+
port.send('onResponse', 200, 'ok'); // you decide when the "network" replies
|
|
358
|
+
await fetcher.sync();
|
|
359
|
+
// fetcher.currentState === Done
|
|
360
|
+
// port.trace === ['request:https://example.com', 'onResponse:200,ok']
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Golden trace — record every posted event
|
|
364
|
+
|
|
365
|
+
Wire `subscribe` to the port message log for a byte-identical transcript across runs:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
const port = new TestPort<HeartbeatTop>();
|
|
369
|
+
const test = makeTestActor(HeartbeatTop, new HeartbeatCtx(), port);
|
|
370
|
+
const sub = test.subscribe(m => port.record(m.event, ...m.payload));
|
|
371
|
+
|
|
372
|
+
test.post('start');
|
|
373
|
+
await test.sync();
|
|
374
|
+
// port.events === ['start']
|
|
375
|
+
|
|
376
|
+
sub.dispose();
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Runnable walkthroughs (timers, fetch, streaming, fault injection, disposables) live under [`examples/testing-*`](./examples/) and on the [Deterministic Testing chapter](https://filasieno.github.io/ihsm/testing). Headless: `npm run test:examples -- --grep 'Testing 0'`.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
252
383
|
## Install
|
|
253
384
|
|
|
254
385
|
Requires [Node.js](https://nodejs.org/) **22+**.
|
|
@@ -257,6 +388,26 @@ Requires [Node.js](https://nodejs.org/) **22+**.
|
|
|
257
388
|
npm install ihsm
|
|
258
389
|
```
|
|
259
390
|
|
|
391
|
+
### Entry points
|
|
392
|
+
|
|
393
|
+
ihsm is a **single package** with two entry points, so there is no second dependency to install or
|
|
394
|
+
version:
|
|
395
|
+
|
|
396
|
+
| Import | Contents | Ships in production? |
|
|
397
|
+
| ------ | -------- | -------------------- |
|
|
398
|
+
| `ihsm` | The runtime: `makeHsm` / `makeActor`, `TopState`, ports, tracing | **yes** |
|
|
399
|
+
| `ihsm/testing` or `@ihsm/core/testing` | Deterministic-testing utilities: `makeTestActor`, `@mock` / `makeTestPort`, `TestPort` (re-exports the core API too) | **no** — test-only |
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import { makeHsm, TopState } from 'ihsm'; // production code
|
|
403
|
+
import { makeTestActor, mock, TestPort } from 'ihsm/testing'; // tests only
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Keeping the test machinery on a separate subpath (with `"sideEffects": false`) means a production
|
|
407
|
+
bundle that only imports `ihsm` never pulls in the mock/clock code. This mirrors how libraries such
|
|
408
|
+
as `rxjs/testing` (its `TestScheduler` virtual clock) and `@apollo/client/testing` ship test helpers
|
|
409
|
+
as a subpath rather than a second package — one install, one version, no dual-package hazard.
|
|
410
|
+
|
|
260
411
|
### Runtime support
|
|
261
412
|
|
|
262
413
|
ihsm ships modern **ES2022** ESM and CommonJS. Supported runtimes:
|
|
@@ -270,7 +421,7 @@ ihsm ships modern **ES2022** ESM and CommonJS. Supported runtimes:
|
|
|
270
421
|
|
|
271
422
|
### Size and dependencies
|
|
272
423
|
|
|
273
|
-
Measured with `esbuild` bundling `lib/esm/index.js` for the browser (full runtime —
|
|
424
|
+
Measured with `esbuild` bundling `lib/esm/index.js` for the browser (full runtime — run-to-completion dispatch, transitions, tracing, typed `call`):
|
|
274
425
|
|
|
275
426
|
| | |
|
|
276
427
|
| --- | --- |
|
|
@@ -288,7 +439,7 @@ No React, no RxJS, no interpreter plugins — just the runtime you import.
|
|
|
288
439
|
|
|
289
440
|
## Why?
|
|
290
441
|
|
|
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
|
|
442
|
+
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 run-to-completion actors — with compile-time safety from a single `Protocol` interface.
|
|
292
443
|
|
|
293
444
|
Good fit when you want:
|
|
294
445
|
|
|
@@ -308,8 +459,10 @@ Inspired by Harel statecharts and the SCXML family of notations.
|
|
|
308
459
|
| Resource | Link |
|
|
309
460
|
| -------- | ---- |
|
|
310
461
|
| **Documentation site** | [filasieno.github.io/ihsm](https://filasieno.github.io/ihsm/) |
|
|
462
|
+
| Deterministic Simulation Testing | [/testing](https://filasieno.github.io/ihsm/testing) |
|
|
311
463
|
| Reference (concepts + interactive examples) | [/reference](https://filasieno.github.io/ihsm/reference) |
|
|
312
464
|
| API reference (TSDoc) | [/api](https://filasieno.github.io/ihsm/api) |
|
|
465
|
+
| Source: DST chapter | [reference/TESTING.md](./reference/TESTING.md) |
|
|
313
466
|
| Source: reference | [reference/REFERENCE.md](./reference/REFERENCE.md) |
|
|
314
467
|
| Source: example machines | [examples/](./examples/) |
|
|
315
468
|
|