@typeonce/effect-machine 0.12.0 → 0.14.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 +130 -30
- package/dist/Machine.d.ts +722 -262
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +130 -62
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +1 -0
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +23 -0
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
- package/dist/internal/machine/commandRuntime.js +2 -5
- package/dist/internal/machine/commandRuntime.js.map +1 -1
- package/dist/internal/machine/configuration.d.ts +5 -0
- package/dist/internal/machine/configuration.d.ts.map +1 -1
- package/dist/internal/machine/configuration.js +6 -2
- 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 +57 -24
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/initialization.d.ts +5 -0
- package/dist/internal/machine/initialization.d.ts.map +1 -0
- package/dist/internal/machine/initialization.js +61 -0
- package/dist/internal/machine/initialization.js.map +1 -0
- package/dist/internal/machine/inspectionRuntime.d.ts +25 -0
- package/dist/internal/machine/inspectionRuntime.d.ts.map +1 -0
- package/dist/internal/machine/inspectionRuntime.js +79 -0
- package/dist/internal/machine/inspectionRuntime.js.map +1 -0
- package/dist/internal/machine/invocation.d.ts +2 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +17 -10
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +4 -3
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +327 -32
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +28 -0
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +132 -51
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +4 -2
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/runtime.d.ts +27 -3
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +440 -64
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/symbols.d.ts +2 -0
- package/dist/internal/machine/symbols.d.ts.map +1 -1
- package/dist/internal/machine/symbols.js +2 -0
- package/dist/internal/machine/symbols.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +25 -0
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +26 -10
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/exploration.d.ts.map +1 -1
- package/dist/internal/testing/machine/exploration.js +11 -2
- package/dist/internal/testing/machine/exploration.js.map +1 -1
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +75 -69
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/transitionCoverage.d.ts +20 -0
- package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -0
- package/dist/internal/testing/machine/transitionCoverage.js +80 -0
- package/dist/internal/testing/machine/transitionCoverage.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +171 -33
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +97 -22
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +58 -16
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +4 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +4 -1
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +21 -3
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +21 -3
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +240 -65
- package/package.json +5 -5
- package/src/Machine.ts +1240 -495
- package/src/internal/machine/atom.ts +26 -0
- package/src/internal/machine/commandRuntime.ts +2 -9
- package/src/internal/machine/configuration.ts +28 -3
- package/src/internal/machine/executionPlan.ts +72 -24
- package/src/internal/machine/initialization.ts +73 -0
- package/src/internal/machine/inspectionRuntime.ts +102 -0
- package/src/internal/machine/invocation.ts +37 -12
- package/src/internal/machine/machine.ts +438 -39
- package/src/internal/machine/planner.ts +180 -55
- package/src/internal/machine/process.ts +4 -2
- package/src/internal/machine/runtime.ts +661 -89
- package/src/internal/machine/symbols.ts +3 -0
- package/src/internal/machine/topology.ts +65 -12
- package/src/internal/testing/machine/exploration.ts +10 -2
- package/src/internal/testing/machine/finiteModel.ts +114 -80
- package/src/internal/testing/machine/transitionCoverage.ts +116 -0
- package/src/internal/testing/machine/verification.ts +216 -58
- package/src/testing/MachineTest.ts +118 -28
- package/src/unstable/cluster/ClusterMachine.ts +4 -1
- package/src/unstable/reactivity/AtomMachine.ts +24 -3
|
@@ -33,7 +33,7 @@ import type { EnsureExecutable } from "./readiness.js"
|
|
|
33
33
|
import * as internalRuntime from "./runtime.js"
|
|
34
34
|
import * as Serialization from "./serialization.js"
|
|
35
35
|
import * as StateDefinition from "./stateDefinition.js"
|
|
36
|
-
import { ChildMachineLogicTypeId } from "./symbols.js"
|
|
36
|
+
import { ChildMachineLogicTypeId, SnapshotBuilderStateTypeId } from "./symbols.js"
|
|
37
37
|
import * as Topology from "./topology.js"
|
|
38
38
|
|
|
39
39
|
export {
|
|
@@ -45,11 +45,11 @@ export {
|
|
|
45
45
|
StartupError,
|
|
46
46
|
StoppedError
|
|
47
47
|
} from "./errors.js"
|
|
48
|
-
export { ChildMachineLogicTypeId, InitialEventTypeId } from "./symbols.js"
|
|
48
|
+
export { ChildMachineLogicTypeId, InitialEventTypeId, SnapshotBuilderStateTypeId } from "./symbols.js"
|
|
49
49
|
|
|
50
50
|
const TypeId = "~effect/Machine"
|
|
51
|
-
export const SnapshotBuilderStateTypeId: unique symbol = Symbol("effect/Machine/SnapshotBuilderState")
|
|
52
51
|
export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke")
|
|
52
|
+
export const TransitionTypeId: unique symbol = Symbol.for("effect/Machine/Transition")
|
|
53
53
|
const ChildMachineTypeId = "~effect/Machine/ChildMachine"
|
|
54
54
|
type IsAny<A> = 0 extends 1 & A ? true : false
|
|
55
55
|
type MachineRuntimeRequirement = internalRuntime.MachineRuntime
|
|
@@ -119,6 +119,7 @@ const cloneWithHandlers = (
|
|
|
119
119
|
machine.input = self.input
|
|
120
120
|
machine.id = self.id
|
|
121
121
|
machine.initial = self.initial
|
|
122
|
+
machine.initialDefinition = self.initialDefinition
|
|
122
123
|
machine.stateNodes = self.stateNodes
|
|
123
124
|
machine.makeTargetBuilder = self.makeTargetBuilder
|
|
124
125
|
machine.handlers = handlers
|
|
@@ -128,47 +129,326 @@ const cloneWithHandlers = (
|
|
|
128
129
|
return machine
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
type DefinitionBranch = {
|
|
133
|
+
readonly title?: string
|
|
134
|
+
readonly when?: (context: any) => Option.Option<unknown>
|
|
135
|
+
readonly target: (selector: unknown) => unknown
|
|
136
|
+
readonly resolve?: (context: any, enqueue: unknown) => unknown
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type CapturedBranch = DefinitionBranch & {
|
|
140
|
+
readonly selection: Topology.TargetSelection
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const transitionTargetSelection = (
|
|
144
|
+
selection: Topology.TargetSelection
|
|
145
|
+
): Machine.TransitionTargetSelection =>
|
|
146
|
+
Object.freeze({
|
|
147
|
+
path: selection.path,
|
|
148
|
+
kind: selection.kind,
|
|
149
|
+
scope: selection.scope
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const makeSelectionMethod = (
|
|
153
|
+
kind: Topology.TargetSelectionKind,
|
|
154
|
+
path: string | undefined,
|
|
155
|
+
scope: Topology.TargetSelectionScope
|
|
156
|
+
): () => Topology.TargetSelection =>
|
|
157
|
+
() => Topology.makeTargetSelection(kind, path, scope)
|
|
158
|
+
|
|
159
|
+
const addSelectionChildren = (
|
|
160
|
+
builder: Record<string, unknown>,
|
|
161
|
+
stateNodes: Machine.StateNodes,
|
|
162
|
+
parent: string,
|
|
163
|
+
scope: "local" | "branch"
|
|
164
|
+
): void => {
|
|
165
|
+
for (const node of stateNodes.byPath.values()) {
|
|
166
|
+
if (node.parent !== parent || node.type === "history") continue
|
|
167
|
+
builder[node.key] = makeSelectionNode(stateNodes, node.path, scope)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const makeSelectionNode = (
|
|
172
|
+
stateNodes: Machine.StateNodes,
|
|
173
|
+
path: string,
|
|
174
|
+
scope: Topology.TargetSelectionScope
|
|
175
|
+
): unknown => {
|
|
176
|
+
const node = getTargetBuilderNode(stateNodes, path)
|
|
177
|
+
const kind: Topology.TargetSelectionKind = node.type === "choice" ? "choice" : "state"
|
|
178
|
+
const method = makeSelectionMethod(kind, path, scope) as unknown as Record<string, unknown>
|
|
179
|
+
if (node.type !== "atomic" && node.type !== "final" && node.type !== "choice" && node.type !== "history") {
|
|
180
|
+
Object.defineProperty(method, "initial", {
|
|
181
|
+
value: makeSelectionMethod("initial", path, scope),
|
|
182
|
+
enumerable: true
|
|
183
|
+
})
|
|
184
|
+
if (scope === "local" || scope === "branch") {
|
|
185
|
+
addSelectionChildren(method, stateNodes, path, scope)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return method
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const makeHistorySelectionTree = (
|
|
132
192
|
stateNodes: Machine.StateNodes,
|
|
193
|
+
parent: string | undefined
|
|
194
|
+
): Record<string, unknown> => {
|
|
195
|
+
const builder: Record<string, unknown> = {}
|
|
196
|
+
for (const node of stateNodes.byPath.values()) {
|
|
197
|
+
if (node.parent !== parent) continue
|
|
198
|
+
if (node.type === "history") {
|
|
199
|
+
builder[node.key] = makeSelectionMethod("history", node.path, "full")
|
|
200
|
+
} else if (node.type !== "choice") {
|
|
201
|
+
const children = makeHistorySelectionTree(stateNodes, node.path)
|
|
202
|
+
if (Object.keys(children).length > 0) builder[node.key] = children
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return builder
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const makeTargetSelector = (
|
|
209
|
+
stateNodes: Machine.StateNodes,
|
|
210
|
+
source: string
|
|
211
|
+
): unknown => {
|
|
212
|
+
const full: Record<string, unknown> = {}
|
|
213
|
+
for (const node of stateNodes.byPath.values()) {
|
|
214
|
+
if (node.parent === undefined && node.type !== "history" && node.type !== "choice") {
|
|
215
|
+
full[node.key] = makeSelectionNode(stateNodes, node.path, "full")
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const branch: Record<string, unknown> = {}
|
|
219
|
+
const root = getTargetBuilderNode(stateNodes, source.split(".")[0]!)
|
|
220
|
+
branch[root.key] = makeSelectionNode(stateNodes, root.path, "branch")
|
|
221
|
+
const local: Record<string, unknown> = {}
|
|
222
|
+
const localScope = getLocalTargetScope(stateNodes, source)
|
|
223
|
+
if (localScope !== undefined) addSelectionChildren(local, stateNodes, localScope, "local")
|
|
224
|
+
return {
|
|
225
|
+
none: makeSelectionMethod("none", undefined, "local"),
|
|
226
|
+
local,
|
|
227
|
+
branch,
|
|
228
|
+
full,
|
|
229
|
+
history: makeHistorySelectionTree(stateNodes, undefined)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const captureDefinitionBranch = (
|
|
234
|
+
branch: unknown,
|
|
235
|
+
selector: unknown,
|
|
133
236
|
path: string,
|
|
134
|
-
trigger: PropertyKey
|
|
135
|
-
|
|
237
|
+
trigger: PropertyKey
|
|
238
|
+
): CapturedBranch => {
|
|
239
|
+
if (
|
|
240
|
+
typeof branch !== "object" || branch === null || !hasProperty(branch, "target") ||
|
|
241
|
+
typeof branch.target !== "function"
|
|
242
|
+
) {
|
|
243
|
+
throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" requires a target selector`)
|
|
244
|
+
}
|
|
245
|
+
const selection = branch.target(selector)
|
|
246
|
+
if (!Topology.isTargetSelection(selection)) {
|
|
247
|
+
throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must select exactly one target`)
|
|
248
|
+
}
|
|
249
|
+
return { ...(branch as DefinitionBranch), selection }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const getSelectionBuilder = (
|
|
253
|
+
target: Record<string, any>,
|
|
254
|
+
selection: Topology.TargetSelection,
|
|
255
|
+
stateNodes: Machine.StateNodes,
|
|
256
|
+
source: string
|
|
257
|
+
): unknown => {
|
|
258
|
+
if (selection.kind === "none") return target.none
|
|
259
|
+
let builder: any
|
|
260
|
+
let parts = selection.path!.split(".")
|
|
261
|
+
if (selection.kind === "history") {
|
|
262
|
+
builder = target.history
|
|
263
|
+
} else if (selection.scope === "local") {
|
|
264
|
+
builder = target.local
|
|
265
|
+
const scope = getLocalTargetScope(stateNodes, source)
|
|
266
|
+
if (scope !== undefined) parts = selection.path!.slice(scope.length + 1).split(".")
|
|
267
|
+
} else if (selection.scope === "branch") {
|
|
268
|
+
builder = target.branch
|
|
269
|
+
} else {
|
|
270
|
+
builder = target.full
|
|
271
|
+
}
|
|
272
|
+
for (const part of parts) builder = builder[part]
|
|
273
|
+
if (selection.kind === "initial") builder = builder.initial
|
|
274
|
+
if (
|
|
275
|
+
typeof builder !== "function" &&
|
|
276
|
+
(typeof builder !== "object" || builder === null || typeof builder.from !== "function")
|
|
277
|
+
) {
|
|
278
|
+
throw new Error(`Machine could not construct selected transition target "${selection.path}"`)
|
|
279
|
+
}
|
|
280
|
+
return builder
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const constructSelectedTarget = (builder: any): unknown => typeof builder === "function" ? builder() : builder.from()
|
|
284
|
+
|
|
285
|
+
const validateResolvedSelection = (
|
|
286
|
+
result: unknown,
|
|
287
|
+
selection: Topology.TargetSelection,
|
|
288
|
+
stateNodes: Machine.StateNodes
|
|
136
289
|
): void => {
|
|
137
|
-
if (
|
|
290
|
+
if (selection.kind === "none") {
|
|
291
|
+
if (result !== undefined) {
|
|
292
|
+
throw new Error("Machine targetless transition resolver must return undefined")
|
|
293
|
+
}
|
|
138
294
|
return
|
|
139
295
|
}
|
|
140
|
-
if (
|
|
296
|
+
if (result === undefined) return
|
|
297
|
+
const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path") &&
|
|
298
|
+
typeof result.path === "string"
|
|
299
|
+
? result.path
|
|
300
|
+
: undefined
|
|
301
|
+
const selectedNode = selection.path === undefined ? undefined : stateNodes.byPath.get(selection.path)
|
|
302
|
+
const acceptsDescendant = (selection.scope === "local" || selection.scope === "branch") &&
|
|
303
|
+
(selectedNode?.type === "compound" || selectedNode?.type === "parallel")
|
|
304
|
+
if (
|
|
305
|
+
resultPath === undefined ||
|
|
306
|
+
(resultPath !== selection.path && !(acceptsDescendant && resultPath.startsWith(`${selection.path}.`)))
|
|
307
|
+
) {
|
|
141
308
|
throw new Error(
|
|
142
|
-
`Machine
|
|
309
|
+
`Machine transition resolver selected "${selection.path}" but constructed "${resultPath ?? "<invalid>"}"`
|
|
143
310
|
)
|
|
144
311
|
}
|
|
145
|
-
for (const target of transition.targets) {
|
|
146
|
-
if (typeof target !== "string" || !stateNodes.byPath.has(target)) {
|
|
147
|
-
throw new Error(
|
|
148
|
-
`Machine transition for state "${path}" on "${String(trigger)}" declares unknown target "${String(target)}"`
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
312
|
}
|
|
153
313
|
|
|
154
|
-
const
|
|
314
|
+
const runCapturedBranch = (
|
|
315
|
+
branch: CapturedBranch,
|
|
316
|
+
context: Record<string, any>,
|
|
317
|
+
enqueue: unknown,
|
|
318
|
+
stateNodes: Machine.StateNodes,
|
|
319
|
+
source: string,
|
|
320
|
+
match?: { readonly value: unknown }
|
|
321
|
+
): unknown => {
|
|
322
|
+
const selectedTarget = getSelectionBuilder(context.target, branch.selection, stateNodes, source)
|
|
323
|
+
if (branch.resolve === undefined) return constructSelectedTarget(selectedTarget)
|
|
324
|
+
const resolverContext = { ...context }
|
|
325
|
+
if (branch.selection.kind === "none") delete resolverContext.target
|
|
326
|
+
else resolverContext.target = selectedTarget
|
|
327
|
+
if (match !== undefined) resolverContext.match = match.value
|
|
328
|
+
const resolved = branch.resolve(resolverContext, enqueue)
|
|
329
|
+
validateResolvedSelection(resolved, branch.selection, stateNodes)
|
|
330
|
+
return resolved === undefined ? constructSelectedTarget(selectedTarget) : resolved
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const captureTransition = (
|
|
334
|
+
transition: unknown,
|
|
335
|
+
stateNodes: Machine.StateNodes,
|
|
336
|
+
path: string,
|
|
337
|
+
trigger: PropertyKey
|
|
338
|
+
): unknown => {
|
|
155
339
|
if (typeof transition !== "object" || transition === null) {
|
|
156
|
-
|
|
340
|
+
throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must be an object`)
|
|
157
341
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
342
|
+
const definition = transition as Record<PropertyKey, unknown>
|
|
343
|
+
const selector = makeTargetSelector(stateNodes, path)
|
|
344
|
+
const reenter = definition.reenter === true
|
|
345
|
+
if (Array.isArray(definition.cases)) {
|
|
346
|
+
const rawCases = definition.cases as ReadonlyArray<unknown>
|
|
347
|
+
if (definition.cases.length === 0 || !hasProperty(definition, "otherwise")) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Machine conditional transition for state "${path}" on "${String(trigger)}" requires cases and otherwise`
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
const cases = rawCases.map((branch) => {
|
|
353
|
+
const captured = captureDefinitionBranch(branch, selector, path, trigger)
|
|
354
|
+
if (typeof captured.title !== "string" || captured.title.length === 0 || typeof captured.when !== "function") {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Machine conditional transition case for state "${path}" on "${String(trigger)}" requires title and when`
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
return captured
|
|
360
|
+
})
|
|
361
|
+
const otherwise = captureDefinitionBranch(definition.otherwise, selector, path, trigger)
|
|
362
|
+
const evaluate = (context: Record<string, any>, enqueue: unknown) => {
|
|
363
|
+
const predicateContext = { ...context }
|
|
364
|
+
delete predicateContext.target
|
|
365
|
+
for (let branchIndex = 0; branchIndex < cases.length; branchIndex++) {
|
|
366
|
+
const branch = cases[branchIndex]!
|
|
367
|
+
const result = branch.when!(predicateContext)
|
|
368
|
+
if (!Option.isOption(result)) {
|
|
369
|
+
throw new Error(`Machine conditional transition case "${branch.title}" must return Option`)
|
|
370
|
+
}
|
|
371
|
+
if (Option.isSome(result)) {
|
|
372
|
+
return {
|
|
373
|
+
result: runCapturedBranch(branch, context, enqueue, stateNodes, path, { value: result.value }),
|
|
374
|
+
branchIndex
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
result: runCapturedBranch(otherwise, context, enqueue, stateNodes, path),
|
|
380
|
+
branchIndex: cases.length
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
reenter,
|
|
385
|
+
targets: [
|
|
386
|
+
...new Set(
|
|
387
|
+
[...cases, otherwise].flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
|
|
388
|
+
)
|
|
389
|
+
],
|
|
390
|
+
branches: [
|
|
391
|
+
...cases.map((branch) => ({
|
|
392
|
+
type: "case" as const,
|
|
393
|
+
title: branch.title!,
|
|
394
|
+
target: branch.selection.path,
|
|
395
|
+
selection: transitionTargetSelection(branch.selection)
|
|
396
|
+
})),
|
|
397
|
+
{
|
|
398
|
+
type: "otherwise" as const,
|
|
399
|
+
target: otherwise.selection.path,
|
|
400
|
+
selection: transitionTargetSelection(otherwise.selection)
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
evaluate,
|
|
404
|
+
transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const branch = captureDefinitionBranch(transition, selector, path, trigger)
|
|
408
|
+
const evaluate = (context: Record<string, any>, enqueue: unknown) => ({
|
|
409
|
+
result: runCapturedBranch(branch, context, enqueue, stateNodes, path),
|
|
410
|
+
branchIndex: 0
|
|
411
|
+
})
|
|
412
|
+
return {
|
|
413
|
+
reenter,
|
|
414
|
+
targets: branch.selection.path === undefined ? [] : [branch.selection.path],
|
|
415
|
+
branches: [{
|
|
416
|
+
type: "direct" as const,
|
|
417
|
+
target: branch.selection.path,
|
|
418
|
+
selection: transitionTargetSelection(branch.selection)
|
|
419
|
+
}],
|
|
420
|
+
evaluate,
|
|
421
|
+
transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
|
|
161
422
|
}
|
|
162
|
-
return captured
|
|
163
423
|
}
|
|
164
424
|
|
|
165
|
-
const captureEventHandlers = (
|
|
425
|
+
const captureEventHandlers = (
|
|
426
|
+
on: object,
|
|
427
|
+
stateNodes: Machine.StateNodes,
|
|
428
|
+
path: string
|
|
429
|
+
): Record<PropertyKey, unknown> => {
|
|
166
430
|
// The machine owns its dispatch table. Compiled plans may snapshot these
|
|
167
431
|
// definitions, so retaining caller-owned containers would let strategies
|
|
168
432
|
// observe different handlers after an unsafe external mutation.
|
|
169
433
|
const captured: Record<PropertyKey, unknown> = Object.create(null)
|
|
170
434
|
for (const event of Reflect.ownKeys(on)) {
|
|
171
|
-
captured[event] = captureTransition((on as Record<PropertyKey, unknown>)[event])
|
|
435
|
+
captured[event] = captureTransition((on as Record<PropertyKey, unknown>)[event], stateNodes, path, event)
|
|
436
|
+
}
|
|
437
|
+
return captured
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const captureInvokeDefinition = (
|
|
441
|
+
invoke: unknown,
|
|
442
|
+
stateNodes: Machine.StateNodes,
|
|
443
|
+
path: string
|
|
444
|
+
): unknown => {
|
|
445
|
+
if (Array.isArray(invoke)) return invoke.map((item) => captureInvokeDefinition(item, stateNodes, path))
|
|
446
|
+
if (typeof invoke !== "object" || invoke === null) return invoke
|
|
447
|
+
const captured = { ...(invoke as Record<PropertyKey, unknown>) }
|
|
448
|
+
for (const key of ["onDone", "onFailure", "onSnapshot"] as const) {
|
|
449
|
+
if (captured[key] !== undefined) {
|
|
450
|
+
captured[key] = captureTransition(captured[key], stateNodes, path, key)
|
|
451
|
+
}
|
|
172
452
|
}
|
|
173
453
|
return captured
|
|
174
454
|
}
|
|
@@ -192,15 +472,21 @@ const flattenHandlers = (
|
|
|
192
472
|
const { states: childConfig, ...stateConfig } = nodeConfig as Record<string, unknown>
|
|
193
473
|
const on = stateConfig.on
|
|
194
474
|
if (typeof on === "object" && on !== null) {
|
|
195
|
-
const capturedOn = captureEventHandlers(on)
|
|
475
|
+
const capturedOn = captureEventHandlers(on, stateNodes, path)
|
|
196
476
|
stateConfig.on = capturedOn
|
|
197
|
-
for (const event of Reflect.ownKeys(capturedOn)) {
|
|
198
|
-
validateTransitionTargets(stateNodes, path, event, capturedOn[event])
|
|
199
|
-
}
|
|
200
477
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
478
|
+
if (stateConfig.always !== undefined) {
|
|
479
|
+
stateConfig.always = captureTransition(stateConfig.always, stateNodes, path, "always")
|
|
480
|
+
}
|
|
481
|
+
if (stateConfig.onDone !== undefined) {
|
|
482
|
+
stateConfig.onDone = captureTransition(stateConfig.onDone, stateNodes, path, "done")
|
|
483
|
+
}
|
|
484
|
+
if (stateConfig.choice !== undefined) {
|
|
485
|
+
stateConfig.choice = captureTransition(stateConfig.choice, stateNodes, path, "choice")
|
|
486
|
+
}
|
|
487
|
+
if (stateConfig.invoke !== undefined) {
|
|
488
|
+
stateConfig.invoke = captureInvokeDefinition(stateConfig.invoke, stateNodes, path)
|
|
489
|
+
}
|
|
204
490
|
const node = stateNodes.byPath.get(path)
|
|
205
491
|
if (node?.type === "choice") {
|
|
206
492
|
if (
|
|
@@ -209,7 +495,7 @@ const flattenHandlers = (
|
|
|
209
495
|
!hasProperty(stateConfig.choice, "targets") || !Array.isArray(stateConfig.choice.targets) ||
|
|
210
496
|
stateConfig.choice.targets.length === 0
|
|
211
497
|
) {
|
|
212
|
-
throw new Error(`Machine choice state "${path}" requires a transition
|
|
498
|
+
throw new Error(`Machine choice state "${path}" requires a transition`)
|
|
213
499
|
}
|
|
214
500
|
}
|
|
215
501
|
handlers[path] = stateConfig as Machine.AnyStateConfig
|
|
@@ -300,6 +586,24 @@ const withFrom = <Method extends (value: unknown, ...args: ReadonlyArray<any>) =
|
|
|
300
586
|
return method as Method & { readonly from: (...args: ReadonlyArray<any>) => unknown }
|
|
301
587
|
}
|
|
302
588
|
|
|
589
|
+
const withInitial = <Builder extends object>(
|
|
590
|
+
builder: Builder,
|
|
591
|
+
path: string,
|
|
592
|
+
valued: boolean,
|
|
593
|
+
values?: Readonly<Record<string, unknown>>
|
|
594
|
+
): Builder => {
|
|
595
|
+
const initial = withFrom(
|
|
596
|
+
(value: unknown) => Topology.makeInitialTarget(path, value, values),
|
|
597
|
+
"leaf",
|
|
598
|
+
valued
|
|
599
|
+
)
|
|
600
|
+
Object.defineProperty(builder, "initial", {
|
|
601
|
+
value: initial,
|
|
602
|
+
enumerable: false
|
|
603
|
+
})
|
|
604
|
+
return builder
|
|
605
|
+
}
|
|
606
|
+
|
|
303
607
|
const makeSnapshotBuilder = (
|
|
304
608
|
states: Machine.StateTree,
|
|
305
609
|
options: SnapshotBuilderOptions
|
|
@@ -317,12 +621,15 @@ const makeSnapshotBuilder = (
|
|
|
317
621
|
continue
|
|
318
622
|
}
|
|
319
623
|
const node = Topology.getStateNodeDefinition(path, definition)
|
|
320
|
-
|
|
624
|
+
const method = withFrom(
|
|
321
625
|
(value: unknown, selector?: (builder: unknown) => unknown) =>
|
|
322
626
|
makeSnapshotForNode(definition, key, value, selector, options),
|
|
323
627
|
node.states === undefined ? "leaf" : "nested",
|
|
324
628
|
node.schema !== undefined
|
|
325
629
|
)
|
|
630
|
+
builder[key] = node.states === undefined || options.mode !== "full" || options.prefix !== ""
|
|
631
|
+
? method
|
|
632
|
+
: withInitial(method, path, node.schema !== undefined)
|
|
326
633
|
}
|
|
327
634
|
return builder
|
|
328
635
|
}
|
|
@@ -348,7 +655,7 @@ const makeParallelSnapshotBuilder = (
|
|
|
348
655
|
}
|
|
349
656
|
const path = options.prefix === "" ? key : `${options.prefix}.${key}`
|
|
350
657
|
const node = Topology.getStateNodeDefinition(path, definition)
|
|
351
|
-
|
|
658
|
+
const method = withFrom(
|
|
352
659
|
(value: unknown, selector?: (builder: unknown) => unknown) => {
|
|
353
660
|
const nextRegions: Record<string, unknown> = {}
|
|
354
661
|
for (const regionKey of Object.keys(regions)) {
|
|
@@ -360,6 +667,7 @@ const makeParallelSnapshotBuilder = (
|
|
|
360
667
|
node.states === undefined ? "leaf" : "nested",
|
|
361
668
|
node.schema !== undefined
|
|
362
669
|
)
|
|
670
|
+
builder[key] = method
|
|
363
671
|
}
|
|
364
672
|
return builder
|
|
365
673
|
}
|
|
@@ -538,7 +846,7 @@ const makeLocalTargetChildBuilder = (
|
|
|
538
846
|
builder[child.key] = () => Topology.makeChoiceTarget(child.path, parent.path, values)
|
|
539
847
|
continue
|
|
540
848
|
}
|
|
541
|
-
|
|
849
|
+
const method = withFrom(
|
|
542
850
|
(value: unknown, selector?: (builder: unknown) => unknown) => {
|
|
543
851
|
if (child.type === "atomic" || child.type === "final") {
|
|
544
852
|
return makeTargetWithValues(child.path, value, values)
|
|
@@ -572,6 +880,9 @@ const makeLocalTargetChildBuilder = (
|
|
|
572
880
|
child.type === "atomic" || child.type === "final" ? "leaf" : "nested",
|
|
573
881
|
child.schema !== undefined
|
|
574
882
|
)
|
|
883
|
+
builder[child.key] = child.type === "atomic" || child.type === "final"
|
|
884
|
+
? method
|
|
885
|
+
: withInitial(method, child.path, child.schema !== undefined, values)
|
|
575
886
|
}
|
|
576
887
|
return builder
|
|
577
888
|
}
|
|
@@ -677,6 +988,7 @@ const makeBranchTargetNodeBuilder = (
|
|
|
677
988
|
"nested",
|
|
678
989
|
node.schema !== undefined
|
|
679
990
|
) as unknown as Record<string, unknown>
|
|
991
|
+
withInitial(builder, node.path, node.schema !== undefined, values)
|
|
680
992
|
if (node.type !== "parallel" || source === node.path || source.startsWith(`${node.path}.`)) {
|
|
681
993
|
addBranchTargetChildren(builder, states, stateNodes, node.path, values, source)
|
|
682
994
|
}
|
|
@@ -739,13 +1051,89 @@ const makeTargetBuilder = <const States extends Machine.StateSchemas>(
|
|
|
739
1051
|
}) as Machine.TargetBuilder<States, Source>
|
|
740
1052
|
}
|
|
741
1053
|
|
|
1054
|
+
const makeInitialSelector = (stateNodes: Machine.StateNodes): unknown => {
|
|
1055
|
+
const selector: Record<string, unknown> = {}
|
|
1056
|
+
for (const node of stateNodes.byPath.values()) {
|
|
1057
|
+
if (node.parent === undefined && node.type !== "history" && node.type !== "choice") {
|
|
1058
|
+
selector[node.key] = makeSelectionNode(stateNodes, node.path, "initial")
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return selector
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const getInitialSelectionBuilder = (
|
|
1065
|
+
initialBuilder: Record<string, any>,
|
|
1066
|
+
selection: Topology.TargetSelection
|
|
1067
|
+
): (...args: ReadonlyArray<any>) => unknown => {
|
|
1068
|
+
const path = selection.path
|
|
1069
|
+
if (path === undefined || path.includes(".")) {
|
|
1070
|
+
throw new Error("Machine initial target must select one top-level state")
|
|
1071
|
+
}
|
|
1072
|
+
const builder = initialBuilder[path]
|
|
1073
|
+
if (typeof builder !== "function") {
|
|
1074
|
+
throw new Error(`Machine could not construct selected initial state "${path}"`)
|
|
1075
|
+
}
|
|
1076
|
+
return builder
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const captureInitialBranch = (
|
|
1080
|
+
branch: unknown,
|
|
1081
|
+
selector: unknown,
|
|
1082
|
+
initialBuilder: Record<string, any>
|
|
1083
|
+
): CapturedBranch & { readonly builder: (...args: ReadonlyArray<any>) => unknown } => {
|
|
1084
|
+
const captured = captureDefinitionBranch(branch, selector, "<machine>", "initial")
|
|
1085
|
+
if (captured.selection.kind !== "state" && captured.selection.kind !== "initial") {
|
|
1086
|
+
throw new Error("Machine initial target must select a top-level state or its declared initial entry")
|
|
1087
|
+
}
|
|
1088
|
+
return { ...captured, builder: getInitialSelectionBuilder(initialBuilder, captured.selection) }
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const validateInitialSelection = (result: unknown, selection: Topology.TargetSelection): void => {
|
|
1092
|
+
if (
|
|
1093
|
+
typeof result !== "object" || result === null || !hasProperty(result, "path") || result.path !== selection.path
|
|
1094
|
+
) {
|
|
1095
|
+
const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path")
|
|
1096
|
+
? String(result.path)
|
|
1097
|
+
: "<invalid>"
|
|
1098
|
+
throw new Error(`Machine initial resolver selected "${selection.path}" but constructed "${resultPath}"`)
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const compileInitial = (
|
|
1103
|
+
definition: unknown,
|
|
1104
|
+
states: Machine.StateTree,
|
|
1105
|
+
stateNodes: Machine.StateNodes
|
|
1106
|
+
): {
|
|
1107
|
+
readonly initial: (input?: unknown) => unknown
|
|
1108
|
+
readonly definition: Machine.InitialDefinition
|
|
1109
|
+
} => {
|
|
1110
|
+
if (typeof definition !== "object" || definition === null) {
|
|
1111
|
+
throw new Error("Machine initial definition must be an object")
|
|
1112
|
+
}
|
|
1113
|
+
const selector = makeInitialSelector(stateNodes)
|
|
1114
|
+
const initialBuilder = makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Record<string, any>
|
|
1115
|
+
const branch = captureInitialBranch(definition, selector, initialBuilder)
|
|
1116
|
+
return {
|
|
1117
|
+
initial: (input?: unknown) => {
|
|
1118
|
+
const result = branch.resolve === undefined
|
|
1119
|
+
? branch.builder()
|
|
1120
|
+
: branch.resolve({ input, target: branch.builder }, undefined)
|
|
1121
|
+
validateInitialSelection(result, branch.selection)
|
|
1122
|
+
return result
|
|
1123
|
+
},
|
|
1124
|
+
definition: Object.freeze({
|
|
1125
|
+
target: branch.selection.path!,
|
|
1126
|
+
selection: transitionTargetSelection(branch.selection) as Machine.InitialDefinition["selection"]
|
|
1127
|
+
})
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
742
1131
|
export const defineStates: DefineStates = (<const States extends Machine.StateSchemas>(
|
|
743
1132
|
states: States
|
|
744
1133
|
): Machine.DefinedStates<States> => {
|
|
745
1134
|
StateDefinition.validateStateDefinitions(states, "Machine.defineStates")
|
|
746
1135
|
return {
|
|
747
1136
|
states,
|
|
748
|
-
initial: makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Machine.InitialBuilder<States>,
|
|
749
1137
|
get:
|
|
750
1138
|
((snapshot: Machine.AtomicSnapshot<string, unknown>, path: string) =>
|
|
751
1139
|
Topology.getSnapshotByPath(snapshot, path).pipe(
|
|
@@ -788,7 +1176,7 @@ type MakeConfig<
|
|
|
788
1176
|
readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
|
|
789
1177
|
readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
|
|
790
1178
|
readonly input?: Input
|
|
791
|
-
readonly initial:
|
|
1179
|
+
readonly initial: unknown
|
|
792
1180
|
}
|
|
793
1181
|
|
|
794
1182
|
type MakeResult<
|
|
@@ -865,7 +1253,7 @@ export const make: Make = (<
|
|
|
865
1253
|
readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
|
|
866
1254
|
readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
|
|
867
1255
|
readonly input?: Input
|
|
868
|
-
readonly initial:
|
|
1256
|
+
readonly initial: unknown
|
|
869
1257
|
}
|
|
870
1258
|
): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents> => {
|
|
871
1259
|
StateDefinition.validateStateDefinitions(config.states, "Machine.make")
|
|
@@ -877,8 +1265,10 @@ export const make: Make = (<
|
|
|
877
1265
|
self.parentEvents = config.parentEvents ?? Protocol.makeEventProtocol("public", [] as const)
|
|
878
1266
|
self.input = config.input
|
|
879
1267
|
self.id = config.id
|
|
880
|
-
self.initial = config.initial
|
|
881
1268
|
self.stateNodes = Topology.compileStateNodes(config.states)
|
|
1269
|
+
const compiledInitial = compileInitial(config.initial, config.states, self.stateNodes)
|
|
1270
|
+
self.initial = compiledInitial.initial
|
|
1271
|
+
self.initialDefinition = compiledInitial.definition
|
|
882
1272
|
self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes)
|
|
883
1273
|
self.handlers = Object.create(null)
|
|
884
1274
|
self.handle = makeHandle(self)
|
|
@@ -1084,6 +1474,15 @@ export const stateNodes = <M extends Machine.Any>(
|
|
|
1084
1474
|
>
|
|
1085
1475
|
>
|
|
1086
1476
|
|
|
1477
|
+
export const initialDefinition = <M extends Machine.Any>(
|
|
1478
|
+
machine: M
|
|
1479
|
+
): Machine.InitialDefinition<
|
|
1480
|
+
Machine.RootStateIdentifier<Machine.StateIdentifier<Machine.States<M>>>
|
|
1481
|
+
> =>
|
|
1482
|
+
machine.initialDefinition as Machine.InitialDefinition<
|
|
1483
|
+
Machine.RootStateIdentifier<Machine.StateIdentifier<Machine.States<M>>>
|
|
1484
|
+
>
|
|
1485
|
+
|
|
1087
1486
|
export const transitionDefinitions = <M extends Machine.Any>(
|
|
1088
1487
|
machine: M
|
|
1089
1488
|
): ReadonlyArray<
|