@typeonce/effect-machine 0.28.0 → 0.30.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 +48 -9
- package/dist/Machine.d.ts +38 -0
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +35 -0
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +1 -0
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +36 -11
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +1 -0
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +1 -0
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +1 -0
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +19 -0
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +47 -15
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +37 -14
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/effect-atom-react.md +272 -137
- package/docs/machine-review.md +26 -12
- package/package.json +1 -1
- package/src/Machine.ts +114 -0
- package/src/internal/machine/atom.ts +95 -30
- package/src/internal/machine/machine.ts +2 -0
- package/src/internal/machine/planner.ts +25 -0
- package/src/unstable/reactivity/AtomMachine.ts +61 -15
|
@@ -52,8 +52,8 @@ const SelectorProjectionTypeId = "~effect/reactivity/AtomMachine/SelectorProject
|
|
|
52
52
|
* Selects the typed value for an active state path.
|
|
53
53
|
*
|
|
54
54
|
* Valid paths and their selected value types are inferred from the bridge.
|
|
55
|
-
* The derived atom suppresses structurally equal updates.
|
|
56
|
-
*
|
|
55
|
+
* The derived atom suppresses structurally equal updates. Repeated calls with
|
|
56
|
+
* the same bridge and path return the same atom.
|
|
57
57
|
*
|
|
58
58
|
* **Example**
|
|
59
59
|
*
|
|
@@ -87,8 +87,8 @@ export const select = dual(2, internal.select);
|
|
|
87
87
|
* Selects the typed logical snapshot for an active state path.
|
|
88
88
|
*
|
|
89
89
|
* Unlike {@link select}, the selected value retains its child snapshot
|
|
90
|
-
* topology. The derived atom suppresses structurally equal updates.
|
|
91
|
-
*
|
|
90
|
+
* topology. The derived atom suppresses structurally equal updates. Repeated
|
|
91
|
+
* calls with the same bridge and path return the same atom.
|
|
92
92
|
*
|
|
93
93
|
* @category combinators
|
|
94
94
|
* @since 0.7.0
|
|
@@ -98,8 +98,8 @@ export const selectSnapshot = dual(2, internal.selectSnapshot);
|
|
|
98
98
|
* Selects the typed value for an active state path in a directly owned child.
|
|
99
99
|
*
|
|
100
100
|
* Valid paths and their selected value types are inferred from the child
|
|
101
|
-
* bridge. An inactive child produces `Option.none()`.
|
|
102
|
-
*
|
|
101
|
+
* bridge. An inactive child produces `Option.none()`. Repeated calls with the
|
|
102
|
+
* same child bridge and path return the same atom.
|
|
103
103
|
*
|
|
104
104
|
* **Example**
|
|
105
105
|
*
|
|
@@ -118,7 +118,8 @@ export const selectChild = dual(2, internal.selectChild);
|
|
|
118
118
|
*
|
|
119
119
|
* An inactive child or state path produces `Option.none()`. Unlike
|
|
120
120
|
* {@link selectChild}, the selected value retains its child snapshot topology.
|
|
121
|
-
* The derived atom suppresses structurally equal updates.
|
|
121
|
+
* The derived atom suppresses structurally equal updates. Repeated calls with
|
|
122
|
+
* the same child bridge and path return the same atom.
|
|
122
123
|
*
|
|
123
124
|
* @category combinators
|
|
124
125
|
* @since 0.7.0
|
|
@@ -128,8 +129,9 @@ export const selectSnapshotChild = dual(2, internal.selectSnapshotChild);
|
|
|
128
129
|
* Returns whether a state path is active.
|
|
129
130
|
*
|
|
130
131
|
* Valid paths are inferred from the bridge snapshot.
|
|
131
|
-
* The derived atom suppresses equal updates.
|
|
132
|
-
*
|
|
132
|
+
* The derived atom suppresses equal updates. Repeated calls with the same
|
|
133
|
+
* bridge and path return the same atom. Runtime failures remain in the typed
|
|
134
|
+
* failure channel.
|
|
133
135
|
*
|
|
134
136
|
* **Example**
|
|
135
137
|
*
|
|
@@ -161,8 +163,8 @@ export const matches = dual(2, internal.matches);
|
|
|
161
163
|
* Returns whether a state path is active in a directly owned child.
|
|
162
164
|
*
|
|
163
165
|
* Valid paths are inferred from the child bridge snapshot.
|
|
164
|
-
* An inactive child produces `false`.
|
|
165
|
-
*
|
|
166
|
+
* An inactive child produces `false`. Repeated calls with the same child
|
|
167
|
+
* bridge and path return the same atom.
|
|
166
168
|
*
|
|
167
169
|
* @category combinators
|
|
168
170
|
* @since 0.4.0
|
|
@@ -255,6 +257,26 @@ export const familyChild = internal.familyChild;
|
|
|
255
257
|
* @since 0.4.0
|
|
256
258
|
*/
|
|
257
259
|
export const make = internal.make;
|
|
260
|
+
/**
|
|
261
|
+
* Specializes a machine definition into a reusable bridge constructor.
|
|
262
|
+
*
|
|
263
|
+
* The returned function preserves the machine's startup input arity and exact
|
|
264
|
+
* bridge type. Every call creates a fresh `MachineAtom`; it does not cache by
|
|
265
|
+
* input or start the machine before an `AtomRegistry` reads or mounts it.
|
|
266
|
+
*
|
|
267
|
+
* **Example**
|
|
268
|
+
*
|
|
269
|
+
* ```ts
|
|
270
|
+
* const makeSearchMachine = AtomMachine.factory(searchMachine)
|
|
271
|
+
* const search = makeSearchMachine({ query: "effect" })
|
|
272
|
+
*
|
|
273
|
+
* type SearchMachineAtom = ReturnType<typeof makeSearchMachine>
|
|
274
|
+
* ```
|
|
275
|
+
*
|
|
276
|
+
* @category constructors
|
|
277
|
+
* @since 0.30.0
|
|
278
|
+
*/
|
|
279
|
+
export const factory = internal.factory;
|
|
258
280
|
/**
|
|
259
281
|
* Creates a lazy atom bridge from a decoded logical snapshot.
|
|
260
282
|
*
|
|
@@ -267,11 +289,12 @@ export const make = internal.make;
|
|
|
267
289
|
*/
|
|
268
290
|
export const resume = internal.resume;
|
|
269
291
|
/**
|
|
270
|
-
*
|
|
292
|
+
* Binds `AtomMachine` constructors to a shared Effect runtime.
|
|
271
293
|
*
|
|
272
294
|
* Use this when an application runs many machines from the same service layer.
|
|
273
|
-
* The returned
|
|
274
|
-
* while every call to `make` still creates an
|
|
295
|
+
* The returned interface keeps runtime provisioning at the composition seam,
|
|
296
|
+
* while every call to `make` or a specialized `factory` still creates an
|
|
297
|
+
* independent machine bridge.
|
|
275
298
|
*
|
|
276
299
|
* @category constructors
|
|
277
300
|
* @since 0.4.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AtomMachine.js","sourceRoot":"","sources":["../../../src/unstable/reactivity/AtomMachine.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAMtC,OAAO,KAAK,QAAQ,MAAM,gCAAgC,CAAA;AAK1D;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAE9D;;;;;GAKG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AAMpE,MAAM,0BAA0B,GAAG,qDAAqD,CAAA;AAoIxF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAE+C,QAAQ,CAAC,SAAS,CAAA;AAEvF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAE+D,QAAQ,CAAC,UAAU,CAAA;AAEzG;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAMvB,QAAQ,CAAC,cAAc,CAAA;AAwJ3B,MAAM,yBAAyB,GAAG,oDAAoD,CAAA;AACtF,MAAM,wBAAwB,GAAG,mDAAmD,CAAA;AA0BpF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,MAAM,GA0Cf,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;AAE5B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GA0CvB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAA;AAEpC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,WAAW,GAsCpB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAA;AAEjC
|
|
1
|
+
{"version":3,"file":"AtomMachine.js","sourceRoot":"","sources":["../../../src/unstable/reactivity/AtomMachine.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAMtC,OAAO,KAAK,QAAQ,MAAM,gCAAgC,CAAA;AAK1D;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAE9D;;;;;GAKG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AAMpE,MAAM,0BAA0B,GAAG,qDAAqD,CAAA;AAoIxF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAE+C,QAAQ,CAAC,SAAS,CAAA;AAEvF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAE+D,QAAQ,CAAC,UAAU,CAAA;AAEzG;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAMvB,QAAQ,CAAC,cAAc,CAAA;AAwJ3B,MAAM,yBAAyB,GAAG,oDAAoD,CAAA;AACtF,MAAM,wBAAwB,GAAG,mDAAmD,CAAA;AA0BpF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,MAAM,GA0Cf,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;AAE5B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GA0CvB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAA;AAEpC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,WAAW,GAsCpB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAA;AAEjC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAoC5B,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,OAAO,GAsChB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;AAE7B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GA2BrB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAA;AAElC,MAAM,uBAAuB,GAAG,kDAAkD,CAAA;AAkJlF,MAAM,yBAAyB,GAAG,oDAAoD,CAAA;AAyGtF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,MAAM,GAkBgE,QAAQ,CAAC,MAAa,CAAA;AAEzG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,WAAW,GAYqC,QAAQ,CAAC,WAAkB,CAAA;AAExF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,IAAI,GAqDb,QAAQ,CAAC,IAAI,CAAA;AAEjB;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,OAAO,GAM+C,QAAQ,CAAC,OAAO,CAAA;AAEnF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,MAAM,GASf,QAAQ,CAAC,MAAM,CAAA;AAEnB;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,IAAI,GAEoB,QAAQ,CAAC,IAAI,CAAA"}
|
|
@@ -1,208 +1,343 @@
|
|
|
1
|
-
# Effect Atom and React
|
|
1
|
+
# Effect Atom and React
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
React code should own one stable machine atom, pass it through props or
|
|
4
|
+
Context, and subscribe in the descendants that render machine state. Keep the
|
|
5
|
+
machine definition free of React dependencies.
|
|
6
|
+
|
|
7
|
+
Read the [Effect Machine agent guide](./agent-guide.md) for statechart
|
|
8
|
+
modeling, transitions, services, and testing.
|
|
7
9
|
|
|
8
10
|
## Recommended folder structure
|
|
9
11
|
|
|
10
12
|
```text
|
|
11
13
|
src/
|
|
12
|
-
├── context/
|
|
13
|
-
│
|
|
14
|
-
│ └── process-context.tsx
|
|
14
|
+
├── context/
|
|
15
|
+
│ └── auth-machine-context.tsx # React ownership and distribution
|
|
15
16
|
├── lib/
|
|
16
|
-
│ ├── atom-runtime.ts
|
|
17
|
-
│ └── services/
|
|
18
|
-
│ └── query-processor.ts
|
|
17
|
+
│ ├── atom-runtime.ts # Shared bound AtomMachine runtime
|
|
18
|
+
│ └── services/
|
|
19
19
|
└── machines/
|
|
20
|
-
|
|
21
|
-
│ ├── machine.ts # Machine implementation
|
|
22
|
-
│ └── atom.ts # Focused atoms for React
|
|
23
|
-
├── process/
|
|
24
|
-
│ ├── machine.ts
|
|
25
|
-
│ └── atom.ts
|
|
26
|
-
└── dialog/
|
|
27
|
-
├── machine.ts
|
|
28
|
-
└── atom.ts
|
|
20
|
+
└── auth-machine.ts # States, events, and behavior
|
|
29
21
|
```
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
`machine.ts` owns the workflow. A Context module only creates and distributes
|
|
24
|
+
the machine atom. State-slot components decide which state paths they render.
|
|
32
25
|
|
|
33
|
-
-
|
|
34
|
-
Effect service requirements. It has no React dependency.
|
|
35
|
-
- `atom.ts` adapts that machine to the shared bound AtomMachine runtime and
|
|
36
|
-
exports the focused atoms React needs.
|
|
37
|
-
- `lib/services/` contains reusable business services used by machines.
|
|
38
|
-
- `context/` is optional. It only distributes an already-created machine scope
|
|
39
|
-
through a React subtree.
|
|
40
|
-
- `lib/atom-runtime.ts` binds AtomMachine once to the application's Effect
|
|
41
|
-
service layer:
|
|
26
|
+
Bind service-backed machines once at the application runtime:
|
|
42
27
|
|
|
43
28
|
```ts
|
|
44
29
|
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
45
30
|
import { Atom } from "effect/unstable/reactivity"
|
|
46
|
-
import {
|
|
31
|
+
import { AppLayer } from "./app-layer"
|
|
47
32
|
|
|
48
|
-
const atomRuntime = Atom.runtime(
|
|
33
|
+
const atomRuntime = Atom.runtime(AppLayer)
|
|
49
34
|
|
|
50
|
-
export const
|
|
35
|
+
export const MachineAtoms = AtomMachine.bind(atomRuntime)
|
|
51
36
|
```
|
|
52
37
|
|
|
53
|
-
|
|
38
|
+
Service-free machines can use `AtomMachine.make` directly.
|
|
54
39
|
|
|
55
|
-
##
|
|
40
|
+
## Own a machine in one React subtree
|
|
56
41
|
|
|
57
|
-
Use
|
|
58
|
-
|
|
42
|
+
Use `useMachineAtom` when a provider, route, dialog, or other React subtree
|
|
43
|
+
owns one machine instance:
|
|
59
44
|
|
|
60
|
-
```
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
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"
|
|
49
|
+
import { MachineAtoms } from "../lib/atom-runtime"
|
|
50
|
+
|
|
51
|
+
const makeAuthMachine = MachineAtoms.factory(AuthMachine)
|
|
52
|
+
type AuthMachineAtom = ReturnType<typeof makeAuthMachine>
|
|
53
|
+
|
|
54
|
+
const AuthMachineContext = createContext<AuthMachineAtom | null>(null)
|
|
64
55
|
|
|
65
|
-
export
|
|
56
|
+
export function AuthMachineProvider({
|
|
57
|
+
children,
|
|
58
|
+
input
|
|
59
|
+
}: {
|
|
60
|
+
readonly children: ReactNode
|
|
61
|
+
readonly input: AuthMachineInput
|
|
62
|
+
}) {
|
|
63
|
+
const machine = useMachineAtom(() => makeAuthMachine(input))
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<AuthMachineContext.Provider value={machine}>
|
|
67
|
+
{children}
|
|
68
|
+
</AuthMachineContext.Provider>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
66
71
|
|
|
67
|
-
export
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
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
|
+
}
|
|
71
79
|
```
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
76
85
|
|
|
77
|
-
|
|
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:
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
key
|
|
90
|
+
```tsx
|
|
91
|
+
<AuthMachineProvider key={attemptId} input={input}>
|
|
92
|
+
<AuthCard />
|
|
93
|
+
</AuthMachineProvider>
|
|
94
|
+
```
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
Put the owner above a Suspense boundary. React can then retain the same machine
|
|
97
|
+
while a state-reading descendant suspends.
|
|
98
|
+
|
|
99
|
+
## Render state-owned data
|
|
100
|
+
|
|
101
|
+
Subscribe in the smallest component that renders a state path:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { useAtomSuspense } from "@effect/atom-react"
|
|
84
105
|
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
85
|
-
import {
|
|
106
|
+
import { Option } from "effect"
|
|
107
|
+
|
|
108
|
+
function EditingFields() {
|
|
109
|
+
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
|
+
})
|
|
118
|
+
}
|
|
119
|
+
```
|
|
86
120
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
`AtomMachine.select` returns the selected state value.
|
|
122
|
+
`AtomMachine.selectSnapshot` also retains the selected state's child topology.
|
|
123
|
+
Both return `Option.none()` while the path is inactive. Do not replace that
|
|
124
|
+
absence with an empty string, `null`, or a global boolean.
|
|
125
|
+
|
|
126
|
+
Repeated calls with the same machine and path return the same atom, so path
|
|
127
|
+
selection is safe during render without `useMemo`. Equal selected values do not
|
|
128
|
+
notify the component.
|
|
129
|
+
|
|
130
|
+
Nested paths keep the same ownership:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
function PasswordField() {
|
|
134
|
+
const machine = useAuthMachine()
|
|
135
|
+
const password = useAtomSuspense(
|
|
136
|
+
AtomMachine.select(machine, "Editing.Password")
|
|
137
|
+
).value
|
|
138
|
+
|
|
139
|
+
return Option.match(password, {
|
|
140
|
+
onNone: () => null,
|
|
141
|
+
onSome: ({ password }) => <input type="password" value={password} />
|
|
142
|
+
})
|
|
143
|
+
}
|
|
95
144
|
```
|
|
96
145
|
|
|
97
|
-
|
|
146
|
+
Place independent subscriptions in independent descendants:
|
|
98
147
|
|
|
99
148
|
```tsx
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
149
|
+
function AuthCard() {
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<EditingFields />
|
|
153
|
+
<VerificationFields />
|
|
154
|
+
<FailureMessage />
|
|
155
|
+
<SubmitButton />
|
|
156
|
+
</>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
103
159
|
```
|
|
104
160
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
161
|
+
Atom granularity cannot isolate hooks that all live in `AuthCard`. Any selected
|
|
162
|
+
change rerenders the component that called the hook.
|
|
163
|
+
|
|
164
|
+
## Send without subscribing
|
|
165
|
+
|
|
166
|
+
Use the writable atom directly:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { useAtomSet } from "@effect/atom-react"
|
|
109
170
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
unreliable. Different input values select independent machines. If a changing
|
|
114
|
-
value should update one running workflow, model the change as an event instead
|
|
115
|
-
of putting it in the machine input.
|
|
171
|
+
function SubmitButton() {
|
|
172
|
+
const machine = useAuthMachine()
|
|
173
|
+
const send = useAtomSet(machine.send)
|
|
116
174
|
|
|
117
|
-
|
|
175
|
+
return (
|
|
176
|
+
<button onClick={() => send({ _tag: "Submitted" })}>
|
|
177
|
+
Continue
|
|
178
|
+
</button>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`useAtomSet` mounts the writable atom and does not subscribe the component to
|
|
184
|
+
its value.
|
|
185
|
+
|
|
186
|
+
## Query concrete event acceptance
|
|
187
|
+
|
|
188
|
+
`Machine.can` composes with `machine.snapshot` through a derived atom. Declare
|
|
189
|
+
the projection once for a module-level machine, or create it once alongside a
|
|
190
|
+
React-owned machine:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { Effect, Equal } from "effect"
|
|
194
|
+
import { Atom } from "effect/unstable/reactivity"
|
|
195
|
+
import { Machine } from "@typeonce/effect-machine"
|
|
196
|
+
import { useAtomSet, useAtomSuspense } from "@effect/atom-react"
|
|
197
|
+
import { useState } from "react"
|
|
198
|
+
|
|
199
|
+
const makeCanSubmitAtom = (machine: AuthMachineAtom) =>
|
|
200
|
+
Atom.make((get) =>
|
|
201
|
+
get.result(machine.snapshot).pipe(
|
|
202
|
+
Effect.flatMap((snapshot) => {
|
|
203
|
+
if (snapshot.status === "active") {
|
|
204
|
+
return Machine.can(AuthMachine, snapshot.state, { _tag: "Submitted" })
|
|
205
|
+
}
|
|
206
|
+
if (snapshot.status === "error") {
|
|
207
|
+
return Effect.failCause(snapshot.cause)
|
|
208
|
+
}
|
|
209
|
+
return Effect.succeed(false)
|
|
210
|
+
})
|
|
211
|
+
)
|
|
212
|
+
).pipe(Atom.withEquality(Equal.equals))
|
|
213
|
+
|
|
214
|
+
function SubmitButton() {
|
|
215
|
+
const machine = useAuthMachine()
|
|
216
|
+
const [canSubmitAtom] = useState(() => makeCanSubmitAtom(machine))
|
|
217
|
+
const canSubmit = useAtomSuspense(canSubmitAtom).value
|
|
218
|
+
const send = useAtomSet(machine.send)
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<button
|
|
222
|
+
disabled={!canSubmit}
|
|
223
|
+
onClick={() => send({ _tag: "Submitted" })}
|
|
224
|
+
>
|
|
225
|
+
Continue
|
|
226
|
+
</button>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Startup still suspends, startup and runtime failures reach the error boundary,
|
|
232
|
+
and invalid event input remains a `MachineSchemaDecodeError`. Done and stopped
|
|
233
|
+
machines return `false`. When the event payload itself changes reactively, read
|
|
234
|
+
it from another atom inside the same derived atom.
|
|
235
|
+
|
|
236
|
+
## Whole-result and custom selections
|
|
237
|
+
|
|
238
|
+
Reading the full result is correct when a component renders the complete
|
|
239
|
+
machine state:
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
function AuthScreen() {
|
|
243
|
+
const machine = useAuthMachine()
|
|
244
|
+
const state = useAtomSuspense(machine.result).value
|
|
245
|
+
|
|
246
|
+
return AuthStates.match(state, {
|
|
247
|
+
Editing: (editing) => <EditingScreen state={editing} />,
|
|
248
|
+
Verification: (verification) => <VerificationScreen state={verification} />,
|
|
249
|
+
Failed: (failed) => <FailureScreen state={failed} />
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
That component rerenders for every result change. Current
|
|
255
|
+
`@effect/atom-react` does not select from the successful value in
|
|
256
|
+
`useAtomSuspense`. Until it does, use typed path selectors for state-owned UI,
|
|
257
|
+
or declare a custom derived atom once in a strongly owned scope. Do not create
|
|
258
|
+
a fresh derived atom on every render.
|
|
259
|
+
|
|
260
|
+
## Share a keyed machine outside one React owner
|
|
261
|
+
|
|
262
|
+
`AtomMachine.family` is for registry-owned machines that unrelated consumers
|
|
263
|
+
find by startup input. It is not the default for one React-owned workflow.
|
|
118
264
|
|
|
119
265
|
```ts
|
|
120
|
-
export const processAtoms =
|
|
266
|
+
export const processAtoms = MachineAtoms.family(ProcessMachine, {
|
|
121
267
|
atoms: {
|
|
122
|
-
details: AtomMachine.select("
|
|
268
|
+
details: AtomMachine.select("Processing"),
|
|
269
|
+
ready: AtomMachine.matches("Ready"),
|
|
123
270
|
send: (machine) => machine.send
|
|
124
271
|
}
|
|
125
272
|
})
|
|
126
273
|
```
|
|
127
274
|
|
|
128
|
-
|
|
275
|
+
Consumers use the input as the shared identity key:
|
|
129
276
|
|
|
130
|
-
|
|
277
|
+
```tsx
|
|
278
|
+
const details = useAtomSuspense(processAtoms.details(input)).value
|
|
279
|
+
const send = useAtomSet(processAtoms.send(input))
|
|
280
|
+
```
|
|
131
281
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
import { dialogMachine } from "./machine"
|
|
282
|
+
Each public projection retains its private machine owner. Keeping only
|
|
283
|
+
`details(input)` or `send(input)` is safe. Do not return a weakly held composite
|
|
284
|
+
scope and retain only one field from it.
|
|
136
285
|
|
|
137
|
-
|
|
138
|
-
|
|
286
|
+
Family keys use Effect `Equal` and `Hash` semantics. Keep them immutable. If a
|
|
287
|
+
changing value should update one running workflow, model it as an event instead
|
|
288
|
+
of changing the family key.
|
|
139
289
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
openStateAtom: AtomMachine.select(machine, "Open"),
|
|
144
|
-
sendAtom: machine.send
|
|
145
|
-
}
|
|
146
|
-
}
|
|
290
|
+
## Module-owned machines
|
|
291
|
+
|
|
292
|
+
A no-input machine may intentionally have one module-owned identity:
|
|
147
293
|
|
|
148
|
-
|
|
294
|
+
```ts
|
|
295
|
+
export const CounterMachineAtom = MachineAtoms.make(CounterMachine)
|
|
296
|
+
export const CounterStateAtom = AtomMachine.select(CounterMachineAtom, "Count")
|
|
149
297
|
```
|
|
150
298
|
|
|
151
|
-
|
|
299
|
+
Every consumer using the same `AtomRegistry` reaches the same running machine.
|
|
300
|
+
Different registries still run independent instances.
|
|
152
301
|
|
|
153
|
-
|
|
154
|
-
const DialogContext = createContext<DialogScope | null>(null)
|
|
302
|
+
## Child machines
|
|
155
303
|
|
|
156
|
-
|
|
157
|
-
const [scope] = useState(makeDialogScope)
|
|
304
|
+
Direct child selectors follow the active child and preserve inactivity:
|
|
158
305
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
306
|
+
```tsx
|
|
307
|
+
const editor = machine.child(Editor)
|
|
308
|
+
const editing = useAtomSuspense(
|
|
309
|
+
AtomMachine.selectSnapshotChild(editor, "Editing")
|
|
310
|
+
).value
|
|
165
311
|
```
|
|
166
312
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
`DialogScope` through props when Context is unnecessary. Do not add a wrapper
|
|
170
|
-
component whose only job is forwarding the scope.
|
|
313
|
+
An inactive child or path returns `Option.none()`. Re-entry follows the
|
|
314
|
+
replacement child instance.
|
|
171
315
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
ID is part of startup semantics, declare it in the machine input and use
|
|
175
|
-
`AtomMachine.family`.
|
|
176
|
-
|
|
177
|
-
## 4. Selecting process-owned child machines
|
|
178
|
-
|
|
179
|
-
Bind a machine definition once when a parent owns a runtime-sized set of child
|
|
180
|
-
machines:
|
|
316
|
+
Use `AtomMachine.familyChild` when a parent owns a runtime-sized set of keyed
|
|
317
|
+
children:
|
|
181
318
|
|
|
182
319
|
```ts
|
|
183
|
-
const Plant = Machine.childFamily(
|
|
184
|
-
|
|
185
|
-
export const centralMachineAtom = machineAtoms.make(centralMachine)
|
|
320
|
+
const Plant = Machine.childFamily(PlantMachine)
|
|
186
321
|
|
|
187
|
-
export const plantAtoms = AtomMachine.familyChild(
|
|
322
|
+
export const plantAtoms = AtomMachine.familyChild(CentralMachineAtom, {
|
|
188
323
|
child: (plantId: string) => Plant(plantId),
|
|
189
324
|
atoms: {
|
|
325
|
+
broken: AtomMachine.matchesChild("Broken"),
|
|
190
326
|
state: (plant) => plant.state,
|
|
191
|
-
|
|
192
|
-
send: (plant) => plant.send,
|
|
193
|
-
stop: (plant) => plant.stop
|
|
327
|
+
send: (plant) => plant.send
|
|
194
328
|
}
|
|
195
329
|
})
|
|
196
|
-
|
|
197
|
-
const broken = useAtomValue(plantAtoms.isBroken(plantId))
|
|
198
|
-
const send = useAtomSet(plantAtoms.send(plantId))
|
|
199
330
|
```
|
|
200
331
|
|
|
201
|
-
|
|
202
|
-
|
|
332
|
+
## Registry and rendering semantics
|
|
333
|
+
|
|
334
|
+
A `MachineAtom` identifies one machine per `AtomRegistry`. Passing the same
|
|
335
|
+
machine atom through two registry providers creates two independent runtimes.
|
|
336
|
+
Unmounting a React owner releases its mount. The registry stops the machine
|
|
337
|
+
after its final subscription and configured idle retention expire.
|
|
338
|
+
`registry.dispose()` stops it immediately.
|
|
203
339
|
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
the inactive values after it stops.
|
|
340
|
+
`useMachineAtom` does not start a machine during server rendering because
|
|
341
|
+
React effects do not run on the server. Reading a machine atom during server
|
|
342
|
+
render follows `@effect/atom-react` server-read behavior, so choose an explicit
|
|
343
|
+
client boundary when server startup would be undesirable.
|
package/docs/machine-review.md
CHANGED
|
@@ -65,11 +65,25 @@ const handlers = {
|
|
|
65
65
|
Review check: search for `.resolve(...)` callbacks that only return an empty
|
|
66
66
|
`target.from()` and remove the callback.
|
|
67
67
|
|
|
68
|
-
##
|
|
68
|
+
## Choose React ownership or keyed family lookup
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
Use `useMachineAtom` when one React subtree owns the workflow, including a
|
|
71
|
+
machine with startup input:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
const makeProcessMachine = machineAtoms.factory(processMachine)
|
|
75
|
+
const machine = useMachineAtom(() => makeProcessMachine(input))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pass the stable machine through props or Context. Startup input is captured
|
|
79
|
+
once. Send an event to change the running workflow, or change the owner's React
|
|
80
|
+
key to replace it.
|
|
81
|
+
|
|
82
|
+
Use `AtomMachine.family` when unrelated consumers must find one shared machine
|
|
83
|
+
by its startup input. Effect Atom keeps a family value for an equal key while
|
|
84
|
+
that returned value is reachable. Current runtimes may hold family values
|
|
85
|
+
through `WeakRef`. Retaining one field from a composite family value does not
|
|
86
|
+
retain the composite itself:
|
|
73
87
|
|
|
74
88
|
```ts
|
|
75
89
|
// Unsafe when consumers retain only stateAtom or sendAtom
|
|
@@ -82,8 +96,8 @@ const processScope = Atom.family((input: ProcessInput) => {
|
|
|
82
96
|
})
|
|
83
97
|
```
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
`AtomMachine.family` returns direct atom families whose atoms retain the
|
|
100
|
+
private machine bridge:
|
|
87
101
|
|
|
88
102
|
```ts
|
|
89
103
|
export const processAtoms = machineAtoms.family(processMachine, {
|
|
@@ -101,14 +115,14 @@ No component `useMemo` is needed. The registry retains the public atom while a
|
|
|
101
115
|
hook subscribes to it, and that atom retains the machine owner. Equal inputs
|
|
102
116
|
use Effect `Equal` and `Hash` semantics and select the same family value.
|
|
103
117
|
|
|
104
|
-
For a no-input machine, use one module-level bridge or
|
|
105
|
-
|
|
118
|
+
For a no-input machine, use one module-level bridge or `useMachineAtom` in the
|
|
119
|
+
owning React subtree. Do not add an unused family key.
|
|
106
120
|
|
|
107
121
|
Review check: search for composite `Atom.family` values that own a machine,
|
|
108
|
-
`useMemo` around family lookup,
|
|
109
|
-
`machineAtoms.make
|
|
110
|
-
`AtomMachine.family
|
|
111
|
-
|
|
122
|
+
`useMemo` around family lookup, repeated input propagation through one React
|
|
123
|
+
subtree, and component-local calls to `machineAtoms.make` without a stable
|
|
124
|
+
owner. Choose `AtomMachine.family` only when consumers need shared keyed
|
|
125
|
+
lookup.
|
|
112
126
|
|
|
113
127
|
## Justify each `RegistryProvider`
|
|
114
128
|
|