@typeonce/effect-machine 0.20.0 → 0.21.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 (34) hide show
  1. package/README.md +41 -0
  2. package/dist/Machine.d.ts +62 -16
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +4 -3
  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 +33 -3
  8. package/dist/internal/machine/executionPlan.js.map +1 -1
  9. package/dist/internal/machine/machine.d.ts.map +1 -1
  10. package/dist/internal/machine/machine.js +38 -9
  11. package/dist/internal/machine/machine.js.map +1 -1
  12. package/dist/internal/machine/planner.d.ts +11 -2
  13. package/dist/internal/machine/planner.d.ts.map +1 -1
  14. package/dist/internal/machine/planner.js +49 -9
  15. package/dist/internal/machine/planner.js.map +1 -1
  16. package/dist/internal/machine/topology.d.ts +9 -1
  17. package/dist/internal/machine/topology.d.ts.map +1 -1
  18. package/dist/internal/machine/topology.js +7 -0
  19. package/dist/internal/machine/topology.js.map +1 -1
  20. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  21. package/dist/internal/testing/machine/verification.js +15 -3
  22. package/dist/internal/testing/machine/verification.js.map +1 -1
  23. package/dist/testing/MachineTest.d.ts +2 -0
  24. package/dist/testing/MachineTest.d.ts.map +1 -1
  25. package/dist/testing/MachineTest.js.map +1 -1
  26. package/docs/agent-guide.md +17 -0
  27. package/package.json +1 -1
  28. package/src/Machine.ts +230 -43
  29. package/src/internal/machine/executionPlan.ts +37 -3
  30. package/src/internal/machine/machine.ts +57 -9
  31. package/src/internal/machine/planner.ts +64 -11
  32. package/src/internal/machine/topology.ts +18 -1
  33. package/src/internal/testing/machine/verification.ts +16 -3
  34. package/src/testing/MachineTest.ts +2 -0
@@ -52,6 +52,7 @@ import {
52
52
  isInitialTarget,
53
53
  isNoTarget,
54
54
  isSnapshot,
55
+ isStateUpdate,
55
56
  isTarget,
56
57
  makeChoiceTarget,
57
58
  makeTarget,
@@ -78,6 +79,11 @@ export type MicrostepPlan<State, Event, E, R> = {
78
79
  readonly changed: boolean
79
80
  }
80
81
 
82
+ type SettlingMicrostep<State, Event, E, R> = MicrostepPlan<State, Event, E, R> & {
83
+ /** Internal signal that eventless stabilization must run again. */
84
+ readonly stabilize: boolean
85
+ }
86
+
81
87
  export type MacrostepPlan<State, Event, E, R, Output> =
82
88
  & {
83
89
  readonly next: State
@@ -580,10 +586,15 @@ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R
580
586
  | Machine.Snapshot<States>
581
587
  | Machine.Target<States, Machine.StateIdentifier<States>>
582
588
  | undefined
589
+ readonly update: {
590
+ readonly path: string
591
+ readonly value: unknown
592
+ } | undefined
583
593
  readonly commands: ReadonlyArray<RuntimeCommand>
584
594
  readonly raisedEvents: ReadonlyArray<Event>
585
595
  readonly emittedEvents: ReadonlyArray<unknown>
586
596
  readonly changed: boolean
597
+ readonly stabilize: boolean
587
598
  readonly exitPaths: ReadonlyArray<string>
588
599
  readonly entryPaths: ReadonlyArray<string>
589
600
  readonly choiceTransitions: ReadonlyArray<{
@@ -1143,7 +1154,9 @@ export const removeConflictingTransitions = <
1143
1154
  let preempted = false
1144
1155
  const transitionsToRemove = new Set<EvaluatedTransition<States, Event, E, R, Context>>()
1145
1156
  for (const selected of filtered) {
1146
- if (hasPathIntersection(transition.exitPaths, selected.exitPaths)) {
1157
+ const writesSameState = transition.update !== undefined && selected.update !== undefined &&
1158
+ transition.update.path === selected.update.path
1159
+ if (hasPathIntersection(transition.exitPaths, selected.exitPaths) || writesSameState) {
1147
1160
  if (isDescendantOf(transition.selection.sourcePath, selected.selection.sourcePath)) {
1148
1161
  transitionsToRemove.add(selected)
1149
1162
  } else {
@@ -1344,6 +1357,7 @@ const collectEvaluatedTransition = <
1344
1357
  throw new Error("Machine transition returned decline without declaring declinable: true")
1345
1358
  }
1346
1359
  const unresolvedTarget = transitionResult.state === undefined
1360
+ || isStateUpdate(transitionResult.state)
1347
1361
  ? undefined
1348
1362
  : transitionResult.state as
1349
1363
  | Machine.Snapshot<States>
@@ -1356,6 +1370,21 @@ const collectEvaluatedTransition = <
1356
1370
  selection.transition.targets,
1357
1371
  unresolvedTarget
1358
1372
  )
1373
+ const update = isStateUpdate(transitionResult.state)
1374
+ ? (() => {
1375
+ const node = getNode(machine, transitionResult.state.path)
1376
+ if (
1377
+ !state.active.has(node.path) || node.schema === undefined ||
1378
+ (node.type !== "compound" && node.type !== "parallel")
1379
+ ) {
1380
+ throw new Error(`Machine state update owner "${node.path}" must be an active valued compound or parallel state`)
1381
+ }
1382
+ return {
1383
+ path: node.path,
1384
+ value: decodeStateValueSync(machine, node, transitionResult.state.value)
1385
+ }
1386
+ })()
1387
+ : undefined
1359
1388
  const choiceResolution = unresolvedTarget === undefined
1360
1389
  ? undefined
1361
1390
  : resolveChoiceTarget(
@@ -1456,7 +1485,10 @@ const collectEvaluatedTransition = <
1456
1485
  ? getTargetNodePath(target)
1457
1486
  : getTargetNodePath(unresolvedTarget)
1458
1487
  let stateAfterTransition = target === undefined
1459
- ? state
1488
+ ? update === undefined ? state : {
1489
+ ...state,
1490
+ values: new Map(state.values).set(update.path, update.value)
1491
+ }
1460
1492
  : normalizeTargetConfigurationSync<States>(machine, state, target)
1461
1493
  for (const additionalTarget of additionalChoiceTargets) {
1462
1494
  stateAfterTransition = normalizeTargetConfigurationSync<States>(
@@ -1466,6 +1498,7 @@ const collectEvaluatedTransition = <
1466
1498
  )
1467
1499
  }
1468
1500
  const changed = selection.transition.reenter || !hasSameActivePaths(state, stateAfterTransition)
1501
+ const stabilize = changed || update !== undefined
1469
1502
 
1470
1503
  if (!changed) {
1471
1504
  return {
@@ -1474,6 +1507,7 @@ const collectEvaluatedTransition = <
1474
1507
  branchKey: transitionResult.branchKey,
1475
1508
  unresolvedTarget,
1476
1509
  target,
1510
+ update,
1477
1511
  commands: [
1478
1512
  ...transitionResult.commands,
1479
1513
  ...(choiceResolution?.commands ?? []),
@@ -1496,6 +1530,7 @@ const collectEvaluatedTransition = <
1496
1530
  ...additionalTargetEmittedEvents
1497
1531
  ],
1498
1532
  changed,
1533
+ stabilize,
1499
1534
  exitPaths: [],
1500
1535
  entryPaths: [],
1501
1536
  choiceTransitions: [
@@ -1521,6 +1556,7 @@ const collectEvaluatedTransition = <
1521
1556
  branchKey: transitionResult.branchKey,
1522
1557
  unresolvedTarget,
1523
1558
  target,
1559
+ update,
1524
1560
  commands: [
1525
1561
  ...transitionResult.commands,
1526
1562
  ...(choiceResolution?.commands ?? []),
@@ -1543,6 +1579,7 @@ const collectEvaluatedTransition = <
1543
1579
  ...additionalTargetEmittedEvents
1544
1580
  ],
1545
1581
  changed,
1582
+ stabilize,
1546
1583
  exitPaths: reenteredHistoryTarget !== undefined
1547
1584
  ? sortExitPaths(
1548
1585
  machine,
@@ -1744,7 +1781,8 @@ export const planInitialSync = <
1744
1781
  emittedEvents: [...choiceResolution.emittedEvents, ...initialHistoryEmittedEvents],
1745
1782
  exitPaths: [],
1746
1783
  entryPaths: [],
1747
- changed: false
1784
+ changed: false,
1785
+ stabilize: false
1748
1786
  }]
1749
1787
  )
1750
1788
 
@@ -1850,7 +1888,8 @@ const microstep = <
1850
1888
  emittedEvents: [],
1851
1889
  exitPaths: [],
1852
1890
  entryPaths: [],
1853
- changed: false
1891
+ changed: false,
1892
+ stabilize: false
1854
1893
  }
1855
1894
  }
1856
1895
 
@@ -1881,6 +1920,17 @@ const microstep = <
1881
1920
  ...transition.choiceTransitions
1882
1921
  ])
1883
1922
  let stateAfterTransition = state
1923
+ // Updates never reactivate topology. Apply every retained value write to the
1924
+ // original active configuration before any control target becomes
1925
+ // authoritative.
1926
+ for (const transition of sortedTransitions) {
1927
+ if (transition.update !== undefined) {
1928
+ stateAfterTransition = {
1929
+ ...stateAfterTransition,
1930
+ values: new Map(stateAfterTransition.values).set(transition.update.path, transition.update.value)
1931
+ }
1932
+ }
1933
+ }
1884
1934
  // Value-only targets are evaluated against the original configuration. If
1885
1935
  // one is applied after a control-changing transition, it can resurrect a
1886
1936
  // branch that the changing transition exited. Apply value-only updates
@@ -1901,6 +1951,7 @@ const microstep = <
1901
1951
  }
1902
1952
 
1903
1953
  const changed = transitions.some((transition) => transition.changed)
1954
+ const stabilize = transitions.some((transition) => transition.stabilize)
1904
1955
  const transitionActions = sortedTransitions
1905
1956
  .flatMap((transition) => transition.commands)
1906
1957
  const transitionRaisedEvents = sortedTransitions
@@ -1918,7 +1969,8 @@ const microstep = <
1918
1969
  emittedEvents: transitionEmittedEvents,
1919
1970
  exitPaths: [],
1920
1971
  entryPaths: [],
1921
- changed: false
1972
+ changed: false,
1973
+ stabilize
1922
1974
  }
1923
1975
  }
1924
1976
 
@@ -1951,7 +2003,8 @@ const microstep = <
1951
2003
  emittedEvents: [...exit.emittedEvents, ...transitionEmittedEvents, ...entry.emittedEvents],
1952
2004
  exitPaths,
1953
2005
  entryPaths,
1954
- changed: true
2006
+ changed: true,
2007
+ stabilize
1955
2008
  }
1956
2009
  }
1957
2010
 
@@ -1974,7 +2027,7 @@ const settle = <
1974
2027
  commands: Array<RuntimeCommand>,
1975
2028
  raisedEvents: Array<Machine.EventOf<Events>>,
1976
2029
  emittedEvents: Array<unknown>,
1977
- microsteps: Array<MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R>>
2030
+ microsteps: Array<SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R>>
1978
2031
  ) => {
1979
2032
  let currentState = state
1980
2033
  let currentEvent = event
@@ -2010,7 +2063,7 @@ const settle = <
2010
2063
  pendingCompletions.length === 0 ? [] : [pendingCompletions.shift()!]
2011
2064
  )
2012
2065
  if (done.length > 0) {
2013
- const doneStep: MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2066
+ const doneStep: SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2014
2067
  machine,
2015
2068
  currentState,
2016
2069
  currentEvent,
@@ -2021,7 +2074,7 @@ const settle = <
2021
2074
  emittedEvents.push(...doneStep.emittedEvents)
2022
2075
  microsteps.push(doneStep)
2023
2076
  currentState = doneStep.next
2024
- shouldRunAlways = doneStep.changed
2077
+ shouldRunAlways = doneStep.stabilize
2025
2078
  continue
2026
2079
  }
2027
2080
  if (isActiveFinalConfiguration(machine, currentState)) {
@@ -2038,7 +2091,7 @@ const settle = <
2038
2091
  ? selectAlwaysTransitions<States, Events, Emits, E, R>(machine, currentState, currentEvent)
2039
2092
  : []
2040
2093
  if (always.length > 0) {
2041
- const alwaysStep: MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2094
+ const alwaysStep: SettlingMicrostep<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
2042
2095
  machine,
2043
2096
  currentState,
2044
2097
  currentEvent,
@@ -2049,7 +2102,7 @@ const settle = <
2049
2102
  emittedEvents.push(...alwaysStep.emittedEvents)
2050
2103
  microsteps.push(alwaysStep)
2051
2104
  currentState = alwaysStep.next
2052
- shouldRunAlways = alwaysStep.changed
2105
+ shouldRunAlways = alwaysStep.stabilize
2053
2106
  continue
2054
2107
  }
2055
2108
 
@@ -29,6 +29,8 @@ 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
+
32
34
  export const SelectedBranchTypeId: unique symbol = Symbol("effect/Machine/SelectedBranch")
33
35
 
34
36
  interface StateInput {
@@ -73,7 +75,7 @@ export interface Declined {
73
75
  readonly [DeclinedTypeId]: typeof DeclinedTypeId
74
76
  }
75
77
 
76
- export type TargetSelectionKind = "state" | "initial" | "history" | "choice" | "none"
78
+ export type TargetSelectionKind = "state" | "initial" | "history" | "choice" | "update" | "none"
77
79
 
78
80
  export type TargetSelectionScope = "local" | "branch" | "full" | "initial"
79
81
 
@@ -111,6 +113,21 @@ export const noneTargetSelection: TargetSelection = makeTargetSelection("none",
111
113
 
112
114
  export const isTargetSelection = (u: unknown): u is TargetSelection => hasProperty(u, TargetSelectionTypeId)
113
115
 
116
+ export interface StateUpdate {
117
+ readonly [StateUpdateTypeId]: typeof StateUpdateTypeId
118
+ readonly path: string
119
+ readonly value: unknown
120
+ }
121
+
122
+ export const makeStateUpdate = (path: string, value: unknown): StateUpdate =>
123
+ Object.freeze({
124
+ [StateUpdateTypeId]: StateUpdateTypeId,
125
+ path,
126
+ value
127
+ })
128
+
129
+ export const isStateUpdate = (u: unknown): u is StateUpdate => hasProperty(u, StateUpdateTypeId)
130
+
114
131
  export const makeSelectedBranch = (
115
132
  owner: object,
116
133
  branchIndex: number,
@@ -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)
@@ -564,6 +564,7 @@ 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)
567
568
 
568
569
  const declaredEvents = publicEventTags(machine)
569
570
  const declaredEventTags = declaredEvents.tags
@@ -577,6 +578,7 @@ export const coverage = <M extends AnyMachine>(
577
578
  let microsteps = 0
578
579
  let changedMicrosteps = 0
579
580
  let targetlessTransitions = 0
581
+ let stateUpdates = 0
580
582
  let raisedEvents = 0
581
583
  let emittedEvents = 0
582
584
  let eventTriggered = 0
@@ -630,7 +632,13 @@ export const coverage = <M extends AnyMachine>(
630
632
  hitPaths(microstep.exitPaths, exitHits)
631
633
  observeSnapshot(microstep.next)
632
634
  for (const retained of microstep.transitions) {
633
- if (retained.target === undefined) targetlessTransitions += 1
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
642
  if (retained.trigger.type === "event") eventTriggered += 1
635
643
  else if (retained.trigger.type === "always") alwaysTriggered += 1
636
644
  else if (retained.trigger.type === "done") doneTriggered += 1
@@ -718,6 +726,7 @@ export const coverage = <M extends AnyMachine>(
718
726
  total: microsteps,
719
727
  changed: changedMicrosteps,
720
728
  targetless: targetlessTransitions,
729
+ updates: stateUpdates,
721
730
  raised: raisedEvents,
722
731
  emitted: emittedEvents,
723
732
  eventTriggered,
@@ -1598,7 +1607,11 @@ export const verify = <M extends AnyMachine>(
1598
1607
  const resolvedTarget = transition.resolvedTarget === undefined ? undefined : String(transition.resolvedTarget)
1599
1608
  const targetNode = target === undefined ? undefined : byPath.get(target)
1600
1609
  let expected = target
1601
- let explanation = target === undefined ? "an unresolved targetless transition" : `target "${target}"`
1610
+ let explanation = branches[index]?.selection.kind === "update"
1611
+ ? `update owner "${String(branches[index]?.selection.path)}"`
1612
+ : target === undefined
1613
+ ? "an unresolved targetless transition"
1614
+ : `target "${target}"`
1602
1615
 
1603
1616
  if (transition.trigger.type === "choice") {
1604
1617
  explanation = target === undefined ? "a targetless choice edge" : `choice edge target "${target}"`
@@ -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