effect-machine 0.11.0 → 0.13.0

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 (68) hide show
  1. package/README.md +128 -324
  2. package/dist/actor.d.ts +52 -31
  3. package/dist/actor.js +218 -283
  4. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  5. package/dist/cluster/adapters/in-memory.js +79 -0
  6. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  7. package/dist/cluster/entity-actor-ref.js +33 -0
  8. package/dist/cluster/entity-machine.d.ts +31 -49
  9. package/dist/cluster/entity-machine.js +178 -52
  10. package/dist/cluster/index.d.ts +5 -2
  11. package/dist/cluster/index.js +4 -1
  12. package/dist/cluster/persistence.d.ts +49 -0
  13. package/dist/cluster/persistence.js +18 -0
  14. package/dist/cluster/to-entity.d.ts +9 -3
  15. package/dist/cluster/to-entity.js +16 -4
  16. package/dist/errors.d.ts +25 -17
  17. package/dist/errors.js +10 -5
  18. package/dist/index.d.ts +6 -4
  19. package/dist/index.js +4 -3
  20. package/dist/internal/brands.d.ts +14 -1
  21. package/dist/internal/runtime.d.ts +142 -0
  22. package/dist/internal/runtime.js +357 -0
  23. package/dist/internal/transition.d.ts +10 -4
  24. package/dist/internal/transition.js +24 -12
  25. package/dist/internal/utils.d.ts +42 -6
  26. package/dist/internal/utils.js +27 -1
  27. package/dist/machine.d.ts +89 -55
  28. package/dist/machine.js +80 -68
  29. package/dist/schema.d.ts +35 -34
  30. package/dist/schema.js +33 -4
  31. package/dist/supervision.d.ts +97 -0
  32. package/dist/supervision.js +42 -0
  33. package/dist/testing.d.ts +17 -8
  34. package/dist/testing.js +22 -23
  35. package/package.json +7 -7
  36. package/v3/dist/actor.d.ts +54 -37
  37. package/v3/dist/actor.js +209 -277
  38. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  39. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  40. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  41. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  42. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  43. package/v3/dist/cluster/entity-machine.js +134 -50
  44. package/v3/dist/cluster/index.d.ts +5 -2
  45. package/v3/dist/cluster/index.js +4 -1
  46. package/v3/dist/cluster/persistence.d.ts +48 -0
  47. package/v3/dist/cluster/persistence.js +14 -0
  48. package/v3/dist/cluster/to-entity.d.ts +5 -2
  49. package/v3/dist/cluster/to-entity.js +12 -4
  50. package/v3/dist/errors.d.ts +18 -8
  51. package/v3/dist/errors.js +9 -4
  52. package/v3/dist/index.d.ts +6 -4
  53. package/v3/dist/index.js +3 -2
  54. package/v3/dist/internal/brands.d.ts +15 -1
  55. package/v3/dist/internal/runtime.d.ts +142 -0
  56. package/v3/dist/internal/runtime.js +335 -0
  57. package/v3/dist/internal/transition.d.ts +10 -4
  58. package/v3/dist/internal/transition.js +23 -11
  59. package/v3/dist/internal/utils.d.ts +42 -6
  60. package/v3/dist/internal/utils.js +27 -1
  61. package/v3/dist/machine.d.ts +35 -47
  62. package/v3/dist/machine.js +62 -64
  63. package/v3/dist/schema.d.ts +35 -34
  64. package/v3/dist/schema.js +29 -3
  65. package/v3/dist/supervision.d.ts +97 -0
  66. package/v3/dist/supervision.js +42 -0
  67. package/v3/dist/testing.d.ts +18 -9
  68. package/v3/dist/testing.js +21 -22
package/README.md CHANGED
@@ -1,401 +1,205 @@
1
1
  # effect-machine
2
2
 
3
- Type-safe state machines for Effect.
3
+ Type-safe state machines for [Effect](https://effect.website).
4
4
 
5
- ## Why State Machines?
5
+ Complex workflows usually fail the same way: one `status` field, a few side booleans, and effects scattered across callbacks. `effect-machine` gives you one typed model for state, events, and transitions, then runs it as a real actor.
6
6
 
7
- State machines eliminate entire categories of bugs:
7
+ Use it when a feature has:
8
8
 
9
- - **No invalid states** - Compile-time enforcement of valid transitions
10
- - **Explicit side effects** - Effects scoped to states, auto-cancelled on exit
11
- - **Testable** - Simulate transitions without actors, assert paths deterministically
12
- - **Serializable** - Schemas power persistence and cluster distribution
9
+ - multiple valid and invalid states
10
+ - async work tied to state entry
11
+ - retries, timeouts, cancellation, or backpressure
12
+ - logic you want to reuse in-process, in tests, and in distributed systems
13
13
 
14
14
  ## Install
15
15
 
16
16
  ```bash
17
17
  bun add effect-machine effect
18
- # or
19
- pnpm add effect-machine effect
20
- # or
21
- npm install effect-machine effect
22
18
  ```
23
19
 
24
- ## Quick Example
20
+ ## Core Pattern
21
+
22
+ States and events are schemas. Types, validation, and serialization from one place.
25
23
 
26
24
  ```ts
27
- import { Effect, Schema } from "effect";
28
- import { Machine, State, Event, Slot, type BuiltMachine } from "effect-machine";
29
-
30
- // Define state schema - states ARE schemas
31
- const OrderState = State({
32
- Pending: { orderId: Schema.String },
33
- Processing: { orderId: Schema.String },
34
- Shipped: { orderId: Schema.String, trackingId: Schema.String },
35
- Cancelled: {},
25
+ import { Schema } from "effect";
26
+ import { Event, Machine, Slot, State } from "effect-machine";
27
+
28
+ const CheckoutState = State({
29
+ ReviewingCart: { cartId: Schema.String, totalCents: Schema.Number },
30
+ ChargingCard: { cartId: Schema.String, totalCents: Schema.Number },
31
+ Confirmed: { cartId: Schema.String, receiptId: Schema.String },
32
+ Failed: { cartId: Schema.String, reason: Schema.String },
36
33
  });
37
34
 
38
- // Define event schema
39
- const OrderEvent = Event({
40
- Process: {},
41
- Ship: { trackingId: Schema.String },
35
+ const CheckoutEvent = Event({
36
+ Submit: {},
37
+ Charged: { receiptId: Schema.String },
38
+ Declined: { reason: Schema.String },
42
39
  Cancel: {},
43
40
  });
44
41
 
45
- // Define effects (side effects scoped to states)
46
- const OrderEffects = Slot.Effects({
47
- notifyWarehouse: { orderId: Schema.String },
42
+ const CheckoutEffects = Slot.Effects({
43
+ chargeCard: { cartId: Schema.String, totalCents: Schema.Number },
48
44
  });
49
45
 
50
- // Build machine with fluent API
51
- const orderMachine = Machine.make({
52
- state: OrderState,
53
- event: OrderEvent,
54
- effects: OrderEffects,
55
- initial: OrderState.Pending({ orderId: "order-1" }),
46
+ const checkoutMachine = Machine.make({
47
+ state: CheckoutState,
48
+ event: CheckoutEvent,
49
+ effects: CheckoutEffects,
50
+ initial: CheckoutState.ReviewingCart({ cartId: "cart_123", totalCents: 4200 }),
56
51
  })
57
- .on(OrderState.Pending, OrderEvent.Process, ({ state }) => OrderState.Processing.derive(state))
58
- .on(OrderState.Processing, OrderEvent.Ship, ({ state, event }) =>
59
- OrderState.Shipped.derive(state, { trackingId: event.trackingId }),
52
+ .on(CheckoutState.ReviewingCart, CheckoutEvent.Submit, ({ state }) =>
53
+ CheckoutState.ChargingCard.derive(state),
60
54
  )
61
- // Cancel from any state
62
- .onAny(OrderEvent.Cancel, () => OrderState.Cancelled)
63
- // Effect runs when entering Processing, cancelled on exit
64
- .spawn(OrderState.Processing, ({ effects, state }) =>
65
- effects.notifyWarehouse({ orderId: state.orderId }),
55
+ .on(CheckoutState.ChargingCard, CheckoutEvent.Charged, ({ state, event }) =>
56
+ CheckoutState.Confirmed.derive(state, { receiptId: event.receiptId }),
66
57
  )
67
- .final(OrderState.Shipped)
68
- .final(OrderState.Cancelled)
69
- .build({
70
- notifyWarehouse: ({ orderId }) => Effect.log(`Warehouse notified: ${orderId}`),
71
- });
72
-
73
- // Run as actor (simple no scope required)
74
- const program = Effect.gen(function* () {
75
- const actor = yield* Machine.spawn(orderMachine);
76
-
77
- // fire-and-forget
78
- yield* actor.send(OrderEvent.Process);
79
-
80
- // request-reply — get ProcessEventResult back
81
- const result = yield* actor.call(OrderEvent.Ship({ trackingId: "TRACK-123" }));
82
- console.log(result.transitioned); // true
83
-
84
- const state = yield* actor.waitFor(OrderState.Shipped);
85
- console.log(state); // Shipped { orderId: "order-1", trackingId: "TRACK-123" }
86
-
87
- yield* actor.stop;
88
- });
89
-
90
- Effect.runPromise(program);
58
+ .on(CheckoutState.ChargingCard, CheckoutEvent.Declined, ({ state, event }) =>
59
+ CheckoutState.Failed.derive(state, { reason: event.reason }),
60
+ )
61
+ .onAny(CheckoutEvent.Cancel, ({ state }) =>
62
+ CheckoutState.Failed.derive(state, { reason: "cancelled" }),
63
+ )
64
+ .spawn(CheckoutState.ChargingCard, ({ effects, state }) =>
65
+ effects.chargeCard({ cartId: state.cartId, totalCents: state.totalCents }),
66
+ )
67
+ .final(CheckoutState.Confirmed)
68
+ .final(CheckoutState.Failed);
91
69
  ```
92
70
 
93
- ## Core Concepts
71
+ A few things to notice:
94
72
 
95
- ### Schema-First
73
+ - Empty variants are values: `State.Idle`. Non-empty are constructors: `State.Loading({ url })`.
74
+ - `State.derive(source, overrides)` carries overlapping fields forward without manual copying.
75
+ - `.onAny(...)` is a fallback; a specific `.on(...)` wins.
76
+ - `.spawn(...)` runs work on state entry and cancels it on state exit.
96
77
 
97
- States and events ARE schemas. Single source of truth for types and serialization:
78
+ The builder also supports `.timeout(state, { duration, event })`, `.postpone(state, event)` for buffering, and `.reenter(...)` for re-running lifecycle on same-state transitions.
98
79
 
99
- ```ts
100
- const MyState = State({
101
- Idle: {}, // Empty = plain value
102
- Loading: { url: Schema.String }, // Non-empty = constructor
103
- });
104
-
105
- MyState.Idle; // Value (no parens)
106
- MyState.Loading({ url: "/api" }); // Constructor
107
- ```
80
+ ## Slots
108
81
 
109
- ### State.derive()
110
-
111
- Construct new states from existing ones — picks overlapping fields, applies overrides:
82
+ Slots separate what a machine needs from how the app provides it. Declare them on the machine, provide implementations where you run it.
112
83
 
113
84
  ```ts
114
- // Same-state: preserve fields, override specific ones
115
- .on(State.Active, Event.Update, ({ state, event }) =>
116
- State.Active.derive(state, { count: event.count })
117
- )
118
-
119
- // Cross-state: picks only target fields from source
120
- .on(State.Processing, Event.Ship, ({ state, event }) =>
121
- State.Shipped.derive(state, { trackingId: event.trackingId })
122
- )
85
+ const actor =
86
+ yield *
87
+ Machine.spawn(checkoutMachine, {
88
+ slots: {
89
+ chargeCard: ({ cartId, totalCents }, { self }) =>
90
+ Effect.gen(function* () {
91
+ const result = yield* PaymentService.charge(cartId, totalCents);
92
+ yield* self.send(
93
+ result.ok
94
+ ? CheckoutEvent.Charged({ receiptId: result.receiptId })
95
+ : CheckoutEvent.Declined({ reason: result.error }),
96
+ );
97
+ }),
98
+ },
99
+ });
123
100
  ```
124
101
 
125
- ### Multi-State Transitions
126
-
127
- Handle the same event from multiple states:
128
-
129
- ```ts
130
- // Array of states — handler receives union type
131
- .on([State.Draft, State.Review], Event.Cancel, () => State.Cancelled)
102
+ The same machine can run with different slot implementations in tests, local apps, or production. Slots are accepted everywhere the machine runs:
132
103
 
133
- // Wildcard — fires from any state (specific .on() takes priority)
134
- .onAny(Event.Cancel, () => State.Cancelled)
135
- ```
104
+ - `Machine.spawn(machine, { slots })`
105
+ - `Machine.replay(machine, events, { slots })`
106
+ - `simulate(machine, events, { slots })`
107
+ - `createTestHarness(machine, { slots })`
136
108
 
137
- ### Guards and Effects as Slots
109
+ ## Running Actors
138
110
 
139
- Define parameterized guards and effects, provide implementations:
111
+ `Machine.spawn` gives you a live actor with a queue and lifecycle management.
140
112
 
141
113
  ```ts
142
- const MyGuards = Slot.Guards({
143
- canRetry: { max: Schema.Number },
144
- });
114
+ const program = Effect.gen(function* () {
115
+ const actor = yield* Machine.spawn(checkoutMachine, {
116
+ slots: {
117
+ chargeCard: ({ cartId }, { self }) =>
118
+ self.send(CheckoutEvent.Charged({ receiptId: `rcpt_${cartId}` })),
119
+ },
120
+ });
145
121
 
146
- const MyEffects = Slot.Effects({
147
- fetchData: { url: Schema.String },
122
+ yield* actor.send(CheckoutEvent.Submit);
123
+ const finalState = yield* actor.awaitFinal;
148
124
  });
149
125
 
150
- machine
151
- .on(MyState.Error, MyEvent.Retry, ({ state, guards }) =>
152
- Effect.gen(function* () {
153
- if (yield* guards.canRetry({ max: 3 })) {
154
- return MyState.Loading({ url: state.url }); // Transition first
155
- }
156
- return MyState.Failed;
157
- }),
158
- )
159
- // Fetch runs when entering Loading, auto-cancelled if state changes
160
- .spawn(MyState.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
161
- .build({
162
- canRetry: ({ max }, { state }) => state.attempts < max,
163
- fetchData: ({ url }, { self }) =>
164
- Effect.gen(function* () {
165
- const data = yield* Http.get(url);
166
- yield* self.send(MyEvent.Resolve({ data }));
167
- }),
168
- });
126
+ Effect.runPromise(Effect.scoped(program));
169
127
  ```
170
128
 
171
- ### State-Scoped Effects
172
-
173
- `.spawn()` runs effects when entering a state, auto-cancelled on exit:
129
+ Key actor operations:
174
130
 
175
- ```ts
176
- machine
177
- .spawn(MyState.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
178
- .spawn(MyState.Polling, ({ effects }) => effects.poll({ interval: "5 seconds" }));
179
- ```
131
+ - `send(event)` queues and returns immediately
132
+ - `call(event)` returns full transition info
133
+ - `ask(event)` returns a typed domain reply (requires `Event.reply(...)`)
134
+ - `waitFor(...)` / `awaitFinal` for coordination
135
+ - `stop` interrupts now; `drain` processes the remaining queue first
136
+ - `watch(other)` completes when another actor stops
180
137
 
181
- `.task()` runs on entry and sends success/failure events:
138
+ For named actors or shared lookup, use an actor system:
182
139
 
183
140
  ```ts
184
- machine.task(State.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }), {
185
- onSuccess: (data) => MyEvent.Resolve({ data }),
186
- onFailure: () => MyEvent.Reject,
187
- });
188
- ```
141
+ import { ActorSystemDefault, ActorSystemService } from "effect-machine";
189
142
 
190
- ### State Timeouts
191
-
192
- `.timeout()` gen_statem-style state timeouts. Timer starts on state entry, cancels on exit:
193
-
194
- ```ts
195
- machine
196
- .timeout(State.Loading, {
197
- duration: Duration.seconds(30),
198
- event: Event.Timeout,
199
- })
200
- // Dynamic duration from state
201
- .timeout(State.Retrying, {
202
- duration: (state) => Duration.seconds(state.backoff),
203
- event: Event.GiveUp,
204
- });
143
+ const program = Effect.gen(function* () {
144
+ const system = yield* ActorSystemService;
145
+ const actor = yield* system.spawn("checkout-123", checkoutMachine);
146
+ yield* actor.send(CheckoutEvent.Submit);
147
+ }).pipe(Effect.provide(ActorSystemDefault));
205
148
  ```
206
149
 
207
- `.reenter()` restarts the timer with fresh state values.
208
-
209
- ### Event Postpone
150
+ ### Typed Replies
210
151
 
211
- `.postpone()` gen_statem-style event postpone. When a matching event arrives in the given state, it is buffered. After the next state transition (tag change), buffered events drain in FIFO order, looping until stable:
152
+ Events can declare typed reply schemas:
212
153
 
213
154
  ```ts
214
- machine
215
- .postpone(State.Connecting, Event.Data) // single event
216
- .postpone(State.Connecting, [Event.Data, Event.Cmd]); // multiple events
217
- ```
218
-
219
- Reply-bearing events (`call`/`ask`) in the postpone buffer are settled with `ActorStoppedError` on stop/interrupt/final-state.
220
-
221
- ### ask / reply
222
-
223
- Handlers can return a domain reply via `{ state, reply }`:
155
+ const CartEvent = Event({
156
+ GetTotal: Event.reply({}, Schema.Number),
157
+ });
224
158
 
225
- ```ts
226
- .on(State.Active, Event.GetCount, ({ state }) => ({
227
- state, // stay in same state
228
- reply: state.count, // domain value returned to caller
229
- }))
159
+ machine.on(State.Active, CartEvent.GetTotal, ({ state }) => Machine.reply(state, state.totalCents));
230
160
 
231
- // Caller side:
232
- const count = yield* actor.ask<number>(Event.GetCount);
161
+ const total = yield * actor.ask(CartEvent.GetTotal); // number
233
162
  ```
234
163
 
235
- `ask` fails with `NoReplyError` if the handler doesn't provide a reply, and `ActorStoppedError` if the actor stops while the request is pending.
164
+ ## Testing
236
165
 
237
- ### Child Actors
238
-
239
- Spawn children from `.spawn()` handlers with `self.spawn`. Children are state-scoped — auto-stopped on state exit:
166
+ Test transitions without spawning actors:
240
167
 
241
168
  ```ts
242
- machine
243
- .spawn(State.Active, ({ self }) =>
244
- Effect.gen(function* () {
245
- const child = yield* self.spawn("worker-1", workerMachine).pipe(Effect.orDie);
246
- yield* child.send(WorkerEvent.Start);
247
- // child auto-stopped when parent exits Active state
248
- }),
249
- )
250
- .build();
169
+ import { simulate } from "effect-machine";
170
+
171
+ const result =
172
+ yield *
173
+ simulate(
174
+ checkoutMachine,
175
+ [CheckoutEvent.Submit, CheckoutEvent.Charged({ receiptId: "rcpt_123" })],
176
+ { slots: { chargeCard: () => Effect.void } },
177
+ );
251
178
 
252
- // Access children externally via actor.system
253
- const parent = yield * Machine.spawn(parentMachine);
254
- yield * parent.send(Event.Activate);
255
- const child = yield * parent.system.get("worker-1"); // Option<ActorRef>
179
+ expect(result.states.map((s) => s._tag)).toEqual(["ReviewingCart", "ChargingCard", "Confirmed"]);
256
180
  ```
257
181
 
258
- Every actor always has a system — `Machine.spawn` creates an implicit one if no `ActorSystem` is in context.
182
+ `simulate` and `createTestHarness` test transition logic. They do not run `.spawn()` or `.background()` effects.
259
183
 
260
- ### Persistence
184
+ ## Cluster
261
185
 
262
- Persistence is composed from primitives no built-in adapter or framework:
186
+ When the same machine needs to run behind `@effect/cluster`, turn it into an entity:
263
187
 
264
188
  ```ts
265
- // Snapshot persistence observe state changes, save externally
266
- yield * actor.changes.pipe(Stream.runForEach((state) => saveSnapshot(actor.id, state)));
267
-
268
- // Event journal — observe transitions
269
- yield * actor.transitions.pipe(Stream.runForEach(({ event }) => appendEvent(actor.id, event)));
189
+ import { EntityMachine, toEntity } from "effect-machine/cluster";
270
190
 
271
- // Restore from snapshot
272
- const savedState = yield * loadSnapshot(id);
273
- const actor = yield * Machine.spawn(machine, { hydrate: savedState });
191
+ const CheckoutEntity = toEntity(checkoutMachine, { type: "Checkout" });
274
192
 
275
- // Restore from event log
276
- const events = yield * loadEvents(id);
277
- const state = yield * Machine.replay(machine, events);
278
- const actor = yield * Machine.spawn(machine, { hydrate: state });
279
-
280
- // Restore from snapshot + tail events
281
- const state = yield * Machine.replay(machine, tailEvents, { from: snapshot });
282
- const actor = yield * Machine.spawn(machine, { hydrate: state });
283
- ```
284
-
285
- ### System Observation
286
-
287
- React to actors joining and leaving the system:
288
-
289
- ```ts
290
- const system = yield * ActorSystemService;
291
-
292
- // Sync callback — like ActorRef.subscribe
293
- const unsub = system.subscribe((event) => {
294
- // event._tag: "ActorSpawned" | "ActorStopped"
295
- console.log(`${event._tag}: ${event.id}`);
193
+ const CheckoutEntityLayer = EntityMachine.layer(CheckoutEntity, checkoutMachine, {
194
+ initializeState: (entityId) => CheckoutState.ReviewingCart({ cartId: entityId, totalCents: 0 }),
195
+ persistence: { strategy: "journal" },
296
196
  });
297
-
298
- // Sync snapshot of all registered actors
299
- const actors = system.actors; // ReadonlyMap<string, ActorRef>
300
-
301
- // Async stream (each subscriber gets own queue)
302
- yield *
303
- system.events.pipe(
304
- Stream.tap((e) => Effect.log(e._tag, e.id)),
305
- Stream.runDrain,
306
- );
307
197
  ```
308
198
 
309
- ### Testing
310
-
311
- Test transitions without actors:
312
-
313
- ```ts
314
- import { simulate, assertPath } from "effect-machine";
315
-
316
- // Simulate events and check path
317
- const result = yield * simulate(machine, [MyEvent.Start, MyEvent.Complete]);
318
- expect(result.states.map((s) => s._tag)).toEqual(["Idle", "Loading", "Done"]);
319
-
320
- // Assert specific path
321
- yield * assertPath(machine, events, ["Idle", "Loading", "Done"]);
322
- ```
199
+ Persistence strategies:
323
200
 
324
- ## API Quick Reference
325
-
326
- ### Building
327
-
328
- | Method | Purpose |
329
- | ----------------------------------------- | ----------------------------------------------------------- |
330
- | `Machine.make({ state, event, initial })` | Create machine |
331
- | `.on(State.X, Event.Y, handler)` | Add transition |
332
- | `.on([State.X, State.Y], Event.Z, h)` | Multi-state transition |
333
- | `.onAny(Event.X, handler)` | Wildcard transition (any state) |
334
- | `.reenter(State.X, Event.Y, handler)` | Force re-entry on same state |
335
- | `.spawn(State.X, handler)` | State-scoped effect |
336
- | `.task(State.X, run, { onSuccess })` | State-scoped task |
337
- | `.timeout(State.X, { duration, event })` | State timeout (gen_statem) |
338
- | `.postpone(State.X, Event.Y)` | Postpone event in state (gen_statem) |
339
- | `.background(handler)` | Machine-lifetime effect |
340
- | `.final(State.X)` | Mark final state |
341
- | `.build({ slot: impl })` | Provide implementations, returns `BuiltMachine` (terminal) |
342
- | `.build()` | Finalize no-slot machine, returns `BuiltMachine` (terminal) |
343
-
344
- ### Running
345
-
346
- | Method | Purpose |
347
- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- |
348
- | `Machine.spawn(machine)` | Single actor, no registry. Caller manages lifetime via `actor.stop`. Auto-cleans up if `Scope` present. |
349
- | `Machine.spawn(machine, id)` | Same as above with custom ID |
350
- | `Machine.spawn(machine, { hydrate: s })` | Restore from saved state — re-runs spawn effects for that state |
351
- | `Machine.replay(machine, events)` | Fold events through handlers to compute state (for event sourcing restore) |
352
- | `system.spawn(id, machine)` | Registry, lookup by ID, bulk ops. Cleans up on system teardown. |
353
-
354
- ### Actor
355
-
356
- | Method | Description |
357
- | -------------------------------- | ----------------------------------------------- |
358
- | `actor.send(event)` | Fire-and-forget (queue event) |
359
- | `actor.cast(event)` | Alias for send (OTP gen_server:cast) |
360
- | `actor.call(event)` | Request-reply, returns `ProcessEventResult` |
361
- | `actor.ask<R>(event)` | Typed domain reply from handler |
362
- | `actor.snapshot` | Get current state |
363
- | `actor.matches(tag)` | Check state tag |
364
- | `actor.can(event)` | Can handle event? |
365
- | `actor.changes` | Stream of state changes |
366
- | `actor.transitions` | Stream of `{ fromState, toState, event }` edges |
367
- | `actor.waitFor(State.X)` | Wait for state (constructor or fn) |
368
- | `actor.awaitFinal` | Wait final state |
369
- | `actor.sendAndWait(ev, State.X)` | Send + wait for state |
370
- | `actor.subscribe(fn)` | Sync callback |
371
- | `actor.sync.send(event)` | Sync fire-and-forget (for UI) |
372
- | `actor.sync.stop()` | Sync stop |
373
- | `actor.sync.snapshot()` | Sync get state |
374
- | `actor.sync.matches(tag)` | Sync check state tag |
375
- | `actor.sync.can(event)` | Sync can handle event? |
376
- | `actor.system` | Access the actor's `ActorSystem` |
377
- | `actor.children` | Child actors (`ReadonlyMap`) |
378
-
379
- ### ActorSystem
380
-
381
- | Method / Property | Description |
382
- | ---------------------- | ------------------------------------------- |
383
- | `system.spawn(id, m)` | Spawn actor |
384
- | `system.get(id)` | Get actor by ID |
385
- | `system.stop(id)` | Stop actor by ID |
386
- | `system.actors` | Sync snapshot of all actors (`ReadonlyMap`) |
387
- | `system.subscribe(fn)` | Sync callback for spawn/stop events |
388
- | `system.events` | Async `Stream<SystemEvent>` for spawn/stop |
389
-
390
- ### Testing
391
-
392
- | Function | Description |
393
- | ------------------------------------------ | ---------------------------------------------------------------- |
394
- | `simulate(machine, events)` | Run events, get all states (accepts `Machine` or `BuiltMachine`) |
395
- | `createTestHarness(machine)` | Step-by-step testing (accepts `Machine` or `BuiltMachine`) |
396
- | `assertPath(machine, events, path)` | Assert exact path |
397
- | `assertReaches(machine, events, tag)` | Assert final state |
398
- | `assertNeverReaches(machine, events, tag)` | Assert state never visited |
201
+ - **Snapshot** saves state periodically, restores on reactivation
202
+ - **Journal** — appends events on each RPC, replays on reactivation
399
203
 
400
204
  ## License
401
205