@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.
- package/README.md +41 -0
- package/dist/Machine.d.ts +62 -16
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +4 -3
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +33 -3
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +38 -9
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +11 -2
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +49 -9
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +9 -1
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +7 -0
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +15 -3
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +2 -0
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js.map +1 -1
- package/docs/agent-guide.md +17 -0
- package/package.json +1 -1
- package/src/Machine.ts +230 -43
- package/src/internal/machine/executionPlan.ts +37 -3
- package/src/internal/machine/machine.ts +57 -9
- package/src/internal/machine/planner.ts +64 -11
- package/src/internal/machine/topology.ts +18 -1
- package/src/internal/testing/machine/verification.ts +16 -3
- 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
|
-
|
|
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<
|
|
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:
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|