ihsm 0.0.23 → 0.1.1
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 +105 -113
- package/lib/cjs/index.d.ts +5 -1394
- package/lib/cjs/index.js +53 -764
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/internal/runtime.d.ts +293 -0
- package/lib/cjs/internal/runtime.js +1906 -0
- package/lib/cjs/internal/runtime.js.map +1 -0
- package/lib/cjs/internal/types.d.ts +348 -0
- package/lib/cjs/internal/types.js +9 -0
- package/lib/cjs/internal/types.js.map +1 -0
- package/lib/cjs/test-only.d.ts +5 -0
- package/lib/cjs/test-only.js +21 -0
- package/lib/cjs/test-only.js.map +1 -0
- package/lib/cjs/testing.d.ts +38 -91
- package/lib/cjs/testing.js +72 -38
- package/lib/cjs/testing.js.map +1 -1
- package/lib/cjs/transition-routines.d.ts +3 -0
- package/lib/cjs/transition-routines.js +11 -0
- package/lib/cjs/transition-routines.js.map +1 -0
- package/lib/cjs/types.d.ts +5 -0
- package/lib/cjs/{internal/defs.private.js → types.js} +1 -1
- package/lib/cjs/types.js.map +1 -0
- package/lib/esm/index.d.ts +5 -1394
- package/lib/esm/index.js +3 -742
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/internal/runtime.d.ts +293 -0
- package/lib/esm/internal/runtime.js +1847 -0
- package/lib/esm/internal/runtime.js.map +1 -0
- package/lib/esm/internal/types.d.ts +348 -0
- package/lib/esm/internal/types.js +6 -0
- package/lib/esm/internal/types.js.map +1 -0
- package/lib/esm/test-only.d.ts +5 -0
- package/lib/esm/test-only.js +15 -0
- package/lib/esm/test-only.js.map +1 -0
- package/lib/esm/testing.d.ts +38 -91
- package/lib/esm/testing.js +72 -38
- package/lib/esm/testing.js.map +1 -1
- package/lib/esm/transition-routines.d.ts +3 -0
- package/lib/esm/transition-routines.js +3 -0
- package/lib/esm/transition-routines.js.map +1 -0
- package/lib/esm/types.d.ts +5 -0
- package/lib/esm/types.js +2 -0
- package/lib/esm/types.js.map +1 -0
- package/package.json +22 -2
- package/lib/cjs/internal/defs.private.d.ts +0 -41
- package/lib/cjs/internal/defs.private.js.map +0 -1
- package/lib/cjs/internal/dispatch.debug.d.ts +0 -4
- package/lib/cjs/internal/dispatch.debug.js +0 -332
- package/lib/cjs/internal/dispatch.debug.js.map +0 -1
- package/lib/cjs/internal/dispatch.production.d.ts +0 -6
- package/lib/cjs/internal/dispatch.production.js +0 -241
- package/lib/cjs/internal/dispatch.production.js.map +0 -1
- package/lib/cjs/internal/dispatch.trace.d.ts +0 -4
- package/lib/cjs/internal/dispatch.trace.js +0 -418
- package/lib/cjs/internal/dispatch.trace.js.map +0 -1
- package/lib/cjs/internal/hsm.d.ts +0 -60
- package/lib/cjs/internal/hsm.js +0 -215
- package/lib/cjs/internal/hsm.js.map +0 -1
- package/lib/cjs/internal/lookup.d.ts +0 -15
- package/lib/cjs/internal/lookup.js +0 -32
- package/lib/cjs/internal/lookup.js.map +0 -1
- package/lib/cjs/internal/utils.d.ts +0 -26
- package/lib/cjs/internal/utils.js +0 -63
- package/lib/cjs/internal/utils.js.map +0 -1
- package/lib/esm/internal/defs.private.d.ts +0 -41
- package/lib/esm/internal/defs.private.js +0 -2
- package/lib/esm/internal/defs.private.js.map +0 -1
- package/lib/esm/internal/dispatch.debug.d.ts +0 -4
- package/lib/esm/internal/dispatch.debug.js +0 -328
- package/lib/esm/internal/dispatch.debug.js.map +0 -1
- package/lib/esm/internal/dispatch.production.d.ts +0 -6
- package/lib/esm/internal/dispatch.production.js +0 -237
- package/lib/esm/internal/dispatch.production.js.map +0 -1
- package/lib/esm/internal/dispatch.trace.d.ts +0 -4
- package/lib/esm/internal/dispatch.trace.js +0 -414
- package/lib/esm/internal/dispatch.trace.js.map +0 -1
- package/lib/esm/internal/hsm.d.ts +0 -60
- package/lib/esm/internal/hsm.js +0 -211
- package/lib/esm/internal/hsm.js.map +0 -1
- package/lib/esm/internal/lookup.d.ts +0 -15
- package/lib/esm/internal/lookup.js +0 -29
- package/lib/esm/internal/lookup.js.map +0 -1
- package/lib/esm/internal/utils.d.ts +0 -26
- package/lib/esm/internal/utils.js +0 -52
- package/lib/esm/internal/utils.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
# ihsm
|
|
8
8
|
|
|
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)** —
|
|
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)** — nominal **`Config`**, generated handles, promise **services**, **zero** production dependencies, **~4.6 KB gzip** in the browser. → [Documentation](https://filasieno.github.io/ihsm/) · [Tutorial 00 — Config](examples/00-config/README.md)
|
|
10
10
|
|
|
11
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
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).
|
|
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 actor.hsm.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).
|
|
14
14
|
|
|
15
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.
|
|
16
16
|
|
|
@@ -20,11 +20,9 @@ It uses event-driven programming, class-based hierarchical statecharts, and the
|
|
|
20
20
|
|
|
21
21
|
📖 [Read the documentation](https://filasieno.github.io/ihsm/)
|
|
22
22
|
|
|
23
|
-
📑 [API reference](https://filasieno.github.io/ihsm/api)
|
|
24
|
-
|
|
25
23
|
📖 [Reference](https://filasieno.github.io/ihsm/reference)
|
|
26
24
|
|
|
27
|
-
🧪 [Deterministic Testing
|
|
25
|
+
🧪 [Deterministic Testing](https://filasieno.github.io/ihsm/testing)
|
|
28
26
|
|
|
29
27
|
💬 [Open an issue](https://github.com/filasieno/ihsm/issues)
|
|
30
28
|
|
|
@@ -53,116 +51,114 @@ npm install ihsm
|
|
|
53
51
|
```
|
|
54
52
|
|
|
55
53
|
```ts
|
|
56
|
-
import { InitialState,
|
|
54
|
+
import { InitialState, makeActor, Port, TopState } from 'ihsm';
|
|
57
55
|
|
|
58
56
|
interface DoorCtx {
|
|
59
57
|
openCount: number;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
interface DoorConfig {
|
|
61
|
+
context: DoorCtx;
|
|
62
|
+
notifications: {
|
|
63
|
+
open(): void;
|
|
64
|
+
close(): void;
|
|
65
|
+
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
|
|
69
|
+
class DoorTop extends TopState<DoorConfig> {
|
|
70
|
+
}
|
|
69
71
|
|
|
70
72
|
@InitialState
|
|
71
73
|
class Closed extends DoorTop {
|
|
72
74
|
open(): void {
|
|
73
75
|
this.ctx.openCount += 1;
|
|
74
|
-
this.transition(Open);
|
|
76
|
+
this.hsm.transition(Open);
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
class Open extends DoorTop {
|
|
79
81
|
close(): void {
|
|
80
|
-
this.transition(Closed);
|
|
82
|
+
this.hsm.transition(Closed);
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
const door =
|
|
85
|
-
await door.sync();
|
|
86
|
+
const door = makeActor(DoorTop, { openCount: 0 }, new Port());
|
|
87
|
+
await door.hsm.sync();
|
|
86
88
|
|
|
87
|
-
door.
|
|
88
|
-
await door.sync();
|
|
89
|
+
door.notify.open();
|
|
90
|
+
await door.hsm.sync();
|
|
89
91
|
|
|
90
|
-
console.log(door.currentStateName); // 'Open'
|
|
91
|
-
console.log(door.ctx.openCount);
|
|
92
|
+
console.log(door.hsm.currentStateName); // 'Open'
|
|
93
|
+
console.log(door.ctx.openCount); // 1
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
See **[examples/00-config/](examples/00-config/README.md)** for the full protocol tour.
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
---
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
## Typed services (promise-returning)
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
Services are declared on the protocol's `services` bucket. The generated client method **always** returns `Promise<Reply>` — callers must `await`, so RTC ordering is explicit.
|
|
101
103
|
|
|
102
104
|
```ts
|
|
103
|
-
import {
|
|
104
|
-
InitialState,
|
|
105
|
-
makeHsm,
|
|
106
|
-
RejectCallback,
|
|
107
|
-
ResolveCallback,
|
|
108
|
-
TopState,
|
|
109
|
-
} from 'ihsm';
|
|
105
|
+
import { InitialState, makeActor, Port, TopState } from 'ihsm';
|
|
110
106
|
|
|
111
107
|
interface WalletCtx {
|
|
112
108
|
balance: number;
|
|
113
109
|
}
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
interface WalletConfig {
|
|
112
|
+
notifications: { deposit(amount: number): void };
|
|
113
|
+
services: {
|
|
114
|
+
getBalance(): Promise<number>;
|
|
115
|
+
withdraw(amount: number): Promise<number>;
|
|
116
|
+
};
|
|
117
|
+
context: WalletCtx;
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
|
|
120
|
+
|
|
121
|
+
class WalletTop extends TopState<WalletConfig> {
|
|
122
|
+
|
|
124
123
|
deposit(amount: number): void {
|
|
125
124
|
this.ctx.balance += amount;
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
getBalance(
|
|
129
|
-
|
|
127
|
+
getBalance(): number {
|
|
128
|
+
return this.ctx.balance;
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
withdraw(
|
|
131
|
+
withdraw(amount: number): number {
|
|
133
132
|
if (amount > this.ctx.balance) {
|
|
134
|
-
|
|
135
|
-
return;
|
|
133
|
+
throw new Error('insufficient funds');
|
|
136
134
|
}
|
|
137
135
|
this.ctx.balance -= amount;
|
|
138
|
-
|
|
136
|
+
return this.ctx.balance;
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
139
|
|
|
142
140
|
@InitialState
|
|
143
141
|
class Open extends WalletTop {}
|
|
144
142
|
|
|
145
|
-
const wallet =
|
|
146
|
-
await wallet.sync();
|
|
143
|
+
const wallet = makeActor(WalletTop, { balance: 100 }, new Port());
|
|
144
|
+
await wallet.hsm.sync();
|
|
147
145
|
|
|
148
|
-
wallet.
|
|
146
|
+
wallet.notify.deposit(50);
|
|
149
147
|
|
|
150
|
-
const balance = await wallet.call(
|
|
148
|
+
const balance = await wallet.call.getBalance();
|
|
151
149
|
|
|
152
150
|
try {
|
|
153
|
-
await wallet.call(
|
|
154
|
-
} catch
|
|
155
|
-
//
|
|
151
|
+
await wallet.call.withdraw(200);
|
|
152
|
+
} catch {
|
|
153
|
+
// handler throw → rejected Promise
|
|
156
154
|
}
|
|
157
155
|
|
|
158
|
-
const left = await wallet.call(
|
|
156
|
+
const left = await wallet.call.getBalance(); // 150
|
|
159
157
|
```
|
|
160
158
|
|
|
161
|
-
**
|
|
159
|
+
**Notifications** → `wallet.notify.deposit(50)` (void). **Services** → `await wallet.call.getBalance()` (Promise). The split is **nominal** via `Config.notifications` vs `Config.services`.
|
|
162
160
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
See [Call services](https://filasieno.github.io/ihsm/reference#_4-messaging-post-call-sync) in the reference.
|
|
161
|
+
See [Tutorial 00 — Config](examples/00-config/README.md) and the [reference](https://filasieno.github.io/ihsm/reference).
|
|
166
162
|
|
|
167
163
|
---
|
|
168
164
|
|
|
@@ -174,36 +170,37 @@ Also not that all states are stateless classes.
|
|
|
174
170
|
All state is stored in the actor context available at `this.ctx`.
|
|
175
171
|
|
|
176
172
|
```ts
|
|
177
|
-
import { InitialState,
|
|
173
|
+
import { InitialState, makeActor, Port, TopState } from 'ihsm';
|
|
178
174
|
|
|
179
175
|
interface PlayerCtx {
|
|
180
176
|
track: string;
|
|
181
177
|
}
|
|
182
178
|
|
|
183
|
-
interface
|
|
184
|
-
|
|
185
|
-
pause(): void;
|
|
186
|
-
stop(): void;
|
|
179
|
+
interface PlayerConfig {
|
|
180
|
+
context: PlayerCtx;
|
|
181
|
+
notifications: { play(): void; pause(): void; stop(): void };
|
|
187
182
|
}
|
|
188
183
|
|
|
189
|
-
|
|
184
|
+
|
|
185
|
+
class PlayerTop extends TopState<PlayerConfig> {
|
|
186
|
+
}
|
|
190
187
|
|
|
191
188
|
class Active extends PlayerTop {
|
|
192
189
|
stop(): void {
|
|
193
|
-
this.transition(Stopped);
|
|
190
|
+
this.hsm.transition(Stopped);
|
|
194
191
|
}
|
|
195
192
|
}
|
|
196
193
|
|
|
197
194
|
@InitialState
|
|
198
195
|
class Playing extends Active {
|
|
199
196
|
pause(): void {
|
|
200
|
-
this.transition(Paused);
|
|
197
|
+
this.hsm.transition(Paused);
|
|
201
198
|
}
|
|
202
199
|
}
|
|
203
200
|
|
|
204
201
|
class Paused extends Active {
|
|
205
202
|
play(): void {
|
|
206
|
-
this.transition(Playing);
|
|
203
|
+
this.hsm.transition(Playing);
|
|
207
204
|
}
|
|
208
205
|
}
|
|
209
206
|
|
|
@@ -211,15 +208,15 @@ class Paused extends Active {
|
|
|
211
208
|
class Stopped extends PlayerTop {
|
|
212
209
|
play(): void {
|
|
213
210
|
this.ctx.track = 'demo.mp3';
|
|
214
|
-
this.transition(Playing);
|
|
211
|
+
this.hsm.transition(Playing);
|
|
215
212
|
}
|
|
216
213
|
}
|
|
217
214
|
|
|
218
|
-
const player =
|
|
219
|
-
await player.sync();
|
|
215
|
+
const player = makeActor(PlayerTop, { track: '' }, new Port());
|
|
216
|
+
await player.hsm.sync();
|
|
220
217
|
|
|
221
|
-
player.
|
|
222
|
-
await player.sync();
|
|
218
|
+
player.notify.play();
|
|
219
|
+
await player.hsm.sync();
|
|
223
220
|
// active leaf: Playing — inherits stop() from Active
|
|
224
221
|
```
|
|
225
222
|
|
|
@@ -227,27 +224,28 @@ See [Hierarchy & transitions](https://filasieno.github.io/ihsm/reference#_5-tran
|
|
|
227
224
|
|
|
228
225
|
---
|
|
229
226
|
|
|
230
|
-
## Messaging:
|
|
227
|
+
## Messaging: notifications, services, and sync
|
|
231
228
|
|
|
232
229
|
Every machine is an actor with **single-threaded, run-to-completion dispatch**. While a handler runs to completion, new messages queue — no re-entrancy.
|
|
233
230
|
|
|
234
231
|
| API | Role | Returns |
|
|
235
232
|
| --- | ---- | ------- |
|
|
236
|
-
| `
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
| `
|
|
233
|
+
| `actor.notify.event(…)` | Fire-and-forget notification | `void` (use `hsm.sync()` to wait) |
|
|
234
|
+
| `actor.notifyNow.event(…)` | Hi-priority notification | `void` |
|
|
235
|
+
| `await actor.call.service(…)` | Typed request/response | `Promise<T>` |
|
|
236
|
+
| `this.hsm.port.defer(ms).event(…)` | Timer then self-notification | handler-only |
|
|
237
|
+
| `await actor.hsm.sync()` | Drain queue up to marker | `Promise<void>` |
|
|
240
238
|
|
|
241
239
|
```ts
|
|
242
|
-
door.
|
|
243
|
-
await door.sync();
|
|
240
|
+
door.notify.open();
|
|
241
|
+
await door.hsm.sync();
|
|
244
242
|
|
|
245
|
-
const id = await account.call('
|
|
243
|
+
const id = await account.call.lookup('user-42');
|
|
246
244
|
```
|
|
247
245
|
|
|
248
|
-
Inside handlers
|
|
246
|
+
Inside handlers use `this.hsm.transition()`, `this.notify`, and `this.notifyNow`. For delays, `await new Promise(r => this.hsm.port.setTimeout(r, ms))`; for timer-driven self-notifications, `this.hsm.port.defer(ms).event(…)`.
|
|
249
247
|
|
|
250
|
-
See [
|
|
248
|
+
See [Messaging](https://filasieno.github.io/ihsm/reference#_4-messaging-notifications-services-sync) in the reference.
|
|
251
249
|
|
|
252
250
|
---
|
|
253
251
|
|
|
@@ -262,7 +260,7 @@ class Idle extends FileTop {
|
|
|
262
260
|
async transfer(from: string, to: string): Promise<void> {
|
|
263
261
|
const data = await readFile(from);
|
|
264
262
|
await writeFile(to, data);
|
|
265
|
-
this.transition(Done);
|
|
263
|
+
this.hsm.transition(Done);
|
|
266
264
|
}
|
|
267
265
|
}
|
|
268
266
|
```
|
|
@@ -275,11 +273,11 @@ See [Async handlers](https://filasieno.github.io/ihsm/reference#_9-async-handler
|
|
|
275
273
|
|
|
276
274
|
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
275
|
|
|
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.
|
|
276
|
+
Two rules: **never perform I/O outside a port**, and **never `sleep()` on wall-clock time in a test** — advance virtual time and `await actor.hsm.sync()` instead.
|
|
279
277
|
|
|
280
278
|
### Virtual clock — simulate days of timers in microseconds
|
|
281
279
|
|
|
282
|
-
`
|
|
280
|
+
`this.hsm.port.defer(ms).onTick()` arms timers through the port. Replace the real clock with `TestPort` and call `advance(ms)` by hand:
|
|
283
281
|
|
|
284
282
|
```ts
|
|
285
283
|
import { InitialState, TopState } from 'ihsm';
|
|
@@ -287,41 +285,36 @@ import { makeTestActor, TestPort } from 'ihsm/testing';
|
|
|
287
285
|
|
|
288
286
|
const HOUR_MS = 60 * 60 * 1000;
|
|
289
287
|
|
|
290
|
-
|
|
291
|
-
ticks
|
|
288
|
+
interface HeartbeatConfig {
|
|
289
|
+
context: { ticks: number };
|
|
290
|
+
notifications: { start(): void };
|
|
291
|
+
internalNotifications: { onTick(): void };
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
start(): void;
|
|
294
|
+
class HeartbeatTop extends TopState<HeartbeatConfig> {
|
|
296
295
|
}
|
|
297
296
|
|
|
298
|
-
interface HeartbeatInternal {
|
|
299
|
-
onTick(): void;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
class HeartbeatTop extends TopState<HeartbeatCtx, HeartbeatPublic, HeartbeatInternal> {}
|
|
303
|
-
|
|
304
297
|
@InitialState
|
|
305
298
|
class Running extends HeartbeatTop {
|
|
306
299
|
start(): void {
|
|
307
|
-
this.
|
|
300
|
+
this.hsm.port.defer(HOUR_MS).onTick();
|
|
308
301
|
}
|
|
309
302
|
onTick(): void {
|
|
310
303
|
this.ctx.ticks += 1;
|
|
311
|
-
this.
|
|
304
|
+
this.hsm.port.defer(HOUR_MS).onTick();
|
|
312
305
|
}
|
|
313
306
|
}
|
|
314
307
|
|
|
315
|
-
const clock = new TestPort<HeartbeatTop>();
|
|
308
|
+
const clock = new TestPort<typeof HeartbeatTop>();
|
|
316
309
|
const test = makeTestActor(HeartbeatTop, new HeartbeatCtx(), clock);
|
|
317
|
-
await test.sync();
|
|
310
|
+
await test.hsm.sync();
|
|
318
311
|
|
|
319
|
-
test.
|
|
320
|
-
await test.sync();
|
|
312
|
+
test.start();
|
|
313
|
+
await test.hsm.sync();
|
|
321
314
|
|
|
322
315
|
for (let hour = 0; hour < 48; hour++) {
|
|
323
316
|
clock.advance(HOUR_MS); // fire the due tick — no real waiting
|
|
324
|
-
await test.sync();
|
|
317
|
+
await test.hsm.sync();
|
|
325
318
|
}
|
|
326
319
|
|
|
327
320
|
// test.ctx.ticks === 48
|
|
@@ -337,7 +330,7 @@ Put `fetch()` behind a port. The mock records outbound calls but does **not** au
|
|
|
337
330
|
import { mock, makeTestActor, makeTestPort, TestPort } from 'ihsm/testing';
|
|
338
331
|
|
|
339
332
|
@mock
|
|
340
|
-
abstract class MockFetchPort extends TestPort<FetchTop> {
|
|
333
|
+
abstract class MockFetchPort extends TestPort<typeof FetchTop> {
|
|
341
334
|
abstract request(url: string): { value: number; subscription: { dispose(): void } };
|
|
342
335
|
}
|
|
343
336
|
|
|
@@ -348,14 +341,14 @@ port.request.default(() => ({
|
|
|
348
341
|
}));
|
|
349
342
|
|
|
350
343
|
const fetcher = makeTestActor(FetchTop, freshCtx(), port);
|
|
351
|
-
await fetcher.sync();
|
|
344
|
+
await fetcher.hsm.sync();
|
|
352
345
|
|
|
353
|
-
fetcher.
|
|
354
|
-
await fetcher.sync();
|
|
346
|
+
fetcher.fetch('https://example.com');
|
|
347
|
+
await fetcher.hsm.sync();
|
|
355
348
|
// fetcher.currentState === Fetching — in-flight, still timer-free
|
|
356
349
|
|
|
357
350
|
port.send('onResponse', 200, 'ok'); // you decide when the "network" replies
|
|
358
|
-
await fetcher.sync();
|
|
351
|
+
await fetcher.hsm.sync();
|
|
359
352
|
// fetcher.currentState === Done
|
|
360
353
|
// port.trace === ['request:https://example.com', 'onResponse:200,ok']
|
|
361
354
|
```
|
|
@@ -365,12 +358,12 @@ await fetcher.sync();
|
|
|
365
358
|
Wire `subscribe` to the port message log for a byte-identical transcript across runs:
|
|
366
359
|
|
|
367
360
|
```ts
|
|
368
|
-
const port = new TestPort<HeartbeatTop>();
|
|
361
|
+
const port = new TestPort<typeof HeartbeatTop>();
|
|
369
362
|
const test = makeTestActor(HeartbeatTop, new HeartbeatCtx(), port);
|
|
370
363
|
const sub = test.subscribe(m => port.record(m.event, ...m.payload));
|
|
371
364
|
|
|
372
|
-
test.
|
|
373
|
-
await test.sync();
|
|
365
|
+
test.start();
|
|
366
|
+
await test.hsm.sync();
|
|
374
367
|
// port.events === ['start']
|
|
375
368
|
|
|
376
369
|
sub.dispose();
|
|
@@ -395,11 +388,11 @@ version:
|
|
|
395
388
|
|
|
396
389
|
| Import | Contents | Ships in production? |
|
|
397
390
|
| ------ | -------- | -------------------- |
|
|
398
|
-
| `ihsm` | The runtime: `
|
|
391
|
+
| `ihsm` | The runtime: `makeActor` / `makeActor`, `TopState`, ports, tracing | **yes** |
|
|
399
392
|
| `ihsm/testing` or `@ihsm/core/testing` | Deterministic-testing utilities: `makeTestActor`, `@mock` / `makeTestPort`, `TestPort` (re-exports the core API too) | **no** — test-only |
|
|
400
393
|
|
|
401
394
|
```ts
|
|
402
|
-
import {
|
|
395
|
+
import { makeActor, TopState } from 'ihsm'; // production code
|
|
403
396
|
import { makeTestActor, mock, TestPort } from 'ihsm/testing'; // tests only
|
|
404
397
|
```
|
|
405
398
|
|
|
@@ -421,7 +414,7 @@ ihsm ships modern **ES2022** ESM and CommonJS. Supported runtimes:
|
|
|
421
414
|
|
|
422
415
|
### Size and dependencies
|
|
423
416
|
|
|
424
|
-
Measured with `esbuild` bundling `lib/esm/index.js` for the browser (full runtime — run-to-completion dispatch, transitions, tracing,
|
|
417
|
+
Measured with `esbuild` bundling `lib/esm/index.js` for the browser (full runtime — run-to-completion dispatch, transitions, tracing, promise services):
|
|
425
418
|
|
|
426
419
|
| | |
|
|
427
420
|
| --- | --- |
|
|
@@ -429,7 +422,7 @@ Measured with `esbuild` bundling `lib/esm/index.js` for the browser (full runtim
|
|
|
429
422
|
| **Published package** | `lib/` only (~46 KB npm tarball) |
|
|
430
423
|
| **Minified bundle** | **~22 KB** (21.7 KiB; single-file ESM/IIFE) |
|
|
431
424
|
| **Gzip** | **~4.6 KB** (typical CDN / HTTP transfer size) |
|
|
432
|
-
| **Tree-shaking** | `"sideEffects": false` — runtime is one cohesive module (~22 KB even when importing only `
|
|
425
|
+
| **Tree-shaking** | `"sideEffects": false` — runtime is one cohesive module (~22 KB even when importing only `makeActor`) |
|
|
433
426
|
|
|
434
427
|
Node loads the unminified `lib/` files directly (~18 KB entry, ~62 KB total); minify numbers apply to browser bundles.
|
|
435
428
|
|
|
@@ -461,12 +454,11 @@ Inspired by Harel statecharts and the SCXML family of notations.
|
|
|
461
454
|
| **Documentation site** | [filasieno.github.io/ihsm](https://filasieno.github.io/ihsm/) |
|
|
462
455
|
| Deterministic Simulation Testing | [/testing](https://filasieno.github.io/ihsm/testing) |
|
|
463
456
|
| Reference (concepts + interactive examples) | [/reference](https://filasieno.github.io/ihsm/reference) |
|
|
464
|
-
| API reference (TSDoc) | [/api](https://filasieno.github.io/ihsm/api) |
|
|
465
457
|
| Source: DST chapter | [reference/TESTING.md](./reference/TESTING.md) |
|
|
466
458
|
| Source: reference | [reference/REFERENCE.md](./reference/REFERENCE.md) |
|
|
467
459
|
| Source: example machines | [examples/](./examples/) |
|
|
468
460
|
|
|
469
|
-
The reference page combines the
|
|
461
|
+
The reference page combines the manual with embedded playgrounds. The testing chapter runs five DST examples first, then the full technique.
|
|
470
462
|
|
|
471
463
|
---
|
|
472
464
|
|