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 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
- OrderState.Processing({ orderId: state.orderId }),
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
- .on(OrderState.Processing, OrderEvent.Ship, ({ event }) =>
61
- OrderState.Shipped({ trackingId: event.trackingId }),
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.snapshot;
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(Effect.scoped(program));
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
- .provide({
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
- | `.reenter(State.X, Event.Y, handler)` | Force re-entry on same state |
197
- | `.spawn(State.X, handler)` | State-scoped effect |
198
- | `.task(State.X, run, { onSuccess })` | State-scoped task |
199
- | `.background(handler)` | Machine-lifetime effect |
200
- | `.provide({ slot: impl })` | Provide implementations |
201
- | `.final(State.X)` | Mark final state |
202
- | `.persist(config)` | Enable persistence |
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)` | Spawn actor (simple, no registry) |
209
- | `Machine.spawn(machine, id)` | Spawn actor with custom ID |
210
- | `system.spawn(id, machine)` | Spawn via ActorSystem (registry/persist) |
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 | Description |
225
- | --------------------- | ----------------- |
226
- | `actor.send(event)` | Queue event |
227
- | `actor.snapshot` | Get current state |
228
- | `actor.matches(tag)` | Check state tag |
229
- | `actor.can(event)` | Can handle event? |
230
- | `actor.changes` | Stream of changes |
231
- | `actor.waitFor(fn)` | Wait for match |
232
- | `actor.awaitFinal` | Wait final state |
233
- | `actor.sendAndWait` | Send + wait |
234
- | `actor.subscribe(fn)` | Sync callback |
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.2.4",
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"