@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.
- package/README.md +83 -14
- package/dist/Machine.d.ts +142 -52
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +2 -2
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +23 -9
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/initialization.d.ts.map +1 -1
- package/dist/internal/machine/initialization.js +7 -2
- package/dist/internal/machine/initialization.js.map +1 -1
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +94 -13
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +5 -0
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +23 -9
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +10 -1
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +10 -2
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +3 -3
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +11 -9
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +3 -3
- package/dist/testing/MachineTest.js +3 -3
- package/dist/unstable/reactivity/AtomMachine.d.ts +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +1 -1
- package/docs/agent-guide.md +36 -5
- package/package.json +1 -1
- package/src/Machine.ts +325 -87
- package/src/internal/machine/executionPlan.ts +31 -9
- package/src/internal/machine/initialization.ts +10 -2
- package/src/internal/machine/machine.ts +131 -15
- package/src/internal/machine/planner.ts +26 -8
- package/src/internal/machine/topology.ts +22 -2
- package/src/internal/testing/machine/finiteModel.ts +7 -3
- package/src/internal/testing/machine/verification.ts +18 -10
- package/src/testing/MachineTest.ts +3 -3
- 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 {
|
|
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
|
|
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(
|
|
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 "${
|
|
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,
|
|
576
|
+
value: decodeStateValueSync(machine, node, stateUpdate.value)
|
|
566
577
|
}
|
|
567
578
|
})()
|
|
568
579
|
: undefined
|
|
569
|
-
const unresolvedTarget =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
):
|
|
1026
|
-
|
|
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
|
|
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
|
-
):
|
|
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:
|
|
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
|
|
1360
|
-
|
|
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
|
-
:
|
|
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
|
|
1380
|
+
const stateUpdate = combined?.update ?? (isStateUpdate(transitionState) ? transitionState : undefined)
|
|
1381
|
+
const update = stateUpdate !== undefined
|
|
1374
1382
|
? (() => {
|
|
1375
|
-
const node = getNode(machine,
|
|
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,
|
|
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 (
|
|
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) =>
|
|
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
|
-
|
|
636
|
-
|
|
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 =
|
|
1611
|
-
?
|
|
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)
|