@typeonce/effect-machine 0.25.0 → 0.26.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 +1 -1
- package/docs/machine-review.md +199 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Cluster and are exposed only through explicit integration boundaries.
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
pnpm add @typeonce/effect-machine effect@4.0.0-rc.
|
|
37
|
+
pnpm add @typeonce/effect-machine effect@4.0.0-rc.112
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
`effect` is an exact peer dependency. Install the version above and upgrade it
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Effect Machine implementation review
|
|
2
|
+
|
|
3
|
+
Use this review after a machine and its UI adapter have been implemented. Look
|
|
4
|
+
for modeling mistakes, redundant API usage, and logic that escaped the machine.
|
|
5
|
+
Do not force a change when the current ownership is intentional.
|
|
6
|
+
|
|
7
|
+
Read the [Effect Machine agent guide](./agent-guide.md) for statechart modeling
|
|
8
|
+
and [Effect Atom and React patterns](./effect-atom-react.md) for integration
|
|
9
|
+
patterns.
|
|
10
|
+
|
|
11
|
+
## Review the responsibility boundaries
|
|
12
|
+
|
|
13
|
+
Use this split when deciding where code belongs:
|
|
14
|
+
|
|
15
|
+
| Layer | Owns |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| Machine | State, transitions, workflow decisions, effect lifetimes, and service execution |
|
|
18
|
+
| Atoms | Reactive selection, projections, and keyed machine lookup |
|
|
19
|
+
| Component | Rendering and translating user interaction into domain events |
|
|
20
|
+
| `RegistryProvider` | An optional Atom registry ownership boundary |
|
|
21
|
+
|
|
22
|
+
If a component coordinates a workflow, an atom performs business work, or a
|
|
23
|
+
provider has no ownership purpose, inspect that code more closely.
|
|
24
|
+
|
|
25
|
+
## Remove identity resolvers
|
|
26
|
+
|
|
27
|
+
A target that supports default construction does not need a resolver whose only
|
|
28
|
+
job is returning `target.from()`.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// Redundant
|
|
32
|
+
const handlers = {
|
|
33
|
+
Start: (to) =>
|
|
34
|
+
to.full.Running().resolve(({ target }) => target.from())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Preferred
|
|
38
|
+
const handlers = {
|
|
39
|
+
Start: (to) => to.full.Running()
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This applies to schema-less states and schemas whose constructor fields are all
|
|
44
|
+
optional or defaulted. The type checker rejects the shorter form when the
|
|
45
|
+
target needs data.
|
|
46
|
+
|
|
47
|
+
Keep `.resolve(...)` when it uses handler context, constructs state data,
|
|
48
|
+
updates a retained owner, chooses a branch, declines a transition, or enqueues
|
|
49
|
+
commands. For resolver-free reentry, use `.reenter()`:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const handlers = {
|
|
53
|
+
Refresh: (to) => to.local.Ready().reenter()
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Review check: search for `.resolve(...)` callbacks that only return an empty
|
|
58
|
+
`target.from()` and remove the callback.
|
|
59
|
+
|
|
60
|
+
## Let `Atom.family` own keyed identity
|
|
61
|
+
|
|
62
|
+
Treat `useMemo` around an atom family lookup as a warning sign. `Atom.family`
|
|
63
|
+
already returns the same retained object for the same key, including when
|
|
64
|
+
separate components perform the lookup.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Redundant and local to one component
|
|
68
|
+
const scope = useMemo(() => processFamily(processId), [processId])
|
|
69
|
+
|
|
70
|
+
// The family owns identity
|
|
71
|
+
const scope = processFamily(processId)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If the component constructs the atoms or machine scope directly, move that
|
|
75
|
+
construction into a module-level family:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
export const processFamily = Atom.family((processId: string) => {
|
|
79
|
+
const machine = machineAtoms.make(processMachine, { processId })
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
stateAtom: AtomMachine.select(machine, "process"),
|
|
83
|
+
sendAtom: machine.send
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Use a stable domain key. A new key means a different machine instance. Send an
|
|
89
|
+
event when a value should update the current workflow instead.
|
|
90
|
+
|
|
91
|
+
`useMemo` may still be useful for unrelated expensive calculations. It should
|
|
92
|
+
not establish atom or machine identity. For one instance owned only by a React
|
|
93
|
+
subtree, use a lazy `useState(makeScope)` initializer as described in the React
|
|
94
|
+
guide.
|
|
95
|
+
|
|
96
|
+
Review check: search for `useMemo` around atom creation, family lookup, or
|
|
97
|
+
`machineAtoms.make`. Replace component-local identity with `Atom.family`, or
|
|
98
|
+
with an intentional component-owned scope.
|
|
99
|
+
|
|
100
|
+
## Justify each `RegistryProvider`
|
|
101
|
+
|
|
102
|
+
Effect Atom hooks use a shared default registry when no provider is present.
|
|
103
|
+
Do not add `RegistryProvider` automatically.
|
|
104
|
+
|
|
105
|
+
Keep a provider when the subtree intentionally needs its own registry. Common
|
|
106
|
+
reasons include:
|
|
107
|
+
|
|
108
|
+
- the same atom or machine descriptors must represent independent instances in
|
|
109
|
+
separate subtrees;
|
|
110
|
+
- the subtree owns registry disposal;
|
|
111
|
+
- server rendering or hydration requires a request-local registry;
|
|
112
|
+
- the registry needs initial values, custom scheduling, or custom idle
|
|
113
|
+
settings.
|
|
114
|
+
|
|
115
|
+
Without one of those requirements, the default registry is enough:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// A shared application instance can use the default registry.
|
|
119
|
+
export function App() {
|
|
120
|
+
return <ProcessScreen />
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Remember that adding or nesting a provider changes identity. The same atom
|
|
125
|
+
descriptor stores separate state in each registry, so an unnecessary provider
|
|
126
|
+
can split a machine that consumers expected to share.
|
|
127
|
+
|
|
128
|
+
Review check: for every `RegistryProvider`, state which registry boundary it
|
|
129
|
+
creates and why. Remove it when there is no deliberate boundary.
|
|
130
|
+
|
|
131
|
+
## Keep workflow logic in the machine
|
|
132
|
+
|
|
133
|
+
The machine owns business flow. Components render selected state and send
|
|
134
|
+
domain events. Atoms adapt machine state for reactive consumers. Neither layer
|
|
135
|
+
should coordinate the workflow.
|
|
136
|
+
|
|
137
|
+
The following component owns too much:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
const submit = async () => {
|
|
141
|
+
send(OrderEvents.SubmitStarted())
|
|
142
|
+
const order = await api.submitOrder(form)
|
|
143
|
+
analytics.track("order submitted", { orderId: order.id })
|
|
144
|
+
send(OrderEvents.SubmitSucceeded({ order }))
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Model `Submit` as the component-facing event. Let a machine state own the work
|
|
149
|
+
and its lifetime:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
machine.handle({
|
|
153
|
+
Editing: {
|
|
154
|
+
on: {
|
|
155
|
+
Submit: (to) =>
|
|
156
|
+
to.full.Submitting().resolve(({ event, target }) =>
|
|
157
|
+
target.from({ order: event.order })
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
Submitting: {
|
|
162
|
+
invoke: (from) =>
|
|
163
|
+
from
|
|
164
|
+
.effect("submit-order", ({ state }) => submitOrder(state.order))
|
|
165
|
+
.onDone((to) =>
|
|
166
|
+
to.full.Complete().resolve(({ output, target }) =>
|
|
167
|
+
target.from({ order: output })
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
.onFailure((to) =>
|
|
171
|
+
to.full.Failed().resolve(({ error, target }) =>
|
|
172
|
+
target.from({ message: String(error) })
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`submitOrder` can use Effect services for the API request and analytics. The
|
|
180
|
+
service implementation may live in its own module, but the machine decides
|
|
181
|
+
when it runs, which state owns it, what cancels it, and how success or failure
|
|
182
|
+
changes the workflow.
|
|
183
|
+
|
|
184
|
+
Apply the same test to browser APIs, storage, timers, analytics, navigation,
|
|
185
|
+
and other effects. If the result or lifetime affects machine behavior, execute
|
|
186
|
+
it through the machine. UI-only work such as focusing an element or measuring
|
|
187
|
+
layout can remain in the component when it does not participate in the domain
|
|
188
|
+
flow.
|
|
189
|
+
|
|
190
|
+
Review check: search components, hooks, and atom modules for service calls,
|
|
191
|
+
`Effect.run*`, promises, browser APIs, analytics, timers, and chains of `send`
|
|
192
|
+
calls. Move workflow coordination into states, transitions, and invoked work.
|
|
193
|
+
|
|
194
|
+
## Report findings
|
|
195
|
+
|
|
196
|
+
For each issue, cite the file and line, name the violated boundary, and show the
|
|
197
|
+
smallest correction. Distinguish a confirmed problem from a provider or
|
|
198
|
+
ownership choice that needs clarification. Do not rewrite correct code merely
|
|
199
|
+
to match an example in this guide.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeonce/effect-machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Schema-first state machines and statecharts for Effect",
|
|
5
5
|
"author": "Sandro Maglione",
|
|
6
6
|
"repository": {
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"provenance": true
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"effect": "4.0.0-rc.
|
|
49
|
+
"effect": "4.0.0-rc.112"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@effect/vitest": "4.0.0-rc.
|
|
52
|
+
"@effect/vitest": "4.0.0-rc.112",
|
|
53
53
|
"@types/node": "25.7.0",
|
|
54
|
-
"effect": "4.0.0-rc.
|
|
54
|
+
"effect": "4.0.0-rc.112",
|
|
55
55
|
"tinybench": "2.9.0",
|
|
56
56
|
"tstyche": "7.2.1",
|
|
57
57
|
"typescript": "6.0.3",
|