@typeonce/effect-machine 0.13.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 +74 -26
- package/dist/Machine.d.ts +379 -188
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +119 -53
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +14 -6
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +3 -1
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +306 -26
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +13 -0
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +15 -7
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +12 -0
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +17 -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 +72 -66
- 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 +12 -3
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +12 -3
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +139 -57
- package/package.json +1 -1
- package/src/Machine.ts +750 -336
- package/src/internal/machine/executionPlan.ts +22 -7
- package/src/internal/machine/machine.ts +407 -33
- package/src/internal/machine/planner.ts +44 -13
- package/src/internal/machine/topology.ts +38 -12
- package/src/internal/testing/machine/exploration.ts +10 -2
- package/src/internal/testing/machine/finiteModel.ts +106 -73
- 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 +12 -3
|
@@ -150,7 +150,7 @@ const compileIndexedExecutionDescriptor = (
|
|
|
150
150
|
}
|
|
151
151
|
const byEvent = new Map<PropertyKey, MicrostepTransition<any, any, any, any>>()
|
|
152
152
|
for (const tag of Reflect.ownKeys(config.on)) {
|
|
153
|
-
const transition = normalizeTransition(config.on[tag])
|
|
153
|
+
const transition = normalizeTransition(config.on[tag] as Parameters<typeof normalizeTransition>[0])
|
|
154
154
|
if (transition !== undefined) {
|
|
155
155
|
byEvent.set(tag, transition)
|
|
156
156
|
}
|
|
@@ -424,12 +424,13 @@ export interface ExecutionMacrostep<State = unknown> {
|
|
|
424
424
|
const collectIndexedTransition = (
|
|
425
425
|
machine: Machine.Any,
|
|
426
426
|
transition: TransitionHandler<any, any, any, any>,
|
|
427
|
-
context: any
|
|
427
|
+
context: any,
|
|
428
|
+
evaluate?: NonNullable<MicrostepTransition<any, any, any, any>["evaluate"]>
|
|
428
429
|
) => {
|
|
429
430
|
let commands: Array<RuntimeCommand> | undefined
|
|
430
431
|
let raisedEvents: Array<any> | undefined
|
|
431
432
|
let emittedEvents: Array<unknown> | undefined
|
|
432
|
-
const
|
|
433
|
+
const enqueue = {
|
|
433
434
|
raise: (event: unknown) => {
|
|
434
435
|
;(raisedEvents ??= []).push(decodeEventSync(machine, event))
|
|
435
436
|
},
|
|
@@ -442,9 +443,13 @@ const collectIndexedTransition = (
|
|
|
442
443
|
stop: (child: unknown) => {
|
|
443
444
|
;(commands ??= []).push({ _tag: "Stop", child: child as any })
|
|
444
445
|
}
|
|
445
|
-
}
|
|
446
|
+
}
|
|
447
|
+
const evaluated = evaluate === undefined
|
|
448
|
+
? { result: transition(context, enqueue), branchIndex: 0 }
|
|
449
|
+
: evaluate(context, enqueue)
|
|
446
450
|
return {
|
|
447
|
-
state: isNoTarget(result) ? undefined : result,
|
|
451
|
+
state: isNoTarget(evaluated.result) ? undefined : evaluated.result,
|
|
452
|
+
branchIndex: evaluated.branchIndex,
|
|
448
453
|
commands: commands ?? emptyExecutionValues,
|
|
449
454
|
raisedEvents: raisedEvents ?? emptyExecutionValues,
|
|
450
455
|
emittedEvents: emittedEvents ?? emptyExecutionValues
|
|
@@ -531,7 +536,12 @@ const collectIndexedEvaluatedTransition = (
|
|
|
531
536
|
state: OwnedIndexedState,
|
|
532
537
|
selection: IndexedSelectedTransition
|
|
533
538
|
): IndexedEvaluatedTransition => {
|
|
534
|
-
const transitionResult = collectIndexedTransition(
|
|
539
|
+
const transitionResult = collectIndexedTransition(
|
|
540
|
+
machine,
|
|
541
|
+
selection.transition.transition,
|
|
542
|
+
selection.context,
|
|
543
|
+
selection.transition.evaluate
|
|
544
|
+
)
|
|
535
545
|
const unresolvedTarget = transitionResult.state
|
|
536
546
|
validateDeclaredTransitionTarget(
|
|
537
547
|
selection.sourcePath,
|
|
@@ -558,6 +568,7 @@ const collectIndexedEvaluatedTransition = (
|
|
|
558
568
|
if (!changed) {
|
|
559
569
|
return {
|
|
560
570
|
selection,
|
|
571
|
+
branchIndex: transitionResult.branchIndex,
|
|
561
572
|
unresolvedTarget: unresolvedTarget as any,
|
|
562
573
|
target: target as any,
|
|
563
574
|
next,
|
|
@@ -581,6 +592,7 @@ const collectIndexedEvaluatedTransition = (
|
|
|
581
592
|
: naturalBoundary
|
|
582
593
|
return {
|
|
583
594
|
selection,
|
|
595
|
+
branchIndex: transitionResult.branchIndex,
|
|
584
596
|
unresolvedTarget: unresolvedTarget as any,
|
|
585
597
|
target: target as any,
|
|
586
598
|
next,
|
|
@@ -606,6 +618,7 @@ const indexedMicrostep = (
|
|
|
606
618
|
source: transition.selection.sourcePath,
|
|
607
619
|
trigger: transition.selection.trigger,
|
|
608
620
|
reenter: transition.selection.transition.reenter,
|
|
621
|
+
branchIndex: transition.branchIndex,
|
|
609
622
|
target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
|
|
610
623
|
resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
|
|
611
624
|
})
|
|
@@ -740,7 +753,8 @@ const planIndexedFlatState = (
|
|
|
740
753
|
event,
|
|
741
754
|
snapshot: snapshotFromIndexedState(descriptor, current),
|
|
742
755
|
target: getTargetBuilder(machine, sourcePath)
|
|
743
|
-
}
|
|
756
|
+
},
|
|
757
|
+
transition.evaluate
|
|
744
758
|
)
|
|
745
759
|
const target = transitionResult.state
|
|
746
760
|
validateDeclaredTransitionTarget(
|
|
@@ -789,6 +803,7 @@ const planIndexedFlatState = (
|
|
|
789
803
|
source: sourcePath,
|
|
790
804
|
trigger: { type: "event", event: event._tag },
|
|
791
805
|
reenter: transition.reenter,
|
|
806
|
+
branchIndex: transitionResult.branchIndex,
|
|
792
807
|
target: target === undefined ? undefined : getTargetNodePath(target as any),
|
|
793
808
|
resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any)
|
|
794
809
|
}]
|
|
@@ -49,6 +49,7 @@ export { ChildMachineLogicTypeId, InitialEventTypeId, SnapshotBuilderStateTypeId
|
|
|
49
49
|
|
|
50
50
|
const TypeId = "~effect/Machine"
|
|
51
51
|
export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke")
|
|
52
|
+
export const TransitionTypeId: unique symbol = Symbol.for("effect/Machine/Transition")
|
|
52
53
|
const ChildMachineTypeId = "~effect/Machine/ChildMachine"
|
|
53
54
|
type IsAny<A> = 0 extends 1 & A ? true : false
|
|
54
55
|
type MachineRuntimeRequirement = internalRuntime.MachineRuntime
|
|
@@ -118,6 +119,7 @@ const cloneWithHandlers = (
|
|
|
118
119
|
machine.input = self.input
|
|
119
120
|
machine.id = self.id
|
|
120
121
|
machine.initial = self.initial
|
|
122
|
+
machine.initialDefinition = self.initialDefinition
|
|
121
123
|
machine.stateNodes = self.stateNodes
|
|
122
124
|
machine.makeTargetBuilder = self.makeTargetBuilder
|
|
123
125
|
machine.handlers = handlers
|
|
@@ -127,47 +129,326 @@ const cloneWithHandlers = (
|
|
|
127
129
|
return machine
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
|
|
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 = (
|
|
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 = (
|
|
131
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,
|
|
132
236
|
path: string,
|
|
133
|
-
trigger: PropertyKey
|
|
134
|
-
|
|
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
|
|
135
289
|
): void => {
|
|
136
|
-
if (
|
|
290
|
+
if (selection.kind === "none") {
|
|
291
|
+
if (result !== undefined) {
|
|
292
|
+
throw new Error("Machine targetless transition resolver must return undefined")
|
|
293
|
+
}
|
|
137
294
|
return
|
|
138
295
|
}
|
|
139
|
-
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
|
+
) {
|
|
140
308
|
throw new Error(
|
|
141
|
-
`Machine
|
|
309
|
+
`Machine transition resolver selected "${selection.path}" but constructed "${resultPath ?? "<invalid>"}"`
|
|
142
310
|
)
|
|
143
311
|
}
|
|
144
|
-
for (const target of transition.targets) {
|
|
145
|
-
if (typeof target !== "string" || !stateNodes.byPath.has(target)) {
|
|
146
|
-
throw new Error(
|
|
147
|
-
`Machine transition for state "${path}" on "${String(trigger)}" declares unknown target "${String(target)}"`
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
312
|
}
|
|
152
313
|
|
|
153
|
-
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 => {
|
|
154
339
|
if (typeof transition !== "object" || transition === null) {
|
|
155
|
-
|
|
340
|
+
throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must be an object`)
|
|
341
|
+
}
|
|
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
|
+
}
|
|
156
406
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
160
422
|
}
|
|
161
|
-
return captured
|
|
162
423
|
}
|
|
163
424
|
|
|
164
|
-
const captureEventHandlers = (
|
|
425
|
+
const captureEventHandlers = (
|
|
426
|
+
on: object,
|
|
427
|
+
stateNodes: Machine.StateNodes,
|
|
428
|
+
path: string
|
|
429
|
+
): Record<PropertyKey, unknown> => {
|
|
165
430
|
// The machine owns its dispatch table. Compiled plans may snapshot these
|
|
166
431
|
// definitions, so retaining caller-owned containers would let strategies
|
|
167
432
|
// observe different handlers after an unsafe external mutation.
|
|
168
433
|
const captured: Record<PropertyKey, unknown> = Object.create(null)
|
|
169
434
|
for (const event of Reflect.ownKeys(on)) {
|
|
170
|
-
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
|
+
}
|
|
171
452
|
}
|
|
172
453
|
return captured
|
|
173
454
|
}
|
|
@@ -191,15 +472,21 @@ const flattenHandlers = (
|
|
|
191
472
|
const { states: childConfig, ...stateConfig } = nodeConfig as Record<string, unknown>
|
|
192
473
|
const on = stateConfig.on
|
|
193
474
|
if (typeof on === "object" && on !== null) {
|
|
194
|
-
const capturedOn = captureEventHandlers(on)
|
|
475
|
+
const capturedOn = captureEventHandlers(on, stateNodes, path)
|
|
195
476
|
stateConfig.on = capturedOn
|
|
196
|
-
for (const event of Reflect.ownKeys(capturedOn)) {
|
|
197
|
-
validateTransitionTargets(stateNodes, path, event, capturedOn[event])
|
|
198
|
-
}
|
|
199
477
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
|
203
490
|
const node = stateNodes.byPath.get(path)
|
|
204
491
|
if (node?.type === "choice") {
|
|
205
492
|
if (
|
|
@@ -208,7 +495,7 @@ const flattenHandlers = (
|
|
|
208
495
|
!hasProperty(stateConfig.choice, "targets") || !Array.isArray(stateConfig.choice.targets) ||
|
|
209
496
|
stateConfig.choice.targets.length === 0
|
|
210
497
|
) {
|
|
211
|
-
throw new Error(`Machine choice state "${path}" requires a transition
|
|
498
|
+
throw new Error(`Machine choice state "${path}" requires a transition`)
|
|
212
499
|
}
|
|
213
500
|
}
|
|
214
501
|
handlers[path] = stateConfig as Machine.AnyStateConfig
|
|
@@ -764,13 +1051,89 @@ const makeTargetBuilder = <const States extends Machine.StateSchemas>(
|
|
|
764
1051
|
}) as Machine.TargetBuilder<States, Source>
|
|
765
1052
|
}
|
|
766
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
|
+
|
|
767
1131
|
export const defineStates: DefineStates = (<const States extends Machine.StateSchemas>(
|
|
768
1132
|
states: States
|
|
769
1133
|
): Machine.DefinedStates<States> => {
|
|
770
1134
|
StateDefinition.validateStateDefinitions(states, "Machine.defineStates")
|
|
771
1135
|
return {
|
|
772
1136
|
states,
|
|
773
|
-
initial: makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Machine.InitialBuilder<States>,
|
|
774
1137
|
get:
|
|
775
1138
|
((snapshot: Machine.AtomicSnapshot<string, unknown>, path: string) =>
|
|
776
1139
|
Topology.getSnapshotByPath(snapshot, path).pipe(
|
|
@@ -813,7 +1176,7 @@ type MakeConfig<
|
|
|
813
1176
|
readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
|
|
814
1177
|
readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
|
|
815
1178
|
readonly input?: Input
|
|
816
|
-
readonly initial:
|
|
1179
|
+
readonly initial: unknown
|
|
817
1180
|
}
|
|
818
1181
|
|
|
819
1182
|
type MakeResult<
|
|
@@ -890,7 +1253,7 @@ export const make: Make = (<
|
|
|
890
1253
|
readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
|
|
891
1254
|
readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
|
|
892
1255
|
readonly input?: Input
|
|
893
|
-
readonly initial:
|
|
1256
|
+
readonly initial: unknown
|
|
894
1257
|
}
|
|
895
1258
|
): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents> => {
|
|
896
1259
|
StateDefinition.validateStateDefinitions(config.states, "Machine.make")
|
|
@@ -902,8 +1265,10 @@ export const make: Make = (<
|
|
|
902
1265
|
self.parentEvents = config.parentEvents ?? Protocol.makeEventProtocol("public", [] as const)
|
|
903
1266
|
self.input = config.input
|
|
904
1267
|
self.id = config.id
|
|
905
|
-
self.initial = config.initial
|
|
906
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
|
|
907
1272
|
self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes)
|
|
908
1273
|
self.handlers = Object.create(null)
|
|
909
1274
|
self.handle = makeHandle(self)
|
|
@@ -1109,6 +1474,15 @@ export const stateNodes = <M extends Machine.Any>(
|
|
|
1109
1474
|
>
|
|
1110
1475
|
>
|
|
1111
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
|
+
|
|
1112
1486
|
export const transitionDefinitions = <M extends Machine.Any>(
|
|
1113
1487
|
machine: M
|
|
1114
1488
|
): ReadonlyArray<
|