@typeonce/effect-machine 0.31.2 → 0.33.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 +168 -162
- package/dist/Machine.d.ts +495 -284
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +91 -111
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +3 -4
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +0 -2
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/childRegistry.d.ts +52 -0
- package/dist/internal/machine/childRegistry.d.ts.map +1 -0
- package/dist/internal/machine/childRegistry.js +74 -0
- package/dist/internal/machine/childRegistry.js.map +1 -0
- package/dist/internal/machine/cluster.d.ts +2 -3
- package/dist/internal/machine/cluster.d.ts.map +1 -1
- package/dist/internal/machine/cluster.js.map +1 -1
- package/dist/internal/machine/configuration.d.ts.map +1 -1
- package/dist/internal/machine/configuration.js +14 -4
- package/dist/internal/machine/configuration.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts +1 -0
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +97 -53
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +4 -2
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/invocationDefinition.d.ts +35 -0
- package/dist/internal/machine/invocationDefinition.d.ts.map +1 -0
- package/dist/internal/machine/invocationDefinition.js +21 -0
- package/dist/internal/machine/invocationDefinition.js.map +1 -0
- package/dist/internal/machine/invocationEvent.d.ts +0 -2
- package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
- package/dist/internal/machine/invocationEvent.js +0 -8
- package/dist/internal/machine/invocationEvent.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +21 -43
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +134 -403
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +13 -19
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +63 -35
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/process.d.ts +6 -8
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +1 -0
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/protocol.d.ts +2 -2
- package/dist/internal/machine/protocol.d.ts.map +1 -1
- package/dist/internal/machine/protocol.js +4 -18
- package/dist/internal/machine/protocol.js.map +1 -1
- package/dist/internal/machine/readiness.d.ts +1 -1
- package/dist/internal/machine/readiness.d.ts.map +1 -1
- package/dist/internal/machine/requirements.d.ts +4 -0
- package/dist/internal/machine/requirements.d.ts.map +1 -0
- package/dist/internal/machine/requirements.js +2 -0
- package/dist/internal/machine/requirements.js.map +1 -0
- package/dist/internal/machine/runtime.d.ts +4 -58
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +1 -69
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/serialization.d.ts.map +1 -1
- package/dist/internal/machine/serialization.js +16 -0
- package/dist/internal/machine/serialization.js.map +1 -1
- package/dist/internal/machine/stateDefinition.d.ts +3 -1
- package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
- package/dist/internal/machine/stateDefinition.js +36 -2
- package/dist/internal/machine/stateDefinition.js.map +1 -1
- package/dist/internal/machine/targetBuilder.d.ts +16 -0
- package/dist/internal/machine/targetBuilder.d.ts.map +1 -0
- package/dist/internal/machine/targetBuilder.js +333 -0
- package/dist/internal/machine/targetBuilder.js.map +1 -0
- package/dist/internal/machine/topology.js +3 -3
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/machine/transition.d.ts +16 -0
- package/dist/internal/machine/transition.d.ts.map +1 -0
- package/dist/internal/machine/transition.js +2 -0
- package/dist/internal/machine/transition.js.map +1 -0
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +11 -13
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/format.d.ts +7 -0
- package/dist/internal/testing/machine/format.d.ts.map +1 -0
- package/dist/internal/testing/machine/format.js +104 -0
- package/dist/internal/testing/machine/format.js.map +1 -0
- package/dist/internal/testing/machine/probe.d.ts +1 -1
- package/dist/internal/testing/machine/probe.d.ts.map +1 -1
- package/dist/internal/testing/machine/probe.js +1 -1
- package/dist/internal/testing/machine/probe.js.map +1 -1
- package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/referenceModel.js +15 -19
- package/dist/internal/testing/machine/referenceModel.js.map +1 -1
- package/dist/internal/testing/machine/trace.d.ts +1 -1
- package/dist/internal/testing/machine/trace.d.ts.map +1 -1
- package/dist/internal/testing/machine/trace.js +3 -3
- package/dist/internal/testing/machine/trace.js.map +1 -1
- package/dist/internal/testing/machine/value.d.ts +8 -0
- package/dist/internal/testing/machine/value.d.ts.map +1 -0
- package/dist/internal/testing/machine/value.js +82 -0
- package/dist/internal/testing/machine/value.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts +2 -13
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +21 -113
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +89 -104
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +70 -84
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +10 -13
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +5 -8
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +26 -61
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +17 -26
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +45 -45
- package/docs/effect-atom-react.md +35 -63
- package/docs/machine-review.md +7 -7
- package/docs/root-api.md +259 -0
- package/package.json +1 -1
- package/src/Machine.ts +1053 -538
- package/src/internal/machine/atom.ts +3 -13
- package/src/internal/machine/childRegistry.ts +141 -0
- package/src/internal/machine/cluster.ts +5 -14
- package/src/internal/machine/configuration.ts +20 -4
- package/src/internal/machine/executionPlan.ts +109 -52
- package/src/internal/machine/invocation.ts +10 -6
- package/src/internal/machine/invocationDefinition.ts +52 -0
- package/src/internal/machine/invocationEvent.ts +0 -8
- package/src/internal/machine/machine.ts +222 -716
- package/src/internal/machine/planner.ts +74 -62
- package/src/internal/machine/process.ts +14 -21
- package/src/internal/machine/protocol.ts +9 -32
- package/src/internal/machine/readiness.ts +1 -0
- package/src/internal/machine/requirements.ts +11 -0
- package/src/internal/machine/runtime.ts +17 -186
- package/src/internal/machine/serialization.ts +22 -0
- package/src/internal/machine/stateDefinition.ts +36 -3
- package/src/internal/machine/targetBuilder.ts +525 -0
- package/src/internal/machine/topology.ts +3 -3
- package/src/internal/machine/transition.ts +26 -0
- package/src/internal/testing/machine/finiteModel.ts +23 -24
- package/src/internal/testing/machine/format.ts +120 -0
- package/src/internal/testing/machine/probe.ts +4 -4
- package/src/internal/testing/machine/referenceModel.ts +25 -17
- package/src/internal/testing/machine/trace.ts +4 -4
- package/src/internal/testing/machine/value.ts +77 -0
- package/src/internal/testing/machine/verification.ts +904 -1109
- package/src/testing/MachineTest.ts +103 -114
- package/src/unstable/cluster/ClusterMachine.ts +10 -20
- package/src/unstable/reactivity/AtomMachine.ts +42 -79
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/** Constructs decoded snapshots and transition targets from a captured state tree. */
|
|
2
|
+
import { hasProperty } from "effect/Predicate"
|
|
3
|
+
import type { Machine } from "../../Machine.js"
|
|
4
|
+
import { SnapshotBuilderStateTypeId } from "./symbols.js"
|
|
5
|
+
import * as Topology from "./topology.js"
|
|
6
|
+
|
|
7
|
+
type SnapshotBuilderOptions = {
|
|
8
|
+
readonly mode: "initial" | "full"
|
|
9
|
+
readonly prefix: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FromMethodKind = "leaf" | "nested"
|
|
13
|
+
|
|
14
|
+
export const withFrom = <Method extends (value: unknown, ...args: ReadonlyArray<any>) => unknown>(
|
|
15
|
+
method: Method,
|
|
16
|
+
kind: FromMethodKind,
|
|
17
|
+
valued: boolean
|
|
18
|
+
): {
|
|
19
|
+
readonly decoded?: Method
|
|
20
|
+
readonly from: (...args: ReadonlyArray<any>) => unknown
|
|
21
|
+
} => {
|
|
22
|
+
const builder: Record<string, unknown> = {}
|
|
23
|
+
if (valued) {
|
|
24
|
+
Object.defineProperty(builder, "decoded", {
|
|
25
|
+
value: method,
|
|
26
|
+
enumerable: false
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(builder, "from", {
|
|
30
|
+
value: (...args: ReadonlyArray<any>) => {
|
|
31
|
+
if (!valued) {
|
|
32
|
+
return method(undefined, ...args)
|
|
33
|
+
}
|
|
34
|
+
const omitted = args.length === 0 || (kind === "nested" && args.length === 1 && typeof args[0] === "function")
|
|
35
|
+
const input = omitted ? {} : args[0]
|
|
36
|
+
const rest = omitted ? args : args.slice(1)
|
|
37
|
+
return method(Topology.makeStateInput(input), ...rest)
|
|
38
|
+
},
|
|
39
|
+
enumerable: false
|
|
40
|
+
})
|
|
41
|
+
return builder as {
|
|
42
|
+
readonly decoded?: Method
|
|
43
|
+
readonly from: (...args: ReadonlyArray<any>) => unknown
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const withInitial = <Builder extends object>(
|
|
48
|
+
builder: Builder,
|
|
49
|
+
path: string,
|
|
50
|
+
valued: boolean,
|
|
51
|
+
values?: Readonly<Record<string, unknown>>
|
|
52
|
+
): Builder => {
|
|
53
|
+
const initial = withFrom(
|
|
54
|
+
(value: unknown) => Topology.makeInitialTarget(path, value, values),
|
|
55
|
+
"leaf",
|
|
56
|
+
valued
|
|
57
|
+
)
|
|
58
|
+
Object.defineProperty(builder, "initial", {
|
|
59
|
+
value: Object.freeze(initial),
|
|
60
|
+
enumerable: false
|
|
61
|
+
})
|
|
62
|
+
return builder
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const makeSnapshotBuilder = (
|
|
66
|
+
states: Machine.StateTree,
|
|
67
|
+
options: SnapshotBuilderOptions
|
|
68
|
+
): unknown => {
|
|
69
|
+
const builder: Record<string, unknown> = {}
|
|
70
|
+
for (const key of Object.keys(states)) {
|
|
71
|
+
const definition = states[key]!
|
|
72
|
+
const pseudoType = (definition as { readonly type?: unknown }).type
|
|
73
|
+
if (pseudoType === "history") {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
const path = options.prefix === "" ? key : `${options.prefix}.${key}`
|
|
77
|
+
if (pseudoType === "choice") {
|
|
78
|
+
builder[key] = Object.freeze(() => Topology.makeChoiceTarget(path, getParentPathRuntime(path)))
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
const node = Topology.getStateNodeDefinition(path, definition)
|
|
82
|
+
const method = withFrom(
|
|
83
|
+
makeSnapshotFactory(definition, key, options),
|
|
84
|
+
node.states === undefined ? "leaf" : "nested",
|
|
85
|
+
node.schema !== undefined
|
|
86
|
+
)
|
|
87
|
+
builder[key] = Object.freeze(
|
|
88
|
+
node.states === undefined || options.mode !== "full" || options.prefix !== ""
|
|
89
|
+
? method
|
|
90
|
+
: withInitial(method, path, node.schema !== undefined)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
return Object.freeze(builder)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const makeParallelSnapshotBuilder = (
|
|
97
|
+
states: Machine.StateTree,
|
|
98
|
+
options: SnapshotBuilderOptions,
|
|
99
|
+
regions: Readonly<Record<string, unknown>>
|
|
100
|
+
): unknown => {
|
|
101
|
+
const builder: Record<string, unknown> = {}
|
|
102
|
+
Object.defineProperty(builder, SnapshotBuilderStateTypeId, {
|
|
103
|
+
value: regions,
|
|
104
|
+
enumerable: false
|
|
105
|
+
})
|
|
106
|
+
for (const key of Object.keys(states)) {
|
|
107
|
+
const definition = states[key]!
|
|
108
|
+
const pseudoType = (definition as { readonly type?: unknown }).type
|
|
109
|
+
if (pseudoType === "history" || pseudoType === "choice") {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
if (hasProperty(regions, key)) {
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
const path = options.prefix === "" ? key : `${options.prefix}.${key}`
|
|
116
|
+
const node = Topology.getStateNodeDefinition(path, definition)
|
|
117
|
+
const method = withFrom(
|
|
118
|
+
(value: unknown, selector?: (builder: unknown) => unknown) => {
|
|
119
|
+
const nextRegions: Record<string, unknown> = {}
|
|
120
|
+
for (const regionKey of Object.keys(regions)) {
|
|
121
|
+
nextRegions[regionKey] = regions[regionKey]
|
|
122
|
+
}
|
|
123
|
+
nextRegions[key] = makeSnapshotForNode(definition, key, value, selector, options)
|
|
124
|
+
return makeParallelSnapshotBuilder(states, options, nextRegions)
|
|
125
|
+
},
|
|
126
|
+
node.states === undefined ? "leaf" : "nested",
|
|
127
|
+
node.schema !== undefined
|
|
128
|
+
)
|
|
129
|
+
builder[key] = method
|
|
130
|
+
}
|
|
131
|
+
return builder
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const getParallelSnapshotBuilderRegions = (
|
|
135
|
+
path: string,
|
|
136
|
+
states: Machine.StateTree,
|
|
137
|
+
builder: unknown
|
|
138
|
+
): Readonly<Record<string, unknown>> => {
|
|
139
|
+
if (typeof builder !== "object" || builder === null || !hasProperty(builder, SnapshotBuilderStateTypeId)) {
|
|
140
|
+
throw new Error(`Machine expected parallel state "${path}" builder callback to return a builder`)
|
|
141
|
+
}
|
|
142
|
+
const regions = (builder as { readonly [SnapshotBuilderStateTypeId]: Readonly<Record<string, unknown>> })[
|
|
143
|
+
SnapshotBuilderStateTypeId
|
|
144
|
+
]
|
|
145
|
+
for (const key of Object.keys(states)) {
|
|
146
|
+
const pseudoType = (states[key] as { readonly type?: unknown }).type
|
|
147
|
+
if (pseudoType === "history" || pseudoType === "choice") {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (!hasProperty(regions, key)) {
|
|
151
|
+
throw new Error(`Machine expected parallel state "${path}" builder callback to provide region "${key}"`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return regions
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A factory owns its lazily compiled child builder. The builder captures only
|
|
159
|
+
* immutable topology; every call constructs fresh values and configurations.
|
|
160
|
+
* Parallel builders remain per-call because they accumulate selected regions.
|
|
161
|
+
*/
|
|
162
|
+
const makeSnapshotFactory = (
|
|
163
|
+
definition: Machine.TaggedSchema | Machine.StateNodeConfig,
|
|
164
|
+
key: string,
|
|
165
|
+
options: SnapshotBuilderOptions
|
|
166
|
+
): (value: unknown, selector?: (builder: unknown) => unknown) => Record<string, unknown> => {
|
|
167
|
+
const path = options.prefix === "" ? key : `${options.prefix}.${key}`
|
|
168
|
+
const mode = options.mode
|
|
169
|
+
const node = Topology.getStateNodeDefinition(path, definition)
|
|
170
|
+
let childBuilder: unknown
|
|
171
|
+
return (value, selector) => {
|
|
172
|
+
const snapshot: Record<string, unknown> = { path, value }
|
|
173
|
+
if (node.states === undefined) return snapshot
|
|
174
|
+
if (selector === undefined) {
|
|
175
|
+
throw new Error(`Machine expected state "${path}" builder to provide active child states`)
|
|
176
|
+
}
|
|
177
|
+
if (node.type === "parallel") {
|
|
178
|
+
const builder = makeParallelSnapshotBuilder(node.states, { mode, prefix: path }, {})
|
|
179
|
+
snapshot.states = getParallelSnapshotBuilderRegions(path, node.states, selector(builder))
|
|
180
|
+
return snapshot
|
|
181
|
+
}
|
|
182
|
+
if (childBuilder === undefined) {
|
|
183
|
+
const childStates = mode === "initial" && node.initial !== undefined
|
|
184
|
+
? { [node.initial]: node.states[node.initial]! }
|
|
185
|
+
: node.states
|
|
186
|
+
childBuilder = makeSnapshotBuilder(childStates, { mode, prefix: path })
|
|
187
|
+
}
|
|
188
|
+
snapshot.state = selector(childBuilder)
|
|
189
|
+
return snapshot
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const makeSnapshotForNode = (
|
|
194
|
+
definition: Machine.TaggedSchema | Machine.StateNodeConfig,
|
|
195
|
+
key: string,
|
|
196
|
+
value: unknown,
|
|
197
|
+
selector: ((builder: unknown) => unknown) | undefined,
|
|
198
|
+
options: SnapshotBuilderOptions
|
|
199
|
+
): Record<string, unknown> => makeSnapshotFactory(definition, key, options)(value, selector)
|
|
200
|
+
|
|
201
|
+
export const getTargetBuilderNode = (
|
|
202
|
+
stateNodes: Machine.StateNodes,
|
|
203
|
+
path: string
|
|
204
|
+
): Machine.StateNode => {
|
|
205
|
+
const node = stateNodes.byPath.get(path)
|
|
206
|
+
if (node === undefined) {
|
|
207
|
+
throw new Error(`Machine expected state path "${path}" to exist`)
|
|
208
|
+
}
|
|
209
|
+
return node
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const getLocalTargetScope = (
|
|
213
|
+
stateNodes: Machine.StateNodes,
|
|
214
|
+
source: string
|
|
215
|
+
): string | undefined => {
|
|
216
|
+
let current: string | undefined = source
|
|
217
|
+
while (current !== undefined) {
|
|
218
|
+
const node = stateNodes.byPath.get(current)
|
|
219
|
+
if (node === undefined) {
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
if (node.type === "compound") {
|
|
223
|
+
return node.path
|
|
224
|
+
}
|
|
225
|
+
current = node.parent
|
|
226
|
+
}
|
|
227
|
+
return undefined
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const hasTargetValues = (
|
|
231
|
+
values: Readonly<Record<string, unknown>> | undefined
|
|
232
|
+
): values is Readonly<Record<string, unknown>> => values !== undefined && Object.keys(values).length > 0
|
|
233
|
+
|
|
234
|
+
const makeTargetWithValues = (
|
|
235
|
+
path: string,
|
|
236
|
+
value: unknown,
|
|
237
|
+
values: Readonly<Record<string, unknown>> | undefined
|
|
238
|
+
): Machine.Target<any, any> =>
|
|
239
|
+
hasTargetValues(values)
|
|
240
|
+
? Topology.makeTarget(path as any, value as any, { values: values as any })
|
|
241
|
+
: Topology.makeTarget(path as any, value as any)
|
|
242
|
+
|
|
243
|
+
const getTargetBuilderDefinition = (
|
|
244
|
+
states: Machine.StateTree,
|
|
245
|
+
targetPath: string
|
|
246
|
+
): Machine.TaggedSchema | Machine.StateNodeConfig => {
|
|
247
|
+
let children = states
|
|
248
|
+
let path = ""
|
|
249
|
+
let definition: Machine.TaggedSchema | Machine.StateNodeConfig | undefined
|
|
250
|
+
const segments = Object.hasOwn(states, "") && targetPath !== ""
|
|
251
|
+
? ["", ...targetPath.split(".")]
|
|
252
|
+
: targetPath.split(".")
|
|
253
|
+
for (const key of segments) {
|
|
254
|
+
if (!hasProperty(children, key)) {
|
|
255
|
+
throw new Error(`Machine expected state path "${targetPath}" to exist`)
|
|
256
|
+
}
|
|
257
|
+
definition = children[key]!
|
|
258
|
+
path = path === "" ? key : `${path}.${key}`
|
|
259
|
+
const node = Topology.getStateNodeDefinition(path, definition)
|
|
260
|
+
children = node.states ?? {}
|
|
261
|
+
}
|
|
262
|
+
return definition!
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const makeParallelTarget = (
|
|
266
|
+
states: Machine.StateTree,
|
|
267
|
+
node: Machine.StateNode,
|
|
268
|
+
value: unknown,
|
|
269
|
+
selector: ((builder: unknown) => unknown) | undefined,
|
|
270
|
+
values: Readonly<Record<string, unknown>> | undefined
|
|
271
|
+
): Machine.Target<any, any> => {
|
|
272
|
+
if (selector === undefined) {
|
|
273
|
+
throw new Error(`Machine expected parallel target "${node.path}" builder to provide every active region`)
|
|
274
|
+
}
|
|
275
|
+
const snapshot = makeSnapshotForNode(
|
|
276
|
+
getTargetBuilderDefinition(states, node.path),
|
|
277
|
+
node.key,
|
|
278
|
+
value,
|
|
279
|
+
selector,
|
|
280
|
+
{ mode: "full", prefix: node.parent ?? "" }
|
|
281
|
+
)
|
|
282
|
+
return Topology.makeTarget(node.path as any, value as any, {
|
|
283
|
+
snapshot: snapshot as any,
|
|
284
|
+
values: values as any
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const extendTargetValues = (
|
|
289
|
+
values: Readonly<Record<string, unknown>> | undefined,
|
|
290
|
+
path: string,
|
|
291
|
+
value: unknown
|
|
292
|
+
): Readonly<Record<string, unknown>> => {
|
|
293
|
+
const next: Record<string, unknown> = {}
|
|
294
|
+
if (values !== undefined) {
|
|
295
|
+
for (const key of Object.keys(values)) {
|
|
296
|
+
next[key] = values[key]
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
next[path] = value
|
|
300
|
+
return next
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const makeLocalTargetChildBuilder = (
|
|
304
|
+
states: Machine.StateTree,
|
|
305
|
+
stateNodes: Machine.StateNodes,
|
|
306
|
+
parentPath: string,
|
|
307
|
+
values: Readonly<Record<string, unknown>> | undefined,
|
|
308
|
+
source: string
|
|
309
|
+
): unknown => {
|
|
310
|
+
const parent = getTargetBuilderNode(stateNodes, parentPath)
|
|
311
|
+
const builder: Record<string, unknown> = {}
|
|
312
|
+
for (
|
|
313
|
+
const childPath of Array.from(stateNodes.byPath.values())
|
|
314
|
+
.filter((node) => node.parent === parent.path && node.type !== "history")
|
|
315
|
+
.map((node) => node.path)
|
|
316
|
+
) {
|
|
317
|
+
const child = getTargetBuilderNode(stateNodes, childPath)
|
|
318
|
+
if (child.type === "choice") {
|
|
319
|
+
builder[child.key] = () => Topology.makeChoiceTarget(child.path, parent.path, values)
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
const method = withFrom(
|
|
323
|
+
(value: unknown, selector?: (builder: unknown) => unknown) => {
|
|
324
|
+
if (child.type === "atomic" || child.type === "final") {
|
|
325
|
+
return makeTargetWithValues(child.path, value, values)
|
|
326
|
+
}
|
|
327
|
+
if (child.type === "parallel") {
|
|
328
|
+
if (source !== child.path && !(child.path === "" || source.startsWith(`${child.path}.`))) {
|
|
329
|
+
return makeParallelTarget(states, child, value, selector, values)
|
|
330
|
+
}
|
|
331
|
+
if (selector === undefined) {
|
|
332
|
+
throw new Error(`Machine expected target "${child.path}" builder to provide an active child state`)
|
|
333
|
+
}
|
|
334
|
+
return selector(makeLocalTargetChildBuilder(
|
|
335
|
+
states,
|
|
336
|
+
stateNodes,
|
|
337
|
+
child.path,
|
|
338
|
+
child.schema === undefined ? values : extendTargetValues(values, child.path, value),
|
|
339
|
+
source
|
|
340
|
+
))
|
|
341
|
+
}
|
|
342
|
+
if (selector === undefined) {
|
|
343
|
+
throw new Error(`Machine expected target "${child.path}" builder to provide an active child state`)
|
|
344
|
+
}
|
|
345
|
+
return selector(makeLocalTargetChildBuilder(
|
|
346
|
+
states,
|
|
347
|
+
stateNodes,
|
|
348
|
+
child.path,
|
|
349
|
+
child.schema === undefined ? values : extendTargetValues(values, child.path, value),
|
|
350
|
+
source
|
|
351
|
+
))
|
|
352
|
+
},
|
|
353
|
+
child.type === "atomic" || child.type === "final" ? "leaf" : "nested",
|
|
354
|
+
child.schema !== undefined
|
|
355
|
+
)
|
|
356
|
+
builder[child.key] = child.type === "atomic" || child.type === "final"
|
|
357
|
+
? method
|
|
358
|
+
: withInitial(method, child.path, child.schema !== undefined, values)
|
|
359
|
+
}
|
|
360
|
+
return builder
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const makeLocalTargetBuilder = (
|
|
364
|
+
states: Machine.StateTree,
|
|
365
|
+
stateNodes: Machine.StateNodes,
|
|
366
|
+
source: string
|
|
367
|
+
): unknown => {
|
|
368
|
+
const scope = getLocalTargetScope(stateNodes, source)
|
|
369
|
+
if (scope === undefined) {
|
|
370
|
+
return {}
|
|
371
|
+
}
|
|
372
|
+
const builder = makeLocalTargetChildBuilder(states, stateNodes, scope, undefined, source) as Record<string, unknown>
|
|
373
|
+
const scopeNode = getTargetBuilderNode(stateNodes, scope)
|
|
374
|
+
if (scopeNode.schema !== undefined) {
|
|
375
|
+
builder.with = withFrom(
|
|
376
|
+
(value: unknown, selector?: (builder: unknown) => unknown) => {
|
|
377
|
+
if (selector === undefined) {
|
|
378
|
+
throw new Error(`Machine expected target "${scope}" builder to provide an active child state`)
|
|
379
|
+
}
|
|
380
|
+
return selector(makeLocalTargetChildBuilder(states, stateNodes, scope, { [scope]: value }, source))
|
|
381
|
+
},
|
|
382
|
+
"nested",
|
|
383
|
+
true
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
return builder
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const addBranchTargetChildren = (
|
|
390
|
+
builder: Record<string, unknown>,
|
|
391
|
+
states: Machine.StateTree,
|
|
392
|
+
stateNodes: Machine.StateNodes,
|
|
393
|
+
parentPath: string,
|
|
394
|
+
values: Readonly<Record<string, unknown>> | undefined,
|
|
395
|
+
source: string
|
|
396
|
+
): void => {
|
|
397
|
+
const parent = getTargetBuilderNode(stateNodes, parentPath)
|
|
398
|
+
for (
|
|
399
|
+
const childPath of Array.from(stateNodes.byPath.values())
|
|
400
|
+
.filter((node) => node.parent === parent.path && node.type !== "history")
|
|
401
|
+
.map((node) => node.path)
|
|
402
|
+
) {
|
|
403
|
+
const child = getTargetBuilderNode(stateNodes, childPath)
|
|
404
|
+
if (child.type === "choice") {
|
|
405
|
+
builder[child.key] = () => Topology.makeChoiceTarget(child.path, parent.path, values)
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
builder[child.key] = makeBranchTargetNodeBuilder(states, stateNodes, child.path, values, source)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const makeBranchTargetNodeBuilder = (
|
|
413
|
+
states: Machine.StateTree,
|
|
414
|
+
stateNodes: Machine.StateNodes,
|
|
415
|
+
path: string,
|
|
416
|
+
values: Readonly<Record<string, unknown>> | undefined,
|
|
417
|
+
source: string
|
|
418
|
+
): unknown => {
|
|
419
|
+
const node = getTargetBuilderNode(stateNodes, path)
|
|
420
|
+
if (node.type === "atomic" || node.type === "final") {
|
|
421
|
+
return withFrom(
|
|
422
|
+
(value: unknown) => makeTargetWithValues(node.path, value, values),
|
|
423
|
+
"leaf",
|
|
424
|
+
node.schema !== undefined
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
const builder = withFrom(
|
|
428
|
+
(value: unknown, selector?: (builder: unknown) => unknown) => {
|
|
429
|
+
if (node.type === "parallel") {
|
|
430
|
+
if (source !== node.path && !(node.path === "" || source.startsWith(`${node.path}.`))) {
|
|
431
|
+
return makeParallelTarget(states, node, value, selector, values)
|
|
432
|
+
}
|
|
433
|
+
if (selector === undefined) {
|
|
434
|
+
throw new Error(`Machine expected target "${node.path}" builder to provide an active child state`)
|
|
435
|
+
}
|
|
436
|
+
const nextBuilder: Record<string, unknown> = {}
|
|
437
|
+
addBranchTargetChildren(
|
|
438
|
+
nextBuilder,
|
|
439
|
+
states,
|
|
440
|
+
stateNodes,
|
|
441
|
+
node.path,
|
|
442
|
+
node.schema === undefined ? values : extendTargetValues(values, node.path, value),
|
|
443
|
+
source
|
|
444
|
+
)
|
|
445
|
+
return selector(nextBuilder)
|
|
446
|
+
}
|
|
447
|
+
if (selector === undefined) {
|
|
448
|
+
throw new Error(`Machine expected target "${node.path}" builder to provide an active child state`)
|
|
449
|
+
}
|
|
450
|
+
const nextBuilder: Record<string, unknown> = {}
|
|
451
|
+
addBranchTargetChildren(
|
|
452
|
+
nextBuilder,
|
|
453
|
+
states,
|
|
454
|
+
stateNodes,
|
|
455
|
+
node.path,
|
|
456
|
+
node.schema === undefined ? values : extendTargetValues(values, node.path, value),
|
|
457
|
+
source
|
|
458
|
+
)
|
|
459
|
+
return selector(nextBuilder)
|
|
460
|
+
},
|
|
461
|
+
"nested",
|
|
462
|
+
node.schema !== undefined
|
|
463
|
+
) as unknown as Record<string, unknown>
|
|
464
|
+
withInitial(builder, node.path, node.schema !== undefined, values)
|
|
465
|
+
if (node.type !== "parallel" || source === node.path || node.path === "" || source.startsWith(`${node.path}.`)) {
|
|
466
|
+
addBranchTargetChildren(builder, states, stateNodes, node.path, values, source)
|
|
467
|
+
}
|
|
468
|
+
return builder
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const makeBranchTargetBuilder = (
|
|
472
|
+
states: Machine.StateTree,
|
|
473
|
+
stateNodes: Machine.StateNodes,
|
|
474
|
+
source: string
|
|
475
|
+
): unknown => {
|
|
476
|
+
const rootPath = stateNodes.roots[0]!
|
|
477
|
+
const root = getTargetBuilderNode(stateNodes, rootPath)
|
|
478
|
+
return {
|
|
479
|
+
[root.key]: makeBranchTargetNodeBuilder(states, stateNodes, root.path, undefined, source)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const makeHistoryTargetBuilder = (
|
|
484
|
+
states: Machine.StateTree,
|
|
485
|
+
prefix: string
|
|
486
|
+
): unknown => {
|
|
487
|
+
const builder: Record<string, unknown> = {}
|
|
488
|
+
for (const key of Object.keys(states)) {
|
|
489
|
+
const path = prefix === "" ? key : `${prefix}.${key}`
|
|
490
|
+
const definition = Topology.getStateNodeDefinition(path, states[key]!)
|
|
491
|
+
if (definition.type === "history") {
|
|
492
|
+
const parent = getParentPathRuntime(path)
|
|
493
|
+
builder[key] = () => Topology.makeHistoryTarget(path, parent)
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
if (definition.states !== undefined) {
|
|
497
|
+
builder[key] = makeHistoryTargetBuilder(definition.states, path)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return builder
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const getParentPathRuntime = (path: string): string => {
|
|
504
|
+
const separator = path.lastIndexOf(".")
|
|
505
|
+
if (separator < 0) {
|
|
506
|
+
return ""
|
|
507
|
+
}
|
|
508
|
+
return path.slice(0, separator)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export const makeTargetBuilder = <const States extends Machine.StateSchemas>(
|
|
512
|
+
states: States,
|
|
513
|
+
stateNodes: Machine.StateNodes
|
|
514
|
+
) => {
|
|
515
|
+
const full = makeSnapshotBuilder(states, { mode: "full", prefix: "" }) as Machine.FullTargetBuilder<States>
|
|
516
|
+
const history = makeHistoryTargetBuilder(states, "") as Machine.HistoryTargetBuilder<States>
|
|
517
|
+
return <Source extends Machine.StateNodeIdentifier<States>>(source: Source): Machine.TargetBuilder<States, Source> =>
|
|
518
|
+
({
|
|
519
|
+
none: Topology.makeNoTarget,
|
|
520
|
+
local: makeLocalTargetBuilder(states, stateNodes, source),
|
|
521
|
+
branch: makeBranchTargetBuilder(states, stateNodes, source),
|
|
522
|
+
full,
|
|
523
|
+
history
|
|
524
|
+
}) as Machine.TargetBuilder<States, Source>
|
|
525
|
+
}
|
|
@@ -374,7 +374,7 @@ export const compileStateNodes = (states: Machine.StateSchemas): Machine.StateNo
|
|
|
374
374
|
if (key.includes(".")) {
|
|
375
375
|
throw new Error(`Machine state keys cannot contain ".": "${key}"`)
|
|
376
376
|
}
|
|
377
|
-
const path = parent === undefined ? key : `${parent}.${key}`
|
|
377
|
+
const path = parent === undefined || parent === "" ? key : `${parent}.${key}`
|
|
378
378
|
const definition = getStateNodeDefinition(path, tree[key]!)
|
|
379
379
|
let node: Machine.StateNode
|
|
380
380
|
let childStates: Machine.StateTree | undefined
|
|
@@ -401,7 +401,7 @@ export const compileStateNodes = (states: Machine.StateSchemas): Machine.StateNo
|
|
|
401
401
|
history: undefined,
|
|
402
402
|
parent,
|
|
403
403
|
children: [],
|
|
404
|
-
initial: `${path}.${definition.initial}`
|
|
404
|
+
initial: path === "" ? definition.initial : `${path}.${definition.initial}`
|
|
405
405
|
}
|
|
406
406
|
childStates = definition.states
|
|
407
407
|
break
|
|
@@ -627,7 +627,7 @@ export const getSnapshotByPath = (
|
|
|
627
627
|
if (snapshot.path === path) {
|
|
628
628
|
return Option.some(snapshot)
|
|
629
629
|
}
|
|
630
|
-
if (!path.startsWith(`${snapshot.path}.`)) {
|
|
630
|
+
if (snapshot.path !== "" && !path.startsWith(`${snapshot.path}.`)) {
|
|
631
631
|
return Option.none()
|
|
632
632
|
}
|
|
633
633
|
if (parents !== undefined) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Enqueue, Machine } from "../../Machine.js"
|
|
2
|
+
|
|
3
|
+
/** Captured callbacks shared by definition construction and both planners. */
|
|
4
|
+
export type TransitionHandler<States extends Machine.StateSchemas, E, R, Context> = (
|
|
5
|
+
context: Context,
|
|
6
|
+
enqueue: Enqueue<any, any>
|
|
7
|
+
) => Machine.HandlerResult<States, E, R> | Machine.Declined
|
|
8
|
+
|
|
9
|
+
export type TransitionEvaluator<States extends Machine.StateSchemas, E, R, Context> = (
|
|
10
|
+
context: Context,
|
|
11
|
+
enqueue: Enqueue<any, any>
|
|
12
|
+
) => {
|
|
13
|
+
readonly result: Machine.HandlerResult<States, E, R> | Machine.Declined
|
|
14
|
+
readonly branchIndex: number
|
|
15
|
+
readonly branchKey: string | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type EventTransition<States extends Machine.StateSchemas, E, R, Context> =
|
|
19
|
+
| TransitionHandler<States, E, R, Context>
|
|
20
|
+
| {
|
|
21
|
+
readonly reenter?: boolean
|
|
22
|
+
readonly declinable?: boolean
|
|
23
|
+
readonly targets?: ReadonlyArray<string>
|
|
24
|
+
readonly transition: TransitionHandler<States, E, R, Context>
|
|
25
|
+
readonly evaluate?: TransitionEvaluator<States, E, R, Context>
|
|
26
|
+
}
|
|
@@ -1352,7 +1352,7 @@ const selectDefinitionTarget = (
|
|
|
1352
1352
|
const sourceRoot = byPath.get(source)!.root
|
|
1353
1353
|
if (selected.root !== sourceRoot) {
|
|
1354
1354
|
const root = target.split(".")[0]!
|
|
1355
|
-
return selector.
|
|
1355
|
+
return selector.branch[root]()
|
|
1356
1356
|
}
|
|
1357
1357
|
const selectable = selectableDefinitionTarget(source, target, byPath)
|
|
1358
1358
|
let current = selector.branch
|
|
@@ -1442,7 +1442,7 @@ const makeHandlers = (
|
|
|
1442
1442
|
? () => undefined
|
|
1443
1443
|
: ({ target }: { readonly target: any }) =>
|
|
1444
1444
|
resolveDefinitionTarget(target, path, transition.target!, byPath, transition.targetValue)
|
|
1445
|
-
return
|
|
1445
|
+
return ("reenter" in transition && transition.reenter ? selected.reenter() : selected).resolve(resolve)
|
|
1446
1446
|
}
|
|
1447
1447
|
if (transition.trigger.type === "event") on[transition.trigger.event] = config
|
|
1448
1448
|
else if (transition.trigger.type === "always") always = config
|
|
@@ -1452,17 +1452,18 @@ const makeHandlers = (
|
|
|
1452
1452
|
? Object.fromEntries(node.states.flatMap((child) => {
|
|
1453
1453
|
if (child._tag !== "History") return []
|
|
1454
1454
|
return [[child.key, {
|
|
1455
|
-
default: ({ target }: { readonly target:
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1455
|
+
default: ({ target: root }: { readonly target: any }) =>
|
|
1456
|
+
root.from((target: Record<string, any>) => {
|
|
1457
|
+
const fallback = byPath.get(child.fallback)!
|
|
1458
|
+
const parts = child.fallback.split(".")
|
|
1459
|
+
const completeRoot = selectSnapshot(target, fallback.root, byPath, parts, 0)
|
|
1460
|
+
if (findSnapshot(completeRoot, path) === undefined) {
|
|
1461
|
+
throw new Error(
|
|
1462
|
+
`MachineTest.compileModel could not construct history fallback for "${path}.${child.key}"`
|
|
1463
|
+
)
|
|
1464
|
+
}
|
|
1465
|
+
return completeRoot
|
|
1466
|
+
})
|
|
1466
1467
|
}]]
|
|
1467
1468
|
}))
|
|
1468
1469
|
: {}
|
|
@@ -1511,23 +1512,21 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => {
|
|
|
1511
1512
|
const flattened = validateModel(model)
|
|
1512
1513
|
const byPath = new Map(flattened.map((state) => [state.path, state]))
|
|
1513
1514
|
const stateTree = makeStateTree(model.roots, undefined)
|
|
1514
|
-
const defined = Machine.states
|
|
1515
|
+
const defined = Machine.state({ initial: model.initial, states: stateTree } as any)
|
|
1515
1516
|
const eventSchemas = model.events.map((event) => Schema.TaggedStruct(event, {}))
|
|
1516
1517
|
const initial = byPath.get(model.initial)!
|
|
1517
1518
|
const machine = Machine.make({
|
|
1518
|
-
|
|
1519
|
-
events: Machine.
|
|
1520
|
-
initial: (to: Record<string, any>) => {
|
|
1521
|
-
const selected = to[initial.path]
|
|
1522
|
-
const selection = typeof selected === "function" ? selected() : selected.initial
|
|
1523
|
-
return selection.resolve(({ target }: { readonly target: any }) =>
|
|
1524
|
-
selectSnapshot({ [initial.path]: target }, initial.path, byPath, [initial.path], 0) as any
|
|
1525
|
-
)
|
|
1526
|
-
}
|
|
1519
|
+
root: defined,
|
|
1520
|
+
events: Machine.eventsFromSchemas(...eventSchemas) as any
|
|
1527
1521
|
} as any)
|
|
1528
1522
|
const transitions = new Map(model.transitions.map((transition) => [
|
|
1529
1523
|
`${transition.source}\u0000${triggerKey(transition.trigger)}`,
|
|
1530
1524
|
transition
|
|
1531
1525
|
]))
|
|
1532
|
-
return machine.handle(
|
|
1526
|
+
return machine.handle(
|
|
1527
|
+
{
|
|
1528
|
+
initialize: ({ builder }: any) => builder.decoded(stateValue(initial)),
|
|
1529
|
+
states: makeHandlers(model.roots, undefined, byPath, transitions)
|
|
1530
|
+
} as any
|
|
1531
|
+
) as Machine.Machine.Any
|
|
1533
1532
|
}
|