effect-machine 0.2.4 → 0.3.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 +93 -52
- package/package.json +2 -2
- package/src/actor.ts +134 -111
- package/src/errors.ts +1 -1
- package/src/index.ts +1 -0
- package/src/internal/transition.ts +8 -2
- package/src/machine.ts +228 -145
- package/src/persistence/adapter.ts +2 -2
- package/src/persistence/persistent-actor.ts +4 -10
- package/src/schema.ts +52 -6
- package/src/testing.ts +35 -27
package/README.md
CHANGED
|
@@ -25,13 +25,13 @@ npm install effect-machine effect
|
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
import { Effect, Schema } from "effect";
|
|
28
|
-
import { Machine, State, Event, Slot } from "effect-machine";
|
|
28
|
+
import { Machine, State, Event, Slot, type BuiltMachine } from "effect-machine";
|
|
29
29
|
|
|
30
30
|
// Define state schema - states ARE schemas
|
|
31
31
|
const OrderState = State({
|
|
32
32
|
Pending: { orderId: Schema.String },
|
|
33
33
|
Processing: { orderId: Schema.String },
|
|
34
|
-
Shipped: { trackingId: Schema.String },
|
|
34
|
+
Shipped: { orderId: Schema.String, trackingId: Schema.String },
|
|
35
35
|
Cancelled: {},
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -54,36 +54,36 @@ const orderMachine = Machine.make({
|
|
|
54
54
|
effects: OrderEffects,
|
|
55
55
|
initial: OrderState.Pending({ orderId: "order-1" }),
|
|
56
56
|
})
|
|
57
|
-
.on(OrderState.Pending, OrderEvent.Process, ({ state }) =>
|
|
58
|
-
|
|
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 }),
|
|
59
60
|
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
.on(OrderState.Pending, OrderEvent.Cancel, () => OrderState.Cancelled)
|
|
64
|
-
.on(OrderState.Processing, OrderEvent.Cancel, () => OrderState.Cancelled)
|
|
61
|
+
// Cancel from any state
|
|
62
|
+
.onAny(OrderEvent.Cancel, () => OrderState.Cancelled)
|
|
65
63
|
// Effect runs when entering Processing, cancelled on exit
|
|
66
64
|
.spawn(OrderState.Processing, ({ effects, state }) =>
|
|
67
65
|
effects.notifyWarehouse({ orderId: state.orderId }),
|
|
68
66
|
)
|
|
69
|
-
.provide({
|
|
70
|
-
notifyWarehouse: ({ orderId }) => Effect.log(`Warehouse notified: ${orderId}`),
|
|
71
|
-
})
|
|
72
67
|
.final(OrderState.Shipped)
|
|
73
|
-
.final(OrderState.Cancelled)
|
|
68
|
+
.final(OrderState.Cancelled)
|
|
69
|
+
.build({
|
|
70
|
+
notifyWarehouse: ({ orderId }) => Effect.log(`Warehouse notified: ${orderId}`),
|
|
71
|
+
});
|
|
74
72
|
|
|
75
|
-
// Run as actor (simple)
|
|
73
|
+
// Run as actor (simple — no scope required)
|
|
76
74
|
const program = Effect.gen(function* () {
|
|
77
75
|
const actor = yield* Machine.spawn(orderMachine);
|
|
78
76
|
|
|
79
77
|
yield* actor.send(OrderEvent.Process);
|
|
80
78
|
yield* actor.send(OrderEvent.Ship({ trackingId: "TRACK-123" }));
|
|
81
79
|
|
|
82
|
-
const state = yield* actor.
|
|
83
|
-
console.log(state); // Shipped { trackingId: "TRACK-123" }
|
|
80
|
+
const state = yield* actor.waitFor(OrderState.Shipped);
|
|
81
|
+
console.log(state); // Shipped { orderId: "order-1", trackingId: "TRACK-123" }
|
|
82
|
+
|
|
83
|
+
yield* actor.stop;
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
Effect.runPromise(
|
|
86
|
+
Effect.runPromise(program);
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
## Core Concepts
|
|
@@ -102,6 +102,34 @@ MyState.Idle; // Value (no parens)
|
|
|
102
102
|
MyState.Loading({ url: "/api" }); // Constructor
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
### State.derive()
|
|
106
|
+
|
|
107
|
+
Construct new states from existing ones — picks overlapping fields, applies overrides:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// Same-state: preserve fields, override specific ones
|
|
111
|
+
.on(State.Active, Event.Update, ({ state, event }) =>
|
|
112
|
+
State.Active.derive(state, { count: event.count })
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// Cross-state: picks only target fields from source
|
|
116
|
+
.on(State.Processing, Event.Ship, ({ state, event }) =>
|
|
117
|
+
State.Shipped.derive(state, { trackingId: event.trackingId })
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Multi-State Transitions
|
|
122
|
+
|
|
123
|
+
Handle the same event from multiple states:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// Array of states — handler receives union type
|
|
127
|
+
.on([State.Draft, State.Review], Event.Cancel, () => State.Cancelled)
|
|
128
|
+
|
|
129
|
+
// Wildcard — fires from any state (specific .on() takes priority)
|
|
130
|
+
.onAny(Event.Cancel, () => State.Cancelled)
|
|
131
|
+
```
|
|
132
|
+
|
|
105
133
|
### Guards and Effects as Slots
|
|
106
134
|
|
|
107
135
|
Define parameterized guards and effects, provide implementations:
|
|
@@ -126,7 +154,7 @@ machine
|
|
|
126
154
|
)
|
|
127
155
|
// Fetch runs when entering Loading, auto-cancelled if state changes
|
|
128
156
|
.spawn(MyState.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
|
|
129
|
-
.
|
|
157
|
+
.build({
|
|
130
158
|
canRetry: ({ max }, { state }) => state.attempts < max,
|
|
131
159
|
fetchData: ({ url }, { self }) =>
|
|
132
160
|
Effect.gen(function* () {
|
|
@@ -189,49 +217,62 @@ See the [primer](./primer/) for comprehensive documentation:
|
|
|
189
217
|
|
|
190
218
|
### Building
|
|
191
219
|
|
|
192
|
-
| Method | Purpose
|
|
193
|
-
| ----------------------------------------- |
|
|
194
|
-
| `Machine.make({ state, event, initial })` | Create machine
|
|
195
|
-
| `.on(State.X, Event.Y, handler)` | Add transition
|
|
196
|
-
| `.
|
|
197
|
-
| `.
|
|
198
|
-
| `.
|
|
199
|
-
| `.
|
|
200
|
-
| `.
|
|
201
|
-
| `.
|
|
202
|
-
| `.
|
|
220
|
+
| Method | Purpose |
|
|
221
|
+
| ----------------------------------------- | ----------------------------------------------------------- |
|
|
222
|
+
| `Machine.make({ state, event, initial })` | Create machine |
|
|
223
|
+
| `.on(State.X, Event.Y, handler)` | Add transition |
|
|
224
|
+
| `.on([State.X, State.Y], Event.Z, h)` | Multi-state transition |
|
|
225
|
+
| `.onAny(Event.X, handler)` | Wildcard transition (any state) |
|
|
226
|
+
| `.reenter(State.X, Event.Y, handler)` | Force re-entry on same state |
|
|
227
|
+
| `.spawn(State.X, handler)` | State-scoped effect |
|
|
228
|
+
| `.task(State.X, run, { onSuccess })` | State-scoped task |
|
|
229
|
+
| `.background(handler)` | Machine-lifetime effect |
|
|
230
|
+
| `.final(State.X)` | Mark final state |
|
|
231
|
+
| `.build({ slot: impl })` | Provide implementations, returns `BuiltMachine` (terminal) |
|
|
232
|
+
| `.build()` | Finalize no-slot machine, returns `BuiltMachine` (terminal) |
|
|
233
|
+
| `.persist(config)` | Enable persistence |
|
|
234
|
+
|
|
235
|
+
### State Constructors
|
|
236
|
+
|
|
237
|
+
| Method | Purpose |
|
|
238
|
+
| -------------------------------------- | ------------------------------ |
|
|
239
|
+
| `State.X.derive(source)` | Pick target fields from source |
|
|
240
|
+
| `State.X.derive(source, { field: v })` | Pick fields + apply overrides |
|
|
241
|
+
| `State.$is("X")(value)` | Type guard |
|
|
242
|
+
| `State.$match(value, { X: fn, ... })` | Pattern matching |
|
|
203
243
|
|
|
204
244
|
### Running
|
|
205
245
|
|
|
206
|
-
| Method | Purpose
|
|
207
|
-
| ---------------------------- |
|
|
208
|
-
| `Machine.spawn(machine)` |
|
|
209
|
-
| `Machine.spawn(machine, id)` |
|
|
210
|
-
| `system.spawn(id, machine)` |
|
|
246
|
+
| Method | Purpose |
|
|
247
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
248
|
+
| `Machine.spawn(machine)` | Single actor, no registry. Caller manages lifetime via `actor.stop`. Auto-cleans up if `Scope` present. |
|
|
249
|
+
| `Machine.spawn(machine, id)` | Same as above with custom ID |
|
|
250
|
+
| `system.spawn(id, machine)` | Registry, lookup by ID, bulk ops, persistence. Cleans up on system teardown. |
|
|
211
251
|
|
|
212
252
|
### Testing
|
|
213
253
|
|
|
214
|
-
| Function | Description
|
|
215
|
-
| ------------------------------------------ |
|
|
216
|
-
| `simulate(machine, events)` | Run events, get all states |
|
|
217
|
-
| `createTestHarness(machine)` | Step-by-step testing |
|
|
218
|
-
| `assertPath(machine, events, path)` | Assert exact path
|
|
219
|
-
| `assertReaches(machine, events, tag)` | Assert final state
|
|
220
|
-
| `assertNeverReaches(machine, events, tag)` | Assert state never visited
|
|
254
|
+
| Function | Description |
|
|
255
|
+
| ------------------------------------------ | ---------------------------------------------------------------- |
|
|
256
|
+
| `simulate(machine, events)` | Run events, get all states (accepts `Machine` or `BuiltMachine`) |
|
|
257
|
+
| `createTestHarness(machine)` | Step-by-step testing (accepts `Machine` or `BuiltMachine`) |
|
|
258
|
+
| `assertPath(machine, events, path)` | Assert exact path |
|
|
259
|
+
| `assertReaches(machine, events, tag)` | Assert final state |
|
|
260
|
+
| `assertNeverReaches(machine, events, tag)` | Assert state never visited |
|
|
221
261
|
|
|
222
262
|
### Actor
|
|
223
263
|
|
|
224
|
-
| Method
|
|
225
|
-
|
|
|
226
|
-
| `actor.send(event)`
|
|
227
|
-
| `actor.
|
|
228
|
-
| `actor.
|
|
229
|
-
| `actor.
|
|
230
|
-
| `actor.
|
|
231
|
-
| `actor.
|
|
232
|
-
| `actor.
|
|
233
|
-
| `actor.
|
|
234
|
-
| `actor.
|
|
264
|
+
| Method | Description |
|
|
265
|
+
| -------------------------------- | ---------------------------------- |
|
|
266
|
+
| `actor.send(event)` | Queue event |
|
|
267
|
+
| `actor.sendSync(event)` | Fire-and-forget (sync, for UI) |
|
|
268
|
+
| `actor.snapshot` | Get current state |
|
|
269
|
+
| `actor.matches(tag)` | Check state tag |
|
|
270
|
+
| `actor.can(event)` | Can handle event? |
|
|
271
|
+
| `actor.changes` | Stream of changes |
|
|
272
|
+
| `actor.waitFor(State.X)` | Wait for state (constructor or fn) |
|
|
273
|
+
| `actor.awaitFinal` | Wait final state |
|
|
274
|
+
| `actor.sendAndWait(ev, State.X)` | Send + wait for state |
|
|
275
|
+
| `actor.subscribe(fn)` | Sync callback |
|
|
235
276
|
|
|
236
277
|
## License
|
|
237
278
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/cevr/effect-machine.git"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"fmt:check": "oxfmt --check",
|
|
26
26
|
"test": "bun test",
|
|
27
27
|
"test:watch": "bun test --watch",
|
|
28
|
-
"gate": "concurrently -n type,lint,fmt,test -c blue,yellow,magenta,green \"bun run typecheck\" \"bun run lint\" \"bun run fmt\" \"bun run test\"",
|
|
28
|
+
"gate": "concurrently -n type,lint,fmt,test -c blue,yellow,magenta,green \"bun run typecheck\" \"bun run lint:fix\" \"bun run fmt\" \"bun run test\"",
|
|
29
29
|
"prepare": "lefthook install || true && effect-language-service patch",
|
|
30
30
|
"version": "changeset version",
|
|
31
31
|
"release": "changeset publish"
|