@typeonce/effect-machine 0.13.0 → 0.14.1
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 +318 -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 +420 -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,339 @@ 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>,
|
|
131
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 = (
|
|
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) {
|
|
224
|
+
const localScopeNode = getTargetBuilderNode(stateNodes, localScope)
|
|
225
|
+
if (localScopeNode.schema !== undefined) {
|
|
226
|
+
local.with = makeSelectionMethod("state", localScope, "local")
|
|
227
|
+
}
|
|
228
|
+
addSelectionChildren(local, stateNodes, localScope, "local")
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
none: makeSelectionMethod("none", undefined, "local"),
|
|
232
|
+
local,
|
|
233
|
+
branch,
|
|
234
|
+
full,
|
|
235
|
+
history: makeHistorySelectionTree(stateNodes, undefined)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const captureDefinitionBranch = (
|
|
240
|
+
branch: unknown,
|
|
241
|
+
selector: unknown,
|
|
132
242
|
path: string,
|
|
133
|
-
trigger: PropertyKey
|
|
134
|
-
|
|
243
|
+
trigger: PropertyKey
|
|
244
|
+
): CapturedBranch => {
|
|
245
|
+
if (
|
|
246
|
+
typeof branch !== "object" || branch === null || !hasProperty(branch, "target") ||
|
|
247
|
+
typeof branch.target !== "function"
|
|
248
|
+
) {
|
|
249
|
+
throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" requires a target selector`)
|
|
250
|
+
}
|
|
251
|
+
const selection = branch.target(selector)
|
|
252
|
+
if (!Topology.isTargetSelection(selection)) {
|
|
253
|
+
throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must select exactly one target`)
|
|
254
|
+
}
|
|
255
|
+
return { ...(branch as DefinitionBranch), selection }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const getSelectionBuilder = (
|
|
259
|
+
target: Record<string, any>,
|
|
260
|
+
selection: Topology.TargetSelection,
|
|
261
|
+
stateNodes: Machine.StateNodes,
|
|
262
|
+
source: string
|
|
263
|
+
): unknown => {
|
|
264
|
+
if (selection.kind === "none") return target.none
|
|
265
|
+
let builder: any
|
|
266
|
+
let parts = selection.path!.split(".")
|
|
267
|
+
if (selection.kind === "history") {
|
|
268
|
+
builder = target.history
|
|
269
|
+
} else if (selection.scope === "local") {
|
|
270
|
+
builder = target.local
|
|
271
|
+
const scope = getLocalTargetScope(stateNodes, source)
|
|
272
|
+
if (scope !== undefined) {
|
|
273
|
+
if (selection.path === scope) {
|
|
274
|
+
builder = builder.with
|
|
275
|
+
parts = []
|
|
276
|
+
} else {
|
|
277
|
+
parts = selection.path!.slice(scope.length + 1).split(".")
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else if (selection.scope === "branch") {
|
|
281
|
+
builder = target.branch
|
|
282
|
+
} else {
|
|
283
|
+
builder = target.full
|
|
284
|
+
}
|
|
285
|
+
for (const part of parts) builder = builder[part]
|
|
286
|
+
if (selection.kind === "initial") builder = builder.initial
|
|
287
|
+
if (
|
|
288
|
+
typeof builder !== "function" &&
|
|
289
|
+
(typeof builder !== "object" || builder === null || typeof builder.from !== "function")
|
|
290
|
+
) {
|
|
291
|
+
throw new Error(`Machine could not construct selected transition target "${selection.path}"`)
|
|
292
|
+
}
|
|
293
|
+
return builder
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const constructSelectedTarget = (builder: any): unknown => typeof builder === "function" ? builder() : builder.from()
|
|
297
|
+
|
|
298
|
+
const validateResolvedSelection = (
|
|
299
|
+
result: unknown,
|
|
300
|
+
selection: Topology.TargetSelection,
|
|
301
|
+
stateNodes: Machine.StateNodes
|
|
135
302
|
): void => {
|
|
136
|
-
if (
|
|
303
|
+
if (selection.kind === "none") {
|
|
304
|
+
if (result !== undefined) {
|
|
305
|
+
throw new Error("Machine targetless transition resolver must return undefined")
|
|
306
|
+
}
|
|
137
307
|
return
|
|
138
308
|
}
|
|
139
|
-
if (
|
|
309
|
+
if (result === undefined) return
|
|
310
|
+
const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path") &&
|
|
311
|
+
typeof result.path === "string"
|
|
312
|
+
? result.path
|
|
313
|
+
: undefined
|
|
314
|
+
const selectedNode = selection.path === undefined ? undefined : stateNodes.byPath.get(selection.path)
|
|
315
|
+
const acceptsDescendant = (selection.scope === "local" || selection.scope === "branch") &&
|
|
316
|
+
(selectedNode?.type === "compound" || selectedNode?.type === "parallel")
|
|
317
|
+
if (
|
|
318
|
+
resultPath === undefined ||
|
|
319
|
+
(resultPath !== selection.path && !(acceptsDescendant && resultPath.startsWith(`${selection.path}.`)))
|
|
320
|
+
) {
|
|
140
321
|
throw new Error(
|
|
141
|
-
`Machine
|
|
322
|
+
`Machine transition resolver selected "${selection.path}" but constructed "${resultPath ?? "<invalid>"}"`
|
|
142
323
|
)
|
|
143
324
|
}
|
|
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
325
|
}
|
|
152
326
|
|
|
153
|
-
const
|
|
327
|
+
const runCapturedBranch = (
|
|
328
|
+
branch: CapturedBranch,
|
|
329
|
+
context: Record<string, any>,
|
|
330
|
+
enqueue: unknown,
|
|
331
|
+
stateNodes: Machine.StateNodes,
|
|
332
|
+
source: string,
|
|
333
|
+
match?: { readonly value: unknown }
|
|
334
|
+
): unknown => {
|
|
335
|
+
const selectedTarget = getSelectionBuilder(context.target, branch.selection, stateNodes, source)
|
|
336
|
+
if (branch.resolve === undefined) return constructSelectedTarget(selectedTarget)
|
|
337
|
+
const resolverContext = { ...context }
|
|
338
|
+
if (branch.selection.kind === "none") delete resolverContext.target
|
|
339
|
+
else resolverContext.target = selectedTarget
|
|
340
|
+
if (match !== undefined) resolverContext.match = match.value
|
|
341
|
+
const resolved = branch.resolve(resolverContext, enqueue)
|
|
342
|
+
validateResolvedSelection(resolved, branch.selection, stateNodes)
|
|
343
|
+
return resolved === undefined ? constructSelectedTarget(selectedTarget) : resolved
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const captureTransition = (
|
|
347
|
+
transition: unknown,
|
|
348
|
+
stateNodes: Machine.StateNodes,
|
|
349
|
+
path: string,
|
|
350
|
+
trigger: PropertyKey
|
|
351
|
+
): unknown => {
|
|
154
352
|
if (typeof transition !== "object" || transition === null) {
|
|
155
|
-
|
|
353
|
+
throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must be an object`)
|
|
156
354
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
355
|
+
const definition = transition as Record<PropertyKey, unknown>
|
|
356
|
+
const selector = makeTargetSelector(stateNodes, path)
|
|
357
|
+
const reenter = definition.reenter === true
|
|
358
|
+
if (Array.isArray(definition.cases)) {
|
|
359
|
+
const rawCases = definition.cases as ReadonlyArray<unknown>
|
|
360
|
+
if (definition.cases.length === 0 || !hasProperty(definition, "otherwise")) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Machine conditional transition for state "${path}" on "${String(trigger)}" requires cases and otherwise`
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
const cases = rawCases.map((branch) => {
|
|
366
|
+
const captured = captureDefinitionBranch(branch, selector, path, trigger)
|
|
367
|
+
if (typeof captured.title !== "string" || captured.title.length === 0 || typeof captured.when !== "function") {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Machine conditional transition case for state "${path}" on "${String(trigger)}" requires title and when`
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
return captured
|
|
373
|
+
})
|
|
374
|
+
const otherwise = captureDefinitionBranch(definition.otherwise, selector, path, trigger)
|
|
375
|
+
const evaluate = (context: Record<string, any>, enqueue: unknown) => {
|
|
376
|
+
const predicateContext = { ...context }
|
|
377
|
+
delete predicateContext.target
|
|
378
|
+
for (let branchIndex = 0; branchIndex < cases.length; branchIndex++) {
|
|
379
|
+
const branch = cases[branchIndex]!
|
|
380
|
+
const result = branch.when!(predicateContext)
|
|
381
|
+
if (!Option.isOption(result)) {
|
|
382
|
+
throw new Error(`Machine conditional transition case "${branch.title}" must return Option`)
|
|
383
|
+
}
|
|
384
|
+
if (Option.isSome(result)) {
|
|
385
|
+
return {
|
|
386
|
+
result: runCapturedBranch(branch, context, enqueue, stateNodes, path, { value: result.value }),
|
|
387
|
+
branchIndex
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
result: runCapturedBranch(otherwise, context, enqueue, stateNodes, path),
|
|
393
|
+
branchIndex: cases.length
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
reenter,
|
|
398
|
+
targets: [
|
|
399
|
+
...new Set(
|
|
400
|
+
[...cases, otherwise].flatMap((branch) => branch.selection.path === undefined ? [] : [branch.selection.path])
|
|
401
|
+
)
|
|
402
|
+
],
|
|
403
|
+
branches: [
|
|
404
|
+
...cases.map((branch) => ({
|
|
405
|
+
type: "case" as const,
|
|
406
|
+
title: branch.title!,
|
|
407
|
+
target: branch.selection.path,
|
|
408
|
+
selection: transitionTargetSelection(branch.selection)
|
|
409
|
+
})),
|
|
410
|
+
{
|
|
411
|
+
type: "otherwise" as const,
|
|
412
|
+
target: otherwise.selection.path,
|
|
413
|
+
selection: transitionTargetSelection(otherwise.selection)
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
evaluate,
|
|
417
|
+
transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const branch = captureDefinitionBranch(transition, selector, path, trigger)
|
|
421
|
+
const evaluate = (context: Record<string, any>, enqueue: unknown) => ({
|
|
422
|
+
result: runCapturedBranch(branch, context, enqueue, stateNodes, path),
|
|
423
|
+
branchIndex: 0
|
|
424
|
+
})
|
|
425
|
+
return {
|
|
426
|
+
reenter,
|
|
427
|
+
targets: branch.selection.path === undefined ? [] : [branch.selection.path],
|
|
428
|
+
branches: [{
|
|
429
|
+
type: "direct" as const,
|
|
430
|
+
target: branch.selection.path,
|
|
431
|
+
selection: transitionTargetSelection(branch.selection)
|
|
432
|
+
}],
|
|
433
|
+
evaluate,
|
|
434
|
+
transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
|
|
160
435
|
}
|
|
161
|
-
return captured
|
|
162
436
|
}
|
|
163
437
|
|
|
164
|
-
const captureEventHandlers = (
|
|
438
|
+
const captureEventHandlers = (
|
|
439
|
+
on: object,
|
|
440
|
+
stateNodes: Machine.StateNodes,
|
|
441
|
+
path: string
|
|
442
|
+
): Record<PropertyKey, unknown> => {
|
|
165
443
|
// The machine owns its dispatch table. Compiled plans may snapshot these
|
|
166
444
|
// definitions, so retaining caller-owned containers would let strategies
|
|
167
445
|
// observe different handlers after an unsafe external mutation.
|
|
168
446
|
const captured: Record<PropertyKey, unknown> = Object.create(null)
|
|
169
447
|
for (const event of Reflect.ownKeys(on)) {
|
|
170
|
-
captured[event] = captureTransition((on as Record<PropertyKey, unknown>)[event])
|
|
448
|
+
captured[event] = captureTransition((on as Record<PropertyKey, unknown>)[event], stateNodes, path, event)
|
|
449
|
+
}
|
|
450
|
+
return captured
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const captureInvokeDefinition = (
|
|
454
|
+
invoke: unknown,
|
|
455
|
+
stateNodes: Machine.StateNodes,
|
|
456
|
+
path: string
|
|
457
|
+
): unknown => {
|
|
458
|
+
if (Array.isArray(invoke)) return invoke.map((item) => captureInvokeDefinition(item, stateNodes, path))
|
|
459
|
+
if (typeof invoke !== "object" || invoke === null) return invoke
|
|
460
|
+
const captured = { ...(invoke as Record<PropertyKey, unknown>) }
|
|
461
|
+
for (const key of ["onDone", "onFailure", "onSnapshot"] as const) {
|
|
462
|
+
if (captured[key] !== undefined) {
|
|
463
|
+
captured[key] = captureTransition(captured[key], stateNodes, path, key)
|
|
464
|
+
}
|
|
171
465
|
}
|
|
172
466
|
return captured
|
|
173
467
|
}
|
|
@@ -191,15 +485,21 @@ const flattenHandlers = (
|
|
|
191
485
|
const { states: childConfig, ...stateConfig } = nodeConfig as Record<string, unknown>
|
|
192
486
|
const on = stateConfig.on
|
|
193
487
|
if (typeof on === "object" && on !== null) {
|
|
194
|
-
const capturedOn = captureEventHandlers(on)
|
|
488
|
+
const capturedOn = captureEventHandlers(on, stateNodes, path)
|
|
195
489
|
stateConfig.on = capturedOn
|
|
196
|
-
for (const event of Reflect.ownKeys(capturedOn)) {
|
|
197
|
-
validateTransitionTargets(stateNodes, path, event, capturedOn[event])
|
|
198
|
-
}
|
|
199
490
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
491
|
+
if (stateConfig.always !== undefined) {
|
|
492
|
+
stateConfig.always = captureTransition(stateConfig.always, stateNodes, path, "always")
|
|
493
|
+
}
|
|
494
|
+
if (stateConfig.onDone !== undefined) {
|
|
495
|
+
stateConfig.onDone = captureTransition(stateConfig.onDone, stateNodes, path, "done")
|
|
496
|
+
}
|
|
497
|
+
if (stateConfig.choice !== undefined) {
|
|
498
|
+
stateConfig.choice = captureTransition(stateConfig.choice, stateNodes, path, "choice")
|
|
499
|
+
}
|
|
500
|
+
if (stateConfig.invoke !== undefined) {
|
|
501
|
+
stateConfig.invoke = captureInvokeDefinition(stateConfig.invoke, stateNodes, path)
|
|
502
|
+
}
|
|
203
503
|
const node = stateNodes.byPath.get(path)
|
|
204
504
|
if (node?.type === "choice") {
|
|
205
505
|
if (
|
|
@@ -208,7 +508,7 @@ const flattenHandlers = (
|
|
|
208
508
|
!hasProperty(stateConfig.choice, "targets") || !Array.isArray(stateConfig.choice.targets) ||
|
|
209
509
|
stateConfig.choice.targets.length === 0
|
|
210
510
|
) {
|
|
211
|
-
throw new Error(`Machine choice state "${path}" requires a transition
|
|
511
|
+
throw new Error(`Machine choice state "${path}" requires a transition`)
|
|
212
512
|
}
|
|
213
513
|
}
|
|
214
514
|
handlers[path] = stateConfig as Machine.AnyStateConfig
|
|
@@ -764,13 +1064,89 @@ const makeTargetBuilder = <const States extends Machine.StateSchemas>(
|
|
|
764
1064
|
}) as Machine.TargetBuilder<States, Source>
|
|
765
1065
|
}
|
|
766
1066
|
|
|
1067
|
+
const makeInitialSelector = (stateNodes: Machine.StateNodes): unknown => {
|
|
1068
|
+
const selector: Record<string, unknown> = {}
|
|
1069
|
+
for (const node of stateNodes.byPath.values()) {
|
|
1070
|
+
if (node.parent === undefined && node.type !== "history" && node.type !== "choice") {
|
|
1071
|
+
selector[node.key] = makeSelectionNode(stateNodes, node.path, "initial")
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return selector
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const getInitialSelectionBuilder = (
|
|
1078
|
+
initialBuilder: Record<string, any>,
|
|
1079
|
+
selection: Topology.TargetSelection
|
|
1080
|
+
): (...args: ReadonlyArray<any>) => unknown => {
|
|
1081
|
+
const path = selection.path
|
|
1082
|
+
if (path === undefined || path.includes(".")) {
|
|
1083
|
+
throw new Error("Machine initial target must select one top-level state")
|
|
1084
|
+
}
|
|
1085
|
+
const builder = initialBuilder[path]
|
|
1086
|
+
if (typeof builder !== "function") {
|
|
1087
|
+
throw new Error(`Machine could not construct selected initial state "${path}"`)
|
|
1088
|
+
}
|
|
1089
|
+
return builder
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const captureInitialBranch = (
|
|
1093
|
+
branch: unknown,
|
|
1094
|
+
selector: unknown,
|
|
1095
|
+
initialBuilder: Record<string, any>
|
|
1096
|
+
): CapturedBranch & { readonly builder: (...args: ReadonlyArray<any>) => unknown } => {
|
|
1097
|
+
const captured = captureDefinitionBranch(branch, selector, "<machine>", "initial")
|
|
1098
|
+
if (captured.selection.kind !== "state" && captured.selection.kind !== "initial") {
|
|
1099
|
+
throw new Error("Machine initial target must select a top-level state or its declared initial entry")
|
|
1100
|
+
}
|
|
1101
|
+
return { ...captured, builder: getInitialSelectionBuilder(initialBuilder, captured.selection) }
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const validateInitialSelection = (result: unknown, selection: Topology.TargetSelection): void => {
|
|
1105
|
+
if (
|
|
1106
|
+
typeof result !== "object" || result === null || !hasProperty(result, "path") || result.path !== selection.path
|
|
1107
|
+
) {
|
|
1108
|
+
const resultPath = typeof result === "object" && result !== null && hasProperty(result, "path")
|
|
1109
|
+
? String(result.path)
|
|
1110
|
+
: "<invalid>"
|
|
1111
|
+
throw new Error(`Machine initial resolver selected "${selection.path}" but constructed "${resultPath}"`)
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const compileInitial = (
|
|
1116
|
+
definition: unknown,
|
|
1117
|
+
states: Machine.StateTree,
|
|
1118
|
+
stateNodes: Machine.StateNodes
|
|
1119
|
+
): {
|
|
1120
|
+
readonly initial: (input?: unknown) => unknown
|
|
1121
|
+
readonly definition: Machine.InitialDefinition
|
|
1122
|
+
} => {
|
|
1123
|
+
if (typeof definition !== "object" || definition === null) {
|
|
1124
|
+
throw new Error("Machine initial definition must be an object")
|
|
1125
|
+
}
|
|
1126
|
+
const selector = makeInitialSelector(stateNodes)
|
|
1127
|
+
const initialBuilder = makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Record<string, any>
|
|
1128
|
+
const branch = captureInitialBranch(definition, selector, initialBuilder)
|
|
1129
|
+
return {
|
|
1130
|
+
initial: (input?: unknown) => {
|
|
1131
|
+
const result = branch.resolve === undefined
|
|
1132
|
+
? branch.builder()
|
|
1133
|
+
: branch.resolve({ input, target: branch.builder }, undefined)
|
|
1134
|
+
validateInitialSelection(result, branch.selection)
|
|
1135
|
+
return result
|
|
1136
|
+
},
|
|
1137
|
+
definition: Object.freeze({
|
|
1138
|
+
target: branch.selection.path!,
|
|
1139
|
+
selection: transitionTargetSelection(branch.selection) as Machine.InitialDefinition["selection"]
|
|
1140
|
+
})
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
767
1144
|
export const defineStates: DefineStates = (<const States extends Machine.StateSchemas>(
|
|
768
1145
|
states: States
|
|
769
1146
|
): Machine.DefinedStates<States> => {
|
|
770
1147
|
StateDefinition.validateStateDefinitions(states, "Machine.defineStates")
|
|
771
1148
|
return {
|
|
772
1149
|
states,
|
|
773
|
-
initial: makeSnapshotBuilder(states, { mode: "initial", prefix: "" }) as Machine.InitialBuilder<States>,
|
|
774
1150
|
get:
|
|
775
1151
|
((snapshot: Machine.AtomicSnapshot<string, unknown>, path: string) =>
|
|
776
1152
|
Topology.getSnapshotByPath(snapshot, path).pipe(
|
|
@@ -813,7 +1189,7 @@ type MakeConfig<
|
|
|
813
1189
|
readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
|
|
814
1190
|
readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
|
|
815
1191
|
readonly input?: Input
|
|
816
|
-
readonly initial:
|
|
1192
|
+
readonly initial: unknown
|
|
817
1193
|
}
|
|
818
1194
|
|
|
819
1195
|
type MakeResult<
|
|
@@ -890,7 +1266,7 @@ export const make: Make = (<
|
|
|
890
1266
|
readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits>
|
|
891
1267
|
readonly parentEvents?: Machine.EventProtocol<"public", ParentEvents>
|
|
892
1268
|
readonly input?: Input
|
|
893
|
-
readonly initial:
|
|
1269
|
+
readonly initial: unknown
|
|
894
1270
|
}
|
|
895
1271
|
): MakeResult<States, InputEvents, Emits, Input, InitialE, InitialR, InternalEvents, ParentEvents> => {
|
|
896
1272
|
StateDefinition.validateStateDefinitions(config.states, "Machine.make")
|
|
@@ -902,8 +1278,10 @@ export const make: Make = (<
|
|
|
902
1278
|
self.parentEvents = config.parentEvents ?? Protocol.makeEventProtocol("public", [] as const)
|
|
903
1279
|
self.input = config.input
|
|
904
1280
|
self.id = config.id
|
|
905
|
-
self.initial = config.initial
|
|
906
1281
|
self.stateNodes = Topology.compileStateNodes(config.states)
|
|
1282
|
+
const compiledInitial = compileInitial(config.initial, config.states, self.stateNodes)
|
|
1283
|
+
self.initial = compiledInitial.initial
|
|
1284
|
+
self.initialDefinition = compiledInitial.definition
|
|
907
1285
|
self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes)
|
|
908
1286
|
self.handlers = Object.create(null)
|
|
909
1287
|
self.handle = makeHandle(self)
|
|
@@ -1109,6 +1487,15 @@ export const stateNodes = <M extends Machine.Any>(
|
|
|
1109
1487
|
>
|
|
1110
1488
|
>
|
|
1111
1489
|
|
|
1490
|
+
export const initialDefinition = <M extends Machine.Any>(
|
|
1491
|
+
machine: M
|
|
1492
|
+
): Machine.InitialDefinition<
|
|
1493
|
+
Machine.RootStateIdentifier<Machine.StateIdentifier<Machine.States<M>>>
|
|
1494
|
+
> =>
|
|
1495
|
+
machine.initialDefinition as Machine.InitialDefinition<
|
|
1496
|
+
Machine.RootStateIdentifier<Machine.StateIdentifier<Machine.States<M>>>
|
|
1497
|
+
>
|
|
1498
|
+
|
|
1112
1499
|
export const transitionDefinitions = <M extends Machine.Any>(
|
|
1113
1500
|
machine: M
|
|
1114
1501
|
): ReadonlyArray<
|