@typeonce/effect-machine 0.19.0 → 0.20.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 +50 -15
- package/dist/Machine.d.ts +93 -2
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +11 -0
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +18 -4
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +7 -0
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +1 -0
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +11 -4
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/runtime.d.ts +4 -3
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +12 -1
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +10 -8
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +2 -2
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +302 -1449
- package/docs/effect-atom-react.md +230 -0
- package/package.json +4 -4
- package/src/Machine.ts +141 -2
- package/src/internal/machine/atom.ts +26 -18
- package/src/internal/machine/invocation.ts +13 -1
- package/src/internal/machine/machine.ts +18 -6
- package/src/internal/machine/runtime.ts +48 -19
- package/src/unstable/reactivity/AtomMachine.ts +10 -8
|
@@ -0,0 +1,230 @@
|
|
|
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.
|
|
203
|
+
|
|
204
|
+
## 4. Selecting process-owned child machines
|
|
205
|
+
|
|
206
|
+
Bind a machine definition once when a parent owns a runtime-sized set of child
|
|
207
|
+
machines:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const Plant = Machine.childFamily(plantMachine)
|
|
211
|
+
|
|
212
|
+
export const centralMachineAtom = machineAtoms.make(centralMachine)
|
|
213
|
+
|
|
214
|
+
export const plantScopeFamily = Atom.family((plantId: string) => {
|
|
215
|
+
const plant = centralMachineAtom.child(Plant(plantId))
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
stateAtom: plant.state,
|
|
219
|
+
isBrokenAtom: AtomMachine.matchesChild(plant, "Broken"),
|
|
220
|
+
sendAtom: plant.send,
|
|
221
|
+
stopAtom: plant.stop
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`Plant(plantId)` may be reconstructed wherever the id is available. Child
|
|
227
|
+
lookup and bridge reuse match by machine identity and id, not descriptor object
|
|
228
|
+
identity. Before the parent spawns that child, selectors contain `Option.none`
|
|
229
|
+
and `matchesChild` is `false`. They follow the child after startup and return to
|
|
230
|
+
the inactive values after it stops.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeonce/effect-machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
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",
|
package/src/Machine.ts
CHANGED
|
@@ -2265,7 +2265,15 @@ export declare namespace Logic {
|
|
|
2265
2265
|
* @category models
|
|
2266
2266
|
* @since 0.4.0
|
|
2267
2267
|
*/
|
|
2268
|
-
export interface Spawn {
|
|
2268
|
+
export interface Spawn<OwnerEvent = unknown> {
|
|
2269
|
+
<const Child extends ChildMachine.Any>(
|
|
2270
|
+
child: Child & ChildMachine.Executable<Child> & ChildMachine.ParentCompatibility<Child, OwnerEvent>,
|
|
2271
|
+
...options: ChildMachine.SpawnArgs<Child>
|
|
2272
|
+
): Effect.Effect<
|
|
2273
|
+
ChildMachine.Ref<Child>,
|
|
2274
|
+
ChildAlreadyExistsError | ChildMachine.StartError<Child>,
|
|
2275
|
+
ChildMachine.StartRequirements<Child>
|
|
2276
|
+
>
|
|
2269
2277
|
<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
|
|
2270
2278
|
logic: Logic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>
|
|
2271
2279
|
): Effect.Effect<
|
|
@@ -2305,7 +2313,7 @@ export declare namespace Logic {
|
|
|
2305
2313
|
readonly parent: Address<unknown> | undefined
|
|
2306
2314
|
|
|
2307
2315
|
/** Starts a child process owned by this scope. */
|
|
2308
|
-
readonly spawn: Spawn
|
|
2316
|
+
readonly spawn: Spawn<Event>
|
|
2309
2317
|
|
|
2310
2318
|
/** Sends an event to a machine target or typed parent-local child address. */
|
|
2311
2319
|
readonly sendTo: {
|
|
@@ -2345,6 +2353,7 @@ export declare namespace Logic {
|
|
|
2345
2353
|
|
|
2346
2354
|
const ChildAddressTypeId = "~effect/Machine/ChildAddress"
|
|
2347
2355
|
const ChildAddressCompatibilityErrorTypeId = "~effect/Machine/ChildAddressCompatibilityError"
|
|
2356
|
+
const ChildParentCompatibilityErrorTypeId = "~effect/Machine/ChildParentCompatibilityError"
|
|
2348
2357
|
const ChildMachineTypeId = "~effect/Machine/ChildMachine"
|
|
2349
2358
|
type InvokeLifecycleId = string & { readonly [ChildAddressTypeId]?: never }
|
|
2350
2359
|
|
|
@@ -2389,6 +2398,95 @@ export declare namespace ChildMachine {
|
|
|
2389
2398
|
*/
|
|
2390
2399
|
export type Any = ChildMachine<string, Machine.Any>
|
|
2391
2400
|
|
|
2401
|
+
/**
|
|
2402
|
+
* Bound constructor for an open family of child descriptors that share one
|
|
2403
|
+
* machine definition.
|
|
2404
|
+
*
|
|
2405
|
+
* @category models
|
|
2406
|
+
* @since 0.20.0
|
|
2407
|
+
*/
|
|
2408
|
+
export interface Family<M extends Machine.Any> {
|
|
2409
|
+
<const Id extends string>(id: Id): ChildMachine<Id, M>
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
/**
|
|
2413
|
+
* Ensures a child machine's declared owner protocol is accepted by the
|
|
2414
|
+
* process that will own it.
|
|
2415
|
+
*
|
|
2416
|
+
* @category utility types
|
|
2417
|
+
* @since 0.20.0
|
|
2418
|
+
*/
|
|
2419
|
+
export type ParentCompatibility<Child extends Any, OwnerEvent> = Child extends ChildMachine<string, infer M> ?
|
|
2420
|
+
Machine.Any extends M ? {
|
|
2421
|
+
readonly [ChildParentCompatibilityErrorTypeId]: {
|
|
2422
|
+
readonly child: unknown
|
|
2423
|
+
readonly owner: OwnerEvent
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
: [Machine.EventOf<Machine.ParentEvents<M>>] extends [OwnerEvent] ? unknown :
|
|
2427
|
+
{
|
|
2428
|
+
readonly [ChildParentCompatibilityErrorTypeId]: {
|
|
2429
|
+
readonly child: Machine.EventOf<Machine.ParentEvents<M>>
|
|
2430
|
+
readonly owner: OwnerEvent
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
: never
|
|
2434
|
+
|
|
2435
|
+
/**
|
|
2436
|
+
* Ensures the selected child machine has complete handlers and outputs.
|
|
2437
|
+
*
|
|
2438
|
+
* @category utility types
|
|
2439
|
+
* @since 0.20.0
|
|
2440
|
+
*/
|
|
2441
|
+
export type Executable<Child extends Any> = Child["machine"] extends EnsureExecutable<
|
|
2442
|
+
Machine.States<Child["machine"]>,
|
|
2443
|
+
Machine.UnhandledStates<Child["machine"]>,
|
|
2444
|
+
Machine.OutputStates<Child["machine"]>
|
|
2445
|
+
> ? unknown
|
|
2446
|
+
: never
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* Startup arguments accepted while spawning a child machine.
|
|
2450
|
+
*
|
|
2451
|
+
* @category utility types
|
|
2452
|
+
* @since 0.20.0
|
|
2453
|
+
*/
|
|
2454
|
+
export type SpawnArgs<Child extends Any> = Machine.InputSchema<Child["machine"]> extends typeof Schema.Void ?
|
|
2455
|
+
[options?: { readonly input?: never }]
|
|
2456
|
+
: [options: { readonly input: Machine.Input<Child["machine"]> }]
|
|
2457
|
+
|
|
2458
|
+
/**
|
|
2459
|
+
* Typed failures that may occur before a spawned child becomes active.
|
|
2460
|
+
*
|
|
2461
|
+
* @category utility types
|
|
2462
|
+
* @since 0.20.0
|
|
2463
|
+
*/
|
|
2464
|
+
export type StartError<Child extends Any> = Child extends ChildMachine<string, infer M> ?
|
|
2465
|
+
| Machine.InitialError<M>
|
|
2466
|
+
| Machine.Error<M>
|
|
2467
|
+
| ActionError<Machine.InitialServices<M> | Machine.Services<M>>
|
|
2468
|
+
| InfiniteTransitionError
|
|
2469
|
+
| MachineSchemaDecodeError
|
|
2470
|
+
| StartupError
|
|
2471
|
+
| StoppedError
|
|
2472
|
+
: never
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* Services needed to initialize a spawned child machine.
|
|
2476
|
+
*
|
|
2477
|
+
* @category utility types
|
|
2478
|
+
* @since 0.20.0
|
|
2479
|
+
*/
|
|
2480
|
+
export type StartRequirements<Child extends Any> = Child extends ChildMachine<string, infer M> ? Exclude<
|
|
2481
|
+
ExcludeCompatibleRuntime<
|
|
2482
|
+
Exclude<ExecutionServices<Machine.InitialServices<M> | Machine.Services<M>>, MachineRuntimeRequirement>,
|
|
2483
|
+
Machine.Event<M>,
|
|
2484
|
+
Machine.Emit<M>
|
|
2485
|
+
>,
|
|
2486
|
+
Scope.Scope
|
|
2487
|
+
>
|
|
2488
|
+
: never
|
|
2489
|
+
|
|
2392
2490
|
/**
|
|
2393
2491
|
* Running machine reference selected by a child descriptor.
|
|
2394
2492
|
*
|
|
@@ -2418,6 +2516,34 @@ export declare namespace ChildMachine {
|
|
|
2418
2516
|
: never
|
|
2419
2517
|
}
|
|
2420
2518
|
|
|
2519
|
+
/**
|
|
2520
|
+
* Effectful operations for child machines owned directly by the current
|
|
2521
|
+
* machine process.
|
|
2522
|
+
*
|
|
2523
|
+
* @category models
|
|
2524
|
+
* @since 0.20.0
|
|
2525
|
+
*/
|
|
2526
|
+
export interface ChildOwner<OwnerEvent> {
|
|
2527
|
+
/** Starts a process-owned child and returns once initialization succeeds. */
|
|
2528
|
+
readonly spawn: <const Child extends ChildMachine.Any>(
|
|
2529
|
+
child: Child & ChildMachine.Executable<Child> & ChildMachine.ParentCompatibility<Child, OwnerEvent>,
|
|
2530
|
+
...options: ChildMachine.SpawnArgs<Child>
|
|
2531
|
+
) => Effect.Effect<
|
|
2532
|
+
ChildMachine.Ref<Child>,
|
|
2533
|
+
ChildAlreadyExistsError | ChildMachine.StartError<Child>,
|
|
2534
|
+
ChildMachine.StartRequirements<Child>
|
|
2535
|
+
>
|
|
2536
|
+
|
|
2537
|
+
/** Sends an event to one active child. Missing children are ignored. */
|
|
2538
|
+
readonly sendTo: <Child extends ChildMachine.Any>(
|
|
2539
|
+
child: Child,
|
|
2540
|
+
event: ChildMachine.Event<Child>
|
|
2541
|
+
) => Effect.Effect<void, StoppedError>
|
|
2542
|
+
|
|
2543
|
+
/** Stops one active child. Missing children are ignored. */
|
|
2544
|
+
readonly stop: <Child extends ChildMachine.Any>(child: Child) => Effect.Effect<void>
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2421
2547
|
/**
|
|
2422
2548
|
* Parent-local address for a child process that can receive events.
|
|
2423
2549
|
*
|
|
@@ -4799,6 +4925,8 @@ export declare namespace Machine {
|
|
|
4799
4925
|
InputEvents extends ReadonlyArray<TaggedSchema> = Events,
|
|
4800
4926
|
ParentEvents extends ReadonlyArray<TaggedSchema> = readonly []
|
|
4801
4927
|
> = MachineReferences<InputEvents, ParentEvents> & {
|
|
4928
|
+
/** Process-owned child operations for dynamic child machine lifecycles. */
|
|
4929
|
+
readonly children: ChildOwner<EventOf<InputEvents>>
|
|
4802
4930
|
/** Value owned by the state that owns this invocation. */
|
|
4803
4931
|
readonly state: StateByIdentifier<States, StateId>
|
|
4804
4932
|
/** Value owned by the nearest schema-backed ancestor, when one exists. */
|
|
@@ -8917,6 +9045,17 @@ export const logic: <
|
|
|
8917
9045
|
export const child: <const Id extends string, M extends Machine.Any>(id: Id, machine: M) => ChildMachine<Id, M> =
|
|
8918
9046
|
internal.child
|
|
8919
9047
|
|
|
9048
|
+
/**
|
|
9049
|
+
* Binds one machine definition to an open family of runtime child ids.
|
|
9050
|
+
*
|
|
9051
|
+
* Descriptors created by the returned function are interchangeable with
|
|
9052
|
+
* {@link child} descriptors for the same id and machine definition.
|
|
9053
|
+
*
|
|
9054
|
+
* @category constructors
|
|
9055
|
+
* @since 0.20.0
|
|
9056
|
+
*/
|
|
9057
|
+
export const childFamily: <M extends Machine.Any>(machine: M) => ChildMachine.Family<M> = internal.childFamily
|
|
9058
|
+
|
|
8920
9059
|
/**
|
|
8921
9060
|
* Creates a typed parent-local address for lower-level child process logic.
|
|
8922
9061
|
*
|
|
@@ -342,15 +342,7 @@ const makeChildFromRefAtom = <Child extends Machine.ChildMachine.Any, StartError
|
|
|
342
342
|
}
|
|
343
343
|
)
|
|
344
344
|
|
|
345
|
-
const
|
|
346
|
-
makeChildFromRefAtom(
|
|
347
|
-
makeChildRefAtom(ref as any, nested),
|
|
348
|
-
nested
|
|
349
|
-
)
|
|
350
|
-
)
|
|
351
|
-
const child = <Nested extends Machine.ChildMachine.Any>(
|
|
352
|
-
nested: Nested
|
|
353
|
-
): ChildMachineAtom<Nested, StartError> => childFamily(nested) as ChildMachineAtom<Nested, StartError>
|
|
345
|
+
const child = makeChildSelector<StartError>(ref as any)
|
|
354
346
|
|
|
355
347
|
return {
|
|
356
348
|
ref,
|
|
@@ -363,6 +355,30 @@ const makeChildFromRefAtom = <Child extends Machine.ChildMachine.Any, StartError
|
|
|
363
355
|
}
|
|
364
356
|
}
|
|
365
357
|
|
|
358
|
+
const makeChildSelector = <StartError>(
|
|
359
|
+
parentRef: Atom.Atom<
|
|
360
|
+
AsyncResult.AsyncResult<Option.Option<Machine.MachineRef<any, any, any, any, any>>, StartError>
|
|
361
|
+
>
|
|
362
|
+
) => {
|
|
363
|
+
const byMachine = new WeakMap<object, (id: string) => ChildMachineAtom<Machine.ChildMachine.Any, StartError>>()
|
|
364
|
+
return <Child extends Machine.ChildMachine.Any>(descriptor: Child): ChildMachineAtom<Child, StartError> => {
|
|
365
|
+
let family = byMachine.get(descriptor.machine)
|
|
366
|
+
if (family === undefined) {
|
|
367
|
+
const machine = descriptor.machine
|
|
368
|
+
const atoms = Atom.family((id: string) => {
|
|
369
|
+
const child = internalMachine.child(id, machine)
|
|
370
|
+
return makeChildFromRefAtom(
|
|
371
|
+
makeChildRefAtom(parentRef as any, child),
|
|
372
|
+
child
|
|
373
|
+
)
|
|
374
|
+
})
|
|
375
|
+
family = (id) => atoms(id) as ChildMachineAtom<Machine.ChildMachine.Any, StartError>
|
|
376
|
+
byMachine.set(machine, family)
|
|
377
|
+
}
|
|
378
|
+
return family(descriptor.id) as ChildMachineAtom<Child, StartError>
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
366
382
|
const makeFromRefAtom = <State, Event, Error, Output, StartError, Emitted>(
|
|
367
383
|
ref: Atom.Atom<AsyncResult.AsyncResult<Machine.MachineRef<State, Event, Error, Output, Emitted>, StartError>>
|
|
368
384
|
): MachineAtom<State, Event, Error, Output, StartError, Emitted> => {
|
|
@@ -438,15 +454,7 @@ const makeFromRefAtom = <State, Event, Error, Output, StartError, Emitted>(
|
|
|
438
454
|
)
|
|
439
455
|
|
|
440
456
|
const optionalRef = Atom.mapResult(ref, Option.some)
|
|
441
|
-
const
|
|
442
|
-
makeChildFromRefAtom(
|
|
443
|
-
makeChildRefAtom(optionalRef as any, descriptor),
|
|
444
|
-
descriptor
|
|
445
|
-
)
|
|
446
|
-
)
|
|
447
|
-
const child = <Child extends Machine.ChildMachine.Any>(
|
|
448
|
-
descriptor: Child
|
|
449
|
-
): ChildMachineAtom<Child, StartError> => childFamily(descriptor) as ChildMachineAtom<Child, StartError>
|
|
457
|
+
const child = makeChildSelector<StartError>(optionalRef as any)
|
|
450
458
|
|
|
451
459
|
return {
|
|
452
460
|
ref,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as Cause from "effect/Cause"
|
|
8
8
|
import * as Effect from "effect/Effect"
|
|
9
9
|
import * as Stream from "effect/Stream"
|
|
10
|
-
import type { ChildMachine, Inspection, Logic, Machine } from "../../Machine.js"
|
|
10
|
+
import type { ChildMachine, ChildOwner, Inspection, Logic, Machine } from "../../Machine.js"
|
|
11
11
|
import * as Configuration from "./configuration.js"
|
|
12
12
|
import { InfiniteTransitionError, MachineSchemaDecodeError, StoppedError } from "./errors.js"
|
|
13
13
|
import * as InvocationEvent from "./invocationEvent.js"
|
|
@@ -63,6 +63,16 @@ const streamLogic = (
|
|
|
63
63
|
const resolveValue = (value: unknown, context: Machine.InvokeContext<any, any, any, any>): unknown =>
|
|
64
64
|
typeof value === "function" ? value(context) : value
|
|
65
65
|
|
|
66
|
+
const makeChildOwner = (scope: Runtime.ProcessScope<any>): ChildOwner<any> => ({
|
|
67
|
+
spawn:
|
|
68
|
+
((descriptor: ChildMachine.Any, options?: { readonly input?: unknown }) =>
|
|
69
|
+
(scope.spawn as any)(descriptor, options)) as ChildOwner<any>["spawn"],
|
|
70
|
+
sendTo: ((descriptor: ChildMachine.Any, event: unknown) => scope.sendTo(descriptor, event)) as ChildOwner<
|
|
71
|
+
any
|
|
72
|
+
>["sendTo"],
|
|
73
|
+
stop: ((descriptor: ChildMachine.Any) => scope.stopChild(descriptor)) as ChildOwner<any>["stop"]
|
|
74
|
+
})
|
|
75
|
+
|
|
66
76
|
const resolveOne = (
|
|
67
77
|
raw: Record<PropertyKey, any>,
|
|
68
78
|
context: Machine.InvokeContext<any, any, any, any>,
|
|
@@ -291,11 +301,13 @@ export const startAll = (
|
|
|
291
301
|
paths: ReadonlyArray<string>,
|
|
292
302
|
event: Machine.LifecycleEvent<any>
|
|
293
303
|
): Effect.Effect<void, any, any> | undefined => {
|
|
304
|
+
const children = makeChildOwner(scope)
|
|
294
305
|
const effects = Planner.sortEntryPaths(machine, paths)
|
|
295
306
|
.filter((path) => configuration.active.has(path))
|
|
296
307
|
.flatMap((path) => {
|
|
297
308
|
const context = {
|
|
298
309
|
...(Configuration.getMachineReferences(configuration) ?? { self: scope.self, parent: scope.parent }),
|
|
310
|
+
children,
|
|
299
311
|
state: configuration.values.get(path),
|
|
300
312
|
containingState: Configuration.getParentValue(machine, configuration, path),
|
|
301
313
|
ancestors: Configuration.getParentValues(machine, configuration, path),
|
|
@@ -2124,15 +2124,30 @@ export const transition = <State, Event, Error = never, Requirements = never>(
|
|
|
2124
2124
|
export const child = <const Id extends string, M extends Machine.Any>(
|
|
2125
2125
|
id: Id,
|
|
2126
2126
|
machine: M
|
|
2127
|
+
): ChildMachine<Id, M> =>
|
|
2128
|
+
makeChild(id, machine, (input) =>
|
|
2129
|
+
machine.input === undefined
|
|
2130
|
+
? (internalProcess.toProcessLogic as any)(machine)
|
|
2131
|
+
: (internalProcess.toProcessLogic as any)(machine, input))
|
|
2132
|
+
|
|
2133
|
+
const makeChild = <const Id extends string, M extends Machine.Any>(
|
|
2134
|
+
id: Id,
|
|
2135
|
+
machine: M,
|
|
2136
|
+
makeLogic: (input?: unknown) => Logic<any, any, any, any, any, any>
|
|
2127
2137
|
): ChildMachine<Id, M> => ({
|
|
2128
2138
|
[ChildMachineTypeId]: ChildMachineTypeId,
|
|
2129
2139
|
id,
|
|
2130
2140
|
machine,
|
|
2131
|
-
[ChildMachineLogicTypeId]:
|
|
2141
|
+
[ChildMachineLogicTypeId]: makeLogic
|
|
2142
|
+
})
|
|
2143
|
+
|
|
2144
|
+
export const childFamily = <M extends Machine.Any>(machine: M): ChildMachine.Family<M> => {
|
|
2145
|
+
const makeLogic = (input?: unknown): Logic<any, any, any, any, any, any> =>
|
|
2132
2146
|
machine.input === undefined
|
|
2133
2147
|
? (internalProcess.toProcessLogic as any)(machine)
|
|
2134
2148
|
: (internalProcess.toProcessLogic as any)(machine, input)
|
|
2135
|
-
|
|
2149
|
+
return (id) => makeChild(id, machine, makeLogic)
|
|
2150
|
+
}
|
|
2136
2151
|
|
|
2137
2152
|
export const childAddress = <Event = never>(id: string): ChildAddress<Event> => id as ChildAddress<Event>
|
|
2138
2153
|
|
|
@@ -2174,10 +2189,7 @@ export const spawn: {
|
|
|
2174
2189
|
SpawnError<Options>,
|
|
2175
2190
|
ChildInitialError
|
|
2176
2191
|
>
|
|
2177
|
-
} = ((
|
|
2178
|
-
logic: Logic<any, any, any, any, any, any>,
|
|
2179
|
-
options?: SpawnOptions
|
|
2180
|
-
) =>
|
|
2192
|
+
} = ((logic: Logic<any, any, any, any, any, any>, options?: SpawnOptions) =>
|
|
2181
2193
|
Effect.flatMap(
|
|
2182
2194
|
internalRuntime.MachineRuntime,
|
|
2183
2195
|
(runtime) => options === undefined ? runtime.spawn(logic) : (runtime.spawn as any)(logic, options)
|