@typeonce/effect-machine 0.27.1 → 0.28.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 +33 -4
- package/dist/internal/machine/atom.d.ts +9 -0
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +54 -1
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +131 -6
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +64 -6
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/effect-atom-react.md +57 -79
- package/docs/machine-review.md +32 -27
- package/package.json +1 -1
- package/src/internal/machine/atom.ts +111 -1
- package/src/unstable/reactivity/AtomMachine.ts +466 -59
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @since 0.4.0
|
|
5
5
|
*/
|
|
6
|
+
import { dual } from "effect/Function";
|
|
6
7
|
import * as internal from "../../internal/machine/atom.js";
|
|
7
8
|
/**
|
|
8
9
|
* Error returned when a machine command is issued before startup completes.
|
|
@@ -45,6 +46,8 @@ export const inspection = internal.inspection;
|
|
|
45
46
|
* @since 0.10.0
|
|
46
47
|
*/
|
|
47
48
|
export const childEmissions = internal.childEmissions;
|
|
49
|
+
const InvalidSelectorPathTypeId = "~effect/reactivity/AtomMachine/InvalidSelectorPath";
|
|
50
|
+
const SelectorProjectionTypeId = "~effect/reactivity/AtomMachine/SelectorProjection";
|
|
48
51
|
/**
|
|
49
52
|
* Selects the typed value for an active state path.
|
|
50
53
|
*
|
|
@@ -79,7 +82,7 @@ export const childEmissions = internal.childEmissions;
|
|
|
79
82
|
* @category combinators
|
|
80
83
|
* @since 0.4.0
|
|
81
84
|
*/
|
|
82
|
-
export const select = internal.select;
|
|
85
|
+
export const select = dual(2, internal.select);
|
|
83
86
|
/**
|
|
84
87
|
* Selects the typed logical snapshot for an active state path.
|
|
85
88
|
*
|
|
@@ -90,7 +93,7 @@ export const select = internal.select;
|
|
|
90
93
|
* @category combinators
|
|
91
94
|
* @since 0.7.0
|
|
92
95
|
*/
|
|
93
|
-
export const selectSnapshot = internal.selectSnapshot;
|
|
96
|
+
export const selectSnapshot = dual(2, internal.selectSnapshot);
|
|
94
97
|
/**
|
|
95
98
|
* Selects the typed value for an active state path in a directly owned child.
|
|
96
99
|
*
|
|
@@ -108,7 +111,7 @@ export const selectSnapshot = internal.selectSnapshot;
|
|
|
108
111
|
* @category combinators
|
|
109
112
|
* @since 0.4.0
|
|
110
113
|
*/
|
|
111
|
-
export const selectChild = internal.selectChild;
|
|
114
|
+
export const selectChild = dual(2, internal.selectChild);
|
|
112
115
|
/**
|
|
113
116
|
* Selects the typed logical snapshot for an active state path in an invoked
|
|
114
117
|
* child.
|
|
@@ -120,7 +123,7 @@ export const selectChild = internal.selectChild;
|
|
|
120
123
|
* @category combinators
|
|
121
124
|
* @since 0.7.0
|
|
122
125
|
*/
|
|
123
|
-
export const selectSnapshotChild = internal.selectSnapshotChild;
|
|
126
|
+
export const selectSnapshotChild = dual(2, internal.selectSnapshotChild);
|
|
124
127
|
/**
|
|
125
128
|
* Returns whether a state path is active.
|
|
126
129
|
*
|
|
@@ -153,7 +156,7 @@ export const selectSnapshotChild = internal.selectSnapshotChild;
|
|
|
153
156
|
* @category combinators
|
|
154
157
|
* @since 0.4.0
|
|
155
158
|
*/
|
|
156
|
-
export const matches = internal.matches;
|
|
159
|
+
export const matches = dual(2, internal.matches);
|
|
157
160
|
/**
|
|
158
161
|
* Returns whether a state path is active in a directly owned child.
|
|
159
162
|
*
|
|
@@ -164,8 +167,63 @@ export const matches = internal.matches;
|
|
|
164
167
|
* @category combinators
|
|
165
168
|
* @since 0.4.0
|
|
166
169
|
*/
|
|
167
|
-
export const matchesChild = internal.matchesChild;
|
|
170
|
+
export const matchesChild = dual(2, internal.matchesChild);
|
|
168
171
|
const BoundRequirementsTypeId = "~effect/reactivity/AtomMachine/BoundRequirements";
|
|
172
|
+
const FamilyInputRequiredTypeId = "~effect/reactivity/AtomMachine/FamilyInputRequired";
|
|
173
|
+
/**
|
|
174
|
+
* Creates retained atom families for independent machine inputs.
|
|
175
|
+
*
|
|
176
|
+
* The machine input is both startup input and family identity. Each property
|
|
177
|
+
* in `atoms` projects one public atom family from a private machine bridge.
|
|
178
|
+
* Retaining any projected atom retains that bridge without keeping its
|
|
179
|
+
* registry runtime mounted. Keys follow Effect `Equal` and `Hash` semantics.
|
|
180
|
+
* The optional `label` function labels each public projected atom.
|
|
181
|
+
*
|
|
182
|
+
* **Example**
|
|
183
|
+
*
|
|
184
|
+
* ```ts
|
|
185
|
+
* const processAtoms = AtomMachine.family(processMachine, {
|
|
186
|
+
* atoms: {
|
|
187
|
+
* ready: AtomMachine.matches("Ready"),
|
|
188
|
+
* state: (machine) => machine.state,
|
|
189
|
+
* send: (machine) => machine.send
|
|
190
|
+
* }
|
|
191
|
+
* })
|
|
192
|
+
*
|
|
193
|
+
* const readyAtom = processAtoms.ready("effect")
|
|
194
|
+
* const sendAtom = processAtoms.send("effect")
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* @category constructors
|
|
198
|
+
* @since 0.28.0
|
|
199
|
+
*/
|
|
200
|
+
export const family = internal.family;
|
|
201
|
+
/**
|
|
202
|
+
* Creates retained atom families for keyed direct-child lookup.
|
|
203
|
+
*
|
|
204
|
+
* The `child` function maps each family key to one direct child descriptor.
|
|
205
|
+
* Every projected atom retains the resulting child bridge while the projected
|
|
206
|
+
* atom remains reachable. Keys follow Effect `Equal` and `Hash` semantics.
|
|
207
|
+
*
|
|
208
|
+
* **Example**
|
|
209
|
+
*
|
|
210
|
+
* ```ts
|
|
211
|
+
* const Plant = Machine.childFamily(plantMachine)
|
|
212
|
+
*
|
|
213
|
+
* const plantAtoms = AtomMachine.familyChild(parentMachineAtom, {
|
|
214
|
+
* child: (plantId: string) => Plant(plantId),
|
|
215
|
+
* atoms: {
|
|
216
|
+
* broken: AtomMachine.matchesChild("Broken"),
|
|
217
|
+
* state: (child) => child.state,
|
|
218
|
+
* send: (child) => child.send
|
|
219
|
+
* }
|
|
220
|
+
* })
|
|
221
|
+
* ```
|
|
222
|
+
*
|
|
223
|
+
* @category constructors
|
|
224
|
+
* @since 0.28.0
|
|
225
|
+
*/
|
|
226
|
+
export const familyChild = internal.familyChild;
|
|
169
227
|
/**
|
|
170
228
|
* Creates atoms backed by a running machine.
|
|
171
229
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AtomMachine.js","sourceRoot":"","sources":["../../../src/unstable/reactivity/AtomMachine.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
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;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAoC5B,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;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;AAyFtF;;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,MAAM,GASf,QAAQ,CAAC,MAAM,CAAA;AAEnB;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,IAAI,GAEoB,QAAQ,CAAC,IAAI,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Effect Atom and React patterns
|
|
2
2
|
|
|
3
|
-
This guide records the folder organization and
|
|
3
|
+
This guide records the folder organization and four integration patterns
|
|
4
4
|
validated in the process app. Use the API reference for individual AtomMachine
|
|
5
5
|
operations. Read the [Effect Machine agent guide](./agent-guide.md) for
|
|
6
6
|
statechart modeling, transitions, services, and testing.
|
|
@@ -74,35 +74,56 @@ export const counterStateAtom = AtomMachine.select(
|
|
|
74
74
|
Consumers read `counterStateAtom` and use `counterMachineAtom.send` directly.
|
|
75
75
|
Do not add a redundant `counterSendAtom` alias.
|
|
76
76
|
|
|
77
|
-
## 2. A machine with startup input
|
|
77
|
+
## 2. A keyed machine with startup input
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
`AtomMachine.family` uses the machine input as both startup input and family
|
|
80
|
+
key. It returns one direct atom family for each entry in `atoms`:
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
83
|
import { machineAtoms } from "@/lib/atom-runtime"
|
|
84
84
|
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
85
|
-
import { Atom } from "effect/unstable/reactivity"
|
|
86
85
|
import { processMachine } from "./machine"
|
|
87
86
|
|
|
88
|
-
export const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
87
|
+
export const processAtoms = machineAtoms.family(processMachine, {
|
|
88
|
+
atoms: {
|
|
89
|
+
details: AtomMachine.select("process"),
|
|
90
|
+
result: AtomMachine.select("process.Ready"),
|
|
91
|
+
send: (machine) => machine.send
|
|
92
|
+
},
|
|
93
|
+
label: (input, name) => `process:${input.query}:${name}`
|
|
96
94
|
})
|
|
97
95
|
```
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
React consumes each projected family directly:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
const input = { query }
|
|
101
|
+
const details = useAtomValue(processAtoms.details(input))
|
|
102
|
+
const send = useAtomSet(processAtoms.send(input))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Each public atom retains its private machine bridge. Keeping only `details` or
|
|
106
|
+
only `send` is safe. The bridge still starts lazily in the registry and stops
|
|
107
|
+
when that registry releases or disposes it. A writable source remains writable,
|
|
108
|
+
and a projection keeps the source atom's equality function.
|
|
109
|
+
|
|
110
|
+
The family uses Effect `Equal` and `Hash` semantics. Equal records such as
|
|
111
|
+
`{ query: "effect" }` select the same family value even when reconstructed.
|
|
112
|
+
Keep inputs immutable because mutating a hashed key makes later lookup
|
|
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.
|
|
103
116
|
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
Service-free machines use the module function directly:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
export const processAtoms = AtomMachine.family(processMachine, {
|
|
121
|
+
atoms: {
|
|
122
|
+
details: AtomMachine.select("process"),
|
|
123
|
+
send: (machine) => machine.send
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
```
|
|
106
127
|
|
|
107
128
|
## 3. Reusing one machine definition for multiple instances
|
|
108
129
|
|
|
@@ -127,8 +148,6 @@ export function makeDialogScope() {
|
|
|
127
148
|
export type DialogScope = ReturnType<typeof makeDialogScope>
|
|
128
149
|
```
|
|
129
150
|
|
|
130
|
-
Choose one of the following ownership forms.
|
|
131
|
-
|
|
132
151
|
### React-tree-owned instance
|
|
133
152
|
|
|
134
153
|
```tsx
|
|
@@ -150,56 +169,10 @@ Each provider owns one independent dialog. Descendants use a small
|
|
|
150
169
|
`DialogScope` through props when Context is unnecessary. Do not add a wrapper
|
|
151
170
|
component whose only job is forwarding the scope.
|
|
152
171
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
172
|
+
For a no-input machine, use one module-level bridge or an explicitly owned
|
|
173
|
+
React scope. Do not add a family key that the machine does not consume. When an
|
|
174
|
+
ID is part of startup semantics, declare it in the machine input and use
|
|
175
|
+
`AtomMachine.family`.
|
|
203
176
|
|
|
204
177
|
## 4. Selecting process-owned child machines
|
|
205
178
|
|
|
@@ -211,18 +184,23 @@ const Plant = Machine.childFamily(plantMachine)
|
|
|
211
184
|
|
|
212
185
|
export const centralMachineAtom = machineAtoms.make(centralMachine)
|
|
213
186
|
|
|
214
|
-
export const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
stopAtom: plant.stop
|
|
187
|
+
export const plantAtoms = AtomMachine.familyChild(centralMachineAtom, {
|
|
188
|
+
child: (plantId: string) => Plant(plantId),
|
|
189
|
+
atoms: {
|
|
190
|
+
state: (plant) => plant.state,
|
|
191
|
+
isBroken: AtomMachine.matchesChild("Broken"),
|
|
192
|
+
send: (plant) => plant.send,
|
|
193
|
+
stop: (plant) => plant.stop
|
|
222
194
|
}
|
|
223
195
|
})
|
|
196
|
+
|
|
197
|
+
const broken = useAtomValue(plantAtoms.isBroken(plantId))
|
|
198
|
+
const send = useAtomSet(plantAtoms.send(plantId))
|
|
224
199
|
```
|
|
225
200
|
|
|
201
|
+
`familyChild` keeps child lookup separate from root machine startup. Each
|
|
202
|
+
projected atom retains the child bridge returned for its key.
|
|
203
|
+
|
|
226
204
|
`Plant(plantId)` may be reconstructed wherever the id is available. Child
|
|
227
205
|
lookup and bridge reuse match by machine identity and id, not descriptor object
|
|
228
206
|
identity. Before the parent spawns that child, selectors contain `Option.none`
|
package/docs/machine-review.md
CHANGED
|
@@ -65,27 +65,16 @@ 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
|
+
## Use retained families for keyed machine input
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```tsx
|
|
75
|
-
// Redundant and local to one component
|
|
76
|
-
const scope = useMemo(() => processFamily(processId), [processId])
|
|
77
|
-
|
|
78
|
-
// The family owns identity
|
|
79
|
-
const scope = processFamily(processId)
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
If the component constructs the atoms or machine scope directly, move that
|
|
83
|
-
construction into a module-level family:
|
|
70
|
+
Effect Atom keeps a family value for an equal key while that returned value is
|
|
71
|
+
reachable. Current runtimes may hold family values through `WeakRef`. Retaining
|
|
72
|
+
one field from a composite family value does not retain the composite itself:
|
|
84
73
|
|
|
85
74
|
```ts
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
// Unsafe when consumers retain only stateAtom or sendAtom
|
|
76
|
+
const processScope = Atom.family((input: ProcessInput) => {
|
|
77
|
+
const machine = machineAtoms.make(processMachine, input)
|
|
89
78
|
return {
|
|
90
79
|
stateAtom: AtomMachine.select(machine, "process"),
|
|
91
80
|
sendAtom: machine.send
|
|
@@ -93,17 +82,33 @@ export const processFamily = Atom.family((processId: string) => {
|
|
|
93
82
|
})
|
|
94
83
|
```
|
|
95
84
|
|
|
96
|
-
Use
|
|
97
|
-
|
|
85
|
+
Use `AtomMachine.family` for an input-bearing machine. It returns direct atom
|
|
86
|
+
families whose atoms retain the private machine bridge:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
export const processAtoms = machineAtoms.family(processMachine, {
|
|
90
|
+
atoms: {
|
|
91
|
+
state: AtomMachine.select("process"),
|
|
92
|
+
send: (machine) => machine.send
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const stateAtom = processAtoms.state(input)
|
|
97
|
+
const sendAtom = processAtoms.send(input)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
No component `useMemo` is needed. The registry retains the public atom while a
|
|
101
|
+
hook subscribes to it, and that atom retains the machine owner. Equal inputs
|
|
102
|
+
use Effect `Equal` and `Hash` semantics and select the same family value.
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
subtree, use a lazy `useState(makeScope)` initializer as described in the React
|
|
102
|
-
guide.
|
|
104
|
+
For a no-input machine, use one module-level bridge or a lazy
|
|
105
|
+
`useState(makeScope)` value owned by a React subtree. Do not add an unused key.
|
|
103
106
|
|
|
104
|
-
Review check: search for `
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
+
Review check: search for composite `Atom.family` values that own a machine,
|
|
108
|
+
`useMemo` around family lookup, and component-local calls to
|
|
109
|
+
`machineAtoms.make`. Replace an input-bearing machine with
|
|
110
|
+
`AtomMachine.family`. Give a no-input instance an explicit module or React-tree
|
|
111
|
+
owner.
|
|
107
112
|
|
|
108
113
|
## Justify each `RegistryProvider`
|
|
109
114
|
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import * as Data from "effect/Data"
|
|
|
8
8
|
import * as Effect from "effect/Effect"
|
|
9
9
|
import * as Equal from "effect/Equal"
|
|
10
10
|
import * as Fiber from "effect/Fiber"
|
|
11
|
+
import * as MutableHashMap from "effect/MutableHashMap"
|
|
11
12
|
import * as Option from "effect/Option"
|
|
12
13
|
import type * as Schema from "effect/Schema"
|
|
13
14
|
import type * as Scope from "effect/Scope"
|
|
@@ -78,6 +79,47 @@ const preparedByMachineAtom = new WeakMap<
|
|
|
78
79
|
Atom.Atom<AsyncResult.AsyncResult<Machine.Prepared<any, any, any, any, any, any, any>, any>>
|
|
79
80
|
>()
|
|
80
81
|
|
|
82
|
+
type WeakFamilyEntry<Value extends object> = {
|
|
83
|
+
readonly ref: WeakRef<Value>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// This follows Atom.family, but cleanup is generation-aware. A finalizer for a
|
|
87
|
+
// collected value must not remove a newer value installed for the same key.
|
|
88
|
+
const retainedFamily = <Key, Value extends object>(
|
|
89
|
+
makeValue: (key: Key) => Value
|
|
90
|
+
): (key: Key) => Value => {
|
|
91
|
+
if (typeof WeakRef === "undefined" || typeof FinalizationRegistry === "undefined") {
|
|
92
|
+
return Atom.family(makeValue)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const values = MutableHashMap.empty<Key, WeakFamilyEntry<Value>>()
|
|
96
|
+
const registry = new FinalizationRegistry<{
|
|
97
|
+
readonly key: Key
|
|
98
|
+
readonly entry: WeakFamilyEntry<Value>
|
|
99
|
+
}>(({ entry, key }) => {
|
|
100
|
+
const current = MutableHashMap.get(values, key)
|
|
101
|
+
if (Option.isSome(current) && current.value === entry) {
|
|
102
|
+
MutableHashMap.remove(values, key)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return (key) => {
|
|
107
|
+
const current = MutableHashMap.get(values, key)
|
|
108
|
+
if (Option.isSome(current)) {
|
|
109
|
+
const value = current.value.ref.deref()
|
|
110
|
+
if (value !== undefined) {
|
|
111
|
+
return value
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const value = makeValue(key)
|
|
116
|
+
const entry = { ref: new WeakRef(value) }
|
|
117
|
+
MutableHashMap.set(values, key, entry)
|
|
118
|
+
registry.register(value, { key, entry })
|
|
119
|
+
return value
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
81
123
|
const runMachineAtomEffect = <State, Event, Error, Output, Emitted, StartError, Requirements>(
|
|
82
124
|
get: Atom.AtomContext,
|
|
83
125
|
start: Effect.Effect<Machine.MachineRef<State, Event, Error, Output, Emitted>, StartError, Requirements>
|
|
@@ -738,6 +780,69 @@ const resumeWithRuntime = (
|
|
|
738
780
|
return makeFromRefAtom(ref as any)
|
|
739
781
|
}
|
|
740
782
|
|
|
783
|
+
type FamilyBridge = MachineAtom<any, never, any, any, any, any> | ChildMachineAtom<any, any>
|
|
784
|
+
|
|
785
|
+
type FamilyOptions = {
|
|
786
|
+
readonly atoms: Readonly<Record<string, (bridge: FamilyBridge) => Atom.Atom<any>>>
|
|
787
|
+
readonly label?: (key: any, atomName: string) => string | undefined
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const retainFamilyOwner = <Source extends Atom.Atom<any>>(
|
|
791
|
+
owner: { readonly bridge: FamilyBridge },
|
|
792
|
+
source: Source,
|
|
793
|
+
label: string | undefined
|
|
794
|
+
): Atom.WithoutSerializable<Source> => {
|
|
795
|
+
const retained = { owner, source }
|
|
796
|
+
let atom: Atom.Atom<any> = Atom.transform(
|
|
797
|
+
source,
|
|
798
|
+
(get) => get(retained.source),
|
|
799
|
+
{ initialValueTarget: source }
|
|
800
|
+
).pipe(
|
|
801
|
+
Atom.withEquality((value, next) => retained.source.equals(value, next))
|
|
802
|
+
)
|
|
803
|
+
if (label !== undefined) {
|
|
804
|
+
atom = atom.pipe(Atom.withLabel(label))
|
|
805
|
+
}
|
|
806
|
+
return atom as unknown as Atom.WithoutSerializable<Source>
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const makeFamily = (
|
|
810
|
+
makeBridge: (key: any) => FamilyBridge,
|
|
811
|
+
options: FamilyOptions
|
|
812
|
+
): Readonly<Record<string, (key: any) => Atom.Atom<any>>> => {
|
|
813
|
+
const owners = retainedFamily((key: any) => ({ bridge: makeBridge(key) }))
|
|
814
|
+
const atoms: Record<string, (key: any) => Atom.Atom<any>> = {}
|
|
815
|
+
|
|
816
|
+
for (const atomName of Object.keys(options.atoms)) {
|
|
817
|
+
const project = options.atoms[atomName]!
|
|
818
|
+
atoms[atomName] = retainedFamily((key: any) => {
|
|
819
|
+
const owner = owners(key)
|
|
820
|
+
const source = project(owner.bridge)
|
|
821
|
+
return retainFamilyOwner(owner, source, options.label?.(key, atomName))
|
|
822
|
+
})
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return atoms
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export const family = (
|
|
829
|
+
machine: Machine.Machine.Any,
|
|
830
|
+
options: FamilyOptions
|
|
831
|
+
): Readonly<Record<string, (key: any) => Atom.Atom<any>>> =>
|
|
832
|
+
makeFamily(
|
|
833
|
+
(input) => (make as any)(machine, input),
|
|
834
|
+
options
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
export const familyChild = (
|
|
838
|
+
parent: MachineAtom<any, never, any, any, any, any> | ChildMachineAtom<any, any>,
|
|
839
|
+
options: FamilyOptions & { readonly child: (key: any) => Machine.ChildMachine.Any }
|
|
840
|
+
): Readonly<Record<string, (key: any) => Atom.Atom<any>>> =>
|
|
841
|
+
makeFamily(
|
|
842
|
+
(key) => parent.child(options.child(key)),
|
|
843
|
+
options
|
|
844
|
+
)
|
|
845
|
+
|
|
741
846
|
export const bind = <Services, RuntimeError>(
|
|
742
847
|
runtime: Atom.AtomRuntime<Services, RuntimeError>
|
|
743
848
|
): Bound<Services, RuntimeError> => ({
|
|
@@ -749,5 +854,10 @@ export const bind = <Services, RuntimeError>(
|
|
|
749
854
|
>["make"],
|
|
750
855
|
resume:
|
|
751
856
|
((machine: Machine.Machine.Any, snapshot: Machine.Machine.Snapshot<any>) =>
|
|
752
|
-
resumeWithRuntime(runtime, machine, snapshot)) as Bound<Services, RuntimeError>["resume"]
|
|
857
|
+
resumeWithRuntime(runtime, machine, snapshot)) as Bound<Services, RuntimeError>["resume"],
|
|
858
|
+
family: ((machine: Machine.Machine.Any, options: FamilyOptions) =>
|
|
859
|
+
makeFamily(
|
|
860
|
+
(input) => makeWithRuntime(runtime, machine, [input]),
|
|
861
|
+
options
|
|
862
|
+
)) as Bound<Services, RuntimeError>["family"]
|
|
753
863
|
})
|