@typeonce/effect-machine 0.31.1 → 0.32.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 (151) hide show
  1. package/README.md +145 -140
  2. package/dist/Machine.d.ts +428 -254
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +91 -111
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +3 -4
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +0 -2
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/childRegistry.d.ts +52 -0
  11. package/dist/internal/machine/childRegistry.d.ts.map +1 -0
  12. package/dist/internal/machine/childRegistry.js +74 -0
  13. package/dist/internal/machine/childRegistry.js.map +1 -0
  14. package/dist/internal/machine/cluster.d.ts +2 -3
  15. package/dist/internal/machine/cluster.d.ts.map +1 -1
  16. package/dist/internal/machine/cluster.js.map +1 -1
  17. package/dist/internal/machine/configuration.d.ts.map +1 -1
  18. package/dist/internal/machine/configuration.js +14 -4
  19. package/dist/internal/machine/configuration.js.map +1 -1
  20. package/dist/internal/machine/executionPlan.d.ts +1 -0
  21. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  22. package/dist/internal/machine/executionPlan.js +97 -53
  23. package/dist/internal/machine/executionPlan.js.map +1 -1
  24. package/dist/internal/machine/invocation.d.ts.map +1 -1
  25. package/dist/internal/machine/invocation.js +4 -2
  26. package/dist/internal/machine/invocation.js.map +1 -1
  27. package/dist/internal/machine/invocationDefinition.d.ts +35 -0
  28. package/dist/internal/machine/invocationDefinition.d.ts.map +1 -0
  29. package/dist/internal/machine/invocationDefinition.js +21 -0
  30. package/dist/internal/machine/invocationDefinition.js.map +1 -0
  31. package/dist/internal/machine/invocationEvent.d.ts +0 -2
  32. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  33. package/dist/internal/machine/invocationEvent.js +0 -8
  34. package/dist/internal/machine/invocationEvent.js.map +1 -1
  35. package/dist/internal/machine/machine.d.ts +21 -43
  36. package/dist/internal/machine/machine.d.ts.map +1 -1
  37. package/dist/internal/machine/machine.js +98 -374
  38. package/dist/internal/machine/machine.js.map +1 -1
  39. package/dist/internal/machine/planner.d.ts +13 -19
  40. package/dist/internal/machine/planner.d.ts.map +1 -1
  41. package/dist/internal/machine/planner.js +71 -39
  42. package/dist/internal/machine/planner.js.map +1 -1
  43. package/dist/internal/machine/process.d.ts +6 -8
  44. package/dist/internal/machine/process.d.ts.map +1 -1
  45. package/dist/internal/machine/process.js +1 -0
  46. package/dist/internal/machine/process.js.map +1 -1
  47. package/dist/internal/machine/protocol.d.ts +2 -2
  48. package/dist/internal/machine/protocol.d.ts.map +1 -1
  49. package/dist/internal/machine/protocol.js +4 -18
  50. package/dist/internal/machine/protocol.js.map +1 -1
  51. package/dist/internal/machine/readiness.d.ts +1 -1
  52. package/dist/internal/machine/readiness.d.ts.map +1 -1
  53. package/dist/internal/machine/requirements.d.ts +4 -0
  54. package/dist/internal/machine/requirements.d.ts.map +1 -0
  55. package/dist/internal/machine/requirements.js +2 -0
  56. package/dist/internal/machine/requirements.js.map +1 -0
  57. package/dist/internal/machine/runtime.d.ts +4 -58
  58. package/dist/internal/machine/runtime.d.ts.map +1 -1
  59. package/dist/internal/machine/runtime.js +1 -69
  60. package/dist/internal/machine/runtime.js.map +1 -1
  61. package/dist/internal/machine/serialization.d.ts.map +1 -1
  62. package/dist/internal/machine/serialization.js +16 -0
  63. package/dist/internal/machine/serialization.js.map +1 -1
  64. package/dist/internal/machine/stateDefinition.d.ts +3 -1
  65. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  66. package/dist/internal/machine/stateDefinition.js +36 -2
  67. package/dist/internal/machine/stateDefinition.js.map +1 -1
  68. package/dist/internal/machine/targetBuilder.d.ts +16 -0
  69. package/dist/internal/machine/targetBuilder.d.ts.map +1 -0
  70. package/dist/internal/machine/targetBuilder.js +333 -0
  71. package/dist/internal/machine/targetBuilder.js.map +1 -0
  72. package/dist/internal/machine/topology.js +3 -3
  73. package/dist/internal/machine/topology.js.map +1 -1
  74. package/dist/internal/machine/transition.d.ts +16 -0
  75. package/dist/internal/machine/transition.d.ts.map +1 -0
  76. package/dist/internal/machine/transition.js +2 -0
  77. package/dist/internal/machine/transition.js.map +1 -0
  78. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  79. package/dist/internal/testing/machine/finiteModel.js +10 -12
  80. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  81. package/dist/internal/testing/machine/format.d.ts +7 -0
  82. package/dist/internal/testing/machine/format.d.ts.map +1 -0
  83. package/dist/internal/testing/machine/format.js +104 -0
  84. package/dist/internal/testing/machine/format.js.map +1 -0
  85. package/dist/internal/testing/machine/probe.d.ts +1 -1
  86. package/dist/internal/testing/machine/probe.d.ts.map +1 -1
  87. package/dist/internal/testing/machine/probe.js +1 -1
  88. package/dist/internal/testing/machine/probe.js.map +1 -1
  89. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -1
  90. package/dist/internal/testing/machine/referenceModel.js +15 -19
  91. package/dist/internal/testing/machine/referenceModel.js.map +1 -1
  92. package/dist/internal/testing/machine/trace.d.ts +1 -1
  93. package/dist/internal/testing/machine/trace.d.ts.map +1 -1
  94. package/dist/internal/testing/machine/trace.js +3 -3
  95. package/dist/internal/testing/machine/trace.js.map +1 -1
  96. package/dist/internal/testing/machine/value.d.ts +8 -0
  97. package/dist/internal/testing/machine/value.d.ts.map +1 -0
  98. package/dist/internal/testing/machine/value.js +82 -0
  99. package/dist/internal/testing/machine/value.js.map +1 -0
  100. package/dist/internal/testing/machine/verification.d.ts +2 -13
  101. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  102. package/dist/internal/testing/machine/verification.js +21 -113
  103. package/dist/internal/testing/machine/verification.js.map +1 -1
  104. package/dist/testing/MachineTest.d.ts +89 -104
  105. package/dist/testing/MachineTest.d.ts.map +1 -1
  106. package/dist/testing/MachineTest.js +70 -84
  107. package/dist/testing/MachineTest.js.map +1 -1
  108. package/dist/unstable/cluster/ClusterMachine.d.ts +10 -13
  109. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  110. package/dist/unstable/cluster/ClusterMachine.js +5 -8
  111. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  112. package/dist/unstable/reactivity/AtomMachine.d.ts +26 -61
  113. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  114. package/dist/unstable/reactivity/AtomMachine.js +17 -26
  115. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  116. package/docs/agent-guide.md +45 -45
  117. package/docs/effect-atom-react.md +35 -63
  118. package/docs/machine-review.md +7 -7
  119. package/docs/root-api.md +214 -0
  120. package/package.json +1 -1
  121. package/src/Machine.ts +778 -437
  122. package/src/internal/machine/atom.ts +3 -13
  123. package/src/internal/machine/childRegistry.ts +141 -0
  124. package/src/internal/machine/cluster.ts +5 -14
  125. package/src/internal/machine/configuration.ts +20 -4
  126. package/src/internal/machine/executionPlan.ts +109 -52
  127. package/src/internal/machine/invocation.ts +10 -6
  128. package/src/internal/machine/invocationDefinition.ts +52 -0
  129. package/src/internal/machine/invocationEvent.ts +0 -8
  130. package/src/internal/machine/machine.ts +154 -679
  131. package/src/internal/machine/planner.ts +82 -66
  132. package/src/internal/machine/process.ts +14 -21
  133. package/src/internal/machine/protocol.ts +9 -32
  134. package/src/internal/machine/readiness.ts +1 -0
  135. package/src/internal/machine/requirements.ts +11 -0
  136. package/src/internal/machine/runtime.ts +17 -186
  137. package/src/internal/machine/serialization.ts +22 -0
  138. package/src/internal/machine/stateDefinition.ts +36 -3
  139. package/src/internal/machine/targetBuilder.ts +525 -0
  140. package/src/internal/machine/topology.ts +3 -3
  141. package/src/internal/machine/transition.ts +26 -0
  142. package/src/internal/testing/machine/finiteModel.ts +22 -23
  143. package/src/internal/testing/machine/format.ts +120 -0
  144. package/src/internal/testing/machine/probe.ts +4 -4
  145. package/src/internal/testing/machine/referenceModel.ts +25 -17
  146. package/src/internal/testing/machine/trace.ts +4 -4
  147. package/src/internal/testing/machine/value.ts +77 -0
  148. package/src/internal/testing/machine/verification.ts +904 -1109
  149. package/src/testing/MachineTest.ts +103 -114
  150. package/src/unstable/cluster/ClusterMachine.ts +10 -20
  151. package/src/unstable/reactivity/AtomMachine.ts +42 -79
@@ -18,6 +18,7 @@ import type * as Machine from "../../Machine.js"
18
18
  import type { Bound, ChildMachineAtom, MachineAtom } from "../../unstable/reactivity/AtomMachine.js"
19
19
  import * as internalMachine from "./machine.js"
20
20
  import type { EnsureExecutable } from "./readiness.js"
21
+ import type { ExcludeCompatibleRuntime as ExcludeCompatibleMachineRuntime, IsAny } from "./requirements.js"
21
22
  import * as Topology from "./topology.js"
22
23
 
23
24
  export class NotReadyError extends Data.TaggedError("NotReadyError") {}
@@ -36,15 +37,6 @@ type EnsureNoExternalRequirements<Requirements> = [ExternalRequirements<Requirem
36
37
  readonly [ExternalRequirementsTypeId]: ExternalRequirements<Requirements>
37
38
  }
38
39
 
39
- type IsAny<A> = 0 extends (1 & A) ? true : false
40
-
41
- type ExcludeCompatibleMachineRuntime<Requirements, Events, Emits> = Requirements extends
42
- Machine.Runtime.Requirement<infer RequiredEvents, infer RequiredEmits> ?
43
- IsAny<Requirements> extends true ? Requirements
44
- : [RequiredEvents] extends [Events] ? [RequiredEmits] extends [Emits] ? never : Requirements
45
- : Requirements
46
- : Requirements
47
-
48
40
  type MachineRequirements<InitialR, R, Events, Emits> = ExcludeCompatibleMachineRuntime<
49
41
  Machine.ExecutionServices<InitialR | R>,
50
42
  Events,
@@ -391,7 +383,6 @@ const makeChildFromRefAtom = <Child extends Machine.ChildMachine.Any, StartError
391
383
  return {
392
384
  ref,
393
385
  snapshot,
394
- state: Atom.mapResult(snapshot, Option.map((snapshot) => snapshot.state)),
395
386
  result: makeChildRuntimeResultAtom(snapshot),
396
387
  send,
397
388
  stop,
@@ -504,7 +495,6 @@ const makeFromRefAtom = <State, Event, Error, Output, StartError, Emitted>(
504
495
  const result = {
505
496
  ref,
506
497
  snapshot,
507
- state: Atom.mapResult(snapshot, (snapshot) => snapshot.state),
508
498
  result: makeRuntimeResultAtom(snapshot),
509
499
  send,
510
500
  stop,
@@ -775,7 +765,7 @@ export const can = (event: unknown) => {
775
765
  type MachineResumeRequirementsOf<M extends Machine.Machine.Any> = MachineResumeRequirements<
776
766
  Machine.Machine.Services<M>,
777
767
  Machine.Machine.Event<M>,
778
- Machine.Machine.Emit<M>
768
+ Machine.Machine.EmittedEvent<M>
779
769
  >
780
770
 
781
771
  type EnsureMachineExecutable<M extends Machine.Machine.Any> = IsAny<Machine.Machine.States<M>> extends true ? {
@@ -835,7 +825,7 @@ export const make: {
835
825
  InitialR,
836
826
  R,
837
827
  Machine.Machine.EventOf<Events>,
838
- Machine.Machine.EmitOf<Emits>
828
+ Machine.Machine.EmittedEventOf<Emits>
839
829
  >
840
830
  >
841
831
  & EnsureExecutable<States, UnhandledStates, OutputStates>
@@ -0,0 +1,141 @@
1
+ /** Owns child reservations and ordered observer buffers for one local runtime. */
2
+ import * as Deferred from "effect/Deferred"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Option from "effect/Option"
5
+ import type * as Scope from "effect/Scope"
6
+ import type { MachineRef } from "./runtime.js"
7
+
8
+ export type ChildDescriptor = {
9
+ readonly id: string
10
+ readonly machine: object
11
+ }
12
+
13
+ export type ChildEntry =
14
+ | {
15
+ readonly _tag: "Starting"
16
+ readonly token: symbol
17
+ readonly ownerKey?: string
18
+ readonly ownerPath?: string
19
+ ownerActive?: boolean
20
+ }
21
+ | {
22
+ readonly _tag: "Started"
23
+ readonly token: symbol
24
+ readonly descriptor: ChildDescriptor | undefined
25
+ readonly ref: MachineRef<any, any, any, any>
26
+ readonly ownerKey?: string
27
+ readonly ownerPath?: string
28
+ ownerActive?: boolean
29
+ }
30
+
31
+ export type ChildSelector = string | ChildDescriptor
32
+ export type ChildKey = string | symbol
33
+
34
+ type ChildObservation = Option.Option<MachineRef<any, any, any, any>>
35
+ type ChildObservationBatch = [ChildObservation, ...Array<ChildObservation>]
36
+
37
+ export interface ChildObserver {
38
+ readonly child: ChildSelector
39
+ readonly id: string
40
+ values: ChildObservationBatch | undefined
41
+ waiter: Deferred.Deferred<void> | undefined
42
+ }
43
+
44
+ export const offerChildObservation = (
45
+ observer: ChildObserver,
46
+ value: ChildObservation
47
+ ): void => {
48
+ if (observer.values === undefined) {
49
+ observer.values = [value]
50
+ } else {
51
+ observer.values.push(value)
52
+ }
53
+ if (observer.waiter !== undefined) {
54
+ const waiter = observer.waiter
55
+ observer.waiter = undefined
56
+ Deferred.doneUnsafe(waiter, Effect.void)
57
+ }
58
+ }
59
+
60
+ export const takeChildObservations = (
61
+ observer: ChildObserver
62
+ ): Effect.Effect<ChildObservationBatch> =>
63
+ Effect.suspend(() => {
64
+ if (observer.values !== undefined) {
65
+ const values = observer.values
66
+ observer.values = undefined
67
+ return Effect.succeed(values)
68
+ }
69
+ const waiter = Deferred.makeUnsafe<void>()
70
+ observer.waiter = waiter
71
+ return Deferred.await(waiter).pipe(Effect.andThen(takeChildObservations(observer)))
72
+ })
73
+
74
+ export interface ChildRegistry {
75
+ closed: boolean
76
+ readonly children: Map<ChildKey, ChildEntry>
77
+ observers: Set<ChildObserver> | undefined
78
+ scope: Scope.Closeable | undefined
79
+ }
80
+
81
+ export const matchesChild = (
82
+ entry: ChildEntry,
83
+ child: ChildSelector
84
+ ): entry is Extract<ChildEntry, { readonly _tag: "Started" }> =>
85
+ entry._tag === "Started" && (typeof child === "string" || (
86
+ entry.descriptor !== undefined &&
87
+ entry.descriptor.id === child.id &&
88
+ entry.descriptor.machine === child.machine
89
+ ))
90
+
91
+ export const selectRegistryChild = (
92
+ registry: ChildRegistry,
93
+ id: string,
94
+ child: ChildSelector
95
+ ): ChildObservation => {
96
+ if (registry.closed) return Option.none()
97
+ const entry = registry.children.get(id)
98
+ return entry !== undefined && matchesChild(entry, child) ? Option.some(entry.ref) : Option.none()
99
+ }
100
+
101
+ export const publishRegistryChange = (registry: ChildRegistry): void => {
102
+ if (registry.observers === undefined) return
103
+ for (const observer of registry.observers) {
104
+ offerChildObservation(observer, selectRegistryChild(registry, observer.id, observer.child))
105
+ }
106
+ }
107
+
108
+ export const unregisterChild = (registry: ChildRegistry, key: ChildKey, token: symbol): void => {
109
+ const entry = registry.children.get(key)
110
+ if (entry === undefined || entry.token !== token) return
111
+ registry.children.delete(key)
112
+ if (typeof key === "string") publishRegistryChange(registry)
113
+ }
114
+
115
+ export const registerChild = (
116
+ registry: ChildRegistry,
117
+ key: ChildKey,
118
+ token: symbol,
119
+ ref: MachineRef<any, any, any, any>,
120
+ descriptor: ChildDescriptor | undefined
121
+ ): boolean => {
122
+ const entry = registry.children.get(key)
123
+ if (registry.closed || entry === undefined || entry._tag !== "Starting" || entry.token !== token) {
124
+ return false
125
+ }
126
+ registry.children.delete(key)
127
+ const started: ChildEntry = entry.ownerKey === undefined
128
+ ? { _tag: "Started", token, descriptor, ref }
129
+ : {
130
+ _tag: "Started",
131
+ token,
132
+ descriptor,
133
+ ref,
134
+ ownerKey: entry.ownerKey,
135
+ ownerPath: entry.ownerPath!,
136
+ ownerActive: entry.ownerActive === true
137
+ }
138
+ registry.children.set(key, started)
139
+ if (typeof key === "string") publishRegistryChange(registry)
140
+ return true
141
+ }
@@ -16,6 +16,7 @@ import type { Checkpoint, ClusterMachine, LoadResult } from "../../unstable/clus
16
16
  import * as internalMachine from "./machine.js"
17
17
  import * as Protocol from "./protocol.js"
18
18
  import type { EnsureExecutable } from "./readiness.js"
19
+ import type { ExcludeCompatibleRuntime } from "./requirements.js"
19
20
 
20
21
  type EntityAddress = EntityAddress.EntityAddress
21
22
  type PersistenceError = ClusterError.PersistenceError
@@ -97,17 +98,7 @@ type SendRpc<Events extends ReadonlyArray<Machine.Machine.TaggedSchema>> = Rpc.R
97
98
 
98
99
  type MachineEvents<M extends Machine.Machine.Any> = Machine.Machine.InputEvents<M>
99
100
 
100
- type MachineEmits<M extends Machine.Machine.Any> = Machine.Machine.Emits<M>
101
-
102
- type IsAny<A> = 0 extends (1 & A) ? true : false
103
-
104
- type ExcludeCompatibleRuntime<Requirements, Events, Emits> = Requirements extends Machine.Runtime.Requirement<
105
- infer RequiredEvents,
106
- infer RequiredEmits
107
- > ? IsAny<Requirements> extends true ? Requirements
108
- : [RequiredEvents] extends [Events] ? [RequiredEmits] extends [Emits] ? never : Requirements
109
- : Requirements
110
- : Requirements
101
+ type MachineEmits<M extends Machine.Machine.Any> = Machine.Machine.EmittedEvents<M>
111
102
 
112
103
  const hasInvokes = (machine: Machine.Machine.Any): boolean =>
113
104
  Reflect.ownKeys(machine.handlers).some((key) => machine.handlers[key as string]?.invoke !== undefined)
@@ -234,7 +225,7 @@ export const make = <
234
225
  | ExcludeCompatibleRuntime<
235
226
  Machine.ExecutionServices<R | InitialR>,
236
227
  Machine.Machine.EventOf<Events>,
237
- Machine.Machine.EmitOf<Emits>
228
+ Machine.Machine.EmittedEventOf<Emits>
238
229
  >
239
230
  | Machine.Machine.SnapshotDecodingServices<States>
240
231
  | Machine.Machine.SnapshotEncodingServices<States>
@@ -270,7 +261,7 @@ export const make = <
270
261
  | ExcludeCompatibleRuntime<
271
262
  Machine.ExecutionServices<R | InitialR>,
272
263
  Machine.Machine.EventOf<Events>,
273
- Machine.Machine.EmitOf<Emits>
264
+ Machine.Machine.EmittedEventOf<Emits>
274
265
  >
275
266
  | Machine.Machine.SnapshotDecodingServices<States>
276
267
  | Machine.Machine.SnapshotEncodingServices<States>
@@ -292,7 +283,7 @@ export const make = <
292
283
  Effect.mapError((error) => reject("PersistenceFailure", String(error.cause)))
293
284
  )
294
285
  let current: Machine.Machine.Snapshot<States> | undefined
295
- const emitted: Array<Machine.Machine.EmitOf<MachineEmits<M>>> = []
286
+ const emitted: Array<Machine.Machine.EmittedEventOf<MachineEmits<M>>> = []
296
287
 
297
288
  if (Option.isSome(loaded.checkpoint)) {
298
289
  const checkpoint = loaded.checkpoint.value
@@ -292,7 +292,8 @@ const pathToRootCache = new WeakMap<Machine.StateNodes, Map<string, ReadonlyArra
292
292
 
293
293
  export const hasOwn = (u: object, key: string): boolean => Object.prototype.hasOwnProperty.call(u, key)
294
294
 
295
- export const isDescendantOf = (path: string, ancestor: string): boolean => path.startsWith(`${ancestor}.`)
295
+ export const isDescendantOf = (path: string, ancestor: string): boolean =>
296
+ path !== ancestor && (ancestor === "" || path.startsWith(`${ancestor}.`))
296
297
 
297
298
  export const isPathInSubtree = (path: string, ancestor: string): boolean =>
298
299
  path === ancestor || isDescendantOf(path, ancestor)
@@ -642,6 +643,14 @@ export const normalizeConfigurationEffect = <const States extends Machine.StateS
642
643
  ): Effect.Effect<ActiveConfiguration, MachineSchemaDecodeError> =>
643
644
  configurationFromSnapshotEffect(machine, state).pipe(
644
645
  Effect.catchCause((cause) => {
646
+ if (Cause.hasInterrupts(cause)) {
647
+ return Effect.failCause(Cause.map(cause, (error) =>
648
+ error instanceof MachineSchemaDecodeError ? error : new MachineSchemaDecodeError({
649
+ machineId: machine.id,
650
+ boundary: "configuration",
651
+ cause: Cause.fail(error)
652
+ })))
653
+ }
645
654
  const error = Cause.findErrorOption(cause)
646
655
  return Option.isSome(error) && error.value instanceof MachineSchemaDecodeError
647
656
  ? Effect.fail(error.value)
@@ -1160,7 +1169,10 @@ export const isActiveFinalNode = (
1160
1169
  const node = getNode(machine, path)
1161
1170
  if (node.type === "compound") {
1162
1171
  const child = getActiveChildPath(machine, configuration, path)
1163
- return child !== undefined && isDirectFinalPath(machine, child)
1172
+ return child !== undefined &&
1173
+ (path === ""
1174
+ ? !hasCompletionHandler(machine, child) && isActiveFinalNode(machine, configuration, child)
1175
+ : isDirectFinalPath(machine, child))
1164
1176
  }
1165
1177
  if (node.type === "parallel") {
1166
1178
  for (const child of node.children) {
@@ -1261,7 +1273,9 @@ export const completeActiveFinalNodeEffect: <
1261
1273
  const node = getNode(machine, path)
1262
1274
  if (node.type === "compound") {
1263
1275
  const child = getActiveChildPath(machine, configuration, path)
1264
- if (child === undefined || !isDirectFinalPath(machine, child)) {
1276
+ if (
1277
+ child === undefined || (path === "" ? hasCompletionHandler(machine, child) : !isDirectFinalPath(machine, child))
1278
+ ) {
1265
1279
  return undefined
1266
1280
  }
1267
1281
  const childCompletion = yield* completeActiveFinalNodeEffect(
@@ -1395,7 +1409,9 @@ const completeActiveFinalNodeSync = <const Events extends ReadonlyArray<Machine.
1395
1409
  const node = getNode(machine, path)
1396
1410
  if (node.type === "compound") {
1397
1411
  const child = getActiveChildPath(machine, configuration, path)
1398
- if (child === undefined || !isDirectFinalPath(machine, child)) return undefined
1412
+ if (
1413
+ child === undefined || (path === "" ? hasCompletionHandler(machine, child) : !isDirectFinalPath(machine, child))
1414
+ ) return undefined
1399
1415
  const childCompletion = completeActiveFinalNodeSync(machine, configuration, child, event, outputs, completions)
1400
1416
  if (childCompletion === undefined) return undefined
1401
1417
  const completion = setCompletedOutput(outputs, path, childCompletion.output)
@@ -59,6 +59,8 @@ import {
59
59
 
60
60
  interface IndexedExecutionDescriptor {
61
61
  readonly flat: boolean
62
+ /** Distinguishes structural nodes from valued slots, including decoded undefined. */
63
+ readonly valued: ReadonlyArray<boolean>
62
64
  readonly nodes: ReadonlyArray<Machine.StateNode>
63
65
  readonly indexByPath: ReadonlyMap<string, number>
64
66
  readonly parentIndices: ReadonlyArray<number>
@@ -140,12 +142,6 @@ const compileIndexedExecutionDescriptor = (
140
142
  if (node.type === "choice" || node.type === "history") {
141
143
  return undefined
142
144
  }
143
- // Structural active states deliberately use the generic semantic
144
- // reference until the indexed value table represents absent values as an
145
- // explicit capability rather than an `undefined` slot.
146
- if (node.schema === undefined) {
147
- return undefined
148
- }
149
145
  if (node.type === "atomic" || node.type === "final") {
150
146
  leafPaths.push(node.path)
151
147
  }
@@ -210,7 +206,20 @@ const compileIndexedExecutionDescriptor = (
210
206
  dispatchByLeaf.set(leafIndex, dispatch)
211
207
  }
212
208
  return {
213
- flat: nodes.every((node) => node.parent === undefined && (node.type === "atomic" || node.type === "final")),
209
+ valued: Object.freeze(nodes.map((node) => node.schema !== undefined)),
210
+ // A structural root with direct leaves has the same event kernel as an
211
+ // atomic root. Root behavior and targets that can rebuild the root retain
212
+ // the hierarchical planner so their entry and exit boundaries stay explicit.
213
+ flat: nodes.every((node) => node.parent === undefined && (node.type === "atomic" || node.type === "final")) || (
214
+ nodes[0]?.path === "" && nodes[0].type === "compound" && nodes[0].schema === undefined &&
215
+ (machine.handlers[""] === undefined || Reflect.ownKeys(machine.handlers[""]).length === 0) &&
216
+ nodes.slice(1).every((node) => node.parent === "" && (node.type === "atomic" || node.type === "final")) &&
217
+ [...transitionsByPath.values()].every((events) =>
218
+ [...events.values()].every((transition) =>
219
+ transition.targets !== undefined && transition.targets.every((path) => path !== "")
220
+ )
221
+ )
222
+ ),
214
223
  nodes,
215
224
  indexByPath,
216
225
  parentIndices,
@@ -276,7 +285,7 @@ const ownedIndexedStateFromActive = (
276
285
  const index = descriptor.indexByPath.get(path)
277
286
  if (index === undefined) throw new Error(`Machine expected indexed active path "${path}"`)
278
287
  active[index] = 1
279
- values[index] = configuration.values.get(path)
288
+ if (descriptor.valued[index]) values[index] = configuration.values.get(path)
280
289
  }
281
290
  for (const [path, output] of configuration.outputs) {
282
291
  const index = descriptor.indexByPath.get(path)
@@ -306,7 +315,7 @@ const activeConfigurationFromIndexedState = (
306
315
  if (configuration.active[index] !== 1) continue
307
316
  const path = descriptor.nodes[index]!.path
308
317
  active.add(path)
309
- values.set(path, configuration.values[index])
318
+ if (descriptor.valued[index]) values.set(path, configuration.values[index])
310
319
  }
311
320
  for (const index of configuration.completedOrder) {
312
321
  if (configuration.completed[index] === 1) {
@@ -324,7 +333,7 @@ const snapshotFromIndexedStateStatePath = (
324
333
  const node = descriptor.nodes[index]!
325
334
  const snapshot: Record<string, unknown> = {
326
335
  path: node.path,
327
- value: configuration.values[index]
336
+ value: descriptor.valued[index] ? configuration.values[index] : undefined
328
337
  }
329
338
  if (node.type === "compound") {
330
339
  const childIndex = descriptor.childIndices[index]!.find((childIndex) => configuration.active[childIndex] === 1)
@@ -383,7 +392,9 @@ const makeIndexedTransitionContext = (
383
392
  const parentIndex = descriptor.parentIndices[sourceIndex]!
384
393
  const ancestors: Record<string, unknown> = {}
385
394
  for (const ancestorIndex of descriptor.ancestorIndices[sourceIndex]!) {
386
- ancestors[descriptor.nodes[ancestorIndex]!.path] = configuration.values[ancestorIndex]
395
+ if (descriptor.valued[ancestorIndex]) {
396
+ ancestors[descriptor.nodes[ancestorIndex]!.path] = configuration.values[ancestorIndex]
397
+ }
387
398
  }
388
399
  return {
389
400
  ...resolveMachineReferences(machine, machineReferences),
@@ -426,6 +437,7 @@ export interface ExecutionMicrostep<State = unknown> {
426
437
  }
427
438
 
428
439
  export interface ExecutionMacrostep<State = unknown> {
440
+ readonly event: unknown
429
441
  readonly next: State
430
442
  readonly commands: ReadonlyArray<RuntimeCommand>
431
443
  readonly emittedEvents: ReadonlyArray<unknown>
@@ -564,11 +576,10 @@ const collectIndexedEvaluatedTransition = (
564
576
  const index = descriptor.indexByPath.get(stateUpdate.path)
565
577
  const node = index === undefined ? undefined : descriptor.nodes[index]
566
578
  if (
567
- index === undefined || node === undefined || state.active[index] !== 1 || node.schema === undefined ||
568
- (node.type !== "compound" && node.type !== "parallel")
579
+ index === undefined || node === undefined || state.active[index] !== 1 || node.schema === undefined
569
580
  ) {
570
581
  throw new Error(
571
- `Machine state update owner "${stateUpdate.path}" must be an active valued compound or parallel state`
582
+ `Machine state update owner "${stateUpdate.path}" must be an active valued state`
572
583
  )
573
584
  }
574
585
  return {
@@ -794,6 +805,7 @@ const planIndexedFlatState = (
794
805
  throw new Error("Machine reached a terminal indexed configuration without a completed root output")
795
806
  }
796
807
  return {
808
+ event: decoded,
797
809
  next: ownedIndexedStateFromActive(descriptor, completed),
798
810
  commands: commands ?? emptyExecutionValues,
799
811
  emittedEvents: emittedEvents ?? emptyExecutionValues,
@@ -820,7 +832,15 @@ const planIndexedFlatState = (
820
832
  },
821
833
  transition.evaluate
822
834
  )
823
- const target = transitionResult.state
835
+ const update = isStateUpdate(transitionResult.state) ? transitionResult.state : undefined
836
+ const target = update === undefined ? transitionResult.state : undefined
837
+ if (update !== undefined) {
838
+ const node = descriptor.nodes[sourceIndex]!
839
+ if (update.path !== sourcePath || node.schema === undefined) {
840
+ throw new Error(`Machine state update owner "${update.path}" must be the active valued state`)
841
+ }
842
+ updateOwnedIndexedValue(current, sourceIndex, decodeStateValueSync(machine, node, update.value))
843
+ }
824
844
  validateDeclaredTransitionTarget(
825
845
  sourcePath,
826
846
  { type: "event", event: event._tag },
@@ -871,7 +891,7 @@ const planIndexedFlatState = (
871
891
  branchKey: transitionResult.branchKey,
872
892
  target: target === undefined ? undefined : getTargetNodePath(target as any),
873
893
  resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any),
874
- updates: []
894
+ updates: update === undefined ? [] : [update.path]
875
895
  }]
876
896
  }
877
897
  : undefined),
@@ -901,6 +921,7 @@ const planIndexedFlatState = (
901
921
  const raised = raisedEvents?.[raisedIndex]
902
922
  if (raised === undefined) {
903
923
  return {
924
+ event: decoded,
904
925
  next: current,
905
926
  commands: commands ?? emptyExecutionValues,
906
927
  emittedEvents: emittedEvents ?? emptyExecutionValues,
@@ -935,6 +956,7 @@ const planIndexedState = (
935
956
  input
936
957
  )
937
958
  return {
959
+ event: planned.event,
938
960
  next: ownedIndexedStateFromActive(descriptor, planned.next),
939
961
  commands: planned.commands,
940
962
  emittedEvents: planned.emittedEvents,
@@ -966,6 +988,7 @@ const planIndexedState = (
966
988
  throw new Error("Machine reached a terminal indexed configuration without a completed root output")
967
989
  }
968
990
  return {
991
+ event: decoded,
969
992
  next: ownedIndexedStateFromActive(descriptor, completed),
970
993
  commands: [],
971
994
  emittedEvents: [],
@@ -979,6 +1002,7 @@ const planIndexedState = (
979
1002
  const selections = selectIndexedEventTransitions(machine, descriptor, configuration, decoded, machineReferences)
980
1003
  if (selections.length === 0) {
981
1004
  return {
1005
+ event: decoded,
982
1006
  next: configuration,
983
1007
  commands: [],
984
1008
  emittedEvents: [],
@@ -1021,6 +1045,7 @@ const planIndexedState = (
1021
1045
  throw new Error("Machine reached a terminal indexed configuration without a completed root output")
1022
1046
  }
1023
1047
  return {
1048
+ event: decoded,
1024
1049
  next: current,
1025
1050
  commands,
1026
1051
  emittedEvents,
@@ -1034,6 +1059,7 @@ const planIndexedState = (
1034
1059
  const raised = raisedEvents[raisedIndex]
1035
1060
  if (raised === undefined) {
1036
1061
  return {
1062
+ event: decoded,
1037
1063
  next: current,
1038
1064
  commands,
1039
1065
  emittedEvents,
@@ -1103,43 +1129,74 @@ const makeIndexedExecutionPlan = (
1103
1129
  snapshot: (state) => snapshotFromIndexedState(indexed, state as OwnedIndexedState),
1104
1130
  plan: (state, event, retainMicrosteps = false, machineReferences) =>
1105
1131
  planIndexedState(machine, indexed, state as OwnedIndexedState, event, retainMicrosteps, machineReferences),
1106
- initial: (args, machineReferences) => {
1107
- const inputArgs = machine.input === undefined
1108
- ? args
1109
- : args.length === 0
1110
- ? (decodeInputSync(machine, machine.input, undefined), args)
1111
- : [decodeInputSync(machine, machine.input, args[0])]
1112
- const initial = machine.initial(...inputArgs as any)
1113
- const normalized = normalizeConfigurationSync(machine, initial as Machine.Snapshot<any>)
1114
- const active = machineReferences === undefined ? normalized : withMachineReferences(normalized, machineReferences)
1115
- validateInitialConfiguration(machine, active)
1116
- const completed = completeConfigurationSync(machine, active, InitialEvent).configuration
1117
- const configuration = ownedIndexedStateFromActive(indexed, completed)
1118
- const state = snapshotFromIndexedState(indexed, configuration)
1119
- const done = isActiveFinalConfiguration(machine, completed)
1120
- if (!done) {
1121
- return {
1122
- state,
1123
- configuration,
1124
- activeConfiguration: completed,
1125
- initialEntryPaths: getInitialEntryPaths(machine, completed),
1126
- done: false,
1127
- output: undefined
1132
+ // Initializers may enqueue commands and emissions. Managed startup owns that
1133
+ // work through the generic initial planner; indexed event execution remains valid.
1134
+ ...(Object.values(machine.handlers as Record<string, Machine.AnyStateConfig>).some((config) =>
1135
+ "initialize" in config
1136
+ ) ?
1137
+ {} :
1138
+ {
1139
+ initial: (args: ReadonlyArray<unknown>, machineReferences?: PlanningMachineReferences) => {
1140
+ const inputArgs = machine.input === undefined
1141
+ ? args
1142
+ : args.length === 0
1143
+ ? (decodeInputSync(machine, machine.input, undefined), args)
1144
+ : [decodeInputSync(machine, machine.input, args[0])]
1145
+ const initial = machine.initial(...inputArgs as any)
1146
+ const resolved = isInitialTarget(initial)
1147
+ ? resolveInitialTarget(
1148
+ machine,
1149
+ {
1150
+ active: new Set(),
1151
+ values: new Map(),
1152
+ outputs: new Map(),
1153
+ history: new Map(),
1154
+ ...(machineReferences === undefined ? {} : { machineReferences })
1155
+ },
1156
+ initial,
1157
+ InitialEvent
1158
+ ).target
1159
+ : initial
1160
+ const normalized = isInitialTarget(initial)
1161
+ ? normalizeTargetConfigurationSync(machine, {
1162
+ active: new Set(),
1163
+ values: new Map(),
1164
+ outputs: new Map(),
1165
+ history: new Map()
1166
+ }, resolved)
1167
+ : normalizeConfigurationSync(machine, resolved as Machine.Snapshot<any>)
1168
+ const active = machineReferences === undefined
1169
+ ? normalized
1170
+ : withMachineReferences(normalized, machineReferences)
1171
+ if (machine.initialDefinition.selection.kind === "initial") validateInitialConfiguration(machine, active)
1172
+ const completed = completeConfigurationSync(machine, active, InitialEvent).configuration
1173
+ const configuration = ownedIndexedStateFromActive(indexed, completed)
1174
+ const state = snapshotFromIndexedState(indexed, configuration)
1175
+ const done = isActiveFinalConfiguration(machine, completed)
1176
+ if (!done) {
1177
+ return {
1178
+ state,
1179
+ configuration,
1180
+ activeConfiguration: completed,
1181
+ initialEntryPaths: getInitialEntryPaths(machine, completed),
1182
+ done: false,
1183
+ output: undefined
1184
+ }
1185
+ }
1186
+ const root = getRootPath(machine, completed)
1187
+ if (!completed.outputs.has(root)) {
1188
+ throw new Error("Machine reached a terminal configuration without a completed root output")
1189
+ }
1190
+ return {
1191
+ state,
1192
+ configuration,
1193
+ activeConfiguration: completed,
1194
+ initialEntryPaths: getInitialEntryPaths(machine, completed),
1195
+ done: true,
1196
+ output: completed.outputs.get(root)
1197
+ }
1128
1198
  }
1129
- }
1130
- const root = getRootPath(machine, completed)
1131
- if (!completed.outputs.has(root)) {
1132
- throw new Error("Machine reached a terminal configuration without a completed root output")
1133
- }
1134
- return {
1135
- state,
1136
- configuration,
1137
- activeConfiguration: completed,
1138
- initialEntryPaths: getInitialEntryPaths(machine, completed),
1139
- done: true,
1140
- output: completed.outputs.get(root)
1141
- }
1142
- }
1199
+ })
1143
1200
  })
1144
1201
 
1145
1202
  export type ExecutionPlanStrategy = "generic" | "indexed-flat" | "indexed-hierarchical" | "auto"
@@ -10,6 +10,7 @@ import * as Stream from "effect/Stream"
10
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
+ import * as InvocationDefinition from "./invocationDefinition.js"
13
14
  import * as InvocationEvent from "./invocationEvent.js"
14
15
  import * as Planner from "./planner.js"
15
16
  import * as Runtime from "./runtime.js"
@@ -74,7 +75,7 @@ const makeChildOwner = (scope: Runtime.ProcessScope<any>): ChildOwner<any> => ({
74
75
  })
75
76
 
76
77
  const resolveOne = (
77
- raw: Record<PropertyKey, any>,
78
+ raw: InvocationDefinition.InvocationDefinition,
78
79
  context: Machine.InvokeContext<any, any, any, any>,
79
80
  path: string
80
81
  ): AnyConfig => {
@@ -134,7 +135,7 @@ const resolveOne = (
134
135
  }
135
136
  }
136
137
  if ("child" in raw) {
137
- const descriptor = raw.child as ChildMachine.Any
138
+ const descriptor = raw.child
138
139
  return {
139
140
  id: descriptor.id,
140
141
  address: descriptor.id,
@@ -148,7 +149,8 @@ const resolveOne = (
148
149
  onSnapshot: raw.onSnapshot
149
150
  }
150
151
  }
151
- throw new Error("Machine invoke must define exactly one of effect, stream, after, logic, or child")
152
+ const unsupported: never = raw
153
+ throw new Error(`Unsupported captured machine invocation: ${String(unsupported)}`)
152
154
  }
153
155
 
154
156
  /** @internal */
@@ -266,12 +268,12 @@ const startStaticChild = (
266
268
  scope: Runtime.ProcessScope<any>,
267
269
  ownedChildren: Runtime.OwnedChildRuntime,
268
270
  path: string,
269
- raw: Record<PropertyKey, any>
271
+ raw: Extract<InvocationDefinition.InvocationDefinition, { readonly child: ChildMachine.Any }>
270
272
  ): Effect.Effect<void, any, any> => {
271
273
  // A zero-input child has no entry-context dependency. Reuse the descriptor's
272
274
  // source function so each running parent does not retain a resolved config
273
275
  // object and an otherwise redundant source closure.
274
- const descriptor = raw.child as ChildMachine.Any
276
+ const descriptor = raw.child
275
277
  return startResolved(
276
278
  scope,
277
279
  ownedChildren,
@@ -313,7 +315,9 @@ export const startAll = (
313
315
  ancestors: Configuration.getParentValues(machine, configuration, path),
314
316
  event
315
317
  }
316
- return InvocationEvent.definitions(Configuration.getStateConfigByPath(machine, path)?.invoke).map((definition) =>
318
+ return InvocationDefinition.definitions(Configuration.getStateConfigByPath(machine, path)?.invoke).map((
319
+ definition
320
+ ) =>
317
321
  "child" in definition && !("input" in definition)
318
322
  ? startStaticChild(scope, ownedChildren, path, definition)
319
323
  : start(scope, ownedChildren, path, resolveOne(definition, context, path))