@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.
Files changed (151) hide show
  1. package/README.md +145 -140
  2. package/dist/Machine.d.ts +428 -254
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +91 -111
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +3 -4
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +0 -2
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/childRegistry.d.ts +52 -0
  11. package/dist/internal/machine/childRegistry.d.ts.map +1 -0
  12. package/dist/internal/machine/childRegistry.js +74 -0
  13. package/dist/internal/machine/childRegistry.js.map +1 -0
  14. package/dist/internal/machine/cluster.d.ts +2 -3
  15. package/dist/internal/machine/cluster.d.ts.map +1 -1
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/configuration.d.ts.map +1 -1
  18. package/dist/internal/machine/configuration.js +14 -4
  19. package/dist/internal/machine/configuration.js.map +1 -1
  20. package/dist/internal/machine/executionPlan.d.ts +1 -0
  21. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  22. package/dist/internal/machine/executionPlan.js +97 -53
  23. package/dist/internal/machine/executionPlan.js.map +1 -1
  24. package/dist/internal/machine/invocation.d.ts.map +1 -1
  25. package/dist/internal/machine/invocation.js +4 -2
  26. package/dist/internal/machine/invocation.js.map +1 -1
  27. package/dist/internal/machine/invocationDefinition.d.ts +35 -0
  28. package/dist/internal/machine/invocationDefinition.d.ts.map +1 -0
  29. package/dist/internal/machine/invocationDefinition.js +21 -0
  30. package/dist/internal/machine/invocationDefinition.js.map +1 -0
  31. package/dist/internal/machine/invocationEvent.d.ts +0 -2
  32. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  33. package/dist/internal/machine/invocationEvent.js +0 -8
  34. package/dist/internal/machine/invocationEvent.js.map +1 -1
  35. package/dist/internal/machine/machine.d.ts +21 -43
  36. package/dist/internal/machine/machine.d.ts.map +1 -1
  37. package/dist/internal/machine/machine.js +98 -374
  38. package/dist/internal/machine/machine.js.map +1 -1
  39. package/dist/internal/machine/planner.d.ts +13 -19
  40. package/dist/internal/machine/planner.d.ts.map +1 -1
  41. package/dist/internal/machine/planner.js +63 -35
  42. package/dist/internal/machine/planner.js.map +1 -1
  43. package/dist/internal/machine/process.d.ts +6 -8
  44. package/dist/internal/machine/process.d.ts.map +1 -1
  45. package/dist/internal/machine/process.js +1 -0
  46. package/dist/internal/machine/process.js.map +1 -1
  47. package/dist/internal/machine/protocol.d.ts +2 -2
  48. package/dist/internal/machine/protocol.d.ts.map +1 -1
  49. package/dist/internal/machine/protocol.js +4 -18
  50. package/dist/internal/machine/protocol.js.map +1 -1
  51. package/dist/internal/machine/readiness.d.ts +1 -1
  52. package/dist/internal/machine/readiness.d.ts.map +1 -1
  53. package/dist/internal/machine/requirements.d.ts +4 -0
  54. package/dist/internal/machine/requirements.d.ts.map +1 -0
  55. package/dist/internal/machine/requirements.js +2 -0
  56. package/dist/internal/machine/requirements.js.map +1 -0
  57. package/dist/internal/machine/runtime.d.ts +4 -58
  58. package/dist/internal/machine/runtime.d.ts.map +1 -1
  59. package/dist/internal/machine/runtime.js +1 -69
  60. package/dist/internal/machine/runtime.js.map +1 -1
  61. package/dist/internal/machine/serialization.d.ts.map +1 -1
  62. package/dist/internal/machine/serialization.js +16 -0
  63. package/dist/internal/machine/serialization.js.map +1 -1
  64. package/dist/internal/machine/stateDefinition.d.ts +3 -1
  65. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  66. package/dist/internal/machine/stateDefinition.js +36 -2
  67. package/dist/internal/machine/stateDefinition.js.map +1 -1
  68. package/dist/internal/machine/targetBuilder.d.ts +16 -0
  69. package/dist/internal/machine/targetBuilder.d.ts.map +1 -0
  70. package/dist/internal/machine/targetBuilder.js +333 -0
  71. package/dist/internal/machine/targetBuilder.js.map +1 -0
  72. package/dist/internal/machine/topology.js +3 -3
  73. package/dist/internal/machine/topology.js.map +1 -1
  74. package/dist/internal/machine/transition.d.ts +16 -0
  75. package/dist/internal/machine/transition.d.ts.map +1 -0
  76. package/dist/internal/machine/transition.js +2 -0
  77. package/dist/internal/machine/transition.js.map +1 -0
  78. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  79. package/dist/internal/testing/machine/finiteModel.js +10 -12
  80. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  81. package/dist/internal/testing/machine/format.d.ts +7 -0
  82. package/dist/internal/testing/machine/format.d.ts.map +1 -0
  83. package/dist/internal/testing/machine/format.js +104 -0
  84. package/dist/internal/testing/machine/format.js.map +1 -0
  85. package/dist/internal/testing/machine/probe.d.ts +1 -1
  86. package/dist/internal/testing/machine/probe.d.ts.map +1 -1
  87. package/dist/internal/testing/machine/probe.js +1 -1
  88. package/dist/internal/testing/machine/probe.js.map +1 -1
  89. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -1
  90. package/dist/internal/testing/machine/referenceModel.js +15 -19
  91. package/dist/internal/testing/machine/referenceModel.js.map +1 -1
  92. package/dist/internal/testing/machine/trace.d.ts +1 -1
  93. package/dist/internal/testing/machine/trace.d.ts.map +1 -1
  94. package/dist/internal/testing/machine/trace.js +3 -3
  95. package/dist/internal/testing/machine/trace.js.map +1 -1
  96. package/dist/internal/testing/machine/value.d.ts +8 -0
  97. package/dist/internal/testing/machine/value.d.ts.map +1 -0
  98. package/dist/internal/testing/machine/value.js +82 -0
  99. package/dist/internal/testing/machine/value.js.map +1 -0
  100. package/dist/internal/testing/machine/verification.d.ts +2 -13
  101. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  102. package/dist/internal/testing/machine/verification.js +21 -113
  103. package/dist/internal/testing/machine/verification.js.map +1 -1
  104. package/dist/testing/MachineTest.d.ts +89 -104
  105. package/dist/testing/MachineTest.d.ts.map +1 -1
  106. package/dist/testing/MachineTest.js +70 -84
  107. package/dist/testing/MachineTest.js.map +1 -1
  108. package/dist/unstable/cluster/ClusterMachine.d.ts +10 -13
  109. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  110. package/dist/unstable/cluster/ClusterMachine.js +5 -8
  111. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  112. package/dist/unstable/reactivity/AtomMachine.d.ts +26 -61
  113. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  114. package/dist/unstable/reactivity/AtomMachine.js +17 -26
  115. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  116. package/docs/agent-guide.md +45 -45
  117. package/docs/effect-atom-react.md +35 -63
  118. package/docs/machine-review.md +7 -7
  119. package/docs/root-api.md +214 -0
  120. package/package.json +1 -1
  121. package/src/Machine.ts +778 -437
  122. package/src/internal/machine/atom.ts +3 -13
  123. package/src/internal/machine/childRegistry.ts +141 -0
  124. package/src/internal/machine/cluster.ts +5 -14
  125. package/src/internal/machine/configuration.ts +20 -4
  126. package/src/internal/machine/executionPlan.ts +109 -52
  127. package/src/internal/machine/invocation.ts +10 -6
  128. package/src/internal/machine/invocationDefinition.ts +52 -0
  129. package/src/internal/machine/invocationEvent.ts +0 -8
  130. package/src/internal/machine/machine.ts +154 -679
  131. package/src/internal/machine/planner.ts +74 -62
  132. package/src/internal/machine/process.ts +14 -21
  133. package/src/internal/machine/protocol.ts +9 -32
  134. package/src/internal/machine/readiness.ts +1 -0
  135. package/src/internal/machine/requirements.ts +11 -0
  136. package/src/internal/machine/runtime.ts +17 -186
  137. package/src/internal/machine/serialization.ts +22 -0
  138. package/src/internal/machine/stateDefinition.ts +36 -3
  139. package/src/internal/machine/targetBuilder.ts +525 -0
  140. package/src/internal/machine/topology.ts +3 -3
  141. package/src/internal/machine/transition.ts +26 -0
  142. package/src/internal/testing/machine/finiteModel.ts +22 -23
  143. package/src/internal/testing/machine/format.ts +120 -0
  144. package/src/internal/testing/machine/probe.ts +4 -4
  145. package/src/internal/testing/machine/referenceModel.ts +25 -17
  146. package/src/internal/testing/machine/trace.ts +4 -4
  147. package/src/internal/testing/machine/value.ts +77 -0
  148. package/src/internal/testing/machine/verification.ts +904 -1109
  149. package/src/testing/MachineTest.ts +103 -114
  150. package/src/unstable/cluster/ClusterMachine.ts +10 -20
  151. package/src/unstable/reactivity/AtomMachine.ts +42 -79
@@ -22,12 +22,12 @@ const CounterState = Schema.TaggedUnion({
22
22
  Running: { count: Schema.Number }
23
23
  })
24
24
 
25
- export const CounterStates = Machine.states({
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.events(
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
- states: CounterStates.states,
40
+ root: CounterStates,
41
41
  events: CounterEvents,
42
- initial: (to) => to.Idle()
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.full.Running().resolve(({ target }) => target.from({ count: 0 }))
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.full.Running().resolve(({ state, target }) => target.from({ count: state.count + 1 })),
52
- Stop: (to) => to.full.Idle()
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.states` declares the state tree and the data owned by each state.
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.states({
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.states({
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.events(
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
- states: DocumentStates.states,
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.full.Closed()
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.events(
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
- states: CheckoutStates.states,
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.full.Submitting()
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.full.Editing()
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.states({
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.states({
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
- states: LoadStates.states,
298
- events: Machine.events(),
299
- initial: (to) => to.Idle()
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.full.Ready().resolve(({ output, target }) => target.from({ content: output })))
307
- .onFailure((to) => to.full.Failed().resolve(({ error, target }) => target.from({ message: String(error) })))
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.full.Running())
360
- .onFailure((to) => to.full.Failed())
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.events(
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
- states: ReviewStates.states,
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.full.Accepted() },
394
- rejected: { target: to.full.Rejected() }
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
- Use `useMachineAtom` when a provider, route, dialog, or other React subtree
43
- owns one machine instance:
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 { useMachineAtom } from "@typeonce/effect-machine-react"
47
- import { createContext, type ReactNode, useContext } from "react"
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 makeAuthMachine = MachineAtoms.factory(AuthMachine)
52
- type AuthMachineAtom = ReturnType<typeof makeAuthMachine>
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
- <AuthMachineContext.Provider value={machine}>
67
- {children}
68
- </AuthMachineContext.Provider>
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
- The provider strongly owns the complete `MachineAtom`. The hook mounts
82
- `machine.ref` after React commits the owner, but it does not read `state`,
83
- `snapshot`, or `result`. Machine updates therefore do not rerender the
84
- provider.
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
- The factory captures startup input once. A later `input` prop change does not
87
- replace the running workflow. Send an event when the change belongs to that
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
- Subscribe in the smallest component that renders a state path:
75
+ Render a typed path in the smallest component that needs it:
102
76
 
103
77
  ```tsx
104
- import { useAtomSuspense } from "@effect/atom-react"
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
- const editing = useAtomSuspense(
111
- AtomMachine.selectSnapshot(machine, "Editing")
112
- ).value
113
-
114
- return Option.match(editing, {
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 AuthStates.match(state, {
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.state,
295
+ state: (plant) => plant.result,
324
296
  send: (plant) => plant.send
325
297
  }
326
298
  })
@@ -39,12 +39,12 @@ job is returning `target.from()`.
39
39
  // Redundant
40
40
  const handlers = {
41
41
  Start: (to) =>
42
- to.full.Running().resolve(({ target }) => target.from())
42
+ to.branch.Running().resolve(({ target }) => target.from())
43
43
  }
44
44
 
45
45
  // Preferred
46
46
  const handlers = {
47
- Start: (to) => to.full.Running()
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.full.Submitting().resolve(({ event, target }) =>
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.full.Complete().resolve(({ output, target }) =>
193
+ to.branch.Complete().resolve(({ output, target }) =>
194
194
  target.from({ order: output })
195
195
  )
196
196
  )
197
197
  .onFailure((to) =>
198
- to.full.Failed().resolve(({ error, target }) =>
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
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/effect-machine",
3
- "version": "0.31.2",
3
+ "version": "0.32.0",
4
4
  "description": "Schema-first state machines and statecharts for Effect",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {