@typeonce/effect-machine 0.5.1 → 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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/package.json +8 -8
  3. package/src/Machine.ts +6873 -0
  4. package/src/index.ts +1 -0
  5. package/src/internal/machine/activities.ts +108 -0
  6. package/src/internal/machine/atom.ts +636 -0
  7. package/src/internal/machine/cluster.ts +394 -0
  8. package/src/internal/machine/command.ts +58 -0
  9. package/src/internal/machine/commandRuntime.ts +43 -0
  10. package/src/internal/machine/configuration.ts +1331 -0
  11. package/src/internal/machine/errors.ts +87 -0
  12. package/src/internal/machine/executionPlan.ts +996 -0
  13. package/src/internal/machine/invocation.ts +119 -0
  14. package/src/internal/machine/machine.ts +1747 -0
  15. package/src/internal/machine/planner.ts +1933 -0
  16. package/src/internal/machine/process.ts +906 -0
  17. package/src/internal/machine/protocol.ts +322 -0
  18. package/src/internal/machine/readiness.ts +10 -0
  19. package/src/internal/machine/runtime.ts +2512 -0
  20. package/src/internal/machine/serialization.ts +498 -0
  21. package/src/internal/machine/stateDefinition.ts +270 -0
  22. package/src/internal/machine/symbols.ts +2 -0
  23. package/src/internal/machine/topology.ts +479 -0
  24. package/src/internal/testing/machine/arbitrary.ts +102 -0
  25. package/src/internal/testing/machine/exploration.ts +331 -0
  26. package/src/internal/testing/machine/finiteModel.ts +1498 -0
  27. package/src/internal/testing/machine/invariant.ts +372 -0
  28. package/src/internal/testing/machine/probe.ts +79 -0
  29. package/src/internal/testing/machine/referenceModel.ts +1505 -0
  30. package/src/internal/testing/machine/runtime.ts +1710 -0
  31. package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
  32. package/src/internal/testing/machine/trace.ts +150 -0
  33. package/src/internal/testing/machine/verification.ts +1890 -0
  34. package/src/testing/MachineTest.ts +2067 -0
  35. package/src/testing/index.ts +7 -0
  36. package/src/unstable/cluster/ClusterMachine.ts +390 -0
  37. package/src/unstable/cluster/index.ts +1 -0
  38. package/src/unstable/reactivity/AtomMachine.ts +649 -0
  39. 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
+ }