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.
Files changed (46) hide show
  1. package/README.md +161 -8
  2. package/lib/cjs/index.d.ts +521 -58
  3. package/lib/cjs/index.js +279 -5
  4. package/lib/cjs/index.js.map +1 -1
  5. package/lib/cjs/internal/defs.private.d.ts +10 -0
  6. package/lib/cjs/internal/dispatch.debug.js +4 -2
  7. package/lib/cjs/internal/dispatch.debug.js.map +1 -1
  8. package/lib/cjs/internal/dispatch.production.js +4 -2
  9. package/lib/cjs/internal/dispatch.production.js.map +1 -1
  10. package/lib/cjs/internal/dispatch.trace.js +10 -2
  11. package/lib/cjs/internal/dispatch.trace.js.map +1 -1
  12. package/lib/cjs/internal/hsm.d.ts +7 -1
  13. package/lib/cjs/internal/hsm.js +36 -3
  14. package/lib/cjs/internal/hsm.js.map +1 -1
  15. package/lib/cjs/internal/lookup.d.ts +15 -0
  16. package/lib/cjs/internal/lookup.js +32 -0
  17. package/lib/cjs/internal/lookup.js.map +1 -0
  18. package/lib/cjs/internal/utils.d.ts +8 -0
  19. package/lib/cjs/internal/utils.js +12 -0
  20. package/lib/cjs/internal/utils.js.map +1 -1
  21. package/lib/cjs/testing.d.ts +279 -0
  22. package/lib/cjs/testing.js +392 -0
  23. package/lib/cjs/testing.js.map +1 -0
  24. package/lib/esm/index.d.ts +521 -58
  25. package/lib/esm/index.js +275 -5
  26. package/lib/esm/index.js.map +1 -1
  27. package/lib/esm/internal/defs.private.d.ts +10 -0
  28. package/lib/esm/internal/dispatch.debug.js +5 -3
  29. package/lib/esm/internal/dispatch.debug.js.map +1 -1
  30. package/lib/esm/internal/dispatch.production.js +5 -3
  31. package/lib/esm/internal/dispatch.production.js.map +1 -1
  32. package/lib/esm/internal/dispatch.trace.js +11 -3
  33. package/lib/esm/internal/dispatch.trace.js.map +1 -1
  34. package/lib/esm/internal/hsm.d.ts +7 -1
  35. package/lib/esm/internal/hsm.js +36 -3
  36. package/lib/esm/internal/hsm.js.map +1 -1
  37. package/lib/esm/internal/lookup.d.ts +15 -0
  38. package/lib/esm/internal/lookup.js +29 -0
  39. package/lib/esm/internal/lookup.js.map +1 -0
  40. package/lib/esm/internal/utils.d.ts +8 -0
  41. package/lib/esm/internal/utils.js +11 -0
  42. package/lib/esm/internal/utils.js.map +1 -1
  43. package/lib/esm/testing.d.ts +279 -0
  44. package/lib/esm/testing.js +370 -0
  45. package/lib/esm/testing.js.map +1 -0
  46. 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 actor mailboxes for TypeScript** — typed `post`/`call`, **zero** production dependencies, **~4.6 KB gzip** in the browser. → [Documentation](https://filasieno.github.io/ihsm/)
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 a serialized mailbox and run-to-completion dispatch.
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 a serialized mailbox with RTC guarantees.
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> implements 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 mailbox, same serialization guarantees, full TypeScript inference on names, payloads, and return types.
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 a **single-threaded mailbox**. While a handler runs, new messages queue — no re-entrancy.
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 — mailbox, transitions, tracing, typed `call`):
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 actor mailboxes — with compile-time safety from a single `Protocol` interface.
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