@typeonce/effect-machine 0.6.0 → 0.6.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/package.json +5 -5
- package/src/Machine.ts +6873 -0
- package/src/index.ts +1 -0
- package/src/internal/machine/activities.ts +108 -0
- package/src/internal/machine/atom.ts +636 -0
- package/src/internal/machine/cluster.ts +394 -0
- package/src/internal/machine/command.ts +58 -0
- package/src/internal/machine/commandRuntime.ts +43 -0
- package/src/internal/machine/configuration.ts +1331 -0
- package/src/internal/machine/errors.ts +87 -0
- package/src/internal/machine/executionPlan.ts +996 -0
- package/src/internal/machine/invocation.ts +119 -0
- package/src/internal/machine/machine.ts +1747 -0
- package/src/internal/machine/planner.ts +1933 -0
- package/src/internal/machine/process.ts +906 -0
- package/src/internal/machine/protocol.ts +322 -0
- package/src/internal/machine/readiness.ts +10 -0
- package/src/internal/machine/runtime.ts +2512 -0
- package/src/internal/machine/serialization.ts +498 -0
- package/src/internal/machine/stateDefinition.ts +270 -0
- package/src/internal/machine/symbols.ts +2 -0
- package/src/internal/machine/topology.ts +479 -0
- package/src/internal/testing/machine/arbitrary.ts +102 -0
- package/src/internal/testing/machine/exploration.ts +331 -0
- package/src/internal/testing/machine/finiteModel.ts +1498 -0
- package/src/internal/testing/machine/invariant.ts +372 -0
- package/src/internal/testing/machine/probe.ts +79 -0
- package/src/internal/testing/machine/referenceModel.ts +1505 -0
- package/src/internal/testing/machine/runtime.ts +1710 -0
- package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
- package/src/internal/testing/machine/trace.ts +150 -0
- package/src/internal/testing/machine/verification.ts +1890 -0
- package/src/testing/MachineTest.ts +2067 -0
- package/src/testing/index.ts +7 -0
- package/src/unstable/cluster/ClusterMachine.ts +390 -0
- package/src/unstable/cluster/index.ts +1 -0
- package/src/unstable/reactivity/AtomMachine.ts +649 -0
- package/src/unstable/reactivity/index.ts +1 -0
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal compiled machine execution plans.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.4.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Machine } from "../../Machine.js"
|
|
8
|
+
import { getTargetBuilder, type RuntimeCommand } from "./command.js"
|
|
9
|
+
import {
|
|
10
|
+
type ActiveConfiguration,
|
|
11
|
+
compareDocumentOrder,
|
|
12
|
+
completeConfigurationSync,
|
|
13
|
+
getInitialEntryPaths,
|
|
14
|
+
getPathToRoot,
|
|
15
|
+
getRootPath,
|
|
16
|
+
isActiveFinalConfiguration,
|
|
17
|
+
isDescendantOf,
|
|
18
|
+
normalizeConfigurationSync,
|
|
19
|
+
normalizeTargetConfigurationSync,
|
|
20
|
+
snapshotFromConfiguration,
|
|
21
|
+
validateInitialConfiguration
|
|
22
|
+
} from "./configuration.js"
|
|
23
|
+
import { InfiniteTransitionError } from "./errors.js"
|
|
24
|
+
import {
|
|
25
|
+
broadenTransitionBoundary,
|
|
26
|
+
type EvaluatedTransition,
|
|
27
|
+
getEntryPaths,
|
|
28
|
+
getExitPaths,
|
|
29
|
+
getLeastCommonAncestor,
|
|
30
|
+
getTargetNodePath,
|
|
31
|
+
InitialEvent,
|
|
32
|
+
MaxMacrostepIterations,
|
|
33
|
+
type MicrostepTransition,
|
|
34
|
+
normalizeTransition,
|
|
35
|
+
planConfiguration,
|
|
36
|
+
removeConflictingTransitions,
|
|
37
|
+
type SelectedTransition,
|
|
38
|
+
sortEntryPaths,
|
|
39
|
+
sortEvaluatedTransitions,
|
|
40
|
+
sortExitPaths,
|
|
41
|
+
type TransitionHandler,
|
|
42
|
+
validateDeclaredTransitionTarget
|
|
43
|
+
} from "./planner.js"
|
|
44
|
+
import { decodeEmitSync, decodeEventSync, decodeInputSync, decodeStateValueSync } from "./protocol.js"
|
|
45
|
+
import { isSnapshot, isTarget, TargetSnapshotTypeId } from "./topology.js"
|
|
46
|
+
|
|
47
|
+
interface IndexedExecutionDescriptor {
|
|
48
|
+
readonly flat: boolean
|
|
49
|
+
readonly nodes: ReadonlyArray<Machine.StateNode>
|
|
50
|
+
readonly indexByPath: ReadonlyMap<string, number>
|
|
51
|
+
readonly parentIndices: ReadonlyArray<number>
|
|
52
|
+
readonly childIndices: ReadonlyArray<ReadonlyArray<number>>
|
|
53
|
+
readonly ancestorIndices: ReadonlyArray<ReadonlyArray<number>>
|
|
54
|
+
readonly rootIndices: ReadonlyArray<number>
|
|
55
|
+
readonly leafIndices: ReadonlyArray<number>
|
|
56
|
+
readonly finalIndices: ReadonlyArray<number>
|
|
57
|
+
readonly dispatchByLeaf: ReadonlyMap<
|
|
58
|
+
number,
|
|
59
|
+
ReadonlyMap<PropertyKey, {
|
|
60
|
+
readonly sourceIndex: number
|
|
61
|
+
readonly transition: MicrostepTransition<any, any, any, any>
|
|
62
|
+
}>
|
|
63
|
+
>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const indexedStateConfigKeys: ReadonlySet<PropertyKey> = new Set([
|
|
67
|
+
"initial",
|
|
68
|
+
"invoke",
|
|
69
|
+
"on",
|
|
70
|
+
"output"
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
// Fail closed so a newly introduced semantic field must explicitly opt into
|
|
74
|
+
// indexed execution instead of being accepted before the kernel supports it.
|
|
75
|
+
const supportsIndexedStateConfig = (config: Machine.AnyStateConfig | undefined): boolean => {
|
|
76
|
+
if (config === undefined) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
for (const key of Reflect.ownKeys(config)) {
|
|
80
|
+
if (!indexedStateConfigKeys.has(key)) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const compileIndexedExecutionDescriptor = (
|
|
88
|
+
machine: Machine.Any
|
|
89
|
+
): IndexedExecutionDescriptor | undefined => {
|
|
90
|
+
const nodes: Array<Machine.StateNode> = []
|
|
91
|
+
const leafPaths: Array<string> = []
|
|
92
|
+
const finalPaths: Array<string> = []
|
|
93
|
+
const transitionsByPath = new Map<
|
|
94
|
+
PropertyKey,
|
|
95
|
+
ReadonlyMap<PropertyKey, MicrostepTransition<any, any, any, any>>
|
|
96
|
+
>()
|
|
97
|
+
|
|
98
|
+
for (const node of machine.stateNodes.byPath.values() as Iterable<Machine.StateNode>) {
|
|
99
|
+
nodes.push(node)
|
|
100
|
+
if (node.type === "choice" || node.type === "history") {
|
|
101
|
+
return undefined
|
|
102
|
+
}
|
|
103
|
+
if (node.type === "atomic" || node.type === "final") {
|
|
104
|
+
leafPaths.push(node.path)
|
|
105
|
+
}
|
|
106
|
+
if (node.type === "final") {
|
|
107
|
+
finalPaths.push(node.path)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const config = machine.handlers[node.path] as Machine.AnyStateConfig | undefined
|
|
111
|
+
if (!supportsIndexedStateConfig(config)) {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
if (config?.on === undefined) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
const byEvent = new Map<PropertyKey, MicrostepTransition<any, any, any, any>>()
|
|
118
|
+
for (const tag of Reflect.ownKeys(config.on)) {
|
|
119
|
+
const transition = normalizeTransition(config.on[tag])
|
|
120
|
+
if (transition !== undefined) {
|
|
121
|
+
byEvent.set(tag, transition)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (byEvent.size > 0) {
|
|
125
|
+
transitionsByPath.set(node.path, byEvent)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
leafPaths.sort((left, right) => compareDocumentOrder(machine, left, right))
|
|
130
|
+
finalPaths.sort((left, right) => compareDocumentOrder(machine, left, right))
|
|
131
|
+
nodes.sort((left, right) => left.order - right.order)
|
|
132
|
+
const indexByPath = new Map(nodes.map((node, index) => [node.path, index]))
|
|
133
|
+
const indexOf = (path: string): number => {
|
|
134
|
+
const index = indexByPath.get(path)
|
|
135
|
+
if (index === undefined) {
|
|
136
|
+
throw new Error(`Machine expected compiled state path "${path}"`)
|
|
137
|
+
}
|
|
138
|
+
return index
|
|
139
|
+
}
|
|
140
|
+
const leafIndices = leafPaths.map(indexOf)
|
|
141
|
+
const parentIndices = nodes.map((node) => node.parent === undefined ? -1 : indexOf(node.parent))
|
|
142
|
+
const childIndices = nodes.map((node) => node.children.map(indexOf))
|
|
143
|
+
const ancestorIndices = nodes.map((node) => getPathToRoot(machine, node.path).slice(0, -1).map(indexOf))
|
|
144
|
+
const transitionsByIndex = nodes.map((node) => transitionsByPath.get(node.path))
|
|
145
|
+
const dispatchByLeaf = new Map<
|
|
146
|
+
number,
|
|
147
|
+
ReadonlyMap<PropertyKey, {
|
|
148
|
+
readonly sourceIndex: number
|
|
149
|
+
readonly transition: MicrostepTransition<any, any, any, any>
|
|
150
|
+
}>
|
|
151
|
+
>()
|
|
152
|
+
for (const leafIndex of leafIndices) {
|
|
153
|
+
const dispatch = new Map<PropertyKey, {
|
|
154
|
+
readonly sourceIndex: number
|
|
155
|
+
readonly transition: MicrostepTransition<any, any, any, any>
|
|
156
|
+
}>()
|
|
157
|
+
const candidates = [leafIndex, ...ancestorIndices[leafIndex]!.slice().reverse()]
|
|
158
|
+
for (const sourceIndex of candidates) {
|
|
159
|
+
for (const [tag, transition] of transitionsByIndex[sourceIndex] ?? []) {
|
|
160
|
+
if (!dispatch.has(tag)) dispatch.set(tag, { sourceIndex, transition })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
dispatchByLeaf.set(leafIndex, dispatch)
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
flat: nodes.every((node) => node.parent === undefined && (node.type === "atomic" || node.type === "final")),
|
|
167
|
+
nodes,
|
|
168
|
+
indexByPath,
|
|
169
|
+
parentIndices,
|
|
170
|
+
childIndices,
|
|
171
|
+
ancestorIndices,
|
|
172
|
+
rootIndices: nodes.flatMap((node, index) => node.parent === undefined ? [index] : []),
|
|
173
|
+
leafIndices,
|
|
174
|
+
finalIndices: finalPaths.map(indexOf),
|
|
175
|
+
dispatchByLeaf
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The compact execution state owned by a single compiled process drain.
|
|
181
|
+
*
|
|
182
|
+
* This representation never crosses the public snapshot boundary. The flat
|
|
183
|
+
* kernel may update `values` in place only after constructing the handler
|
|
184
|
+
* context, whose snapshot is detached from this storage. Hierarchical plans
|
|
185
|
+
* copy the table whenever simultaneous transitions need their common source
|
|
186
|
+
* state to remain stable.
|
|
187
|
+
*/
|
|
188
|
+
interface OwnedIndexedState {
|
|
189
|
+
readonly active: Uint8Array
|
|
190
|
+
readonly activeLeaves: ReadonlyArray<number>
|
|
191
|
+
readonly values: Array<unknown>
|
|
192
|
+
readonly completed: Uint8Array
|
|
193
|
+
readonly outputs: ReadonlyArray<unknown>
|
|
194
|
+
readonly completedOrder: ReadonlyArray<number>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const copyOwnedIndexedState = (state: OwnedIndexedState): OwnedIndexedState => ({
|
|
198
|
+
active: state.active.slice(),
|
|
199
|
+
activeLeaves: state.activeLeaves.slice(),
|
|
200
|
+
values: state.values.slice(),
|
|
201
|
+
completed: state.completed.slice(),
|
|
202
|
+
outputs: state.outputs.slice(),
|
|
203
|
+
completedOrder: state.completedOrder.slice()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const retainIndexedMicrostep = (
|
|
207
|
+
step: ExecutionMicrostep<OwnedIndexedState>,
|
|
208
|
+
retain: boolean
|
|
209
|
+
): ExecutionMicrostep<OwnedIndexedState> => retain ? { ...step, next: copyOwnedIndexedState(step.next) } : step
|
|
210
|
+
|
|
211
|
+
const updateOwnedIndexedValue = (
|
|
212
|
+
state: OwnedIndexedState,
|
|
213
|
+
index: number,
|
|
214
|
+
value: unknown
|
|
215
|
+
): void => {
|
|
216
|
+
state.values[index] = value
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ownedIndexedStateFromActive = (
|
|
220
|
+
descriptor: IndexedExecutionDescriptor,
|
|
221
|
+
configuration: ActiveConfiguration
|
|
222
|
+
): OwnedIndexedState => {
|
|
223
|
+
const active = new Uint8Array(descriptor.nodes.length)
|
|
224
|
+
const values: Array<unknown> = new Array(descriptor.nodes.length)
|
|
225
|
+
const completed = new Uint8Array(descriptor.nodes.length)
|
|
226
|
+
const outputs: Array<unknown> = new Array(descriptor.nodes.length)
|
|
227
|
+
const completedOrder: Array<number> = []
|
|
228
|
+
for (const path of configuration.active) {
|
|
229
|
+
const index = descriptor.indexByPath.get(path)
|
|
230
|
+
if (index === undefined) throw new Error(`Machine expected indexed active path "${path}"`)
|
|
231
|
+
active[index] = 1
|
|
232
|
+
values[index] = configuration.values.get(path)
|
|
233
|
+
}
|
|
234
|
+
for (const [path, output] of configuration.outputs) {
|
|
235
|
+
const index = descriptor.indexByPath.get(path)
|
|
236
|
+
if (index === undefined) throw new Error(`Machine expected indexed completed path "${path}"`)
|
|
237
|
+
completed[index] = 1
|
|
238
|
+
outputs[index] = output
|
|
239
|
+
completedOrder.push(index)
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
active,
|
|
243
|
+
activeLeaves: descriptor.leafIndices.filter((index) => active[index] === 1),
|
|
244
|
+
values,
|
|
245
|
+
completed,
|
|
246
|
+
outputs,
|
|
247
|
+
completedOrder
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const activeConfigurationFromIndexedState = (
|
|
252
|
+
descriptor: IndexedExecutionDescriptor,
|
|
253
|
+
configuration: OwnedIndexedState
|
|
254
|
+
): ActiveConfiguration => {
|
|
255
|
+
const active = new Set<string>()
|
|
256
|
+
const values = new Map<string, unknown>()
|
|
257
|
+
const outputs = new Map<string, unknown>()
|
|
258
|
+
for (let index = 0; index < descriptor.nodes.length; index++) {
|
|
259
|
+
if (configuration.active[index] !== 1) continue
|
|
260
|
+
const path = descriptor.nodes[index]!.path
|
|
261
|
+
active.add(path)
|
|
262
|
+
values.set(path, configuration.values[index])
|
|
263
|
+
}
|
|
264
|
+
for (const index of configuration.completedOrder) {
|
|
265
|
+
if (configuration.completed[index] === 1) {
|
|
266
|
+
outputs.set(descriptor.nodes[index]!.path, configuration.outputs[index])
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { active, values, outputs, history: new Map() }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const snapshotFromIndexedStateStatePath = (
|
|
273
|
+
descriptor: IndexedExecutionDescriptor,
|
|
274
|
+
configuration: OwnedIndexedState,
|
|
275
|
+
index: number
|
|
276
|
+
): Machine.AtomicSnapshot<string, unknown> => {
|
|
277
|
+
const node = descriptor.nodes[index]!
|
|
278
|
+
const snapshot: Record<string, unknown> = {
|
|
279
|
+
path: node.path,
|
|
280
|
+
value: configuration.values[index]
|
|
281
|
+
}
|
|
282
|
+
if (node.type === "compound") {
|
|
283
|
+
const childIndex = descriptor.childIndices[index]!.find((childIndex) => configuration.active[childIndex] === 1)
|
|
284
|
+
if (childIndex === undefined) {
|
|
285
|
+
throw new Error(`Machine expected indexed compound state "${node.path}" to have an active child`)
|
|
286
|
+
}
|
|
287
|
+
snapshot.state = snapshotFromIndexedStateStatePath(descriptor, configuration, childIndex)
|
|
288
|
+
} else if (node.type === "parallel") {
|
|
289
|
+
const states: Record<string, unknown> = {}
|
|
290
|
+
for (const childIndex of descriptor.childIndices[index]!) {
|
|
291
|
+
if (configuration.active[childIndex] !== 1) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Machine expected indexed parallel state "${node.path}" to have active region "${
|
|
294
|
+
descriptor.nodes[childIndex]!.path
|
|
295
|
+
}"`
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
states[descriptor.nodes[childIndex]!.key] = snapshotFromIndexedStateStatePath(
|
|
299
|
+
descriptor,
|
|
300
|
+
configuration,
|
|
301
|
+
childIndex
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
snapshot.states = states
|
|
305
|
+
}
|
|
306
|
+
return snapshot as unknown as Machine.AtomicSnapshot<string, unknown>
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const snapshotFromIndexedState = (
|
|
310
|
+
descriptor: IndexedExecutionDescriptor,
|
|
311
|
+
configuration: OwnedIndexedState
|
|
312
|
+
): Machine.Snapshot<any> => {
|
|
313
|
+
const rootIndex = descriptor.rootIndices.find((index) => configuration.active[index] === 1)
|
|
314
|
+
if (rootIndex === undefined) throw new Error("Machine expected an active indexed root state")
|
|
315
|
+
const snapshot = snapshotFromIndexedStateStatePath(descriptor, configuration, rootIndex) as Machine.Snapshot<any>
|
|
316
|
+
if (configuration.completedOrder.length > 0) {
|
|
317
|
+
;(snapshot as Machine.AtomicSnapshot<string, unknown> & {
|
|
318
|
+
completed: ReadonlyArray<Machine.SnapshotCompletion>
|
|
319
|
+
}).completed = configuration.completedOrder.map((index) => ({
|
|
320
|
+
path: descriptor.nodes[index]!.path,
|
|
321
|
+
output: configuration.outputs[index]
|
|
322
|
+
}))
|
|
323
|
+
}
|
|
324
|
+
return snapshot
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const makeIndexedTransitionContext = (
|
|
328
|
+
machine: Machine.Any,
|
|
329
|
+
descriptor: IndexedExecutionDescriptor,
|
|
330
|
+
configuration: OwnedIndexedState,
|
|
331
|
+
sourceIndex: number,
|
|
332
|
+
event: any
|
|
333
|
+
): any => {
|
|
334
|
+
const source = descriptor.nodes[sourceIndex]!
|
|
335
|
+
const parentIndex = descriptor.parentIndices[sourceIndex]!
|
|
336
|
+
const parents: Record<string, unknown> = {}
|
|
337
|
+
for (const ancestorIndex of descriptor.ancestorIndices[sourceIndex]!) {
|
|
338
|
+
parents[descriptor.nodes[ancestorIndex]!.path] = configuration.values[ancestorIndex]
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
state: configuration.values[sourceIndex],
|
|
342
|
+
parent: parentIndex < 0 ? undefined : configuration.values[parentIndex],
|
|
343
|
+
parents,
|
|
344
|
+
event,
|
|
345
|
+
snapshot: snapshotFromIndexedState(descriptor, configuration),
|
|
346
|
+
target: getTargetBuilder(machine, source.path)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
type IndexedSelectedTransition = SelectedTransition<any, any, any, any> & {
|
|
351
|
+
readonly sourceIndex: number
|
|
352
|
+
readonly leafIndex: number
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
type IndexedEvaluatedTransition =
|
|
356
|
+
& Omit<
|
|
357
|
+
EvaluatedTransition<any, any, any, any, any>,
|
|
358
|
+
"selection"
|
|
359
|
+
>
|
|
360
|
+
& {
|
|
361
|
+
readonly selection: IndexedSelectedTransition
|
|
362
|
+
readonly next: OwnedIndexedState
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const emptyExecutionValues: ReadonlyArray<never> = Object.freeze([])
|
|
366
|
+
|
|
367
|
+
export interface ExecutionMicrostep<State = unknown> {
|
|
368
|
+
readonly next: State
|
|
369
|
+
readonly event: unknown
|
|
370
|
+
readonly commands: ReadonlyArray<RuntimeCommand>
|
|
371
|
+
readonly raisedEvents: ReadonlyArray<unknown>
|
|
372
|
+
readonly emittedEvents: ReadonlyArray<unknown>
|
|
373
|
+
readonly exitPaths: ReadonlyArray<string>
|
|
374
|
+
readonly entryPaths: ReadonlyArray<string>
|
|
375
|
+
readonly changed: boolean
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export interface ExecutionMacrostep<State = unknown> {
|
|
379
|
+
readonly next: State
|
|
380
|
+
readonly commands: ReadonlyArray<RuntimeCommand>
|
|
381
|
+
readonly emittedEvents: ReadonlyArray<unknown>
|
|
382
|
+
readonly microsteps: ReadonlyArray<ExecutionMicrostep<State>>
|
|
383
|
+
readonly done: boolean
|
|
384
|
+
readonly output: unknown
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const collectIndexedTransition = (
|
|
388
|
+
machine: Machine.Any,
|
|
389
|
+
transition: TransitionHandler<any, any, any, any>,
|
|
390
|
+
context: any
|
|
391
|
+
) => {
|
|
392
|
+
let commands: Array<RuntimeCommand> | undefined
|
|
393
|
+
let raisedEvents: Array<any> | undefined
|
|
394
|
+
let emittedEvents: Array<unknown> | undefined
|
|
395
|
+
const state = transition(context, {
|
|
396
|
+
raise: (event: unknown) => {
|
|
397
|
+
;(raisedEvents ??= []).push(decodeEventSync(machine, event))
|
|
398
|
+
},
|
|
399
|
+
emit: (event: unknown) => {
|
|
400
|
+
;(emittedEvents ??= []).push(decodeEmitSync(machine, event))
|
|
401
|
+
},
|
|
402
|
+
sendTo: (child: unknown, event: unknown) => {
|
|
403
|
+
;(commands ??= []).push({ _tag: "SendTo", child: child as any, event })
|
|
404
|
+
},
|
|
405
|
+
stop: (child: unknown) => {
|
|
406
|
+
;(commands ??= []).push({ _tag: "Stop", child: child as any })
|
|
407
|
+
}
|
|
408
|
+
})
|
|
409
|
+
return {
|
|
410
|
+
state,
|
|
411
|
+
commands: commands ?? emptyExecutionValues,
|
|
412
|
+
raisedEvents: raisedEvents ?? emptyExecutionValues,
|
|
413
|
+
emittedEvents: emittedEvents ?? emptyExecutionValues
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const selectIndexedEventTransitions = (
|
|
418
|
+
machine: Machine.Any,
|
|
419
|
+
descriptor: IndexedExecutionDescriptor,
|
|
420
|
+
configuration: OwnedIndexedState,
|
|
421
|
+
event: any
|
|
422
|
+
): ReadonlyArray<IndexedSelectedTransition> => {
|
|
423
|
+
const selected: Array<IndexedSelectedTransition> = []
|
|
424
|
+
for (const leafIndex of configuration.activeLeaves) {
|
|
425
|
+
const dispatched = descriptor.dispatchByLeaf.get(leafIndex)!.get(event._tag)
|
|
426
|
+
if (dispatched !== undefined) {
|
|
427
|
+
const { sourceIndex, transition } = dispatched
|
|
428
|
+
if (!selected.some((selection) => selection.sourceIndex === sourceIndex)) {
|
|
429
|
+
const sourcePath = descriptor.nodes[sourceIndex]!.path
|
|
430
|
+
selected.push({
|
|
431
|
+
sourceIndex,
|
|
432
|
+
leafIndex,
|
|
433
|
+
sourcePath,
|
|
434
|
+
leafPath: descriptor.nodes[leafIndex]!.path,
|
|
435
|
+
trigger: { type: "event", event: event._tag },
|
|
436
|
+
transition,
|
|
437
|
+
context: makeIndexedTransitionContext(
|
|
438
|
+
machine,
|
|
439
|
+
descriptor,
|
|
440
|
+
configuration,
|
|
441
|
+
sourceIndex,
|
|
442
|
+
event
|
|
443
|
+
)
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return selected
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const hasSameIndexedActive = (left: OwnedIndexedState, right: OwnedIndexedState): boolean => {
|
|
452
|
+
if (left.active === right.active) return true
|
|
453
|
+
for (let index = 0; index < left.active.length; index++) {
|
|
454
|
+
if (left.active[index] !== right.active[index]) return false
|
|
455
|
+
}
|
|
456
|
+
return true
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const normalizeIndexedTargetStateSync = (
|
|
460
|
+
machine: Machine.Any,
|
|
461
|
+
descriptor: IndexedExecutionDescriptor,
|
|
462
|
+
current: OwnedIndexedState,
|
|
463
|
+
target: Machine.Target<any, any> | Machine.Snapshot<any>,
|
|
464
|
+
activeLeafIndex: number
|
|
465
|
+
): OwnedIndexedState => {
|
|
466
|
+
const targetIndex = isTarget(target) ? descriptor.indexByPath.get(String(target.path)) : undefined
|
|
467
|
+
if (
|
|
468
|
+
targetIndex === activeLeafIndex && current.active[activeLeafIndex] === 1 && isTarget(target) &&
|
|
469
|
+
target[TargetSnapshotTypeId] === undefined && target.values === undefined && current.completedOrder.length === 0
|
|
470
|
+
) {
|
|
471
|
+
const values = current.values.slice()
|
|
472
|
+
values[activeLeafIndex] = decodeStateValueSync(
|
|
473
|
+
machine,
|
|
474
|
+
descriptor.nodes[activeLeafIndex]!,
|
|
475
|
+
target.value
|
|
476
|
+
)
|
|
477
|
+
return { ...current, values }
|
|
478
|
+
}
|
|
479
|
+
return ownedIndexedStateFromActive(
|
|
480
|
+
descriptor,
|
|
481
|
+
normalizeTargetConfigurationSync(
|
|
482
|
+
machine,
|
|
483
|
+
activeConfigurationFromIndexedState(descriptor, current),
|
|
484
|
+
target
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const collectIndexedEvaluatedTransition = (
|
|
490
|
+
machine: Machine.Any,
|
|
491
|
+
descriptor: IndexedExecutionDescriptor,
|
|
492
|
+
state: OwnedIndexedState,
|
|
493
|
+
selection: IndexedSelectedTransition
|
|
494
|
+
): IndexedEvaluatedTransition => {
|
|
495
|
+
const transitionResult = collectIndexedTransition(machine, selection.transition.transition, selection.context)
|
|
496
|
+
const target = transitionResult.state
|
|
497
|
+
validateDeclaredTransitionTarget(
|
|
498
|
+
selection.sourcePath,
|
|
499
|
+
selection.trigger,
|
|
500
|
+
selection.transition.targets,
|
|
501
|
+
target
|
|
502
|
+
)
|
|
503
|
+
if (target !== undefined && !isTarget(target) && !isSnapshot(target)) {
|
|
504
|
+
throw new Error("Machine expected indexed transition target to be a snapshot or target builder result")
|
|
505
|
+
}
|
|
506
|
+
const next = target === undefined
|
|
507
|
+
? state
|
|
508
|
+
: normalizeIndexedTargetStateSync(machine, descriptor, state, target as any, selection.leafIndex)
|
|
509
|
+
const changed = selection.transition.reenter || !hasSameIndexedActive(state, next)
|
|
510
|
+
if (!changed) {
|
|
511
|
+
return {
|
|
512
|
+
selection,
|
|
513
|
+
unresolvedTarget: target as any,
|
|
514
|
+
target: target as any,
|
|
515
|
+
next,
|
|
516
|
+
commands: transitionResult.commands,
|
|
517
|
+
raisedEvents: transitionResult.raisedEvents,
|
|
518
|
+
emittedEvents: transitionResult.emittedEvents,
|
|
519
|
+
changed: false,
|
|
520
|
+
exitPaths: [],
|
|
521
|
+
entryPaths: [],
|
|
522
|
+
choiceTransitions: []
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const targetPath = target === undefined ? undefined : getTargetNodePath(target as any)
|
|
527
|
+
const naturalBoundary = targetPath === undefined
|
|
528
|
+
? descriptor.nodes[selection.sourceIndex]!.parent
|
|
529
|
+
: getLeastCommonAncestor(machine, selection.leafPath, targetPath)
|
|
530
|
+
const reentryBoundary = descriptor.nodes[selection.sourceIndex]!.parent
|
|
531
|
+
const boundary = selection.transition.reenter
|
|
532
|
+
? broadenTransitionBoundary(naturalBoundary, reentryBoundary)
|
|
533
|
+
: naturalBoundary
|
|
534
|
+
return {
|
|
535
|
+
selection,
|
|
536
|
+
unresolvedTarget: target as any,
|
|
537
|
+
target: target as any,
|
|
538
|
+
next,
|
|
539
|
+
commands: transitionResult.commands,
|
|
540
|
+
raisedEvents: transitionResult.raisedEvents,
|
|
541
|
+
emittedEvents: transitionResult.emittedEvents,
|
|
542
|
+
changed: true,
|
|
543
|
+
exitPaths: getExitPaths(machine, activeConfigurationFromIndexedState(descriptor, state), boundary),
|
|
544
|
+
entryPaths: getEntryPaths(machine, activeConfigurationFromIndexedState(descriptor, next), boundary),
|
|
545
|
+
choiceTransitions: []
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const indexedMicrostep = (
|
|
550
|
+
machine: Machine.Any,
|
|
551
|
+
descriptor: IndexedExecutionDescriptor,
|
|
552
|
+
state: OwnedIndexedState,
|
|
553
|
+
event: any,
|
|
554
|
+
selections: ReadonlyArray<IndexedSelectedTransition>
|
|
555
|
+
): ExecutionMicrostep<OwnedIndexedState> => {
|
|
556
|
+
if (selections.length === 1) {
|
|
557
|
+
const transition = collectIndexedEvaluatedTransition(machine, descriptor, state, selections[0]!)
|
|
558
|
+
return {
|
|
559
|
+
next: transition.next,
|
|
560
|
+
event,
|
|
561
|
+
commands: transition.commands,
|
|
562
|
+
raisedEvents: transition.raisedEvents,
|
|
563
|
+
emittedEvents: transition.emittedEvents,
|
|
564
|
+
exitPaths: transition.exitPaths,
|
|
565
|
+
entryPaths: transition.entryPaths,
|
|
566
|
+
changed: transition.changed
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const activeSelections = selections.filter((selection) =>
|
|
570
|
+
!selections.some((other) =>
|
|
571
|
+
other.sourceIndex !== selection.sourceIndex &&
|
|
572
|
+
isDescendantOf(other.sourcePath, selection.sourcePath)
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
const evaluated = activeSelections.map((selection) =>
|
|
576
|
+
collectIndexedEvaluatedTransition(machine, descriptor, state, selection)
|
|
577
|
+
)
|
|
578
|
+
const transitions = sortEvaluatedTransitions(
|
|
579
|
+
machine,
|
|
580
|
+
removeConflictingTransitions(machine, evaluated as any)
|
|
581
|
+
) as ReadonlyArray<IndexedEvaluatedTransition>
|
|
582
|
+
|
|
583
|
+
let next = state
|
|
584
|
+
if (transitions.length === 1) {
|
|
585
|
+
next = transitions[0]!.next
|
|
586
|
+
} else {
|
|
587
|
+
const applicationOrder = [
|
|
588
|
+
...transitions.filter((transition) => !transition.changed),
|
|
589
|
+
...transitions.filter((transition) => transition.changed)
|
|
590
|
+
]
|
|
591
|
+
for (const transition of applicationOrder) {
|
|
592
|
+
if (transition.target !== undefined) {
|
|
593
|
+
next = normalizeIndexedTargetStateSync(
|
|
594
|
+
machine,
|
|
595
|
+
descriptor,
|
|
596
|
+
next,
|
|
597
|
+
transition.target,
|
|
598
|
+
transition.selection.leafIndex
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const commands = transitions.flatMap((transition) => transition.commands)
|
|
605
|
+
const raisedEvents = transitions.flatMap((transition) => transition.raisedEvents)
|
|
606
|
+
const emittedEvents = transitions.flatMap((transition) => transition.emittedEvents)
|
|
607
|
+
const changed = transitions.some((transition) => transition.changed)
|
|
608
|
+
return {
|
|
609
|
+
next,
|
|
610
|
+
event,
|
|
611
|
+
commands,
|
|
612
|
+
raisedEvents,
|
|
613
|
+
emittedEvents,
|
|
614
|
+
exitPaths: changed ? sortExitPaths(machine, transitions.flatMap((transition) => transition.exitPaths)) : [],
|
|
615
|
+
entryPaths: changed ? sortEntryPaths(machine, transitions.flatMap((transition) => transition.entryPaths)) : [],
|
|
616
|
+
changed
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const planIndexedFlatState = (
|
|
621
|
+
machine: Machine.Any,
|
|
622
|
+
descriptor: IndexedExecutionDescriptor,
|
|
623
|
+
configuration: OwnedIndexedState,
|
|
624
|
+
decoded: { readonly _tag: PropertyKey },
|
|
625
|
+
retainMicrosteps: boolean
|
|
626
|
+
): ExecutionMacrostep<OwnedIndexedState> => {
|
|
627
|
+
let current = configuration
|
|
628
|
+
let event: any = decoded
|
|
629
|
+
let commands: Array<RuntimeCommand> | undefined
|
|
630
|
+
let raisedEvents: Array<any> | undefined
|
|
631
|
+
let emittedEvents: Array<unknown> | undefined
|
|
632
|
+
let microsteps: Array<ExecutionMicrostep<OwnedIndexedState>> | undefined
|
|
633
|
+
let raisedIndex = 0
|
|
634
|
+
let iterations = 0
|
|
635
|
+
|
|
636
|
+
while (true) {
|
|
637
|
+
iterations += 1
|
|
638
|
+
if (iterations > MaxMacrostepIterations) {
|
|
639
|
+
throw new InfiniteTransitionError({
|
|
640
|
+
machineId: machine.id,
|
|
641
|
+
state: descriptor.nodes[current.activeLeaves[0]!]!.path,
|
|
642
|
+
maxIterations: MaxMacrostepIterations
|
|
643
|
+
})
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const sourceIndex = current.activeLeaves[0]
|
|
647
|
+
if (sourceIndex === undefined) {
|
|
648
|
+
throw new Error("Machine expected an active indexed root state")
|
|
649
|
+
}
|
|
650
|
+
if (descriptor.nodes[sourceIndex]!.type === "final") {
|
|
651
|
+
const completed = completeConfigurationSync(
|
|
652
|
+
machine,
|
|
653
|
+
activeConfigurationFromIndexedState(descriptor, current),
|
|
654
|
+
event
|
|
655
|
+
).configuration
|
|
656
|
+
const root = getRootPath(machine, completed)
|
|
657
|
+
if (!completed.outputs.has(root)) {
|
|
658
|
+
throw new Error("Machine reached a terminal indexed configuration without a completed root output")
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
next: ownedIndexedStateFromActive(descriptor, completed),
|
|
662
|
+
commands: commands ?? emptyExecutionValues,
|
|
663
|
+
emittedEvents: emittedEvents ?? emptyExecutionValues,
|
|
664
|
+
microsteps: microsteps ?? emptyExecutionValues,
|
|
665
|
+
done: true,
|
|
666
|
+
output: completed.outputs.get(root)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const sourcePath = descriptor.nodes[sourceIndex]!.path
|
|
671
|
+
const transition = normalizeTransition(machine.handlers[sourcePath]?.on?.[event._tag])
|
|
672
|
+
if (transition !== undefined) {
|
|
673
|
+
const transitionResult = collectIndexedTransition(
|
|
674
|
+
machine,
|
|
675
|
+
transition.transition,
|
|
676
|
+
{
|
|
677
|
+
state: current.values[sourceIndex],
|
|
678
|
+
parent: undefined,
|
|
679
|
+
parents: {},
|
|
680
|
+
event,
|
|
681
|
+
snapshot: snapshotFromIndexedState(descriptor, current),
|
|
682
|
+
target: getTargetBuilder(machine, sourcePath)
|
|
683
|
+
}
|
|
684
|
+
)
|
|
685
|
+
const target = transitionResult.state
|
|
686
|
+
validateDeclaredTransitionTarget(
|
|
687
|
+
sourcePath,
|
|
688
|
+
{ type: "event", event: event._tag },
|
|
689
|
+
transition.targets,
|
|
690
|
+
target
|
|
691
|
+
)
|
|
692
|
+
if (target !== undefined && !isTarget(target) && !isSnapshot(target)) {
|
|
693
|
+
throw new Error("Machine expected indexed transition target to be a snapshot or target builder result")
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let next = current
|
|
697
|
+
if (target !== undefined) {
|
|
698
|
+
const targetIndex = descriptor.indexByPath.get(String(target.path))
|
|
699
|
+
const isSimpleTarget = isTarget(target)
|
|
700
|
+
? target[TargetSnapshotTypeId] === undefined && target.values === undefined
|
|
701
|
+
: !("state" in target) && !("states" in target) && !("completed" in target) && !("history" in target)
|
|
702
|
+
if (
|
|
703
|
+
targetIndex === sourceIndex && isSimpleTarget && current.completedOrder.length === 0
|
|
704
|
+
) {
|
|
705
|
+
updateOwnedIndexedValue(
|
|
706
|
+
current,
|
|
707
|
+
sourceIndex,
|
|
708
|
+
decodeStateValueSync(
|
|
709
|
+
machine,
|
|
710
|
+
descriptor.nodes[sourceIndex]!,
|
|
711
|
+
target.value
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
} else {
|
|
715
|
+
next = normalizeIndexedTargetStateSync(machine, descriptor, current, target as any, sourceIndex)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const changed = transition.reenter || !hasSameIndexedActive(current, next)
|
|
719
|
+
const nextIndex = next.activeLeaves[0]
|
|
720
|
+
if (nextIndex === undefined) {
|
|
721
|
+
throw new Error("Machine expected an active indexed transition target")
|
|
722
|
+
}
|
|
723
|
+
const step: ExecutionMicrostep<OwnedIndexedState> = {
|
|
724
|
+
next,
|
|
725
|
+
event,
|
|
726
|
+
commands: transitionResult.commands,
|
|
727
|
+
raisedEvents: transitionResult.raisedEvents,
|
|
728
|
+
emittedEvents: transitionResult.emittedEvents,
|
|
729
|
+
exitPaths: changed ? [sourcePath] : emptyExecutionValues,
|
|
730
|
+
entryPaths: changed ? [descriptor.nodes[nextIndex]!.path] : emptyExecutionValues,
|
|
731
|
+
changed
|
|
732
|
+
}
|
|
733
|
+
current = next
|
|
734
|
+
;(microsteps ??= []).push(retainIndexedMicrostep(step, retainMicrosteps))
|
|
735
|
+
if (transitionResult.commands.length > 0) {
|
|
736
|
+
;(commands ??= []).push(...transitionResult.commands)
|
|
737
|
+
}
|
|
738
|
+
if (transitionResult.raisedEvents.length > 0) {
|
|
739
|
+
;(raisedEvents ??= []).push(...transitionResult.raisedEvents)
|
|
740
|
+
}
|
|
741
|
+
if (transitionResult.emittedEvents.length > 0) {
|
|
742
|
+
;(emittedEvents ??= []).push(...transitionResult.emittedEvents)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (descriptor.nodes[current.activeLeaves[0]!]!.type === "final") {
|
|
747
|
+
continue
|
|
748
|
+
}
|
|
749
|
+
const raised = raisedEvents?.[raisedIndex]
|
|
750
|
+
if (raised === undefined) {
|
|
751
|
+
return {
|
|
752
|
+
next: current,
|
|
753
|
+
commands: commands ?? emptyExecutionValues,
|
|
754
|
+
emittedEvents: emittedEvents ?? emptyExecutionValues,
|
|
755
|
+
microsteps: microsteps ?? emptyExecutionValues,
|
|
756
|
+
done: false,
|
|
757
|
+
output: undefined
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
raisedIndex += 1
|
|
761
|
+
event = raised
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const planIndexedState = (
|
|
766
|
+
machine: Machine.Any,
|
|
767
|
+
descriptor: IndexedExecutionDescriptor,
|
|
768
|
+
configuration: OwnedIndexedState,
|
|
769
|
+
input: unknown,
|
|
770
|
+
retainMicrosteps: boolean
|
|
771
|
+
): ExecutionMacrostep<OwnedIndexedState> => {
|
|
772
|
+
const decoded = decodeEventSync(machine, input)
|
|
773
|
+
if (descriptor.flat) {
|
|
774
|
+
return planIndexedFlatState(machine, descriptor, configuration, decoded, retainMicrosteps)
|
|
775
|
+
}
|
|
776
|
+
if (descriptor.finalIndices.some((index) => configuration.active[index] === 1)) {
|
|
777
|
+
const active = activeConfigurationFromIndexedState(descriptor, configuration)
|
|
778
|
+
if (isActiveFinalConfiguration(machine, active)) {
|
|
779
|
+
const completed = completeConfigurationSync(machine, active, decoded).configuration
|
|
780
|
+
const root = getRootPath(machine, completed)
|
|
781
|
+
if (!completed.outputs.has(root)) {
|
|
782
|
+
throw new Error("Machine reached a terminal indexed configuration without a completed root output")
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
next: ownedIndexedStateFromActive(descriptor, completed),
|
|
786
|
+
commands: [],
|
|
787
|
+
emittedEvents: [],
|
|
788
|
+
microsteps: [],
|
|
789
|
+
done: true,
|
|
790
|
+
output: completed.outputs.get(root)
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const selections = selectIndexedEventTransitions(machine, descriptor, configuration, decoded)
|
|
796
|
+
if (selections.length === 0) {
|
|
797
|
+
return {
|
|
798
|
+
next: configuration,
|
|
799
|
+
commands: [],
|
|
800
|
+
emittedEvents: [],
|
|
801
|
+
microsteps: [],
|
|
802
|
+
done: false,
|
|
803
|
+
output: undefined
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const first = indexedMicrostep(machine, descriptor, configuration, decoded, selections)
|
|
808
|
+
let current = first.next
|
|
809
|
+
let currentEvent: any = decoded
|
|
810
|
+
const commands = [...first.commands]
|
|
811
|
+
const raisedEvents = [...first.raisedEvents]
|
|
812
|
+
const emittedEvents = [...first.emittedEvents]
|
|
813
|
+
const microsteps = [retainIndexedMicrostep(first, retainMicrosteps)]
|
|
814
|
+
let raisedIndex = 0
|
|
815
|
+
let iterations = 0
|
|
816
|
+
|
|
817
|
+
while (true) {
|
|
818
|
+
iterations += 1
|
|
819
|
+
if (iterations > MaxMacrostepIterations) {
|
|
820
|
+
throw new InfiniteTransitionError({
|
|
821
|
+
machineId: machine.id,
|
|
822
|
+
state: descriptor.nodes[descriptor.leafIndices.find((index) => current.active[index] === 1)!]!.path,
|
|
823
|
+
maxIterations: MaxMacrostepIterations
|
|
824
|
+
})
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (descriptor.finalIndices.some((index) => current.active[index] === 1)) {
|
|
828
|
+
const completed = completeConfigurationSync(
|
|
829
|
+
machine,
|
|
830
|
+
activeConfigurationFromIndexedState(descriptor, current),
|
|
831
|
+
currentEvent
|
|
832
|
+
).configuration
|
|
833
|
+
current = ownedIndexedStateFromActive(descriptor, completed)
|
|
834
|
+
if (isActiveFinalConfiguration(machine, completed)) {
|
|
835
|
+
const root = getRootPath(machine, completed)
|
|
836
|
+
if (!completed.outputs.has(root)) {
|
|
837
|
+
throw new Error("Machine reached a terminal indexed configuration without a completed root output")
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
next: current,
|
|
841
|
+
commands,
|
|
842
|
+
emittedEvents,
|
|
843
|
+
microsteps,
|
|
844
|
+
done: true,
|
|
845
|
+
output: completed.outputs.get(root)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const raised = raisedEvents[raisedIndex]
|
|
851
|
+
if (raised === undefined) {
|
|
852
|
+
return {
|
|
853
|
+
next: current,
|
|
854
|
+
commands,
|
|
855
|
+
emittedEvents,
|
|
856
|
+
microsteps,
|
|
857
|
+
done: false,
|
|
858
|
+
output: undefined
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
raisedIndex += 1
|
|
862
|
+
currentEvent = raised
|
|
863
|
+
const raisedSelections = selectIndexedEventTransitions(machine, descriptor, current, raised)
|
|
864
|
+
if (raisedSelections.length === 0) continue
|
|
865
|
+
const step = indexedMicrostep(machine, descriptor, current, raised, raisedSelections)
|
|
866
|
+
current = step.next
|
|
867
|
+
commands.push(...step.commands)
|
|
868
|
+
raisedEvents.push(...step.raisedEvents)
|
|
869
|
+
emittedEvents.push(...step.emittedEvents)
|
|
870
|
+
microsteps.push(retainIndexedMicrostep(step, retainMicrosteps))
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export interface CompiledExecutionPlan {
|
|
875
|
+
readonly fromConfiguration: (configuration: ActiveConfiguration) => unknown
|
|
876
|
+
readonly toConfiguration: (state: unknown) => ActiveConfiguration
|
|
877
|
+
readonly snapshot: (state: unknown) => Machine.Snapshot<any>
|
|
878
|
+
readonly plan: (
|
|
879
|
+
state: unknown,
|
|
880
|
+
event: unknown,
|
|
881
|
+
retainMicrosteps?: boolean
|
|
882
|
+
) => ExecutionMacrostep
|
|
883
|
+
readonly initial?: (
|
|
884
|
+
args: ReadonlyArray<unknown>
|
|
885
|
+
) => {
|
|
886
|
+
readonly state: Machine.Snapshot<any>
|
|
887
|
+
readonly configuration: unknown
|
|
888
|
+
readonly activeConfiguration: ActiveConfiguration
|
|
889
|
+
readonly initialEntryPaths: ReadonlyArray<string>
|
|
890
|
+
readonly done: boolean
|
|
891
|
+
readonly output: unknown
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const executionPlanCache = new WeakMap<Machine.Any, CompiledExecutionPlan>()
|
|
896
|
+
|
|
897
|
+
const makeActiveExecutionPlan = (machine: Machine.Any): CompiledExecutionPlan => ({
|
|
898
|
+
fromConfiguration: (configuration) => configuration,
|
|
899
|
+
toConfiguration: (state) => state as ActiveConfiguration,
|
|
900
|
+
snapshot: (state) => snapshotFromConfiguration(machine, state as ActiveConfiguration),
|
|
901
|
+
plan: (state, event) => planConfiguration(machine as any, state as ActiveConfiguration, event as any)
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
const makeIndexedExecutionPlan = (
|
|
905
|
+
machine: Machine.Any,
|
|
906
|
+
indexed: IndexedExecutionDescriptor
|
|
907
|
+
): CompiledExecutionPlan => ({
|
|
908
|
+
fromConfiguration: (configuration) => ownedIndexedStateFromActive(indexed, configuration),
|
|
909
|
+
toConfiguration: (state) => activeConfigurationFromIndexedState(indexed, state as OwnedIndexedState),
|
|
910
|
+
snapshot: (state) => snapshotFromIndexedState(indexed, state as OwnedIndexedState),
|
|
911
|
+
plan: (state, event, retainMicrosteps = false) =>
|
|
912
|
+
planIndexedState(machine, indexed, state as OwnedIndexedState, event, retainMicrosteps),
|
|
913
|
+
initial: (args) => {
|
|
914
|
+
const inputArgs = machine.input === undefined
|
|
915
|
+
? args
|
|
916
|
+
: args.length === 0
|
|
917
|
+
? (decodeInputSync(machine, machine.input, undefined), args)
|
|
918
|
+
: [decodeInputSync(machine, machine.input, args[0])]
|
|
919
|
+
const initial = machine.initial(...inputArgs as any)
|
|
920
|
+
const active = normalizeConfigurationSync(machine, initial as Machine.Snapshot<any>)
|
|
921
|
+
validateInitialConfiguration(machine, active)
|
|
922
|
+
const completed = completeConfigurationSync(machine, active, InitialEvent).configuration
|
|
923
|
+
const configuration = ownedIndexedStateFromActive(indexed, completed)
|
|
924
|
+
const state = snapshotFromIndexedState(indexed, configuration)
|
|
925
|
+
const done = isActiveFinalConfiguration(machine, completed)
|
|
926
|
+
if (!done) {
|
|
927
|
+
return {
|
|
928
|
+
state,
|
|
929
|
+
configuration,
|
|
930
|
+
activeConfiguration: completed,
|
|
931
|
+
initialEntryPaths: getInitialEntryPaths(machine, completed),
|
|
932
|
+
done: false,
|
|
933
|
+
output: undefined
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const root = getRootPath(machine, completed)
|
|
937
|
+
if (!completed.outputs.has(root)) {
|
|
938
|
+
throw new Error("Machine reached a terminal configuration without a completed root output")
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
state,
|
|
942
|
+
configuration,
|
|
943
|
+
activeConfiguration: completed,
|
|
944
|
+
initialEntryPaths: getInitialEntryPaths(machine, completed),
|
|
945
|
+
done: true,
|
|
946
|
+
output: completed.outputs.get(root)
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
})
|
|
950
|
+
|
|
951
|
+
export type ExecutionPlanStrategy = "generic" | "indexed-flat" | "indexed-hierarchical" | "auto"
|
|
952
|
+
|
|
953
|
+
export interface SelectedExecutionPlan {
|
|
954
|
+
readonly strategy: Exclude<ExecutionPlanStrategy, "auto">
|
|
955
|
+
readonly plan: CompiledExecutionPlan
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const selectExecutionPlan = (
|
|
959
|
+
machine: Machine.Any,
|
|
960
|
+
strategy: ExecutionPlanStrategy
|
|
961
|
+
): SelectedExecutionPlan => {
|
|
962
|
+
if (strategy === "generic") {
|
|
963
|
+
return { strategy, plan: makeActiveExecutionPlan(machine) }
|
|
964
|
+
}
|
|
965
|
+
const indexed = compileIndexedExecutionDescriptor(machine)
|
|
966
|
+
if (indexed === undefined) {
|
|
967
|
+
if (strategy === "auto") {
|
|
968
|
+
return { strategy: "generic", plan: makeActiveExecutionPlan(machine) }
|
|
969
|
+
}
|
|
970
|
+
throw new Error(`Machine cannot compile the requested ${strategy} execution plan`)
|
|
971
|
+
}
|
|
972
|
+
const selected = indexed.flat ? "indexed-flat" : "indexed-hierarchical"
|
|
973
|
+
if (strategy !== "auto" && strategy !== selected) {
|
|
974
|
+
throw new Error(`Machine compiled ${selected}, not the requested ${strategy} execution plan`)
|
|
975
|
+
}
|
|
976
|
+
return { strategy: selected, plan: makeIndexedExecutionPlan(machine, indexed) }
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** @internal Test-only uncached strategy selection. */
|
|
980
|
+
export const selectExecutionPlanForTesting = (
|
|
981
|
+
machine: Machine.Any,
|
|
982
|
+
strategy: ExecutionPlanStrategy
|
|
983
|
+
): SelectedExecutionPlan => selectExecutionPlan(machine, strategy)
|
|
984
|
+
|
|
985
|
+
export const compileExecutionPlan = (machine: Machine.Any): CompiledExecutionPlan => {
|
|
986
|
+
const cached = executionPlanCache.get(machine)
|
|
987
|
+
if (cached !== undefined) {
|
|
988
|
+
return cached
|
|
989
|
+
}
|
|
990
|
+
const indexed = compileIndexedExecutionDescriptor(machine)
|
|
991
|
+
const compiled = indexed === undefined
|
|
992
|
+
? makeActiveExecutionPlan(machine)
|
|
993
|
+
: makeIndexedExecutionPlan(machine, indexed)
|
|
994
|
+
executionPlanCache.set(machine, compiled)
|
|
995
|
+
return compiled
|
|
996
|
+
}
|