@typeonce/effect-machine 0.20.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 (46) hide show
  1. package/README.md +115 -5
  2. package/dist/Machine.d.ts +188 -52
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +6 -5
  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 +50 -6
  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 +132 -22
  14. package/dist/internal/machine/machine.js.map +1 -1
  15. package/dist/internal/machine/planner.d.ts +16 -2
  16. package/dist/internal/machine/planner.d.ts.map +1 -1
  17. package/dist/internal/machine/planner.js +67 -13
  18. package/dist/internal/machine/planner.js.map +1 -1
  19. package/dist/internal/machine/topology.d.ts +19 -2
  20. package/dist/internal/machine/topology.d.ts.map +1 -1
  21. package/dist/internal/machine/topology.js +17 -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 +17 -3
  28. package/dist/internal/testing/machine/verification.js.map +1 -1
  29. package/dist/testing/MachineTest.d.ts +5 -3
  30. package/dist/testing/MachineTest.d.ts.map +1 -1
  31. package/dist/testing/MachineTest.js +3 -3
  32. package/dist/testing/MachineTest.js.map +1 -1
  33. package/dist/unstable/reactivity/AtomMachine.d.ts +1 -1
  34. package/dist/unstable/reactivity/AtomMachine.js +1 -1
  35. package/docs/agent-guide.md +51 -3
  36. package/package.json +1 -1
  37. package/src/Machine.ts +532 -107
  38. package/src/internal/machine/executionPlan.ts +62 -6
  39. package/src/internal/machine/initialization.ts +10 -2
  40. package/src/internal/machine/machine.ts +188 -24
  41. package/src/internal/machine/planner.ts +86 -15
  42. package/src/internal/machine/topology.ts +40 -3
  43. package/src/internal/testing/machine/finiteModel.ts +7 -3
  44. package/src/internal/testing/machine/verification.ts +24 -3
  45. package/src/testing/MachineTest.ts +5 -3
  46. package/src/unstable/reactivity/AtomMachine.ts +1 -1
@@ -47,11 +47,13 @@ import {
47
47
  getNode,
48
48
  type InitialTarget as InitialTargetInstruction,
49
49
  isChoiceTarget,
50
+ isCombinedTarget,
50
51
  isDeclined,
51
52
  isHistoryTarget,
52
53
  isInitialTarget,
53
54
  isNoTarget,
54
55
  isSnapshot,
56
+ isStateUpdate,
55
57
  isTarget,
56
58
  makeChoiceTarget,
57
59
  makeTarget,
@@ -69,6 +71,7 @@ export type MicrostepPlan<State, Event, E, R> = {
69
71
  readonly branchKey: string | undefined
70
72
  readonly target: string | undefined
71
73
  readonly resolvedTarget: string | undefined
74
+ readonly updates: ReadonlyArray<string>
72
75
  }>
73
76
  readonly commands: ReadonlyArray<RuntimeCommand>
74
77
  readonly raisedEvents: ReadonlyArray<Event>
@@ -78,6 +81,11 @@ export type MicrostepPlan<State, Event, E, R> = {
78
81
  readonly changed: boolean
79
82
  }
80
83
 
84
+ type SettlingMicrostep<State, Event, E, R> = MicrostepPlan<State, Event, E, R> & {
85
+ /** Internal signal that eventless stabilization must run again. */
86
+ readonly stabilize: boolean
87
+ }
88
+
81
89
  export type MacrostepPlan<State, Event, E, R, Output> =
82
90
  & {
83
91
  readonly next: State
@@ -580,10 +588,15 @@ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R
580
588
  | Machine.Snapshot<States>
581
589
  | Machine.Target<States, Machine.StateIdentifier<States>>
582
590
  | undefined
591
+ readonly update: {
592
+ readonly path: string
593
+ readonly value: unknown
594
+ } | undefined
583
595
  readonly commands: ReadonlyArray<RuntimeCommand>
584
596
  readonly raisedEvents: ReadonlyArray<Event>
585
597
  readonly emittedEvents: ReadonlyArray<unknown>
586
598
  readonly changed: boolean
599
+ readonly stabilize: boolean
587
600
  readonly exitPaths: ReadonlyArray<string>
588
601
  readonly entryPaths: ReadonlyArray<string>
589
602
  readonly choiceTransitions: ReadonlyArray<{
@@ -594,6 +607,7 @@ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R
594
607
  readonly branchKey: string | undefined
595
608
  readonly target: string
596
609
  readonly resolvedTarget: string
610
+ readonly updates: ReadonlyArray<string>
597
611
  }>
598
612
  }
599
613
 
@@ -1143,7 +1157,9 @@ export const removeConflictingTransitions = <
1143
1157
  let preempted = false
1144
1158
  const transitionsToRemove = new Set<EvaluatedTransition<States, Event, E, R, Context>>()
1145
1159
  for (const selected of filtered) {
1146
- if (hasPathIntersection(transition.exitPaths, selected.exitPaths)) {
1160
+ const writesSameState = transition.update !== undefined && selected.update !== undefined &&
1161
+ transition.update.path === selected.update.path
1162
+ if (hasPathIntersection(transition.exitPaths, selected.exitPaths) || writesSameState) {
1147
1163
  if (isDescendantOf(transition.selection.sourcePath, selected.selection.sourcePath)) {
1148
1164
  transitionsToRemove.add(selected)
1149
1165
  } else {
@@ -1221,6 +1237,7 @@ interface ResolvedChoiceTransition {
1221
1237
  readonly branchKey: string | undefined
1222
1238
  readonly target: string
1223
1239
  readonly resolvedTarget: string
1240
+ readonly updates: ReadonlyArray<string>
1224
1241
  }
1225
1242
 
1226
1243
  function resolveChoiceTarget(
@@ -1299,7 +1316,8 @@ function resolveChoiceTarget(
1299
1316
  branchIndex: collected.branchIndex,
1300
1317
  branchKey: collected.branchKey,
1301
1318
  target: returnedPath,
1302
- resolvedTarget: nested?.target.path ?? returnedPath
1319
+ resolvedTarget: nested?.target.path ?? returnedPath,
1320
+ updates: []
1303
1321
  })
1304
1322
  commands.push(...collected.commands)
1305
1323
  raisedEvents.push(...collected.raisedEvents)
@@ -1343,9 +1361,12 @@ const collectEvaluatedTransition = <
1343
1361
  if (transitionResult.declined) {
1344
1362
  throw new Error("Machine transition returned decline without declaring declinable: true")
1345
1363
  }
1346
- const unresolvedTarget = transitionResult.state === undefined
1364
+ const combined = isCombinedTarget(transitionResult.state) ? transitionResult.state : undefined
1365
+ const transitionState = combined?.target ?? transitionResult.state
1366
+ const unresolvedTarget = transitionState === undefined
1367
+ || isStateUpdate(transitionState)
1347
1368
  ? undefined
1348
- : transitionResult.state as
1369
+ : transitionState as
1349
1370
  | Machine.Snapshot<States>
1350
1371
  | Machine.Target<States, Machine.StateIdentifier<States>>
1351
1372
  | Machine.HistoryTarget<States, Machine.HistoryIdentifier<States>>
@@ -1356,6 +1377,22 @@ const collectEvaluatedTransition = <
1356
1377
  selection.transition.targets,
1357
1378
  unresolvedTarget
1358
1379
  )
1380
+ const stateUpdate = combined?.update ?? (isStateUpdate(transitionState) ? transitionState : undefined)
1381
+ const update = stateUpdate !== undefined
1382
+ ? (() => {
1383
+ const node = getNode(machine, stateUpdate.path)
1384
+ if (
1385
+ !state.active.has(node.path) || node.schema === undefined ||
1386
+ (node.type !== "compound" && node.type !== "parallel")
1387
+ ) {
1388
+ throw new Error(`Machine state update owner "${node.path}" must be an active valued compound or parallel state`)
1389
+ }
1390
+ return {
1391
+ path: node.path,
1392
+ value: decodeStateValueSync(machine, node, stateUpdate.value)
1393
+ }
1394
+ })()
1395
+ : undefined
1359
1396
  const choiceResolution = unresolvedTarget === undefined
1360
1397
  ? undefined
1361
1398
  : resolveChoiceTarget(
@@ -1456,7 +1493,10 @@ const collectEvaluatedTransition = <
1456
1493
  ? getTargetNodePath(target)
1457
1494
  : getTargetNodePath(unresolvedTarget)
1458
1495
  let stateAfterTransition = target === undefined
1459
- ? state
1496
+ ? update === undefined ? state : {
1497
+ ...state,
1498
+ values: new Map(state.values).set(update.path, update.value)
1499
+ }
1460
1500
  : normalizeTargetConfigurationSync<States>(machine, state, target)
1461
1501
  for (const additionalTarget of additionalChoiceTargets) {
1462
1502
  stateAfterTransition = normalizeTargetConfigurationSync<States>(
@@ -1465,7 +1505,17 @@ const collectEvaluatedTransition = <
1465
1505
  additionalTarget
1466
1506
  )
1467
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
+ }
1468
1517
  const changed = selection.transition.reenter || !hasSameActivePaths(state, stateAfterTransition)
1518
+ const stabilize = changed || update !== undefined
1469
1519
 
1470
1520
  if (!changed) {
1471
1521
  return {
@@ -1474,6 +1524,7 @@ const collectEvaluatedTransition = <
1474
1524
  branchKey: transitionResult.branchKey,
1475
1525
  unresolvedTarget,
1476
1526
  target,
1527
+ update,
1477
1528
  commands: [
1478
1529
  ...transitionResult.commands,
1479
1530
  ...(choiceResolution?.commands ?? []),
@@ -1496,6 +1547,7 @@ const collectEvaluatedTransition = <
1496
1547
  ...additionalTargetEmittedEvents
1497
1548
  ],
1498
1549
  changed,
1550
+ stabilize,
1499
1551
  exitPaths: [],
1500
1552
  entryPaths: [],
1501
1553
  choiceTransitions: [
@@ -1521,6 +1573,7 @@ const collectEvaluatedTransition = <
1521
1573
  branchKey: transitionResult.branchKey,
1522
1574
  unresolvedTarget,
1523
1575
  target,
1576
+ update,
1524
1577
  commands: [
1525
1578
  ...transitionResult.commands,
1526
1579
  ...(choiceResolution?.commands ?? []),
@@ -1543,6 +1596,7 @@ const collectEvaluatedTransition = <
1543
1596
  ...additionalTargetEmittedEvents
1544
1597
  ],
1545
1598
  changed,
1599
+ stabilize,
1546
1600
  exitPaths: reenteredHistoryTarget !== undefined
1547
1601
  ? sortExitPaths(
1548
1602
  machine,
@@ -1744,7 +1798,8 @@ export const planInitialSync = <
1744
1798
  emittedEvents: [...choiceResolution.emittedEvents, ...initialHistoryEmittedEvents],
1745
1799
  exitPaths: [],
1746
1800
  entryPaths: [],
1747
- changed: false
1801
+ changed: false,
1802
+ stabilize: false
1748
1803
  }]
1749
1804
  )
1750
1805
 
@@ -1850,7 +1905,8 @@ const microstep = <
1850
1905
  emittedEvents: [],
1851
1906
  exitPaths: [],
1852
1907
  entryPaths: [],
1853
- changed: false
1908
+ changed: false,
1909
+ stabilize: false
1854
1910
  }
1855
1911
  }
1856
1912
 
@@ -1876,11 +1932,23 @@ const microstep = <
1876
1932
  branchIndex: transition.branchIndex,
1877
1933
  branchKey: transition.branchKey,
1878
1934
  target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
1879
- 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]
1880
1937
  },
1881
1938
  ...transition.choiceTransitions
1882
1939
  ])
1883
1940
  let stateAfterTransition = state
1941
+ // Updates never reactivate topology. Apply every retained value write to the
1942
+ // original active configuration before any control target becomes
1943
+ // authoritative.
1944
+ for (const transition of sortedTransitions) {
1945
+ if (transition.update !== undefined) {
1946
+ stateAfterTransition = {
1947
+ ...stateAfterTransition,
1948
+ values: new Map(stateAfterTransition.values).set(transition.update.path, transition.update.value)
1949
+ }
1950
+ }
1951
+ }
1884
1952
  // Value-only targets are evaluated against the original configuration. If
1885
1953
  // one is applied after a control-changing transition, it can resurrect a
1886
1954
  // branch that the changing transition exited. Apply value-only updates
@@ -1901,6 +1969,7 @@ const microstep = <
1901
1969
  }
1902
1970
 
1903
1971
  const changed = transitions.some((transition) => transition.changed)
1972
+ const stabilize = transitions.some((transition) => transition.stabilize)
1904
1973
  const transitionActions = sortedTransitions
1905
1974
  .flatMap((transition) => transition.commands)
1906
1975
  const transitionRaisedEvents = sortedTransitions
@@ -1918,7 +1987,8 @@ const microstep = <
1918
1987
  emittedEvents: transitionEmittedEvents,
1919
1988
  exitPaths: [],
1920
1989
  entryPaths: [],
1921
- changed: false
1990
+ changed: false,
1991
+ stabilize
1922
1992
  }
1923
1993
  }
1924
1994
 
@@ -1951,7 +2021,8 @@ const microstep = <
1951
2021
  emittedEvents: [...exit.emittedEvents, ...transitionEmittedEvents, ...entry.emittedEvents],
1952
2022
  exitPaths,
1953
2023
  entryPaths,
1954
- changed: true
2024
+ changed: true,
2025
+ stabilize
1955
2026
  }
1956
2027
  }
1957
2028
 
@@ -1974,7 +2045,7 @@ const settle = <
1974
2045
  commands: Array<RuntimeCommand>,
1975
2046
  raisedEvents: Array<Machine.EventOf<Events>>,
1976
2047
  emittedEvents: Array<unknown>,
1977
- microsteps: Array<MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R>>
2048
+ microsteps: Array<SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R>>
1978
2049
  ) => {
1979
2050
  let currentState = state
1980
2051
  let currentEvent = event
@@ -2010,7 +2081,7 @@ const settle = <
2010
2081
  pendingCompletions.length === 0 ? [] : [pendingCompletions.shift()!]
2011
2082
  )
2012
2083
  if (done.length > 0) {
2013
- const doneStep: MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2084
+ const doneStep: SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2014
2085
  machine,
2015
2086
  currentState,
2016
2087
  currentEvent,
@@ -2021,7 +2092,7 @@ const settle = <
2021
2092
  emittedEvents.push(...doneStep.emittedEvents)
2022
2093
  microsteps.push(doneStep)
2023
2094
  currentState = doneStep.next
2024
- shouldRunAlways = doneStep.changed
2095
+ shouldRunAlways = doneStep.stabilize
2025
2096
  continue
2026
2097
  }
2027
2098
  if (isActiveFinalConfiguration(machine, currentState)) {
@@ -2038,7 +2109,7 @@ const settle = <
2038
2109
  ? selectAlwaysTransitions<States, Events, Emits, E, R>(machine, currentState, currentEvent)
2039
2110
  : []
2040
2111
  if (always.length > 0) {
2041
- const alwaysStep: MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2112
+ const alwaysStep: SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2042
2113
  machine,
2043
2114
  currentState,
2044
2115
  currentEvent,
@@ -2049,7 +2120,7 @@ const settle = <
2049
2120
  emittedEvents.push(...alwaysStep.emittedEvents)
2050
2121
  microsteps.push(alwaysStep)
2051
2122
  currentState = alwaysStep.next
2052
- shouldRunAlways = alwaysStep.changed
2123
+ shouldRunAlways = alwaysStep.stabilize
2053
2124
  continue
2054
2125
  }
2055
2126
 
@@ -29,6 +29,10 @@ export const DeclinedTypeId: unique symbol = Symbol("effect/Machine/Declined")
29
29
 
30
30
  export const TargetSelectionTypeId: unique symbol = Symbol("effect/Machine/TargetSelection")
31
31
 
32
+ export const StateUpdateTypeId: unique symbol = Symbol("effect/Machine/StateUpdate")
33
+
34
+ export const CombinedTargetTypeId: unique symbol = Symbol("effect/Machine/CombinedTarget")
35
+
32
36
  export const SelectedBranchTypeId: unique symbol = Symbol("effect/Machine/SelectedBranch")
33
37
 
34
38
  interface StateInput {
@@ -73,7 +77,7 @@ export interface Declined {
73
77
  readonly [DeclinedTypeId]: typeof DeclinedTypeId
74
78
  }
75
79
 
76
- export type TargetSelectionKind = "state" | "initial" | "history" | "choice" | "none"
80
+ export type TargetSelectionKind = "state" | "initial" | "history" | "choice" | "update" | "none"
77
81
 
78
82
  export type TargetSelectionScope = "local" | "branch" | "full" | "initial"
79
83
 
@@ -83,6 +87,7 @@ export interface TargetSelection {
83
87
  readonly kind: TargetSelectionKind
84
88
  readonly scope: TargetSelectionScope | undefined
85
89
  readonly path: string | undefined
90
+ readonly updatePath?: string
86
91
  }
87
92
 
88
93
  /** One branch selection returned by a compiled branching transition. */
@@ -97,13 +102,15 @@ export interface SelectedBranch {
97
102
  export const makeTargetSelection = (
98
103
  kind: TargetSelectionKind,
99
104
  path?: string,
100
- scope?: TargetSelectionScope
105
+ scope?: TargetSelectionScope,
106
+ updatePath?: string
101
107
  ): TargetSelection =>
102
108
  Object.freeze({
103
109
  [TargetSelectionTypeId]: TargetSelectionTypeId as typeof TargetSelectionTypeId,
104
110
  kind,
105
111
  scope,
106
- path
112
+ path,
113
+ ...(updatePath === undefined ? {} : { updatePath })
107
114
  })
108
115
 
109
116
  /** Source-independent definition-time selection for an explicitly targetless transition. */
@@ -111,6 +118,36 @@ export const noneTargetSelection: TargetSelection = makeTargetSelection("none",
111
118
 
112
119
  export const isTargetSelection = (u: unknown): u is TargetSelection => hasProperty(u, TargetSelectionTypeId)
113
120
 
121
+ export interface StateUpdate {
122
+ readonly [StateUpdateTypeId]: typeof StateUpdateTypeId
123
+ readonly path: string
124
+ readonly value: unknown
125
+ }
126
+
127
+ export const makeStateUpdate = (path: string, value: unknown): StateUpdate =>
128
+ Object.freeze({
129
+ [StateUpdateTypeId]: StateUpdateTypeId,
130
+ path,
131
+ value
132
+ })
133
+
134
+ export const isStateUpdate = (u: unknown): u is StateUpdate => hasProperty(u, StateUpdateTypeId)
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
+
114
151
  export const makeSelectedBranch = (
115
152
  owner: object,
116
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
  }),
@@ -475,7 +475,7 @@ const targetWithinSelection = (
475
475
  nodeByPath: ReadonlyMap<string, Machine.Machine.StateNode>
476
476
  ): boolean => {
477
477
  const selection = branch.selection
478
- if (selection.kind === "none") return target === undefined
478
+ if (selection.kind === "none" || selection.kind === "update") return target === undefined
479
479
  if (target === undefined || selection.path === undefined) return false
480
480
  if (target === selection.path) return true
481
481
  const selectedNode = nodeByPath.get(selection.path)
@@ -577,6 +577,7 @@ export const coverage = <M extends AnyMachine>(
577
577
  let microsteps = 0
578
578
  let changedMicrosteps = 0
579
579
  let targetlessTransitions = 0
580
+ let stateUpdates = 0
580
581
  let raisedEvents = 0
581
582
  let emittedEvents = 0
582
583
  let eventTriggered = 0
@@ -630,7 +631,8 @@ export const coverage = <M extends AnyMachine>(
630
631
  hitPaths(microstep.exitPaths, exitHits)
631
632
  observeSnapshot(microstep.next)
632
633
  for (const retained of microstep.transitions) {
633
- if (retained.target === undefined) targetlessTransitions += 1
634
+ stateUpdates += retained.updates.length
635
+ if (retained.target === undefined && retained.updates.length === 0) targetlessTransitions += 1
634
636
  if (retained.trigger.type === "event") eventTriggered += 1
635
637
  else if (retained.trigger.type === "always") alwaysTriggered += 1
636
638
  else if (retained.trigger.type === "done") doneTriggered += 1
@@ -718,6 +720,7 @@ export const coverage = <M extends AnyMachine>(
718
720
  total: microsteps,
719
721
  changed: changedMicrosteps,
720
722
  targetless: targetlessTransitions,
723
+ updates: stateUpdates,
721
724
  raised: raisedEvents,
722
725
  emitted: emittedEvents,
723
726
  eventTriggered,
@@ -1540,6 +1543,18 @@ export const verify = <M extends AnyMachine>(
1540
1543
  transition.target === undefined ? transition.source : String(transition.target)
1541
1544
  )
1542
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
+ }
1543
1558
  return branch
1544
1559
  }
1545
1560
 
@@ -1598,7 +1613,13 @@ export const verify = <M extends AnyMachine>(
1598
1613
  const resolvedTarget = transition.resolvedTarget === undefined ? undefined : String(transition.resolvedTarget)
1599
1614
  const targetNode = target === undefined ? undefined : byPath.get(target)
1600
1615
  let expected = target
1601
- let explanation = target === undefined ? "an unresolved targetless transition" : `target "${target}"`
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("\", \"")}"`
1620
+ : target === undefined
1621
+ ? "an unresolved targetless transition"
1622
+ : `target "${target}"`
1602
1623
 
1603
1624
  if (transition.trigger.type === "choice") {
1604
1625
  explanation = target === undefined ? "a targetless choice edge" : `choice edge 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
  * })
@@ -1822,6 +1822,8 @@ export interface MicrostepCoverageEvidence {
1822
1822
  readonly total: number
1823
1823
  readonly changed: number
1824
1824
  readonly targetless: number
1825
+ /** Retained state-value update operations. */
1826
+ readonly updates: number
1825
1827
  readonly raised: number
1826
1828
  readonly emitted: number
1827
1829
  readonly eventTriggered: number
@@ -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)