@typeonce/effect-machine 0.21.0 → 0.22.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 (44) hide show
  1. package/README.md +83 -14
  2. package/dist/Machine.d.ts +142 -52
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +2 -2
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  7. package/dist/internal/machine/executionPlan.js +23 -9
  8. package/dist/internal/machine/executionPlan.js.map +1 -1
  9. package/dist/internal/machine/initialization.d.ts.map +1 -1
  10. package/dist/internal/machine/initialization.js +7 -2
  11. package/dist/internal/machine/initialization.js.map +1 -1
  12. package/dist/internal/machine/machine.d.ts.map +1 -1
  13. package/dist/internal/machine/machine.js +94 -13
  14. package/dist/internal/machine/machine.js.map +1 -1
  15. package/dist/internal/machine/planner.d.ts +5 -0
  16. package/dist/internal/machine/planner.d.ts.map +1 -1
  17. package/dist/internal/machine/planner.js +23 -9
  18. package/dist/internal/machine/planner.js.map +1 -1
  19. package/dist/internal/machine/topology.d.ts +10 -1
  20. package/dist/internal/machine/topology.d.ts.map +1 -1
  21. package/dist/internal/machine/topology.js +10 -2
  22. package/dist/internal/machine/topology.js.map +1 -1
  23. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  24. package/dist/internal/testing/machine/finiteModel.js +3 -3
  25. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  26. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  27. package/dist/internal/testing/machine/verification.js +11 -9
  28. package/dist/internal/testing/machine/verification.js.map +1 -1
  29. package/dist/testing/MachineTest.d.ts +3 -3
  30. package/dist/testing/MachineTest.js +3 -3
  31. package/dist/unstable/reactivity/AtomMachine.d.ts +1 -1
  32. package/dist/unstable/reactivity/AtomMachine.js +1 -1
  33. package/docs/agent-guide.md +36 -5
  34. package/package.json +1 -1
  35. package/src/Machine.ts +325 -87
  36. package/src/internal/machine/executionPlan.ts +31 -9
  37. package/src/internal/machine/initialization.ts +10 -2
  38. package/src/internal/machine/machine.ts +131 -15
  39. package/src/internal/machine/planner.ts +26 -8
  40. package/src/internal/machine/topology.ts +22 -2
  41. package/src/internal/testing/machine/finiteModel.ts +7 -3
  42. package/src/internal/testing/machine/verification.ts +18 -10
  43. package/src/testing/MachineTest.ts +3 -3
  44. package/src/unstable/reactivity/AtomMachine.ts +1 -1
@@ -47,7 +47,15 @@ import {
47
47
  validateDeclaredTransitionTarget
48
48
  } from "./planner.js"
49
49
  import { decodeEmitSync, decodeEventSync, decodeInputSync, decodeStateValueSync } from "./protocol.js"
50
- import { isInitialTarget, isNoTarget, isSnapshot, isStateUpdate, isTarget, TargetSnapshotTypeId } from "./topology.js"
50
+ import {
51
+ isCombinedTarget,
52
+ isInitialTarget,
53
+ isNoTarget,
54
+ isSnapshot,
55
+ isStateUpdate,
56
+ isTarget,
57
+ TargetSnapshotTypeId
58
+ } from "./topology.js"
51
59
 
52
60
  interface IndexedExecutionDescriptor {
53
61
  readonly flat: boolean
@@ -548,25 +556,28 @@ const collectIndexedEvaluatedTransition = (
548
556
  selection.context,
549
557
  selection.transition.evaluate
550
558
  )
551
- const update = isStateUpdate(transitionResult.state)
559
+ const combined = isCombinedTarget(transitionResult.state) ? transitionResult.state : undefined
560
+ const transitionState = combined?.target ?? transitionResult.state
561
+ const stateUpdate = combined?.update ?? (isStateUpdate(transitionState) ? transitionState : undefined)
562
+ const update = stateUpdate !== undefined
552
563
  ? (() => {
553
- const index = descriptor.indexByPath.get(transitionResult.state.path)
564
+ const index = descriptor.indexByPath.get(stateUpdate.path)
554
565
  const node = index === undefined ? undefined : descriptor.nodes[index]
555
566
  if (
556
567
  index === undefined || node === undefined || state.active[index] !== 1 || node.schema === undefined ||
557
568
  (node.type !== "compound" && node.type !== "parallel")
558
569
  ) {
559
570
  throw new Error(
560
- `Machine state update owner "${transitionResult.state.path}" must be an active valued compound or parallel state`
571
+ `Machine state update owner "${stateUpdate.path}" must be an active valued compound or parallel state`
561
572
  )
562
573
  }
563
574
  return {
564
575
  path: node.path,
565
- value: decodeStateValueSync(machine, node, transitionResult.state.value)
576
+ value: decodeStateValueSync(machine, node, stateUpdate.value)
566
577
  }
567
578
  })()
568
579
  : undefined
569
- const unresolvedTarget = update === undefined ? transitionResult.state : undefined
580
+ const unresolvedTarget = transitionState === undefined || isStateUpdate(transitionState) ? undefined : transitionState
570
581
  validateDeclaredTransitionTarget(
571
582
  selection.sourcePath,
572
583
  selection.trigger,
@@ -585,13 +596,22 @@ const collectIndexedEvaluatedTransition = (
585
596
  if (target !== undefined && !isTarget(target) && !isSnapshot(target)) {
586
597
  throw new Error("Machine expected indexed transition target to be a snapshot or target builder result")
587
598
  }
588
- const next = target === undefined
599
+ let next = target === undefined
589
600
  ? update === undefined ? state : (() => {
590
601
  const next = copyOwnedIndexedState(state)
591
602
  next.values[descriptor.indexByPath.get(update.path)!] = update.value
592
603
  return next
593
604
  })()
594
605
  : normalizeIndexedTargetStateSync(machine, descriptor, state, target as any, selection.leafIndex)
606
+ if (combined !== undefined && update !== undefined) {
607
+ const ownerIndex = descriptor.indexByPath.get(update.path)!
608
+ if (next.active[ownerIndex] !== 1) {
609
+ throw new Error(`Machine combined target exited its updating owner "${update.path}"`)
610
+ }
611
+ const values = next.values.slice()
612
+ values[ownerIndex] = update.value
613
+ next = { ...next, values }
614
+ }
595
615
  const changed = selection.transition.reenter || !hasSameIndexedActive(state, next)
596
616
  const stabilize = changed || update !== undefined
597
617
  if (!changed) {
@@ -656,7 +676,8 @@ const indexedMicrostep = (
656
676
  branchIndex: transition.branchIndex,
657
677
  branchKey: transition.branchKey,
658
678
  target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
659
- resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
679
+ resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target),
680
+ updates: transition.update === undefined ? [] : [transition.update.path]
660
681
  })
661
682
  if (selections.length === 1) {
662
683
  const transition = collectIndexedEvaluatedTransition(machine, descriptor, state, selections[0]!)
@@ -849,7 +870,8 @@ const planIndexedFlatState = (
849
870
  branchIndex: transitionResult.branchIndex,
850
871
  branchKey: transitionResult.branchKey,
851
872
  target: target === undefined ? undefined : getTargetNodePath(target as any),
852
- resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any)
873
+ resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any),
874
+ updates: []
853
875
  }]
854
876
  }
855
877
  : undefined),
@@ -14,11 +14,19 @@ const getNode = (machine: Machine.Any, path: string): Machine.StateNode => {
14
14
  }
15
15
 
16
16
  const withFrom = <Method extends (value: unknown) => unknown>(method: Method) => {
17
- Object.defineProperty(method, "from", {
17
+ const builder = {}
18
+ Object.defineProperty(builder, "decoded", {
19
+ value: method,
20
+ enumerable: false
21
+ })
22
+ Object.defineProperty(builder, "from", {
18
23
  value: (...args: ReadonlyArray<unknown>) => method(Topology.makeStateInput(args.length === 0 ? {} : args[0])),
19
24
  enumerable: false
20
25
  })
21
- return method as Method & { readonly from: (...args: ReadonlyArray<unknown>) => unknown }
26
+ return builder as {
27
+ readonly decoded: Method
28
+ readonly from: (...args: ReadonlyArray<unknown>) => unknown
29
+ }
22
30
  }
23
31
 
24
32
  const makeCompletion = (values: Readonly<Record<string, unknown>>): object => {
@@ -191,7 +191,7 @@ type InitialBuilderDescriptor = {
191
191
  const plainTargetSelection = (selection: Topology.TargetSelection): Topology.TargetSelection =>
192
192
  selection.kind === "none"
193
193
  ? Topology.noneTargetSelection
194
- : Topology.makeTargetSelection(selection.kind, selection.path, selection.scope)
194
+ : Topology.makeTargetSelection(selection.kind, selection.path, selection.scope, selection.updatePath)
195
195
 
196
196
  const transitionOptions = (options: unknown): { readonly reenter: boolean; readonly declinable: boolean } => {
197
197
  const configuration = typeof options === "object" && options !== null
@@ -224,7 +224,22 @@ const decorateTransitionSelection = (selection: Topology.TargetSelection): Topol
224
224
  ...selection,
225
225
  resolve: (resolve: (context: any, enqueue: unknown) => unknown, options?: unknown) =>
226
226
  makeDirectTransitionDescriptor(selection, resolve, options),
227
- reenter: () => makeDirectTransitionDescriptor(selection, undefined, { reenter: true })
227
+ reenter: () => makeDirectTransitionDescriptor(selection, undefined, { reenter: true }),
228
+ updating: (owner: unknown) => {
229
+ if (typeof owner !== "function") {
230
+ throw new Error("Machine updating owner must be a state selector")
231
+ }
232
+ const ownerSelection = owner()
233
+ if (
234
+ !Topology.isTargetSelection(ownerSelection) || ownerSelection.kind !== "state" ||
235
+ ownerSelection.scope !== "branch"
236
+ ) {
237
+ throw new Error("Machine updating owner must be addressed by one branch state selector")
238
+ }
239
+ return decorateTransitionSelection(
240
+ Topology.makeTargetSelection(selection.kind, selection.path, selection.scope, ownerSelection.path)
241
+ )
242
+ }
228
243
  })
229
244
 
230
245
  const decorateStateUpdateSelection = (selection: Topology.TargetSelection): Topology.TargetSelection => {
@@ -360,6 +375,11 @@ const transitionTargetSelection = (
360
375
  scope: selection.scope
361
376
  })
362
377
 
378
+ const selectionUpdates = (selection: Topology.TargetSelection): ReadonlyArray<string> =>
379
+ selection.updatePath === undefined ?
380
+ selection.kind === "update" && selection.path !== undefined ? [selection.path] : []
381
+ : [selection.updatePath]
382
+
363
383
  const makeSelectionMethod = (
364
384
  kind: Topology.TargetSelectionKind,
365
385
  path: string | undefined,
@@ -476,6 +496,7 @@ const makeTargetSelector = (
476
496
  const captureDefinitionBranch = (
477
497
  branch: unknown,
478
498
  selector: unknown,
499
+ stateNodes: Machine.StateNodes,
479
500
  path: string,
480
501
  trigger: PropertyKey
481
502
  ): CapturedBranch => {
@@ -489,9 +510,58 @@ const captureDefinitionBranch = (
489
510
  if (!Topology.isTargetSelection(selection)) {
490
511
  throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must select exactly one target`)
491
512
  }
513
+ if (selection.updatePath !== undefined) {
514
+ const owner = stateNodes.byPath.get(selection.updatePath)
515
+ if (
516
+ selection.kind !== "state" || (selection.scope !== "local" && selection.scope !== "branch") ||
517
+ selection.path === undefined || owner === undefined || owner.schema === undefined ||
518
+ (owner.type !== "compound" && owner.type !== "parallel") ||
519
+ !Configuration.isDescendantOf(path, owner.path) ||
520
+ !Configuration.isDescendantOf(selection.path, owner.path)
521
+ ) {
522
+ throw new Error(
523
+ `Machine updating owner "${selection.updatePath}" must be a valued ancestor retained by source "${path}" and target "${selection.path}"`
524
+ )
525
+ }
526
+ }
492
527
  return { ...(branch as DefinitionBranch), selection }
493
528
  }
494
529
 
530
+ const makeUpdatingConstruction = (
531
+ target: unknown,
532
+ ownerPath: string
533
+ ): { readonly update: (update: unknown) => Topology.CombinedTarget } =>
534
+ Object.freeze({
535
+ update: (update: unknown) => {
536
+ if (!Topology.isStateUpdate(update) || update.path !== ownerPath) {
537
+ throw new Error(`Machine combined target must update its declared owner "${ownerPath}"`)
538
+ }
539
+ return Topology.makeCombinedTarget(target, update)
540
+ }
541
+ })
542
+
543
+ const makeUpdatingTargetBuilder = (
544
+ builder: unknown,
545
+ ownerPath: string
546
+ ): unknown => {
547
+ if (typeof builder !== "object" || builder === null) {
548
+ throw new Error("Machine combined target requires a state construction builder")
549
+ }
550
+ const updating: Record<PropertyKey, unknown> = {}
551
+ for (const property of Reflect.ownKeys(builder)) {
552
+ const descriptor = Object.getOwnPropertyDescriptor(builder, property)
553
+ if (descriptor === undefined) continue
554
+ if (
555
+ (property === "from" || property === "decoded") && "value" in descriptor && typeof descriptor.value === "function"
556
+ ) {
557
+ const construct = descriptor.value
558
+ descriptor.value = (...args: ReadonlyArray<unknown>) => makeUpdatingConstruction(construct(...args), ownerPath)
559
+ }
560
+ Object.defineProperty(updating, property, descriptor)
561
+ }
562
+ return Object.freeze(updating)
563
+ }
564
+
495
565
  const getSelectionBuilder = (
496
566
  target: Record<string, any>,
497
567
  selection: Topology.TargetSelection,
@@ -534,7 +604,7 @@ const getSelectionBuilder = (
534
604
  ) {
535
605
  throw new Error(`Machine could not construct selected transition target "${selection.path}"`)
536
606
  }
537
- return builder
607
+ return selection.updatePath === undefined ? builder : makeUpdatingTargetBuilder(builder, selection.updatePath)
538
608
  }
539
609
 
540
610
  const constructSelectedTarget = (builder: any): unknown =>
@@ -557,10 +627,18 @@ const validateResolvedSelection = (
557
627
  }
558
628
  return
559
629
  }
630
+ if (selection.updatePath !== undefined) {
631
+ if (!Topology.isCombinedTarget(result) || result.update.path !== selection.updatePath) {
632
+ throw new Error(`Machine target updating "${selection.updatePath}" must return target construction .update(...)`)
633
+ }
634
+ } else if (Topology.isCombinedTarget(result)) {
635
+ throw new Error("Machine combined target requires an updating owner declaration")
636
+ }
560
637
  if (result === undefined) return
561
- const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path") &&
562
- typeof result.path === "string"
563
- ? result.path
638
+ const target = Topology.isCombinedTarget(result) ? result.target : result
639
+ const resultPath = typeof target === "object" && target !== null && hasProperty(target, "path") &&
640
+ typeof target.path === "string"
641
+ ? target.path
564
642
  : undefined
565
643
  const selectedNode = selection.path === undefined ? undefined : stateNodes.byPath.get(selection.path)
566
644
  const acceptsDescendant = (selection.scope === "local" || selection.scope === "branch") &&
@@ -587,6 +665,26 @@ const runCapturedBranch = (
587
665
  const resolverContext = { ...context }
588
666
  if (branch.selection.kind === "none") delete resolverContext.target
589
667
  else resolverContext.target = selectedTarget
668
+ if (branch.selection.kind === "update") {
669
+ const ownerPath = branch.selection.path!
670
+ delete resolverContext.target
671
+ resolverContext.current = context.ancestors[ownerPath] ?? context.state
672
+ resolverContext.owner = getSelectionBuilder(
673
+ context.target,
674
+ makeStateUpdateSelection(ownerPath, branch.selection.scope === "local" ? "local" : "branch"),
675
+ stateNodes,
676
+ source
677
+ )
678
+ } else if (branch.selection.updatePath !== undefined) {
679
+ const ownerPath = branch.selection.updatePath
680
+ resolverContext.current = context.ancestors[ownerPath]
681
+ resolverContext.owner = getSelectionBuilder(
682
+ context.target,
683
+ makeStateUpdateSelection(ownerPath, "branch"),
684
+ stateNodes,
685
+ source
686
+ )
687
+ }
590
688
  if (branch.declinable === true) resolverContext.decline = Topology.makeDeclined
591
689
  const resolved = branch.resolve(resolverContext, enqueue)
592
690
  if (Topology.isDeclined(resolved)) {
@@ -636,6 +734,9 @@ const captureNamedBranches = (
636
734
  if (!Topology.isTargetSelection(target)) {
637
735
  throw new Error(`Machine transition branch "${key}" must select exactly one target`)
638
736
  }
737
+ if (target.updatePath !== undefined) {
738
+ throw new Error(`Machine transition branch "${key}" cannot declare an updating target`)
739
+ }
639
740
  if (title !== undefined && (typeof title !== "string" || title.length === 0)) {
640
741
  throw new Error(`Machine transition branch "${key}" title must be a non-empty string`)
641
742
  }
@@ -795,14 +896,15 @@ const captureTransition = (
795
896
  key: branch.key,
796
897
  title: branch.title,
797
898
  target: topologyTargetPath(branch.selection),
798
- selection: transitionTargetSelection(branch.selection)
899
+ selection: transitionTargetSelection(branch.selection),
900
+ updates: selectionUpdates(branch.selection)
799
901
  })
800
902
  ),
801
903
  evaluate,
802
904
  transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
803
905
  }
804
906
  }
805
- const branch = captureDefinitionBranch(transition, selector, path, trigger)
907
+ const branch = captureDefinitionBranch(transition, selector, stateNodes, path, trigger)
806
908
  const evaluate = (context: Record<string, any>, enqueue: unknown) => ({
807
909
  result: runCapturedBranch(branch, context, enqueue, stateNodes, path),
808
910
  branchIndex: 0,
@@ -815,7 +917,8 @@ const captureTransition = (
815
917
  branches: [{
816
918
  type: "direct" as const,
817
919
  target: topologyTargetPath(branch.selection),
818
- selection: transitionTargetSelection(branch.selection)
920
+ selection: transitionTargetSelection(branch.selection),
921
+ updates: selectionUpdates(branch.selection)
819
922
  }],
820
923
  evaluate,
821
924
  transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
@@ -1022,8 +1125,18 @@ const withFrom = <Method extends (value: unknown, ...args: ReadonlyArray<any>) =
1022
1125
  method: Method,
1023
1126
  kind: FromMethodKind,
1024
1127
  valued: boolean
1025
- ): Method & { readonly from: (...args: ReadonlyArray<any>) => unknown } => {
1026
- Object.defineProperty(method, "from", {
1128
+ ): {
1129
+ readonly decoded?: Method
1130
+ readonly from: (...args: ReadonlyArray<any>) => unknown
1131
+ } => {
1132
+ const builder: Record<string, unknown> = {}
1133
+ if (valued) {
1134
+ Object.defineProperty(builder, "decoded", {
1135
+ value: method,
1136
+ enumerable: false
1137
+ })
1138
+ }
1139
+ Object.defineProperty(builder, "from", {
1027
1140
  value: (...args: ReadonlyArray<any>) => {
1028
1141
  if (!valued) {
1029
1142
  return method(undefined, ...args)
@@ -1035,7 +1148,10 @@ const withFrom = <Method extends (value: unknown, ...args: ReadonlyArray<any>) =
1035
1148
  },
1036
1149
  enumerable: false
1037
1150
  })
1038
- return method as Method & { readonly from: (...args: ReadonlyArray<any>) => unknown }
1151
+ return builder as {
1152
+ readonly decoded?: Method
1153
+ readonly from: (...args: ReadonlyArray<any>) => unknown
1154
+ }
1039
1155
  }
1040
1156
 
1041
1157
  const withInitial = <Builder extends object>(
@@ -1518,13 +1634,13 @@ const makeInitialSelector = (stateNodes: Machine.StateNodes): unknown => {
1518
1634
  const getInitialSelectionBuilder = (
1519
1635
  initialBuilder: Record<string, any>,
1520
1636
  selection: Topology.TargetSelection
1521
- ): (...args: ReadonlyArray<any>) => unknown => {
1637
+ ): Record<string, any> => {
1522
1638
  const path = selection.path
1523
1639
  if (path === undefined || path.includes(".")) {
1524
1640
  throw new Error("Machine initial target must select one top-level state")
1525
1641
  }
1526
1642
  const builder = initialBuilder[path]
1527
- if (typeof builder !== "function") {
1643
+ if (typeof builder !== "object" || builder === null || typeof builder.from !== "function") {
1528
1644
  throw new Error(`Machine could not construct selected initial state "${path}"`)
1529
1645
  }
1530
1646
  return builder
@@ -1537,7 +1653,7 @@ const captureInitialBranch = (
1537
1653
  ): {
1538
1654
  readonly selection: Topology.TargetSelection
1539
1655
  readonly resolve?: (context: any) => unknown
1540
- readonly builder: (...args: ReadonlyArray<any>) => unknown
1656
+ readonly builder: Record<string, any>
1541
1657
  } => {
1542
1658
  if (typeof definition !== "function") {
1543
1659
  throw new Error("Machine initial definition must be a target-first callback")
@@ -47,6 +47,7 @@ import {
47
47
  getNode,
48
48
  type InitialTarget as InitialTargetInstruction,
49
49
  isChoiceTarget,
50
+ isCombinedTarget,
50
51
  isDeclined,
51
52
  isHistoryTarget,
52
53
  isInitialTarget,
@@ -70,6 +71,7 @@ export type MicrostepPlan<State, Event, E, R> = {
70
71
  readonly branchKey: string | undefined
71
72
  readonly target: string | undefined
72
73
  readonly resolvedTarget: string | undefined
74
+ readonly updates: ReadonlyArray<string>
73
75
  }>
74
76
  readonly commands: ReadonlyArray<RuntimeCommand>
75
77
  readonly raisedEvents: ReadonlyArray<Event>
@@ -605,6 +607,7 @@ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R
605
607
  readonly branchKey: string | undefined
606
608
  readonly target: string
607
609
  readonly resolvedTarget: string
610
+ readonly updates: ReadonlyArray<string>
608
611
  }>
609
612
  }
610
613
 
@@ -1234,6 +1237,7 @@ interface ResolvedChoiceTransition {
1234
1237
  readonly branchKey: string | undefined
1235
1238
  readonly target: string
1236
1239
  readonly resolvedTarget: string
1240
+ readonly updates: ReadonlyArray<string>
1237
1241
  }
1238
1242
 
1239
1243
  function resolveChoiceTarget(
@@ -1312,7 +1316,8 @@ function resolveChoiceTarget(
1312
1316
  branchIndex: collected.branchIndex,
1313
1317
  branchKey: collected.branchKey,
1314
1318
  target: returnedPath,
1315
- resolvedTarget: nested?.target.path ?? returnedPath
1319
+ resolvedTarget: nested?.target.path ?? returnedPath,
1320
+ updates: []
1316
1321
  })
1317
1322
  commands.push(...collected.commands)
1318
1323
  raisedEvents.push(...collected.raisedEvents)
@@ -1356,10 +1361,12 @@ const collectEvaluatedTransition = <
1356
1361
  if (transitionResult.declined) {
1357
1362
  throw new Error("Machine transition returned decline without declaring declinable: true")
1358
1363
  }
1359
- const unresolvedTarget = transitionResult.state === undefined
1360
- || isStateUpdate(transitionResult.state)
1364
+ const combined = isCombinedTarget(transitionResult.state) ? transitionResult.state : undefined
1365
+ const transitionState = combined?.target ?? transitionResult.state
1366
+ const unresolvedTarget = transitionState === undefined
1367
+ || isStateUpdate(transitionState)
1361
1368
  ? undefined
1362
- : transitionResult.state as
1369
+ : transitionState as
1363
1370
  | Machine.Snapshot<States>
1364
1371
  | Machine.Target<States, Machine.StateIdentifier<States>>
1365
1372
  | Machine.HistoryTarget<States, Machine.HistoryIdentifier<States>>
@@ -1370,9 +1377,10 @@ const collectEvaluatedTransition = <
1370
1377
  selection.transition.targets,
1371
1378
  unresolvedTarget
1372
1379
  )
1373
- const update = isStateUpdate(transitionResult.state)
1380
+ const stateUpdate = combined?.update ?? (isStateUpdate(transitionState) ? transitionState : undefined)
1381
+ const update = stateUpdate !== undefined
1374
1382
  ? (() => {
1375
- const node = getNode(machine, transitionResult.state.path)
1383
+ const node = getNode(machine, stateUpdate.path)
1376
1384
  if (
1377
1385
  !state.active.has(node.path) || node.schema === undefined ||
1378
1386
  (node.type !== "compound" && node.type !== "parallel")
@@ -1381,7 +1389,7 @@ const collectEvaluatedTransition = <
1381
1389
  }
1382
1390
  return {
1383
1391
  path: node.path,
1384
- value: decodeStateValueSync(machine, node, transitionResult.state.value)
1392
+ value: decodeStateValueSync(machine, node, stateUpdate.value)
1385
1393
  }
1386
1394
  })()
1387
1395
  : undefined
@@ -1497,6 +1505,15 @@ const collectEvaluatedTransition = <
1497
1505
  additionalTarget
1498
1506
  )
1499
1507
  }
1508
+ if (combined !== undefined && update !== undefined && !stateAfterTransition.active.has(update.path)) {
1509
+ throw new Error(`Machine combined target exited its updating owner "${update.path}"`)
1510
+ }
1511
+ if (update !== undefined) {
1512
+ stateAfterTransition = {
1513
+ ...stateAfterTransition,
1514
+ values: new Map(stateAfterTransition.values).set(update.path, update.value)
1515
+ }
1516
+ }
1500
1517
  const changed = selection.transition.reenter || !hasSameActivePaths(state, stateAfterTransition)
1501
1518
  const stabilize = changed || update !== undefined
1502
1519
 
@@ -1915,7 +1932,8 @@ const microstep = <
1915
1932
  branchIndex: transition.branchIndex,
1916
1933
  branchKey: transition.branchKey,
1917
1934
  target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
1918
- resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
1935
+ resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target),
1936
+ updates: transition.update === undefined ? [] : [transition.update.path]
1919
1937
  },
1920
1938
  ...transition.choiceTransitions
1921
1939
  ])
@@ -31,6 +31,8 @@ export const TargetSelectionTypeId: unique symbol = Symbol("effect/Machine/Targe
31
31
 
32
32
  export const StateUpdateTypeId: unique symbol = Symbol("effect/Machine/StateUpdate")
33
33
 
34
+ export const CombinedTargetTypeId: unique symbol = Symbol("effect/Machine/CombinedTarget")
35
+
34
36
  export const SelectedBranchTypeId: unique symbol = Symbol("effect/Machine/SelectedBranch")
35
37
 
36
38
  interface StateInput {
@@ -85,6 +87,7 @@ export interface TargetSelection {
85
87
  readonly kind: TargetSelectionKind
86
88
  readonly scope: TargetSelectionScope | undefined
87
89
  readonly path: string | undefined
90
+ readonly updatePath?: string
88
91
  }
89
92
 
90
93
  /** One branch selection returned by a compiled branching transition. */
@@ -99,13 +102,15 @@ export interface SelectedBranch {
99
102
  export const makeTargetSelection = (
100
103
  kind: TargetSelectionKind,
101
104
  path?: string,
102
- scope?: TargetSelectionScope
105
+ scope?: TargetSelectionScope,
106
+ updatePath?: string
103
107
  ): TargetSelection =>
104
108
  Object.freeze({
105
109
  [TargetSelectionTypeId]: TargetSelectionTypeId as typeof TargetSelectionTypeId,
106
110
  kind,
107
111
  scope,
108
- path
112
+ path,
113
+ ...(updatePath === undefined ? {} : { updatePath })
109
114
  })
110
115
 
111
116
  /** Source-independent definition-time selection for an explicitly targetless transition. */
@@ -128,6 +133,21 @@ export const makeStateUpdate = (path: string, value: unknown): StateUpdate =>
128
133
 
129
134
  export const isStateUpdate = (u: unknown): u is StateUpdate => hasProperty(u, StateUpdateTypeId)
130
135
 
136
+ export interface CombinedTarget {
137
+ readonly [CombinedTargetTypeId]: typeof CombinedTargetTypeId
138
+ readonly target: unknown
139
+ readonly update: StateUpdate
140
+ }
141
+
142
+ export const makeCombinedTarget = (target: unknown, update: StateUpdate): CombinedTarget =>
143
+ Object.freeze({
144
+ [CombinedTargetTypeId]: CombinedTargetTypeId,
145
+ target,
146
+ update
147
+ })
148
+
149
+ export const isCombinedTarget = (u: unknown): u is CombinedTarget => hasProperty(u, CombinedTargetTypeId)
150
+
131
151
  export const makeSelectedBranch = (
132
152
  owner: object,
133
153
  branchIndex: number,
@@ -1234,7 +1234,10 @@ const selectSnapshot = (
1234
1234
  if (state.node._tag === "Choice") {
1235
1235
  return (builder[state.node.key] as () => unknown)()
1236
1236
  }
1237
- const method = builder[state.node.key] as (value: unknown, selector?: (builder: any) => unknown) => unknown
1237
+ const method = builder[state.node.key].decoded as (
1238
+ value: unknown,
1239
+ selector?: (builder: any) => unknown
1240
+ ) => unknown
1238
1241
  const value = stateValue(state, path === requestedParts?.join(".") ? requestedValue : undefined)
1239
1242
  if (state.node._tag === "Atomic" || state.node._tag === "Final") return method(value)
1240
1243
 
@@ -1473,14 +1476,15 @@ const makeHandlers = (
1473
1476
  ...(Object.keys(history).length === 0 ? {} : { history }),
1474
1477
  ...(node._tag === "Compound"
1475
1478
  ? {
1476
- initialize: ({ builder }: any) => builder(stateValue(byPath.get(`${path}.${node.initial}`)!))
1479
+ initialize: ({ builder }: any) => builder.decoded(stateValue(byPath.get(`${path}.${node.initial}`)!))
1477
1480
  }
1478
1481
  : {
1479
1482
  initialize: ({ builder }: any) =>
1480
1483
  node.states
1481
1484
  .filter((child) => child._tag !== "History" && child._tag !== "Choice")
1482
1485
  .reduce(
1483
- (current: any, child) => current[child.key](stateValue(byPath.get(`${path}.${child.key}`)!)),
1486
+ (current: any, child) =>
1487
+ current[child.key].decoded(stateValue(byPath.get(`${path}.${child.key}`)!)),
1484
1488
  builder
1485
1489
  )
1486
1490
  }),
@@ -564,7 +564,6 @@ export const coverage = <M extends AnyMachine>(
564
564
  const exitHits = new Set<number>()
565
565
 
566
566
  const transitionCoverage = makeTransitionCoverageCollector(machine)
567
- const transitionDefinitions = Machine.transitionDefinitions(machine)
568
567
 
569
568
  const declaredEvents = publicEventTags(machine)
570
569
  const declaredEventTags = declaredEvents.tags
@@ -632,13 +631,8 @@ export const coverage = <M extends AnyMachine>(
632
631
  hitPaths(microstep.exitPaths, exitHits)
633
632
  observeSnapshot(microstep.next)
634
633
  for (const retained of microstep.transitions) {
635
- const definition = transitionDefinitions.find((candidate) =>
636
- candidate.source === retained.source && candidate.reenter === retained.reenter &&
637
- sameTransitionTrigger(candidate.trigger, retained.trigger)
638
- )
639
- const branch = definition?.branches[retained.branchIndex]
640
- if (branch?.selection.kind === "update") stateUpdates += 1
641
- else if (retained.target === undefined) targetlessTransitions += 1
634
+ stateUpdates += retained.updates.length
635
+ if (retained.target === undefined && retained.updates.length === 0) targetlessTransitions += 1
642
636
  if (retained.trigger.type === "event") eventTriggered += 1
643
637
  else if (retained.trigger.type === "always") alwaysTriggered += 1
644
638
  else if (retained.trigger.type === "done") doneTriggered += 1
@@ -1549,6 +1543,18 @@ export const verify = <M extends AnyMachine>(
1549
1543
  transition.target === undefined ? transition.source : String(transition.target)
1550
1544
  )
1551
1545
  }
1546
+ if (
1547
+ transition.updates.length !== branch.updates.length ||
1548
+ transition.updates.some((path, index) => path !== branch.updates[index])
1549
+ ) {
1550
+ add(
1551
+ "definitions.selection",
1552
+ location,
1553
+ `transition branch ${transition.branchIndex} from "${transition.source}" reported retained owner updates ` +
1554
+ `${JSON.stringify(transition.updates)} instead of ${JSON.stringify(branch.updates)}`,
1555
+ transition.source
1556
+ )
1557
+ }
1552
1558
  return branch
1553
1559
  }
1554
1560
 
@@ -1607,8 +1613,10 @@ export const verify = <M extends AnyMachine>(
1607
1613
  const resolvedTarget = transition.resolvedTarget === undefined ? undefined : String(transition.resolvedTarget)
1608
1614
  const targetNode = target === undefined ? undefined : byPath.get(target)
1609
1615
  let expected = target
1610
- let explanation = branches[index]?.selection.kind === "update"
1611
- ? `update owner "${String(branches[index]?.selection.path)}"`
1616
+ let explanation = transition.updates.length > 0
1617
+ ? target === undefined
1618
+ ? `update owner "${transition.updates.join("\", \"")}"`
1619
+ : `target "${target}" and update owner "${transition.updates.join("\", \"")}"`
1612
1620
  : target === undefined
1613
1621
  ? "an unresolved targetless transition"
1614
1622
  : `target "${target}"`
@@ -1109,7 +1109,7 @@ export const Invariant: {
1109
1109
  * events: Machine.events(),
1110
1110
  * initial: {
1111
1111
  * target: (to) => to.Count(),
1112
- * resolve: ({ target }) => target(new Count({ value: 0 }))
1112
+ * resolve: ({ target }) => target.decoded(new Count({ value: 0 }))
1113
1113
  * }
1114
1114
  * }).handle({ Count: {} })
1115
1115
  *
@@ -1444,14 +1444,14 @@ export type ExploreOptions<M extends AnyMachine, Key extends ExplorationKey = Ex
1444
1444
  * events: Machine.events(Increment),
1445
1445
  * initial: {
1446
1446
  * target: (to) => to.Count(),
1447
- * resolve: ({ target }) => target(new Count({ value: 0 }))
1447
+ * resolve: ({ target }) => target.decoded(new Count({ value: 0 }))
1448
1448
  * }
1449
1449
  * }).handle({
1450
1450
  * Count: {
1451
1451
  * on: {
1452
1452
  * Increment: (to) =>
1453
1453
  * to.full.Count().resolve(({ state, target }) =>
1454
- * target(new Count({ value: state.value + 1 })))
1454
+ * target.decoded(new Count({ value: state.value + 1 })))
1455
1455
  * }
1456
1456
  * }
1457
1457
  * })
@@ -374,7 +374,7 @@ type ChildState<Child extends Machine.ChildMachine.Any> = RefState<Machine.Child
374
374
  * events: Machine.events(),
375
375
  * initial: {
376
376
  * target: (to) => to.Count(),
377
- * resolve: ({ target }) => target(new Count({ value: 0 }))
377
+ * resolve: ({ target }) => target.decoded(new Count({ value: 0 }))
378
378
  * }
379
379
  * }).handle({ Count: {} })
380
380
  * const machineAtom = AtomMachine.make(machine)