@typeonce/effect-machine 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -16
- package/dist/Machine.d.ts +300 -30
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +10 -6
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/cluster.d.ts +2 -2
- package/dist/internal/machine/cluster.d.ts.map +1 -1
- package/dist/internal/machine/cluster.js +2 -1
- package/dist/internal/machine/cluster.js.map +1 -1
- package/dist/internal/machine/serialization.d.ts.map +1 -1
- package/dist/internal/machine/serialization.js +75 -18
- package/dist/internal/machine/serialization.js.map +1 -1
- package/dist/internal/testing/machine/verification.d.ts +1 -1
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +9 -6
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +9 -6
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +5 -3
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +22 -2
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +4 -0
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/docs/agent-guide.md +278 -1389
- package/docs/effect-atom-react.md +202 -0
- package/package.json +4 -4
- package/src/Machine.ts +315 -33
- package/src/internal/machine/cluster.ts +4 -1
- package/src/internal/machine/serialization.ts +100 -25
- package/src/internal/testing/machine/verification.ts +15 -10
- package/src/testing/MachineTest.ts +9 -6
- package/src/unstable/cluster/ClusterMachine.ts +51 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Effect Atom and React patterns
|
|
2
|
+
|
|
3
|
+
This guide records the folder organization and three integration patterns
|
|
4
|
+
validated in the process app. Use the API reference for individual AtomMachine
|
|
5
|
+
operations. Read the [Effect Machine agent guide](./agent-guide.md) for
|
|
6
|
+
statechart modeling, transitions, services, and testing.
|
|
7
|
+
|
|
8
|
+
## Recommended folder structure
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
src/
|
|
12
|
+
├── context/ # Optional React Context adapters
|
|
13
|
+
│ ├── dialog-context.tsx
|
|
14
|
+
│ └── process-context.tsx
|
|
15
|
+
├── lib/
|
|
16
|
+
│ ├── atom-runtime.ts # Shared bound AtomMachine runtime
|
|
17
|
+
│ └── services/ # Generic Effect business services
|
|
18
|
+
│ └── query-processor.ts
|
|
19
|
+
└── machines/
|
|
20
|
+
├── counter/
|
|
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
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Keep these responsibilities separate:
|
|
32
|
+
|
|
33
|
+
- `machine.ts` defines states, events, transitions, statechart behavior, and
|
|
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:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
45
|
+
import { Atom } from "effect/unstable/reactivity"
|
|
46
|
+
import { QueryProcessor } from "./services/query-processor"
|
|
47
|
+
|
|
48
|
+
const atomRuntime = Atom.runtime(QueryProcessor.layer)
|
|
49
|
+
|
|
50
|
+
export const machineAtoms = AtomMachine.bind(atomRuntime)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Each `machineAtoms.make` call still creates an independent machine bridge.
|
|
54
|
+
|
|
55
|
+
## 1. One global actor with no input
|
|
56
|
+
|
|
57
|
+
Use a module-level bridge when a no-input machine intentionally has one
|
|
58
|
+
application-wide instance:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { machineAtoms } from "@/lib/atom-runtime"
|
|
62
|
+
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
63
|
+
import { counterMachine } from "./machine"
|
|
64
|
+
|
|
65
|
+
export const counterMachineAtom = machineAtoms.make(counterMachine)
|
|
66
|
+
|
|
67
|
+
export const counterStateAtom = AtomMachine.select(
|
|
68
|
+
counterMachineAtom,
|
|
69
|
+
"counter"
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
"Global" means every import reaches this bridge under the same atom registry.
|
|
74
|
+
Consumers read `counterStateAtom` and use `counterMachineAtom.send` directly.
|
|
75
|
+
Do not add a redundant `counterSendAtom` alias.
|
|
76
|
+
|
|
77
|
+
## 2. A machine with startup input selected through a family
|
|
78
|
+
|
|
79
|
+
Use the family key as machine identity. The same value can also be startup
|
|
80
|
+
input:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { machineAtoms } from "@/lib/atom-runtime"
|
|
84
|
+
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
85
|
+
import { Atom } from "effect/unstable/reactivity"
|
|
86
|
+
import { processMachine } from "./machine"
|
|
87
|
+
|
|
88
|
+
export const processFamily = Atom.family((query: string) => {
|
|
89
|
+
const machine = machineAtoms.make(processMachine, { query })
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
detailsAtom: AtomMachine.select(machine, "process"),
|
|
93
|
+
resultAtom: AtomMachine.select(machine, "process.Ready"),
|
|
94
|
+
sendAtom: machine.send
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A consumer calls `processFamily(query)` and uses the returned focused atoms. If
|
|
100
|
+
several nested components need the same scope, an optional Context can expose
|
|
101
|
+
`ReturnType<typeof processFamily>`. The provider resolves the query once instead
|
|
102
|
+
of drilling it through every component.
|
|
103
|
+
|
|
104
|
+
Changing `query` selects another family member and therefore another machine.
|
|
105
|
+
If a changing value should update the current workflow, model it as an event.
|
|
106
|
+
|
|
107
|
+
## 3. Reusing one machine definition for multiple instances
|
|
108
|
+
|
|
109
|
+
Define the dialog adapter once:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { machineAtoms } from "@/lib/atom-runtime"
|
|
113
|
+
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
114
|
+
import { dialogMachine } from "./machine"
|
|
115
|
+
|
|
116
|
+
export function makeDialogScope() {
|
|
117
|
+
const machine = machineAtoms.make(dialogMachine)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
isOpenAtom: AtomMachine.matches(machine, "Open"),
|
|
121
|
+
isClosedAtom: AtomMachine.matches(machine, "Closed"),
|
|
122
|
+
openStateAtom: AtomMachine.select(machine, "Open"),
|
|
123
|
+
sendAtom: machine.send
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type DialogScope = ReturnType<typeof makeDialogScope>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Choose one of the following ownership forms.
|
|
131
|
+
|
|
132
|
+
### React-tree-owned instance
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
const DialogContext = createContext<DialogScope | null>(null)
|
|
136
|
+
|
|
137
|
+
export function DialogProvider({ children }: { children: ReactNode }) {
|
|
138
|
+
const [scope] = useState(makeDialogScope)
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<DialogContext.Provider value={scope}>
|
|
142
|
+
{children}
|
|
143
|
+
</DialogContext.Provider>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Each provider owns one independent dialog. Descendants use a small
|
|
149
|
+
`useDialog()` hook and subscribe to the focused atom they need. Pass
|
|
150
|
+
`DialogScope` through props when Context is unnecessary. Do not add a wrapper
|
|
151
|
+
component whose only job is forwarding the scope.
|
|
152
|
+
|
|
153
|
+
### Stable keyed instances shared across scattered components
|
|
154
|
+
|
|
155
|
+
Use one private family to create the scope for an ID. Public selector families
|
|
156
|
+
reach that same scope:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { Atom } from "effect/unstable/reactivity"
|
|
160
|
+
|
|
161
|
+
const dialogScopeFamily = Atom.family((dialogId: string) => {
|
|
162
|
+
const scope = makeDialogScope()
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
isOpenAtom: scope.isOpenAtom.pipe(
|
|
166
|
+
Atom.withLabel(`dialog:${dialogId}:isOpen-source`)
|
|
167
|
+
),
|
|
168
|
+
openStateAtom: scope.openStateAtom.pipe(
|
|
169
|
+
Atom.withLabel(`dialog:${dialogId}:openState-source`)
|
|
170
|
+
),
|
|
171
|
+
sendAtom: scope.sendAtom.pipe(
|
|
172
|
+
Atom.withLabel(`dialog:${dialogId}:send-source`)
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
export const dialogIsOpenFamily = Atom.family((dialogId: string) => {
|
|
178
|
+
const scope = dialogScopeFamily(dialogId)
|
|
179
|
+
|
|
180
|
+
return Atom.transform(
|
|
181
|
+
scope.sendAtom,
|
|
182
|
+
(get) => get(scope.isOpenAtom)
|
|
183
|
+
).pipe(Atom.withLabel(`dialog:${dialogId}:isOpen`))
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
export const dialogOpenStateFamily = Atom.family((dialogId: string) => {
|
|
187
|
+
const scope = dialogScopeFamily(dialogId)
|
|
188
|
+
|
|
189
|
+
return Atom.transform(
|
|
190
|
+
scope.sendAtom,
|
|
191
|
+
(get) => get(scope.openStateAtom)
|
|
192
|
+
).pipe(Atom.withLabel(`dialog:${dialogId}:openState`))
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Components using the same `dialogId` share one machine. Different IDs create
|
|
197
|
+
independent machines. The two public projections let consumers subscribe
|
|
198
|
+
independently. Both remain writable through `Atom.transform`, so either can send
|
|
199
|
+
the inferred dialog events.
|
|
200
|
+
|
|
201
|
+
Use `dialogId` in atom labels for diagnostics. Do not pass it into
|
|
202
|
+
`dialogMachine` as unused fake input.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeonce/effect-machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Schema-first state machines and statecharts for Effect",
|
|
5
5
|
"author": "Sandro Maglione",
|
|
6
6
|
"repository": {
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
"provenance": true
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"effect": "4.0.0-rc.
|
|
49
|
+
"effect": "4.0.0-rc.111"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@changesets/cli": "2.31.0",
|
|
53
|
-
"@effect/vitest": "4.0.0-rc.
|
|
53
|
+
"@effect/vitest": "4.0.0-rc.111",
|
|
54
54
|
"@types/node": "25.7.0",
|
|
55
55
|
"dprint": "0.55.2",
|
|
56
|
-
"effect": "4.0.0-rc.
|
|
56
|
+
"effect": "4.0.0-rc.111",
|
|
57
57
|
"pagefind": "1.5.2",
|
|
58
58
|
"tinybench": "2.9.0",
|
|
59
59
|
"tstyche": "7.2.1",
|