@typeonce/effect-machine 0.31.2 → 0.32.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 +145 -140
- package/dist/Machine.d.ts +428 -254
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +91 -111
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +3 -4
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +0 -2
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/childRegistry.d.ts +52 -0
- package/dist/internal/machine/childRegistry.d.ts.map +1 -0
- package/dist/internal/machine/childRegistry.js +74 -0
- package/dist/internal/machine/childRegistry.js.map +1 -0
- package/dist/internal/machine/cluster.d.ts +2 -3
- package/dist/internal/machine/cluster.d.ts.map +1 -1
- package/dist/internal/machine/cluster.js.map +1 -1
- package/dist/internal/machine/configuration.d.ts.map +1 -1
- package/dist/internal/machine/configuration.js +14 -4
- package/dist/internal/machine/configuration.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts +1 -0
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +97 -53
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +4 -2
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/invocationDefinition.d.ts +35 -0
- package/dist/internal/machine/invocationDefinition.d.ts.map +1 -0
- package/dist/internal/machine/invocationDefinition.js +21 -0
- package/dist/internal/machine/invocationDefinition.js.map +1 -0
- package/dist/internal/machine/invocationEvent.d.ts +0 -2
- package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
- package/dist/internal/machine/invocationEvent.js +0 -8
- package/dist/internal/machine/invocationEvent.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +21 -43
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +98 -374
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +13 -19
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +63 -35
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/process.d.ts +6 -8
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +1 -0
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/protocol.d.ts +2 -2
- package/dist/internal/machine/protocol.d.ts.map +1 -1
- package/dist/internal/machine/protocol.js +4 -18
- package/dist/internal/machine/protocol.js.map +1 -1
- package/dist/internal/machine/readiness.d.ts +1 -1
- package/dist/internal/machine/readiness.d.ts.map +1 -1
- package/dist/internal/machine/requirements.d.ts +4 -0
- package/dist/internal/machine/requirements.d.ts.map +1 -0
- package/dist/internal/machine/requirements.js +2 -0
- package/dist/internal/machine/requirements.js.map +1 -0
- package/dist/internal/machine/runtime.d.ts +4 -58
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +1 -69
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/serialization.d.ts.map +1 -1
- package/dist/internal/machine/serialization.js +16 -0
- package/dist/internal/machine/serialization.js.map +1 -1
- package/dist/internal/machine/stateDefinition.d.ts +3 -1
- package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
- package/dist/internal/machine/stateDefinition.js +36 -2
- package/dist/internal/machine/stateDefinition.js.map +1 -1
- package/dist/internal/machine/targetBuilder.d.ts +16 -0
- package/dist/internal/machine/targetBuilder.d.ts.map +1 -0
- package/dist/internal/machine/targetBuilder.js +333 -0
- package/dist/internal/machine/targetBuilder.js.map +1 -0
- package/dist/internal/machine/topology.js +3 -3
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/machine/transition.d.ts +16 -0
- package/dist/internal/machine/transition.d.ts.map +1 -0
- package/dist/internal/machine/transition.js +2 -0
- package/dist/internal/machine/transition.js.map +1 -0
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +10 -12
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/format.d.ts +7 -0
- package/dist/internal/testing/machine/format.d.ts.map +1 -0
- package/dist/internal/testing/machine/format.js +104 -0
- package/dist/internal/testing/machine/format.js.map +1 -0
- package/dist/internal/testing/machine/probe.d.ts +1 -1
- package/dist/internal/testing/machine/probe.d.ts.map +1 -1
- package/dist/internal/testing/machine/probe.js +1 -1
- package/dist/internal/testing/machine/probe.js.map +1 -1
- package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/referenceModel.js +15 -19
- package/dist/internal/testing/machine/referenceModel.js.map +1 -1
- package/dist/internal/testing/machine/trace.d.ts +1 -1
- package/dist/internal/testing/machine/trace.d.ts.map +1 -1
- package/dist/internal/testing/machine/trace.js +3 -3
- package/dist/internal/testing/machine/trace.js.map +1 -1
- package/dist/internal/testing/machine/value.d.ts +8 -0
- package/dist/internal/testing/machine/value.d.ts.map +1 -0
- package/dist/internal/testing/machine/value.js +82 -0
- package/dist/internal/testing/machine/value.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts +2 -13
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +21 -113
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +89 -104
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +70 -84
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +10 -13
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +5 -8
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +26 -61
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +17 -26
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +45 -45
- package/docs/effect-atom-react.md +35 -63
- package/docs/machine-review.md +7 -7
- package/docs/root-api.md +214 -0
- package/package.json +1 -1
- package/src/Machine.ts +778 -437
- package/src/internal/machine/atom.ts +3 -13
- package/src/internal/machine/childRegistry.ts +141 -0
- package/src/internal/machine/cluster.ts +5 -14
- package/src/internal/machine/configuration.ts +20 -4
- package/src/internal/machine/executionPlan.ts +109 -52
- package/src/internal/machine/invocation.ts +10 -6
- package/src/internal/machine/invocationDefinition.ts +52 -0
- package/src/internal/machine/invocationEvent.ts +0 -8
- package/src/internal/machine/machine.ts +154 -679
- package/src/internal/machine/planner.ts +74 -62
- package/src/internal/machine/process.ts +14 -21
- package/src/internal/machine/protocol.ts +9 -32
- package/src/internal/machine/readiness.ts +1 -0
- package/src/internal/machine/requirements.ts +11 -0
- package/src/internal/machine/runtime.ts +17 -186
- package/src/internal/machine/serialization.ts +22 -0
- package/src/internal/machine/stateDefinition.ts +36 -3
- package/src/internal/machine/targetBuilder.ts +525 -0
- package/src/internal/machine/topology.ts +3 -3
- package/src/internal/machine/transition.ts +26 -0
- package/src/internal/testing/machine/finiteModel.ts +22 -23
- package/src/internal/testing/machine/format.ts +120 -0
- package/src/internal/testing/machine/probe.ts +4 -4
- package/src/internal/testing/machine/referenceModel.ts +25 -17
- package/src/internal/testing/machine/trace.ts +4 -4
- package/src/internal/testing/machine/value.ts +77 -0
- package/src/internal/testing/machine/verification.ts +904 -1109
- package/src/testing/MachineTest.ts +103 -114
- package/src/unstable/cluster/ClusterMachine.ts +10 -20
- package/src/unstable/reactivity/AtomMachine.ts +42 -79
package/docs/agent-guide.md
CHANGED
|
@@ -22,12 +22,12 @@ const CounterState = Schema.TaggedUnion({
|
|
|
22
22
|
Running: { count: Schema.Number }
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
export const CounterStates = Machine.
|
|
25
|
+
export const CounterStates = Machine.state({ initial: "Idle", states: {
|
|
26
26
|
Idle: {},
|
|
27
27
|
Running: CounterState.cases.Running
|
|
28
|
-
})
|
|
28
|
+
} })
|
|
29
29
|
|
|
30
|
-
export const CounterEvents = Machine.
|
|
30
|
+
export const CounterEvents = Machine.eventsFromSchemas(
|
|
31
31
|
Schema.TaggedUnion({
|
|
32
32
|
Start: {},
|
|
33
33
|
Increment: {},
|
|
@@ -37,27 +37,27 @@ export const CounterEvents = Machine.events(
|
|
|
37
37
|
|
|
38
38
|
export const CounterMachine = Machine.make({
|
|
39
39
|
id: "Counter",
|
|
40
|
-
|
|
40
|
+
root: CounterStates,
|
|
41
41
|
events: CounterEvents,
|
|
42
|
-
|
|
43
|
-
}).handle({
|
|
42
|
+
initialConfiguration: root => root.resolve(({ target }) => target.from(to => to.Idle.from()))
|
|
43
|
+
}).handle({ states: {
|
|
44
44
|
Idle: {
|
|
45
45
|
on: {
|
|
46
|
-
Start: (to) => to.
|
|
46
|
+
Start: (to) => to.branch.Running().resolve(({ target }) => target.from({ count: 0 }))
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
Running: {
|
|
50
50
|
on: {
|
|
51
|
-
Increment: (to) => to.
|
|
52
|
-
Stop: (to) => to.
|
|
51
|
+
Increment: (to) => to.branch.Running().resolve(({ state, target }) => target.from({ count: state.count + 1 })),
|
|
52
|
+
Stop: (to) => to.branch.Idle()
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
})
|
|
55
|
+
} })
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Each step has one job:
|
|
59
59
|
|
|
60
|
-
- `Machine.
|
|
60
|
+
- `Machine.state` declares the root, its child topology, and state-owned data.
|
|
61
61
|
- `Machine.events` declares the public messages the machine accepts and returns
|
|
62
62
|
typed event constructors.
|
|
63
63
|
- `Machine.make` joins the state tree, event protocol, input, and initial state.
|
|
@@ -106,12 +106,12 @@ const RequestState = Schema.TaggedUnion({
|
|
|
106
106
|
Failed: { message: Schema.String }
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
-
const RequestStates = Machine.
|
|
109
|
+
const RequestStates = Machine.state({ initial: "Idle", states: {
|
|
110
110
|
Idle: {},
|
|
111
111
|
Loading: {},
|
|
112
112
|
Ready: RequestState.cases.Ready,
|
|
113
113
|
Failed: RequestState.cases.Failed
|
|
114
|
-
})
|
|
114
|
+
} })
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
The machine can now be `Loading`, `Ready`, or `Failed`. It cannot construct a
|
|
@@ -139,7 +139,7 @@ const DocumentState = Schema.TaggedUnion({
|
|
|
139
139
|
}
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
-
const DocumentStates = Machine.
|
|
142
|
+
const DocumentStates = Machine.state({ initial: "Closed", states: {
|
|
143
143
|
Closed: {},
|
|
144
144
|
Open: {
|
|
145
145
|
// Editing, Saving, and SaveFailed all need the document and draft.
|
|
@@ -152,7 +152,7 @@ const DocumentStates = Machine.states({
|
|
|
152
152
|
SaveFailed: DocumentState.cases.SaveFailed
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
-
})
|
|
155
|
+
} })
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
Do not copy `documentId` and `draft` into every child. Copies can disagree after
|
|
@@ -165,22 +165,22 @@ Hierarchy owns behavior as well as data. Define a transition on the lowest
|
|
|
165
165
|
compound state whose children share it.
|
|
166
166
|
|
|
167
167
|
```ts
|
|
168
|
-
const DocumentEvents = Machine.
|
|
168
|
+
const DocumentEvents = Machine.eventsFromSchemas(
|
|
169
169
|
Schema.TaggedUnion({
|
|
170
170
|
Close: {}
|
|
171
171
|
})
|
|
172
172
|
)
|
|
173
173
|
|
|
174
174
|
const DocumentMachine = Machine.make({
|
|
175
|
-
|
|
175
|
+
root: DocumentStates,
|
|
176
176
|
events: DocumentEvents,
|
|
177
177
|
initial: (to) => to.Closed()
|
|
178
|
-
}).handle({
|
|
178
|
+
}).handle({ states: {
|
|
179
179
|
Closed: {},
|
|
180
180
|
Open: {
|
|
181
181
|
on: {
|
|
182
182
|
// All Open children close the document in the same way.
|
|
183
|
-
Close: (to) => to.
|
|
183
|
+
Close: (to) => to.branch.Closed()
|
|
184
184
|
},
|
|
185
185
|
states: {
|
|
186
186
|
Editing: {},
|
|
@@ -188,7 +188,7 @@ const DocumentMachine = Machine.make({
|
|
|
188
188
|
SaveFailed: {}
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
})
|
|
191
|
+
} })
|
|
192
192
|
```
|
|
193
193
|
|
|
194
194
|
The machine checks the deepest active state first, then its ancestors. Put a
|
|
@@ -202,7 +202,7 @@ after domain actions and outcomes. Do not expose state setters such as
|
|
|
202
202
|
`SetLoading` or `SetError`.
|
|
203
203
|
|
|
204
204
|
```ts
|
|
205
|
-
export const CheckoutEvents = Machine.
|
|
205
|
+
export const CheckoutEvents = Machine.eventsFromSchemas(
|
|
206
206
|
Schema.TaggedUnion({
|
|
207
207
|
Submit: {},
|
|
208
208
|
Cancel: {}
|
|
@@ -210,23 +210,23 @@ export const CheckoutEvents = Machine.events(
|
|
|
210
210
|
)
|
|
211
211
|
|
|
212
212
|
const CheckoutMachine = Machine.make({
|
|
213
|
-
|
|
213
|
+
root: CheckoutStates,
|
|
214
214
|
events: CheckoutEvents,
|
|
215
215
|
initial: (to) => to.Editing()
|
|
216
|
-
}).handle({
|
|
216
|
+
}).handle({ states: {
|
|
217
217
|
Editing: {
|
|
218
218
|
on: {
|
|
219
|
-
Submit: (to) => to.
|
|
219
|
+
Submit: (to) => to.branch.Submitting()
|
|
220
220
|
}
|
|
221
221
|
},
|
|
222
222
|
Submitting: {
|
|
223
223
|
on: {
|
|
224
224
|
// Cancel has meaning while work is in progress.
|
|
225
|
-
Cancel: (to) => to.
|
|
225
|
+
Cancel: (to) => to.branch.Editing()
|
|
226
226
|
}
|
|
227
227
|
},
|
|
228
228
|
Complete: {}
|
|
229
|
-
})
|
|
229
|
+
} })
|
|
230
230
|
```
|
|
231
231
|
|
|
232
232
|
The sender requests `Submit`. The machine decides whether `Submit` has a
|
|
@@ -243,7 +243,7 @@ child in every region. A parallel model therefore accepts the full product of
|
|
|
243
243
|
those regions.
|
|
244
244
|
|
|
245
245
|
```ts
|
|
246
|
-
const ScreenStates = Machine.
|
|
246
|
+
const ScreenStates = Machine.state({ initial: "Screen", states: {
|
|
247
247
|
Screen: {
|
|
248
248
|
type: "parallel",
|
|
249
249
|
states: {
|
|
@@ -263,7 +263,7 @@ const ScreenStates = Machine.states({
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
-
})
|
|
266
|
+
} })
|
|
267
267
|
```
|
|
268
268
|
|
|
269
269
|
This model permits all four combinations: online with a closed panel, online
|
|
@@ -286,29 +286,29 @@ const LoadState = Schema.TaggedUnion({
|
|
|
286
286
|
Failed: { message: Schema.String }
|
|
287
287
|
})
|
|
288
288
|
|
|
289
|
-
const LoadStates = Machine.
|
|
289
|
+
const LoadStates = Machine.state({ initial: "Idle", states: {
|
|
290
290
|
Idle: {},
|
|
291
291
|
Loading: LoadState.cases.Loading,
|
|
292
292
|
Ready: LoadState.cases.Ready,
|
|
293
293
|
Failed: LoadState.cases.Failed
|
|
294
|
-
})
|
|
294
|
+
} })
|
|
295
295
|
|
|
296
296
|
const LoadMachine = Machine.make({
|
|
297
|
-
|
|
298
|
-
events: Machine.
|
|
299
|
-
|
|
300
|
-
}).handle({
|
|
297
|
+
root: LoadStates,
|
|
298
|
+
events: Machine.eventsFromSchemas(),
|
|
299
|
+
initialConfiguration: root => root.resolve(({ target }) => target.from(to => to.Idle.from()))
|
|
300
|
+
}).handle({ states: {
|
|
301
301
|
Idle: {},
|
|
302
302
|
Loading: {
|
|
303
303
|
invoke: (from) =>
|
|
304
304
|
from
|
|
305
305
|
.effect("load-document", ({ state }) => loadDocument(state.documentId))
|
|
306
|
-
.onDone((to) => to.
|
|
307
|
-
.onFailure((to) => to.
|
|
306
|
+
.onDone((to) => to.branch.Ready().resolve(({ output, target }) => target.from({ content: output })))
|
|
307
|
+
.onFailure((to) => to.branch.Failed().resolve(({ error, target }) => target.from({ message: String(error) })))
|
|
308
308
|
},
|
|
309
309
|
Ready: {},
|
|
310
310
|
Failed: {}
|
|
311
|
-
})
|
|
311
|
+
} })
|
|
312
312
|
```
|
|
313
313
|
|
|
314
314
|
`loadDocument` may require Effect services. Those requirements remain on the
|
|
@@ -356,8 +356,8 @@ Commissioning: {
|
|
|
356
356
|
{ discard: true }
|
|
357
357
|
)
|
|
358
358
|
)
|
|
359
|
-
.onDone((to) => to.
|
|
360
|
-
.onFailure((to) => to.
|
|
359
|
+
.onDone((to) => to.branch.Running())
|
|
360
|
+
.onFailure((to) => to.branch.Failed())
|
|
361
361
|
}
|
|
362
362
|
```
|
|
363
363
|
|
|
@@ -374,24 +374,24 @@ A transition should choose the next state from the current snapshot and event.
|
|
|
374
374
|
Use ordinary TypeScript conditions when one event has several valid outcomes.
|
|
375
375
|
|
|
376
376
|
```ts
|
|
377
|
-
const ReviewEvents = Machine.
|
|
377
|
+
const ReviewEvents = Machine.eventsFromSchemas(
|
|
378
378
|
Schema.TaggedUnion({
|
|
379
379
|
Evaluate: { score: Schema.Number }
|
|
380
380
|
})
|
|
381
381
|
)
|
|
382
382
|
|
|
383
383
|
const ReviewMachine = Machine.make({
|
|
384
|
-
|
|
384
|
+
root: ReviewStates,
|
|
385
385
|
events: ReviewEvents,
|
|
386
386
|
initial: (to) => to.Pending()
|
|
387
|
-
}).handle({
|
|
387
|
+
}).handle({ states: {
|
|
388
388
|
Pending: {
|
|
389
389
|
on: {
|
|
390
390
|
Evaluate: (to) =>
|
|
391
391
|
to
|
|
392
392
|
.branches({
|
|
393
|
-
accepted: { target: to.
|
|
394
|
-
rejected: { target: to.
|
|
393
|
+
accepted: { target: to.branch.Accepted() },
|
|
394
|
+
rejected: { target: to.branch.Rejected() }
|
|
395
395
|
})
|
|
396
396
|
.resolve(({ event, select }) =>
|
|
397
397
|
event.score >= 80
|
|
@@ -402,7 +402,7 @@ const ReviewMachine = Machine.make({
|
|
|
402
402
|
},
|
|
403
403
|
Accepted: {},
|
|
404
404
|
Rejected: {}
|
|
405
|
-
})
|
|
405
|
+
} })
|
|
406
406
|
```
|
|
407
407
|
|
|
408
408
|
Given the same snapshot and event, the handler should choose the same result.
|
|
@@ -39,85 +39,61 @@ Service-free machines can use `AtomMachine.make` directly.
|
|
|
39
39
|
|
|
40
40
|
## Own a machine in one React subtree
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
Create a context from a factory. Each Provider owns a fresh bridge in its current
|
|
43
|
+
Atom registry; it does not subscribe to state changes.
|
|
44
44
|
|
|
45
45
|
```tsx
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import { AuthMachine, type AuthMachineInput } from "../machines/auth-machine"
|
|
46
|
+
import { createMachineContext } from "@typeonce/effect-machine-react"
|
|
47
|
+
import { AuthMachine } from "../machines/auth-machine"
|
|
49
48
|
import { MachineAtoms } from "../lib/atom-runtime"
|
|
50
49
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const AuthMachineContext = createContext<AuthMachineAtom | null>(null)
|
|
55
|
-
|
|
56
|
-
export function AuthMachineProvider({
|
|
57
|
-
children,
|
|
58
|
-
input
|
|
59
|
-
}: {
|
|
60
|
-
readonly children: ReactNode
|
|
61
|
-
readonly input: AuthMachineInput
|
|
62
|
-
}) {
|
|
63
|
-
const machine = useMachineAtom(() => makeAuthMachine(input))
|
|
50
|
+
export const Auth = createMachineContext(MachineAtoms.factory(AuthMachine))
|
|
51
|
+
export const useAuthMachine = Auth.useMachine
|
|
64
52
|
|
|
53
|
+
function AuthRoute({ input, attemptId }: Props) {
|
|
65
54
|
return (
|
|
66
|
-
<
|
|
67
|
-
{
|
|
68
|
-
|
|
55
|
+
<Auth.Provider key={attemptId} input={input}>
|
|
56
|
+
<Suspense fallback={<Loading />}>
|
|
57
|
+
<AuthCard />
|
|
58
|
+
</Suspense>
|
|
59
|
+
</Auth.Provider>
|
|
69
60
|
)
|
|
70
61
|
}
|
|
71
|
-
|
|
72
|
-
export function useAuthMachine(): AuthMachineAtom {
|
|
73
|
-
const machine = useContext(AuthMachineContext)
|
|
74
|
-
if (machine === null) {
|
|
75
|
-
throw new Error("useAuthMachine must be used inside AuthMachineProvider")
|
|
76
|
-
}
|
|
77
|
-
return machine
|
|
78
|
-
}
|
|
79
62
|
```
|
|
80
63
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
64
|
+
Input is startup-only. Send events to update the existing workflow; change the
|
|
65
|
+
Provider's React key to create a new one. Keep the Provider above Suspense so
|
|
66
|
+
suspension does not discard the owner. Supply an Atom RegistryProvider at the
|
|
67
|
+
application boundary. The same factory used by two Providers creates isolated
|
|
68
|
+
machines, even when their inputs are equal.
|
|
85
69
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
workflow. Change the provider's React key when React should own a new machine:
|
|
89
|
-
|
|
90
|
-
```tsx
|
|
91
|
-
<AuthMachineProvider key={attemptId} input={input}>
|
|
92
|
-
<AuthCard />
|
|
93
|
-
</AuthMachineProvider>
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Put the owner above a Suspense boundary. React can then retain the same machine
|
|
97
|
-
while a state-reading descendant suspends.
|
|
70
|
+
`useMachineAtom(() => makeMachine(input))` remains available for custom owners.
|
|
71
|
+
It mounts the reference after commit without reading `result` or `snapshot`.
|
|
98
72
|
|
|
99
73
|
## Render state-owned data
|
|
100
74
|
|
|
101
|
-
|
|
75
|
+
Render a typed path in the smallest component that needs it:
|
|
102
76
|
|
|
103
77
|
```tsx
|
|
104
|
-
import {
|
|
105
|
-
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
106
|
-
import { Option } from "effect"
|
|
78
|
+
import { MachineState } from "@typeonce/effect-machine-react"
|
|
107
79
|
|
|
108
80
|
function EditingFields() {
|
|
109
81
|
const machine = useAuthMachine()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
onNone: () => null,
|
|
116
|
-
onSome: ({ value }) => <EmailField email={value.email} />
|
|
117
|
-
})
|
|
82
|
+
return (
|
|
83
|
+
<MachineState machine={machine} path="Editing" inactive={null}>
|
|
84
|
+
{({ value }) => <EmailField email={value.email} />}
|
|
85
|
+
</MachineState>
|
|
86
|
+
)
|
|
118
87
|
}
|
|
119
88
|
```
|
|
120
89
|
|
|
90
|
+
`MachineState` subscribes only its renderer. The path determines the callback's
|
|
91
|
+
snapshot type. Startup suspends, failures reach the nearest error boundary,
|
|
92
|
+
and an inactive path renders `inactive` (null by default).
|
|
93
|
+
|
|
94
|
+
For custom hooks, `useAtomSuspense(AtomMachine.selectSnapshot(machine, path)).value`
|
|
95
|
+
returns an Option of the selected snapshot.
|
|
96
|
+
|
|
121
97
|
`AtomMachine.select` returns the selected state value.
|
|
122
98
|
`AtomMachine.selectSnapshot` also retains the selected state's child topology.
|
|
123
99
|
Both return `Option.none()` while the path is inactive. Do not replace that
|
|
@@ -240,11 +216,7 @@ function AuthScreen() {
|
|
|
240
216
|
const machine = useAuthMachine()
|
|
241
217
|
const state = useAtomSuspense(machine.result).value
|
|
242
218
|
|
|
243
|
-
return
|
|
244
|
-
Editing: (editing) => <EditingScreen state={editing} />,
|
|
245
|
-
Verification: (verification) => <VerificationScreen state={verification} />,
|
|
246
|
-
Failed: (failed) => <FailureScreen state={failed} />
|
|
247
|
-
})
|
|
219
|
+
return <MachineSummary snapshot={state} />
|
|
248
220
|
}
|
|
249
221
|
```
|
|
250
222
|
|
|
@@ -320,7 +292,7 @@ export const plantAtoms = AtomMachine.familyChild(CentralMachineAtom, {
|
|
|
320
292
|
child: (plantId: string) => Plant(plantId),
|
|
321
293
|
atoms: {
|
|
322
294
|
broken: AtomMachine.matchesChild("Broken"),
|
|
323
|
-
state: (plant) => plant.
|
|
295
|
+
state: (plant) => plant.result,
|
|
324
296
|
send: (plant) => plant.send
|
|
325
297
|
}
|
|
326
298
|
})
|
package/docs/machine-review.md
CHANGED
|
@@ -39,12 +39,12 @@ job is returning `target.from()`.
|
|
|
39
39
|
// Redundant
|
|
40
40
|
const handlers = {
|
|
41
41
|
Start: (to) =>
|
|
42
|
-
to.
|
|
42
|
+
to.branch.Running().resolve(({ target }) => target.from())
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Preferred
|
|
46
46
|
const handlers = {
|
|
47
|
-
Start: (to) => to.
|
|
47
|
+
Start: (to) => to.branch.Running()
|
|
48
48
|
}
|
|
49
49
|
```
|
|
50
50
|
|
|
@@ -176,11 +176,11 @@ Model `Submit` as the component-facing event. Let a machine state own the work
|
|
|
176
176
|
and its lifetime:
|
|
177
177
|
|
|
178
178
|
```ts
|
|
179
|
-
machine.handle({
|
|
179
|
+
machine.handle({ states: {
|
|
180
180
|
Editing: {
|
|
181
181
|
on: {
|
|
182
182
|
Submit: (to) =>
|
|
183
|
-
to.
|
|
183
|
+
to.branch.Submitting().resolve(({ event, target }) =>
|
|
184
184
|
target.from({ order: event.order })
|
|
185
185
|
)
|
|
186
186
|
}
|
|
@@ -190,17 +190,17 @@ machine.handle({
|
|
|
190
190
|
from
|
|
191
191
|
.effect("submit-order", ({ state }) => submitOrder(state.order))
|
|
192
192
|
.onDone((to) =>
|
|
193
|
-
to.
|
|
193
|
+
to.branch.Complete().resolve(({ output, target }) =>
|
|
194
194
|
target.from({ order: output })
|
|
195
195
|
)
|
|
196
196
|
)
|
|
197
197
|
.onFailure((to) =>
|
|
198
|
-
to.
|
|
198
|
+
to.branch.Failed().resolve(({ error, target }) =>
|
|
199
199
|
target.from({ message: String(error) })
|
|
200
200
|
)
|
|
201
201
|
)
|
|
202
202
|
}
|
|
203
|
-
})
|
|
203
|
+
} })
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
`submitOrder` can use Effect services for the API request and analytics. The
|
package/docs/root-api.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Root machine API
|
|
2
|
+
|
|
3
|
+
A machine is one explicit root state. The root can own data, child topology,
|
|
4
|
+
both, or neither. There is no separate global context store.
|
|
5
|
+
|
|
6
|
+
## Events and transitions only
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { Machine } from "@typeonce/effect-machine"
|
|
10
|
+
import { Schema } from "effect"
|
|
11
|
+
|
|
12
|
+
const Events = Machine.events({ Open: {}, Close: {} })
|
|
13
|
+
const Root = Machine.state({
|
|
14
|
+
initial: "Closed",
|
|
15
|
+
states: { Closed: {}, Open: {} }
|
|
16
|
+
})
|
|
17
|
+
const Door = Machine.make({ root: Root, events: Events }).handle({
|
|
18
|
+
states: {
|
|
19
|
+
Closed: { on: { Open: (to) => to.local.Open() } },
|
|
20
|
+
Open: { on: { Close: (to) => to.local.Closed() } }
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The root declares the default child once. No startup target, empty schema, or
|
|
26
|
+
resolver is needed. Structural nodes have `value: undefined` in snapshots.
|
|
27
|
+
|
|
28
|
+
## Root data and child data
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const Root = Machine.state({
|
|
32
|
+
fields: { title: Schema.String },
|
|
33
|
+
initial: "Editing",
|
|
34
|
+
states: {
|
|
35
|
+
Editing: { fields: { draft: Schema.String } },
|
|
36
|
+
Saving: {}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
const Events = Machine.events({ Rename: { title: Schema.String }, Save: {} })
|
|
40
|
+
const Editor = Machine.make({
|
|
41
|
+
root: Root,
|
|
42
|
+
events: Events,
|
|
43
|
+
initial: (root) => root.from(() => ({ title: "Untitled" }))
|
|
44
|
+
}).handle({
|
|
45
|
+
initialize: ({ builder, state }) => builder.from({ draft: state.title }),
|
|
46
|
+
on: {
|
|
47
|
+
Rename: (to) => to.self.update.from(({ event }) => ({ title: event.title }))
|
|
48
|
+
},
|
|
49
|
+
states: {
|
|
50
|
+
Editing: { on: { Save: (to) => to.local.Saving() } }
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`initialize` constructs the declared child configuration. Required values must
|
|
56
|
+
be supplied before the machine becomes executable. Values whose schema make
|
|
57
|
+
input is optional can use their schema defaults. A parallel owner's initializer
|
|
58
|
+
constructs every required region through the typed builder.
|
|
59
|
+
|
|
60
|
+
The complete snapshot always includes the root:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// While Editing is active:
|
|
64
|
+
{
|
|
65
|
+
path: "",
|
|
66
|
+
value: { _tag: "", title: "Untitled" },
|
|
67
|
+
state: {
|
|
68
|
+
path: "Editing",
|
|
69
|
+
value: { _tag: "Editing", draft: "Untitled" }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Root handlers remain active across child transitions. Child handlers receive
|
|
75
|
+
their own state, typed ancestors, and the complete snapshot. `to.root.update`
|
|
76
|
+
addresses root data from a child. `to.self.update` addresses the handler owner.
|
|
77
|
+
Both replace the entire value; omitted fields are not silently retained.
|
|
78
|
+
Updates preserve the active topology and do not restart scoped work.
|
|
79
|
+
|
|
80
|
+
Simultaneous transitions retain conflict checking. Two parallel handlers cannot
|
|
81
|
+
silently overwrite the same owner. Use `.updating(owner)` to combine a selected
|
|
82
|
+
destination and a retained owner's value in one atomic transition.
|
|
83
|
+
|
|
84
|
+
## Schemas and reusable descriptors
|
|
85
|
+
|
|
86
|
+
Use `fields` for local data or `schema` for an existing tagged schema. They are
|
|
87
|
+
mutually exclusive. A schema preserves class identity, refinements, defaults,
|
|
88
|
+
and transformations; its tag is independent of its mount path.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
class Ready extends Schema.TaggedClass<Ready>("Ready")("Ready", {
|
|
92
|
+
name: Schema.String
|
|
93
|
+
}) {}
|
|
94
|
+
|
|
95
|
+
const Form = Machine.state({
|
|
96
|
+
initial: "Idle",
|
|
97
|
+
states: { Idle: {}, Ready: { schema: Ready } }
|
|
98
|
+
})
|
|
99
|
+
const Root = Machine.state({
|
|
100
|
+
type: "parallel",
|
|
101
|
+
states: { First: Form, Second: Form }
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Each mount has its own path and lifecycle. Descriptors capture their definition
|
|
106
|
+
without retaining caller-owned mutable topology containers. `Machine.make`
|
|
107
|
+
accepts the complete descriptor as `root`; callers do not extract its internals.
|
|
108
|
+
|
|
109
|
+
`Machine.events({ Save: { title: Schema.String } })` creates a deferred
|
|
110
|
+
`Events.Save({ title })` constructor. The machine validates it when processing
|
|
111
|
+
the event. To reuse tagged classes or a tagged union, use
|
|
112
|
+
`Machine.eventsFromSchemas(...)`. Internal and emitted protocols have distinct
|
|
113
|
+
constructors with the same fields/import distinction. Public sends cannot send
|
|
114
|
+
internal events, and emissions do not become machine input events implicitly.
|
|
115
|
+
|
|
116
|
+
## Default initialization and explicit configuration
|
|
117
|
+
|
|
118
|
+
`initial` supplies root values while following the topology's declared child
|
|
119
|
+
defaults. It does not override those defaults. A structural root usually needs
|
|
120
|
+
no initializer.
|
|
121
|
+
|
|
122
|
+
Use the separately named `initialConfiguration` to select a complete startup
|
|
123
|
+
configuration, for example to start the door open:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const DoorOpen = Machine.make({
|
|
127
|
+
root: Root,
|
|
128
|
+
events: Events,
|
|
129
|
+
initialConfiguration: (root) => root.resolve(({ target }) =>
|
|
130
|
+
target.from((states) => states.Open.from())
|
|
131
|
+
)
|
|
132
|
+
}).handle({})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This example uses the door root and events from the first section. Every active
|
|
136
|
+
value and region in an explicit configuration must be provided. `initial` and
|
|
137
|
+
`initialConfiguration` cannot be combined. Use `Machine.resume` when restoring
|
|
138
|
+
a validated snapshot, including its completion and history metadata.
|
|
139
|
+
|
|
140
|
+
## Construction, guards, and branches
|
|
141
|
+
|
|
142
|
+
A selected target supports a direct callback:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
Save: (to) => to.local.Saving().from(({ event }) => ({ requestId: event.requestId }))
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Use `.decoded(({ state }) => state)` for already decoded schema values. Use
|
|
149
|
+
`.resolve((context, enqueue) => ...)` when constructing child configurations or
|
|
150
|
+
queuing commands; its `context.target.from(...)` and `.decoded(...)` take values
|
|
151
|
+
and explicit child builders.
|
|
152
|
+
|
|
153
|
+
A guard runs before construction and commands:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
Save: (to) => to.local.Saving()
|
|
157
|
+
.guard(({ state }) => state.draft.length > 0)
|
|
158
|
+
.from(({ state }) => ({ requestId: state.draft }))
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A false guard declines this handler, allowing ancestor fallback. Guards are not
|
|
162
|
+
available for mandatory initial resolution or a total choice. `.none` accepts
|
|
163
|
+
an event without changing topology; declining and accepting have different
|
|
164
|
+
semantics. Named branches keep `{ target, title? }`: `target` identifies the
|
|
165
|
+
checked destination and `title` supplies optional presentation metadata.
|
|
166
|
+
|
|
167
|
+
## Completion and history
|
|
168
|
+
|
|
169
|
+
A compound root returns its completed direct workflow's output when that child
|
|
170
|
+
has no `onDone` handler. A child with `onDone` first handles its completion.
|
|
171
|
+
Nested compound states retain statechart completion rules: an arbitrary
|
|
172
|
+
completed descendant does not make every ancestor final. Parallel roots
|
|
173
|
+
complete when all required regions complete.
|
|
174
|
+
|
|
175
|
+
Restoring history beneath the root preserves current root data. Restoring the
|
|
176
|
+
root's own history can restore root-owned data. History fallback builders must
|
|
177
|
+
construct a complete configuration containing the history owner.
|
|
178
|
+
|
|
179
|
+
## Observation and testing
|
|
180
|
+
|
|
181
|
+
- `MachineRef.state` reads the complete logical root snapshot.
|
|
182
|
+
- `MachineRef.snapshot` includes runtime status and completion information.
|
|
183
|
+
- `MachineAtom.result` retains asynchronous startup, failure, and the successful
|
|
184
|
+
logical snapshot. `MachineAtom.snapshot` exposes the runtime snapshot.
|
|
185
|
+
- `AtomMachine.select` selects an optional value; `selectSnapshot` selects an
|
|
186
|
+
optional subtree. Child inactivity remains `Option.none()`.
|
|
187
|
+
- `MachineState` renders a typed path in React. `createMachineContext(factory)`
|
|
188
|
+
owns a separate machine per Provider without subscribing the Provider to it.
|
|
189
|
+
|
|
190
|
+
The Atom bridge and child bridge no longer expose `.state`; use `.result` or a
|
|
191
|
+
path selector. The core `MachineRef.state` contract remains available.
|
|
192
|
+
|
|
193
|
+
`MachineTest.run` and `MachineTest.probe` accept the same public event inputs as
|
|
194
|
+
production sends, including deferred constructors. Traces and acknowledgements
|
|
195
|
+
record the decoded event that was processed, including ignored events.
|
|
196
|
+
|
|
197
|
+
## Migration
|
|
198
|
+
|
|
199
|
+
Replace `Machine.states(tree)` and `Machine.make({ states: States.states })`
|
|
200
|
+
with one `Machine.state({ initial, states: tree })` descriptor and
|
|
201
|
+
`Machine.make({ root })`. Move the old root-level handler map under
|
|
202
|
+
`handle({ states: ... })`; place machine-wide behavior directly in `handle`.
|
|
203
|
+
Use root `fields` to replace a wrapper state introduced only to carry shared data.
|
|
204
|
+
Use `Machine.Snapshot<typeof root>` for the complete logical snapshot. `Machine.StateAccessors` replaces the former `Machine.DefinedStates`
|
|
205
|
+
helper interface; it exposes paths and projections without a public state map.
|
|
206
|
+
|
|
207
|
+
Replace schema arguments to `Machine.events`, `internalEvents`, and
|
|
208
|
+
`emittedEvents` with field records, or use their explicit `FromSchemas`
|
|
209
|
+
constructors. Replace old Atom `.state` reads with `.result`. Use
|
|
210
|
+
`EmittedEvent`, `EmittedEvents`, `EmittedEventOf`, and `SchemaLessStateAnnotations` in place
|
|
211
|
+
of the removed deprecated type aliases.
|
|
212
|
+
|
|
213
|
+
Encoded snapshots now use version 2 and include the root at path `""`.
|
|
214
|
+
Version 1 payloads must be explicitly migrated; the decoder rejects them.
|