effect-machine 0.12.0 → 0.14.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.
- package/README.md +133 -324
- package/dist/actor.d.ts +46 -28
- package/dist/actor.js +276 -315
- package/dist/cluster/entity-machine.d.ts +1 -1
- package/dist/cluster/entity-machine.js +20 -9
- package/dist/cluster/to-entity.d.ts +3 -3
- package/dist/errors.d.ts +24 -20
- package/dist/errors.js +10 -6
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/internal/runtime.d.ts +82 -7
- package/dist/internal/runtime.js +162 -58
- package/dist/internal/transition.d.ts +12 -11
- package/dist/internal/transition.js +12 -14
- package/dist/machine.d.ts +148 -140
- package/dist/machine.js +141 -155
- package/dist/schema.d.ts +14 -0
- package/dist/schema.js +10 -1
- package/dist/slot.d.ts +112 -86
- package/dist/slot.js +92 -59
- package/dist/supervision.d.ts +97 -0
- package/dist/supervision.js +42 -0
- package/dist/testing.d.ts +21 -12
- package/dist/testing.js +23 -26
- package/package.json +7 -7
- package/v3/dist/actor.d.ts +53 -30
- package/v3/dist/actor.js +286 -311
- package/v3/dist/cluster/entity-machine.d.ts +1 -1
- package/v3/dist/cluster/entity-machine.js +5 -5
- package/v3/dist/cluster/to-entity.d.ts +1 -1
- package/v3/dist/errors.d.ts +14 -10
- package/v3/dist/errors.js +11 -7
- package/v3/dist/index.d.ts +6 -5
- package/v3/dist/index.js +3 -2
- package/v3/dist/inspection.d.ts +3 -22
- package/v3/dist/inspection.js +1 -15
- package/v3/dist/internal/brands.d.ts +4 -8
- package/v3/dist/internal/inspection.js +1 -1
- package/v3/dist/internal/runtime.d.ts +87 -10
- package/v3/dist/internal/runtime.js +177 -61
- package/v3/dist/internal/transition.d.ts +13 -12
- package/v3/dist/internal/transition.js +14 -16
- package/v3/dist/internal/utils.js +5 -1
- package/v3/dist/machine.d.ts +158 -143
- package/v3/dist/machine.js +148 -155
- package/v3/dist/schema.d.ts +25 -11
- package/v3/dist/schema.js +18 -5
- package/v3/dist/slot.d.ts +112 -86
- package/v3/dist/slot.js +92 -59
- package/v3/dist/supervision.d.ts +97 -0
- package/v3/dist/supervision.js +42 -0
- package/v3/dist/testing.d.ts +21 -12
- package/v3/dist/testing.js +23 -24
package/README.md
CHANGED
|
@@ -1,401 +1,210 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
Use it when a feature has:
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
##
|
|
20
|
+
## Core Pattern
|
|
21
|
+
|
|
22
|
+
States and events are schemas. Types, validation, and serialization from one place.
|
|
25
23
|
|
|
26
24
|
```ts
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const CheckoutEvent = Event({
|
|
36
|
+
Submit: {},
|
|
37
|
+
Charged: { receiptId: Schema.String },
|
|
38
|
+
Declined: { reason: Schema.String },
|
|
42
39
|
Cancel: {},
|
|
43
40
|
});
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
notifyWarehouse: { orderId: Schema.String },
|
|
42
|
+
const CheckoutSlots = Slot.define({
|
|
43
|
+
chargeCard: Slot.fn({ cartId: Schema.String, totalCents: Schema.Number }),
|
|
48
44
|
});
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
initial: OrderState.Pending({ orderId: "order-1" }),
|
|
46
|
+
const checkoutMachine = Machine.make({
|
|
47
|
+
state: CheckoutState,
|
|
48
|
+
event: CheckoutEvent,
|
|
49
|
+
slots: CheckoutSlots,
|
|
50
|
+
initial: CheckoutState.ReviewingCart({ cartId: "cart_123", totalCents: 4200 }),
|
|
56
51
|
})
|
|
57
|
-
.on(
|
|
58
|
-
|
|
59
|
-
OrderState.Shipped.derive(state, { trackingId: event.trackingId }),
|
|
52
|
+
.on(CheckoutState.ReviewingCart, CheckoutEvent.Submit, ({ state }) =>
|
|
53
|
+
CheckoutState.ChargingCard.derive(state),
|
|
60
54
|
)
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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, ({ slots, state }) =>
|
|
65
|
+
slots.chargeCard({ cartId: state.cartId, totalCents: state.totalCents }),
|
|
66
|
+
)
|
|
67
|
+
.final(CheckoutState.Confirmed)
|
|
68
|
+
.final(CheckoutState.Failed);
|
|
91
69
|
```
|
|
92
70
|
|
|
93
|
-
|
|
71
|
+
A few things to notice:
|
|
94
72
|
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
85
|
+
const actor =
|
|
86
|
+
yield *
|
|
87
|
+
Machine.spawn(checkoutMachine, {
|
|
88
|
+
slots: {
|
|
89
|
+
chargeCard: ({ cartId, totalCents }) =>
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
const ctx = yield* checkoutMachine.Context;
|
|
92
|
+
const result = yield* PaymentService.charge(cartId, totalCents);
|
|
93
|
+
yield* ctx.self.send(
|
|
94
|
+
result.ok
|
|
95
|
+
? CheckoutEvent.Charged({ receiptId: result.receiptId })
|
|
96
|
+
: CheckoutEvent.Declined({ reason: result.error }),
|
|
97
|
+
);
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
123
101
|
```
|
|
124
102
|
|
|
125
|
-
|
|
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)
|
|
103
|
+
The same machine can run with different slot implementations in tests, local apps, or production. Slots are accepted everywhere the machine runs:
|
|
132
104
|
|
|
133
|
-
|
|
134
|
-
.
|
|
135
|
-
|
|
105
|
+
- `Machine.spawn(machine, { slots })`
|
|
106
|
+
- `Machine.replay(machine, events, { slots })`
|
|
107
|
+
- `simulate(machine, events, { slots })`
|
|
108
|
+
- `createTestHarness(machine, { slots })`
|
|
136
109
|
|
|
137
|
-
|
|
110
|
+
## Running Actors
|
|
138
111
|
|
|
139
|
-
|
|
112
|
+
`Machine.spawn` gives you a live actor with a queue and lifecycle management.
|
|
140
113
|
|
|
141
114
|
```ts
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
115
|
+
const program = Effect.gen(function* () {
|
|
116
|
+
const actor = yield* Machine.spawn(checkoutMachine, {
|
|
117
|
+
slots: {
|
|
118
|
+
chargeCard: ({ cartId }) =>
|
|
119
|
+
checkoutMachine.Context.pipe(
|
|
120
|
+
Effect.flatMap((ctx) =>
|
|
121
|
+
ctx.self.send(CheckoutEvent.Charged({ receiptId: `rcpt_${cartId}` })),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
145
126
|
|
|
146
|
-
|
|
147
|
-
|
|
127
|
+
yield* actor.send(CheckoutEvent.Submit);
|
|
128
|
+
const finalState = yield* actor.awaitFinal;
|
|
148
129
|
});
|
|
149
130
|
|
|
150
|
-
|
|
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
|
-
});
|
|
131
|
+
Effect.runPromise(Effect.scoped(program));
|
|
169
132
|
```
|
|
170
133
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
`.spawn()` runs effects when entering a state, auto-cancelled on exit:
|
|
134
|
+
Key actor operations:
|
|
174
135
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
136
|
+
- `send(event)` queues and returns immediately
|
|
137
|
+
- `call(event)` returns full transition info
|
|
138
|
+
- `ask(event)` returns a typed domain reply (requires `Event.reply(...)`)
|
|
139
|
+
- `waitFor(...)` / `awaitFinal` for coordination
|
|
140
|
+
- `stop` interrupts now; `drain` processes the remaining queue first
|
|
141
|
+
- `watch(other)` completes when another actor stops
|
|
180
142
|
|
|
181
|
-
|
|
143
|
+
For named actors or shared lookup, use an actor system:
|
|
182
144
|
|
|
183
145
|
```ts
|
|
184
|
-
|
|
185
|
-
onSuccess: (data) => MyEvent.Resolve({ data }),
|
|
186
|
-
onFailure: () => MyEvent.Reject,
|
|
187
|
-
});
|
|
188
|
-
```
|
|
146
|
+
import { ActorSystemDefault, ActorSystemService } from "effect-machine";
|
|
189
147
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
});
|
|
148
|
+
const program = Effect.gen(function* () {
|
|
149
|
+
const system = yield* ActorSystemService;
|
|
150
|
+
const actor = yield* system.spawn("checkout-123", checkoutMachine);
|
|
151
|
+
yield* actor.send(CheckoutEvent.Submit);
|
|
152
|
+
}).pipe(Effect.provide(ActorSystemDefault));
|
|
205
153
|
```
|
|
206
154
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
### Event Postpone
|
|
155
|
+
### Typed Replies
|
|
210
156
|
|
|
211
|
-
|
|
157
|
+
Events can declare typed reply schemas:
|
|
212
158
|
|
|
213
159
|
```ts
|
|
214
|
-
|
|
215
|
-
.
|
|
216
|
-
|
|
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 }`:
|
|
160
|
+
const CartEvent = Event({
|
|
161
|
+
GetTotal: Event.reply({}, Schema.Number),
|
|
162
|
+
});
|
|
224
163
|
|
|
225
|
-
|
|
226
|
-
.on(State.Active, Event.GetCount, ({ state }) => ({
|
|
227
|
-
state, // stay in same state
|
|
228
|
-
reply: state.count, // domain value returned to caller
|
|
229
|
-
}))
|
|
164
|
+
machine.on(State.Active, CartEvent.GetTotal, ({ state }) => Machine.reply(state, state.totalCents));
|
|
230
165
|
|
|
231
|
-
//
|
|
232
|
-
const count = yield* actor.ask<number>(Event.GetCount);
|
|
166
|
+
const total = yield * actor.ask(CartEvent.GetTotal); // number
|
|
233
167
|
```
|
|
234
168
|
|
|
235
|
-
|
|
169
|
+
## Testing
|
|
236
170
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
Spawn children from `.spawn()` handlers with `self.spawn`. Children are state-scoped — auto-stopped on state exit:
|
|
171
|
+
Test transitions without spawning actors:
|
|
240
172
|
|
|
241
173
|
```ts
|
|
242
|
-
machine
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}),
|
|
249
|
-
|
|
250
|
-
|
|
174
|
+
import { simulate } from "effect-machine";
|
|
175
|
+
|
|
176
|
+
const result =
|
|
177
|
+
yield *
|
|
178
|
+
simulate(
|
|
179
|
+
checkoutMachine,
|
|
180
|
+
[CheckoutEvent.Submit, CheckoutEvent.Charged({ receiptId: "rcpt_123" })],
|
|
181
|
+
{ slots: { chargeCard: () => Effect.void } },
|
|
182
|
+
);
|
|
251
183
|
|
|
252
|
-
|
|
253
|
-
const parent = yield * Machine.spawn(parentMachine);
|
|
254
|
-
yield * parent.send(Event.Activate);
|
|
255
|
-
const child = yield * parent.system.get("worker-1"); // Option<ActorRef>
|
|
184
|
+
expect(result.states.map((s) => s._tag)).toEqual(["ReviewingCart", "ChargingCard", "Confirmed"]);
|
|
256
185
|
```
|
|
257
186
|
|
|
258
|
-
|
|
187
|
+
`simulate` and `createTestHarness` test transition logic. They do not run `.spawn()` or `.background()` effects.
|
|
259
188
|
|
|
260
|
-
|
|
189
|
+
## Cluster
|
|
261
190
|
|
|
262
|
-
|
|
191
|
+
When the same machine needs to run behind `@effect/cluster`, turn it into an entity:
|
|
263
192
|
|
|
264
193
|
```ts
|
|
265
|
-
|
|
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)));
|
|
194
|
+
import { EntityMachine, toEntity } from "effect-machine/cluster";
|
|
270
195
|
|
|
271
|
-
|
|
272
|
-
const savedState = yield * loadSnapshot(id);
|
|
273
|
-
const actor = yield * Machine.spawn(machine, { hydrate: savedState });
|
|
196
|
+
const CheckoutEntity = toEntity(checkoutMachine, { type: "Checkout" });
|
|
274
197
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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}`);
|
|
198
|
+
const CheckoutEntityLayer = EntityMachine.layer(CheckoutEntity, checkoutMachine, {
|
|
199
|
+
initializeState: (entityId) => CheckoutState.ReviewingCart({ cartId: entityId, totalCents: 0 }),
|
|
200
|
+
persistence: { strategy: "journal" },
|
|
296
201
|
});
|
|
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
202
|
```
|
|
308
203
|
|
|
309
|
-
|
|
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
|
-
```
|
|
204
|
+
Persistence strategies:
|
|
323
205
|
|
|
324
|
-
|
|
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 |
|
|
206
|
+
- **Snapshot** — saves state periodically, restores on reactivation
|
|
207
|
+
- **Journal** — appends events on each RPC, replays on reactivation
|
|
399
208
|
|
|
400
209
|
## License
|
|
401
210
|
|