@@ -5,10 +5,9 @@ Type-safe state machines for Effect. XState-inspired API with full Effect integr
55## Features
66
77- ** Schema-first** - ` State ` and ` Event ` ARE schemas. Single source of truth for types and serialization
8- - ** Fluent builder** - Chain ` .on() ` , ` .always () ` , ` .delay() ` etc. with full type inference
8+ - ** Fluent builder** - Chain ` .on() ` , ` .spawn () ` , ` .delay() ` etc. with full type inference
99- ** Type-safe transitions** - types inferred from schemas, no manual type params needed
1010- ** Guard composition** - ` Guard.and ` , ` Guard.or ` , ` Guard.not `
11- - ** Eventless transitions** - ` always ` for computed state changes
1211- ** Delayed transitions** - ` delay ` with TestClock support
1312- ** Actor model** - spawn machines as actors with lifecycle management
1413- ** Persistence** - snapshot and event sourcing with pluggable adapters
@@ -70,41 +69,40 @@ Effect.runPromise(
7069);
7170```
7271
73- ## Effect Slots
72+ ## State Effects with spawn
7473
75- Effects ( ` invoke ` , ` onEnter ` , ` onExit ` ) use named slots. Provide handlers via ` .provide() ` :
74+ Use ` .spawn() ` for state-scoped effects. Use ` Effect.addFinalizer ` for cleanup :
7675
7776``` typescript
78- // Define machine with effect slots
79- const baseMachine = Machine .make ({
77+ const machine = Machine .make ({
8078 state: MyState ,
8179 event: MyEvent ,
8280 initial: MyState .Idle ,
8381})
8482 .on (MyState .Idle , MyEvent .Fetch , ({ event }) => MyState .Loading ({ url: event .url }))
8583 .on (MyState .Loading , MyEvent .Resolve , ({ event }) => MyState .Success ({ data: event .data }))
86- .invoke (MyState .Loading , " fetchData" )
87- .onEnter (MyState .Success , " notifyUser" )
88- .final (MyState .Success );
89-
90- // Production: provide real implementations
91- const machine = baseMachine .provide ({
92- fetchData : ({ state , self }) =>
84+ .spawn (MyState .Loading , ({ state , self }) =>
9385 Effect .gen (function * () {
86+ // Cleanup runs when state exits
87+ yield * Effect .addFinalizer (() => Effect .log (" Leaving Loading" ));
88+
89+ // Main work - auto-cancelled on state exit
9490 const data = yield * fetchFromApi (state .url );
9591 yield * self .send (MyEvent .Resolve ({ data }));
9692 }),
97- notifyUser : ({ state }) => Effect .log (` Success: ${state .data } ` ),
98- });
93+ )
94+ .final (MyState .Success );
95+ ```
9996
100- // Test: provide mock implementations
101- const testMachine = baseMachine .provide ({
102- fetchData : ({ self }) => self .send (MyEvent .Resolve ({ data: " mock" })),
103- notifyUser : () => Effect .void ,
104- });
97+ Use ` .background() ` for machine-lifetime effects:
98+
99+ ``` typescript
100+ machine .background (" heartbeat" , ({ self }) =>
101+ Effect .forever (Effect .sleep (" 30 seconds" ).pipe (Effect .andThen (self .send (MyEvent .Ping )))),
102+ );
105103```
106104
107- ` simulate() ` works without providing effects (pure transitions only).
105+ ` simulate() ` works without spawn/background effects (pure transitions only).
108106
109107## Persistence
110108
@@ -149,61 +147,50 @@ See the [primer](./primer/) for comprehensive documentation:
149147
150148### Fluent Methods
151149
152- | Method | Description |
153- | ------------ | ------------------------------------------------ |
154- | ` .on() ` | Add state/event transition |
155- | ` .reenter() ` | Transition with forced reentry (runs exit/enter) |
156- | ` .always() ` | Eventless transitions with guard cascade |
157- | ` .delay() ` | Schedule event after duration |
158- | ` .invoke() ` | Register invoke slot (state-scoped or root) |
159- | ` .onEnter() ` | Register entry effect slot |
160- | ` .onExit() ` | Register exit effect slot |
161- | ` .provide() ` | Wire effect handlers to named slots |
162- | ` .final() ` | Mark state as final |
163- | ` .persist() ` | Add persistence (schemas from machine) |
164-
165- ### Helpers
166-
167- | Export | Description |
168- | ---------------- | -------------------------------- |
169- | ` Machine.assign ` | Helper for partial state updates |
150+ | Method | Description |
151+ | --------------- | ------------------------------------------------ |
152+ | ` .on() ` | Add state/event transition |
153+ | ` .reenter() ` | Transition with forced reentry (runs exit/enter) |
154+ | ` .delay() ` | Schedule event after duration |
155+ | ` .spawn() ` | State-scoped effect (cancelled on exit) |
156+ | ` .background() ` | Machine-lifetime effect |
157+ | ` .provide() ` | Wire handlers to guard/effect slots |
158+ | ` .final() ` | Mark state as final |
159+ | ` .persist() ` | Add persistence (schemas from machine) |
170160
171161### Guards
172162
173- | Export | Description |
174- | ------------ | ---------------------------------- |
175- | ` Guard.make ` | Create named guard slot |
176- | ` Guard.and ` | Combine guards with AND (parallel) |
177- | ` Guard.or ` | Combine guards with OR (parallel) |
178- | ` Guard.not ` | Negate a guard |
163+ | Export | Description |
164+ | ------------- | ---------------------------------- |
165+ | ` Slot.Guards ` | Define guard slots with params |
166+ | ` Guard.and ` | Combine guards with AND (parallel) |
167+ | ` Guard.or ` | Combine guards with OR (parallel) |
168+ | ` Guard.not ` | Negate a guard |
179169
180- Guards are ** slots-only** - declare with name , provide implementation via ` .provide() ` :
170+ Guards are ** slots-only** - declare with schema params , provide implementation via ` .provide() ` :
181171
182172``` typescript
183- // Declare guard slot
184- const canRetry = Guard .make (" canRetry" );
185-
186- // Composition with string shorthand
187- Guard .and (" isAdmin" , " isActive" );
188- Guard .or (" isOwner" , " isAdmin" );
189- Guard .and (Guard .or (" isAdmin" , " isMod" ), " isActive" ); // nested
190-
191- // Use in transition
192- machine .on (State .Idle , Event .Start , () => State .Running , {
193- guard: canRetry ,
173+ const MyGuards = Slot .Guards ({
174+ canRetry: { max: Schema .Number },
194175});
195176
196- // Provide implementation (sync boolean or async Effect<boolean>)
197- const provided = machine .provide ({
198- canRetry : ({ state }) => state .retries < 3 , // sync
199- hasPermission : (
200- { state }, // async
201- ) =>
177+ const machine = Machine .make ({
178+ state: MyState ,
179+ event: MyEvent ,
180+ guards: MyGuards ,
181+ initial: MyState .Idle ,
182+ })
183+ .on (MyState .Error , MyEvent .Retry , ({ state , guards }) =>
202184 Effect .gen (function * () {
203- const auth = yield * AuthService ;
204- return yield * auth .check (state .userId );
185+ if (yield * guards .canRetry ({ max: 3 })) {
186+ return MyState .Retrying ;
187+ }
188+ return MyState .Failed ;
205189 }),
206- });
190+ )
191+ .provide ({
192+ canRetry : ({ max }, { state }) => state .attempts < max ,
193+ });
207194```
208195
209196### Testing
@@ -213,7 +200,6 @@ const provided = machine.provide({
213200| ` simulate ` | Run events and get all states |
214201| ` createTestHarness ` | Step-by-step testing |
215202| ` assertReaches ` | Assert machine reaches state |
216- | ` yieldFibers ` | Yield to background fibers |
217203
218204### Actors
219205
0 commit comments