@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.
Files changed (33) hide show
  1. package/README.md +54 -16
  2. package/dist/Machine.d.ts +300 -30
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +10 -6
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/cluster.d.ts +2 -2
  7. package/dist/internal/machine/cluster.d.ts.map +1 -1
  8. package/dist/internal/machine/cluster.js +2 -1
  9. package/dist/internal/machine/cluster.js.map +1 -1
  10. package/dist/internal/machine/serialization.d.ts.map +1 -1
  11. package/dist/internal/machine/serialization.js +75 -18
  12. package/dist/internal/machine/serialization.js.map +1 -1
  13. package/dist/internal/testing/machine/verification.d.ts +1 -1
  14. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  15. package/dist/internal/testing/machine/verification.js +9 -6
  16. package/dist/internal/testing/machine/verification.js.map +1 -1
  17. package/dist/testing/MachineTest.d.ts +9 -6
  18. package/dist/testing/MachineTest.d.ts.map +1 -1
  19. package/dist/testing/MachineTest.js +5 -3
  20. package/dist/testing/MachineTest.js.map +1 -1
  21. package/dist/unstable/cluster/ClusterMachine.d.ts +22 -2
  22. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  23. package/dist/unstable/cluster/ClusterMachine.js +4 -0
  24. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  25. package/docs/agent-guide.md +278 -1389
  26. package/docs/effect-atom-react.md +202 -0
  27. package/package.json +4 -4
  28. package/src/Machine.ts +315 -33
  29. package/src/internal/machine/cluster.ts +4 -1
  30. package/src/internal/machine/serialization.ts +100 -25
  31. package/src/internal/testing/machine/verification.ts +15 -10
  32. package/src/testing/MachineTest.ts +9 -6
  33. 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.18.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.109"
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.109",
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.109",
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",