@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 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.111
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.25.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.111"
49
+ "effect": "4.0.0-rc.112"
50
50
  },
51
51
  "devDependencies": {
52
- "@effect/vitest": "4.0.0-rc.111",
52
+ "@effect/vitest": "4.0.0-rc.112",
53
53
  "@types/node": "25.7.0",
54
- "effect": "4.0.0-rc.111",
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",