effect-machine 0.19.0 → 0.21.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 +171 -173
- package/dist/actor.d.ts +95 -24
- package/dist/actor.js +215 -84
- package/dist/atom.d.ts +56 -3
- package/dist/atom.js +33 -2
- package/dist/cluster/entity-machine.d.ts +9 -3
- package/dist/cluster/entity-machine.js +8 -8
- package/dist/cluster/index.d.ts +2 -2
- package/dist/cluster/to-entity.d.ts +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/inspection.d.ts +31 -3
- package/dist/inspection.js +21 -0
- package/dist/internal/inspection.d.ts +1 -1
- package/dist/internal/inspection.js +23 -1
- package/dist/internal/machine-definition.d.ts +1 -0
- package/dist/internal/machine-initialization.d.ts +21 -0
- package/dist/internal/machine-initialization.js +27 -0
- package/dist/internal/runtime.d.ts +15 -1
- package/dist/internal/runtime.js +67 -26
- package/dist/internal/transition.d.ts +51 -3
- package/dist/internal/transition.js +216 -38
- package/dist/internal/utils.js +1 -0
- package/dist/machine.d.ts +151 -40
- package/dist/machine.js +139 -40
- package/dist/supervision.d.ts +3 -2
- package/dist/supervision.js +3 -2
- package/dist/testing.d.ts +39 -67
- package/dist/testing.js +13 -52
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,14 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Type-safe state machines for [Effect](https://effect.website).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Effect Machine gives one actor a schema-first state model, a typed event mailbox, scoped Effect work, typed input and output, supervision, persistence hooks, inspection, and framework-neutral Atom integration.
|
|
6
6
|
|
|
7
|
-
Use it when a feature has
|
|
8
|
-
|
|
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
|
|
7
|
+
Use it when a feature has several valid states, invalid transitions, state-owned async work, timeouts, cancellation, actor coordination, or UI views that need precise subscriptions.
|
|
13
8
|
|
|
14
9
|
## Install
|
|
15
10
|
|
|
@@ -17,244 +12,247 @@ Use it when a feature has:
|
|
|
17
12
|
bun add effect-machine effect
|
|
18
13
|
```
|
|
19
14
|
|
|
20
|
-
`effect` is a peer dependency.
|
|
21
|
-
|
|
15
|
+
`effect` is a required peer dependency.
|
|
16
|
+
|
|
17
|
+
## Imports
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Event, Machine, State } from "effect-machine";
|
|
21
|
+
import * as ActorAtom from "effect-machine/atom";
|
|
22
|
+
import { EntityMachine, toEntity } from "effect-machine/cluster";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use `effect-machine` for local machines and actors. Use `effect-machine/atom` for React, Solid, or another Effect Atom binding. Use `effect-machine/cluster` for distributed entity machines.
|
|
22
26
|
|
|
23
|
-
##
|
|
27
|
+
## First machine
|
|
24
28
|
|
|
25
|
-
States and events are schemas.
|
|
29
|
+
States and events are Effect schemas.
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
|
-
import {
|
|
32
|
+
import { Effect, Schema } from "effect";
|
|
29
33
|
import { Event, Machine, State } from "effect-machine";
|
|
30
34
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Failed: {
|
|
35
|
+
const DownloadState = State({
|
|
36
|
+
Idle: {},
|
|
37
|
+
Downloading: { url: Schema.String },
|
|
38
|
+
Done: { url: Schema.String, bytes: Schema.Finite },
|
|
39
|
+
Failed: { url: Schema.String, message: Schema.String },
|
|
36
40
|
});
|
|
37
41
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Cancel: {},
|
|
42
|
+
const DownloadEvent = Event({
|
|
43
|
+
Start: { url: Schema.String },
|
|
44
|
+
Completed: { bytes: Schema.Finite },
|
|
45
|
+
Failed: { message: Schema.String },
|
|
43
46
|
});
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
cartId: string,
|
|
50
|
-
totalCents: number,
|
|
51
|
-
) => Effect.Effect<{ readonly receiptId: string }>;
|
|
52
|
-
}
|
|
53
|
-
>()("app/PaymentService") {}
|
|
54
|
-
|
|
55
|
-
const checkoutMachine = Machine.make({
|
|
56
|
-
state: CheckoutState,
|
|
57
|
-
event: CheckoutEvent,
|
|
58
|
-
initial: CheckoutState.ReviewingCart({ cartId: "cart_123", totalCents: 4200 }),
|
|
48
|
+
const downloadMachine = Machine.make({
|
|
49
|
+
state: DownloadState,
|
|
50
|
+
event: DownloadEvent,
|
|
51
|
+
initial: DownloadState.Idle,
|
|
59
52
|
})
|
|
60
|
-
.on(
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
.on(CheckoutState.ChargingCard, CheckoutEvent.Charged, ({ state, event }) =>
|
|
64
|
-
CheckoutState.Confirmed.with(state, { receiptId: event.receiptId }),
|
|
53
|
+
.on(DownloadState.Idle, DownloadEvent.Start, ({ event }) =>
|
|
54
|
+
DownloadState.Downloading({ url: event.url }),
|
|
65
55
|
)
|
|
66
|
-
.on(
|
|
67
|
-
|
|
56
|
+
.on(DownloadState.Downloading, DownloadEvent.Completed, ({ state, event }) =>
|
|
57
|
+
DownloadState.Done.with(state, { bytes: event.bytes }),
|
|
68
58
|
)
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
.task(
|
|
73
|
-
CheckoutState.ChargingCard,
|
|
74
|
-
({ state }) =>
|
|
75
|
-
Effect.flatMap(PaymentService, (payment) =>
|
|
76
|
-
payment.chargeCard(state.cartId, state.totalCents),
|
|
77
|
-
),
|
|
78
|
-
{
|
|
79
|
-
onSuccess: ({ receiptId }) => CheckoutEvent.Charged({ receiptId }),
|
|
80
|
-
onFailure: (cause) => CheckoutEvent.Declined({ reason: Cause.pretty(cause) }),
|
|
81
|
-
},
|
|
59
|
+
.on(DownloadState.Downloading, DownloadEvent.Failed, ({ state, event }) =>
|
|
60
|
+
DownloadState.Failed.with(state, { message: event.message }),
|
|
82
61
|
)
|
|
83
|
-
.final(
|
|
84
|
-
.final(
|
|
62
|
+
.final(DownloadState.Done, ({ state }) => state.bytes)
|
|
63
|
+
.final(DownloadState.Failed, () => 0);
|
|
64
|
+
|
|
65
|
+
const program = Effect.scoped(
|
|
66
|
+
Machine.scoped(
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const actor = yield* Machine.spawn(downloadMachine);
|
|
69
|
+
yield* actor.start;
|
|
70
|
+
yield* actor.send(DownloadEvent.Start({ url: "/report.pdf" }));
|
|
71
|
+
yield* actor.send(DownloadEvent.Completed({ bytes: 1024 }));
|
|
72
|
+
return yield* actor.awaitOutput;
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
85
76
|
```
|
|
86
77
|
|
|
87
|
-
A
|
|
78
|
+
An empty variant is a value such as `DownloadState.Idle`. A non-empty variant is a constructor such as `DownloadState.Downloading({ url })`.
|
|
79
|
+
|
|
80
|
+
`State.with(source, fields)` copies matching fields into the target variant. It prevents manual context spreading across different states.
|
|
88
81
|
|
|
89
|
-
|
|
90
|
-
- `State.with(source, overrides)` carries overlapping fields forward without manual copying.
|
|
91
|
-
- `.onAny(...)` is a fallback; a specific `.on(...)` wins.
|
|
92
|
-
- `.spawn(...)` runs work on state entry and cancels it on state exit.
|
|
82
|
+
## Effect is the composition layer
|
|
93
83
|
|
|
94
|
-
|
|
84
|
+
Effect Machine does not add an action queue or a second context system.
|
|
95
85
|
|
|
96
|
-
|
|
86
|
+
| Work | API |
|
|
87
|
+
| ------------------------------------- | -------------------------------------------- |
|
|
88
|
+
| Unconditional state change | `.on`, `.reenter`, or `.immediate` |
|
|
89
|
+
| Conditional state change | `.when`, `.reenterWhen`, or `.immediateWhen` |
|
|
90
|
+
| Work that produces a completion event | `.task` |
|
|
91
|
+
| State-owned stream or resource | `.spawn` |
|
|
92
|
+
| Actor-owned stream or resource | `.background` |
|
|
93
|
+
| Autonomous machine sequence | `Machine.run` with `Effect.flatMap` |
|
|
94
|
+
| Interactive multi-phase flow | Parent machine with child actors |
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
Effect requirements remain in `R`. A machine cannot start until the application provides every required service. Effectful transition handlers must have `never` in their error channel. Convert expected failures to states or events.
|
|
97
|
+
|
|
98
|
+
Read [the Effect model](./docs/effect-model.md) and [async work ownership](./docs/async-work.md).
|
|
99
|
+
|
|
100
|
+
## Guards and stable state
|
|
101
|
+
|
|
102
|
+
Register ordered candidates for one state and event. The first passing guard wins. An unguarded candidate is the fallback.
|
|
99
103
|
|
|
100
104
|
```ts
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
machine
|
|
106
|
+
.when(
|
|
107
|
+
State.Checking,
|
|
108
|
+
Event.Continue,
|
|
109
|
+
function hasStock({ state }) {
|
|
110
|
+
return state.stock > 0;
|
|
111
|
+
},
|
|
112
|
+
() => State.Accepted,
|
|
113
|
+
)
|
|
114
|
+
.on(State.Checking, Event.Continue, () => State.Rejected)
|
|
115
|
+
.immediate(State.Accepted, ({ state }) => State.Ready.with(state));
|
|
109
116
|
```
|
|
110
117
|
|
|
111
|
-
`
|
|
112
|
-
|
|
113
|
-
Transition handlers in `.on()` and `.reenter()` stay pure. Use services only in `.task()`, `.spawn()`, and `.background()`.
|
|
118
|
+
The predicate can return a Boolean or `Effect<boolean, never, R>`. Its requirements flow into the machine type. `actor.can(event)` evaluates the same predicate with the actor's captured context. `ActorAtom.can(actor, event)` exposes the result to React and Solid. The inspector uses the predicate function name.
|
|
114
119
|
|
|
115
|
-
|
|
120
|
+
Immediate transitions run until the state is stable. Subscribers see only the stable state. The runtime stops an accidental eventless loop after 100 edges.
|
|
116
121
|
|
|
117
|
-
|
|
122
|
+
## Effect services and tasks
|
|
118
123
|
|
|
119
124
|
```ts
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
class Api extends Context.Service<
|
|
126
|
+
Api,
|
|
127
|
+
{ readonly load: (id: string) => Effect.Effect<Data, ApiError> }
|
|
128
|
+
>()("app/Api") {}
|
|
129
|
+
|
|
130
|
+
machine.task(State.Loading, ({ state }) => Effect.flatMap(Api, (api) => api.load(state.id)), {
|
|
131
|
+
name: "load-data",
|
|
132
|
+
onSuccess: (data) => Event.Loaded({ data }),
|
|
133
|
+
onFailure: (error) => Event.LoadFailed({ message: String(error) }),
|
|
126
134
|
});
|
|
127
135
|
|
|
128
|
-
Effect.
|
|
129
|
-
|
|
130
|
-
Effect.provideService(PaymentService, {
|
|
131
|
-
chargeCard: (cartId) => Effect.succeed({ receiptId: `rcpt_${cartId}` }),
|
|
132
|
-
}),
|
|
133
|
-
),
|
|
134
|
-
);
|
|
136
|
+
const actor = yield * Machine.spawn(machine).pipe(Effect.provide(ApiLive));
|
|
137
|
+
yield * actor.start;
|
|
135
138
|
```
|
|
136
139
|
|
|
137
|
-
|
|
140
|
+
The actor captures the Effect context during allocation. It keeps those services when it starts later.
|
|
138
141
|
|
|
139
|
-
|
|
140
|
-
- `send(event)` queues and returns immediately
|
|
141
|
-
- `call(event)` returns full transition info
|
|
142
|
-
- `ask(event)` returns a typed domain reply (requires `Event.reply(...)`)
|
|
143
|
-
- `waitFor(...)` / `awaitFinal` for coordination
|
|
144
|
-
- `stop` interrupts now; `drain` processes the remaining queue first
|
|
145
|
-
- `awaitExit` completes when the actor stops
|
|
142
|
+
`onFailure` receives the typed Effect error. A defect does not enter `onFailure`. It stops the actor or starts supervision.
|
|
146
143
|
|
|
147
|
-
|
|
144
|
+
## Input, output, and composition
|
|
148
145
|
|
|
149
146
|
```ts
|
|
150
|
-
|
|
147
|
+
const checkoutMachine = Machine.make({
|
|
148
|
+
state: CheckoutState,
|
|
149
|
+
event: CheckoutEvent,
|
|
150
|
+
initial: (input: CheckoutInput) => CheckoutState.Reviewing(input),
|
|
151
|
+
}).final(CheckoutState.Done, ({ state }) => ({ receiptId: state.receiptId }));
|
|
151
152
|
|
|
152
|
-
const program =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
yield* actor.send(CheckoutEvent.Submit);
|
|
156
|
-
}).pipe(Effect.provide(ActorSystemDefault));
|
|
153
|
+
const program = Machine.run(cartMachine).pipe(
|
|
154
|
+
Effect.flatMap((cart) => Machine.run(checkoutMachine, { input: cart })),
|
|
155
|
+
);
|
|
157
156
|
```
|
|
158
157
|
|
|
159
|
-
|
|
158
|
+
`Machine.run` starts one actor, waits for output, and always stops it. Interruption releases actor resources. The final actor state remains available when you use `Machine.spawn` and retain the actor reference.
|
|
160
159
|
|
|
161
|
-
|
|
160
|
+
Use a parent machine when a UI must route between phases, keep shared values, or support back navigation. Read [Actors and systems](./docs/actors.md).
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
const CartEvent = Event({
|
|
165
|
-
GetTotal: Event.reply({}, Schema.Number),
|
|
166
|
-
});
|
|
162
|
+
## ActorRef
|
|
167
163
|
|
|
168
|
-
|
|
164
|
+
| Member | Use |
|
|
165
|
+
| --------------------------- | -------------------------------------------------- |
|
|
166
|
+
| `start` | Start a direct actor |
|
|
167
|
+
| `send(event)` | Queue an event |
|
|
168
|
+
| `call(event)` | Process an event and return transition information |
|
|
169
|
+
| `ask(event)` | Return a typed reply from an `Event.reply` event |
|
|
170
|
+
| `waitFor(state)` | Wait for a state constructor or predicate |
|
|
171
|
+
| `sendAndWait(event, state)` | Send and wait for a state |
|
|
172
|
+
| `snapshot` | Read the current state as an Effect |
|
|
173
|
+
| `awaitFinal` | Wait for the final state |
|
|
174
|
+
| `awaitOutput` | Wait for typed output |
|
|
175
|
+
| `awaitExit` | Wait for `Final`, `Stopped`, or `Defect` |
|
|
176
|
+
| `drain` | Process queued events and stop |
|
|
177
|
+
| `subscribe` | Observe state with a host callback |
|
|
178
|
+
| `client` | Use the actor outside Effect |
|
|
179
|
+
| `system` | Access named actors |
|
|
180
|
+
| `children` | Read direct child actors |
|
|
169
181
|
|
|
170
|
-
|
|
171
|
-
```
|
|
182
|
+
`Machine.spawn` returns an unstarted actor. `system.spawn` starts the actor.
|
|
172
183
|
|
|
173
|
-
|
|
184
|
+
Use `actor.client` in a JavaScript callback or application that does not run inside Effect. `client.can(event)` returns a Promise and supports Effect predicates. `client.canSync(event)` supports Boolean predicates only. React and Solid should use Actor Atoms.
|
|
174
185
|
|
|
175
|
-
|
|
186
|
+
## Atom, React, and Solid
|
|
176
187
|
|
|
177
188
|
```ts
|
|
178
|
-
import
|
|
179
|
-
|
|
180
|
-
const result =
|
|
181
|
-
yield *
|
|
182
|
-
simulate(checkoutMachine, [
|
|
183
|
-
CheckoutEvent.Submit,
|
|
184
|
-
CheckoutEvent.Charged({ receiptId: "rcpt_123" }),
|
|
185
|
-
]);
|
|
189
|
+
import * as ActorAtom from "effect-machine/atom";
|
|
186
190
|
|
|
187
|
-
|
|
191
|
+
const stateAtom = ActorAtom.make(actor);
|
|
192
|
+
const countAtom = ActorAtom.select(stateAtom, (state) => state.count);
|
|
188
193
|
```
|
|
189
194
|
|
|
190
|
-
|
|
195
|
+
The selected Atom stays writable. Writes send machine events. A selector publishes only when its selected value changes.
|
|
191
196
|
|
|
192
|
-
|
|
197
|
+
The React example uses `useAtomSuspense` and Motion. The Solid example uses `useAtomResource`, Suspense, and `solid-transition-group`. Both include performance tests. Both retain exit-animation data in the terminal machine state.
|
|
193
198
|
|
|
194
|
-
|
|
199
|
+
Read [Atom and UI integration](./docs/atom-and-ui.md) and browse [all examples](./examples/README.md).
|
|
195
200
|
|
|
196
|
-
|
|
197
|
-
import { useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
198
|
-
import * as ActorAtom from "effect-machine/atom";
|
|
201
|
+
## Persistence, supervision, and inspection
|
|
199
202
|
|
|
200
|
-
|
|
203
|
+
- Recovery resolves state during actor startup.
|
|
204
|
+
- Durability saves committed transitions.
|
|
205
|
+
- Supervision restarts defects within an Effect `Schedule` budget.
|
|
206
|
+
- Inspection reports events, named transition operations, transitions, named guards, tasks, Effects, errors, stops, and actor generations.
|
|
201
207
|
|
|
202
|
-
|
|
203
|
-
const totalCents = useAtomValue(checkoutAtom, (state) =>
|
|
204
|
-
"totalCents" in state ? state.totalCents : 0,
|
|
205
|
-
);
|
|
206
|
-
const send = useAtomSet(checkoutAtom);
|
|
208
|
+
Read [Persistence and supervision](./docs/persistence-and-supervision.md) and [Inspection](./docs/inspection.md).
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
```
|
|
210
|
+
## Testing
|
|
211
211
|
|
|
212
|
-
`
|
|
212
|
+
Use `simulate` or `createTestHarness` for transition paths. Spawn a real actor for tasks, services, resources, persistence, supervision, inspection, and actor topology.
|
|
213
213
|
|
|
214
214
|
```ts
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
(value, next) => value.cents === next.cents,
|
|
219
|
-
);
|
|
215
|
+
const result = yield * simulate(machine, events, { input });
|
|
216
|
+
yield * assertPath(machine, events, ["Idle", "Loading", "Done"]);
|
|
217
|
+
yield * assertNeverReaches(machine, events, "Failed");
|
|
220
218
|
```
|
|
221
219
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
Runnable React and Solid package examples are in `examples/react` and `examples/solid`.
|
|
220
|
+
Read [Testing](./docs/testing.md).
|
|
225
221
|
|
|
226
|
-
|
|
227
|
-
bun run example:react
|
|
228
|
-
bun run example:solid
|
|
229
|
-
```
|
|
222
|
+
## XState migration
|
|
230
223
|
|
|
231
|
-
|
|
224
|
+
The [migration guide](./docs/xstate-migration.md) covers context, assign, actions, invoked promise and callback actors, root routers, actor registries, selectors, inspection, persistence, and exit animation values. Its patterns come from a large XState kiosk application.
|
|
232
225
|
|
|
233
|
-
|
|
226
|
+
## Cluster entities
|
|
234
227
|
|
|
235
|
-
|
|
228
|
+
Use `effect-machine/cluster` to expose a machine through Effect Cluster. It supports typed send, ask, state reads, state watches, input adapters, snapshot persistence, and journal persistence.
|
|
236
229
|
|
|
237
|
-
|
|
230
|
+
Read [Cluster entities](./docs/cluster.md).
|
|
238
231
|
|
|
239
|
-
##
|
|
232
|
+
## Examples
|
|
240
233
|
|
|
241
|
-
|
|
234
|
+
The examples directory is a Bun workspace.
|
|
242
235
|
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const CheckoutEntityLayer = EntityMachine.layer(CheckoutEntity, checkoutMachine, {
|
|
249
|
-
initializeState: (entityId) => CheckoutState.ReviewingCart({ cartId: entityId, totalCents: 0 }),
|
|
250
|
-
persistence: { strategy: "journal" },
|
|
251
|
-
});
|
|
236
|
+
```bash
|
|
237
|
+
bun run examples:gate
|
|
238
|
+
bun run example:react
|
|
239
|
+
bun run example:solid
|
|
252
240
|
```
|
|
253
241
|
|
|
254
|
-
|
|
242
|
+
The [example matrix](./examples/README.md) links every pattern to executable code.
|
|
243
|
+
|
|
244
|
+
## Documentation
|
|
255
245
|
|
|
256
|
-
-
|
|
257
|
-
-
|
|
246
|
+
- [Effect model](./docs/effect-model.md)
|
|
247
|
+
- [Async work](./docs/async-work.md)
|
|
248
|
+
- [Actors and systems](./docs/actors.md)
|
|
249
|
+
- [Atom and UI integration](./docs/atom-and-ui.md)
|
|
250
|
+
- [Persistence and supervision](./docs/persistence-and-supervision.md)
|
|
251
|
+
- [Inspection](./docs/inspection.md)
|
|
252
|
+
- [Testing](./docs/testing.md)
|
|
253
|
+
- [Migration from XState](./docs/xstate-migration.md)
|
|
254
|
+
- [Cluster entities](./docs/cluster.md)
|
|
255
|
+
- [AI agent reference](./SKILL.md)
|
|
258
256
|
|
|
259
257
|
## License
|
|
260
258
|
|