@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.
Files changed (32) hide show
  1. package/README.md +50 -15
  2. package/dist/Machine.d.ts +93 -2
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +11 -0
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts.map +1 -1
  7. package/dist/internal/machine/atom.js +18 -4
  8. package/dist/internal/machine/atom.js.map +1 -1
  9. package/dist/internal/machine/invocation.d.ts.map +1 -1
  10. package/dist/internal/machine/invocation.js +7 -0
  11. package/dist/internal/machine/invocation.js.map +1 -1
  12. package/dist/internal/machine/machine.d.ts +1 -0
  13. package/dist/internal/machine/machine.d.ts.map +1 -1
  14. package/dist/internal/machine/machine.js +11 -4
  15. package/dist/internal/machine/machine.js.map +1 -1
  16. package/dist/internal/machine/runtime.d.ts +4 -3
  17. package/dist/internal/machine/runtime.d.ts.map +1 -1
  18. package/dist/internal/machine/runtime.js +12 -1
  19. package/dist/internal/machine/runtime.js.map +1 -1
  20. package/dist/unstable/reactivity/AtomMachine.d.ts +10 -8
  21. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  22. package/dist/unstable/reactivity/AtomMachine.js +2 -2
  23. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  24. package/docs/agent-guide.md +302 -1449
  25. package/docs/effect-atom-react.md +230 -0
  26. package/package.json +4 -4
  27. package/src/Machine.ts +141 -2
  28. package/src/internal/machine/atom.ts +26 -18
  29. package/src/internal/machine/invocation.ts +13 -1
  30. package/src/internal/machine/machine.ts +18 -6
  31. package/src/internal/machine/runtime.ts +48 -19
  32. 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.19.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.110"
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.110",
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.110",
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 childFamily = Atom.family((nested: Machine.ChildMachine.Any) =>
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 childFamily = Atom.family((descriptor: Machine.ChildMachine.Any) =>
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]: (input) =>
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)