@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,1933 @@
1
+ /**
2
+ * Internal machine planning helpers.
3
+ *
4
+ * @since 0.4.0
5
+ */
6
+
7
+ import * as Cause from "effect/Cause"
8
+ import * as Effect from "effect/Effect"
9
+ import type * as Schema from "effect/Schema"
10
+ import type { Enqueue, InitialEvent as MachineInitialEvent, Machine } from "../../Machine.js"
11
+ import { getTargetBuilder, makeCollector, type RuntimeCommand } from "./command.js"
12
+ import {
13
+ type ActiveConfiguration,
14
+ captureHistory,
15
+ compareDocumentOrder,
16
+ completeConfigurationEffect,
17
+ completeConfigurationSync,
18
+ configurationFromHistoryRecord,
19
+ getActiveLeafPathFrom,
20
+ getActiveLeafPaths,
21
+ getActiveValue,
22
+ getHistoryRecord,
23
+ getInitialEntryPaths,
24
+ getLeafPath,
25
+ getParentValue,
26
+ getParentValues,
27
+ getPathToRoot,
28
+ getRootPath,
29
+ isActiveFinalConfiguration,
30
+ isDescendantOf,
31
+ isPathInSubtree,
32
+ normalizeConfiguration,
33
+ normalizeConfigurationEffect,
34
+ normalizeConfigurationSync,
35
+ normalizeTargetConfigurationSync,
36
+ pathDepth,
37
+ snapshotFromConfiguration,
38
+ snapshotFromConfigurationAtPath,
39
+ validateInitialConfiguration
40
+ } from "./configuration.js"
41
+ import { InfiniteTransitionError, MachineSchemaDecodeError, StartupError } from "./errors.js"
42
+ import { decodeEventSync, decodeInputSync, decodeStateValueSync } from "./protocol.js"
43
+ import { InitialEventTypeId } from "./symbols.js"
44
+ import {
45
+ getNode,
46
+ isChoiceTarget,
47
+ isHistoryTarget,
48
+ isSnapshot,
49
+ isTarget,
50
+ makeChoiceTarget,
51
+ makeTarget,
52
+ TargetSnapshotTypeId
53
+ } from "./topology.js"
54
+
55
+ export type MicrostepPlan<State, Event, E, R> = {
56
+ readonly next: State
57
+ readonly event: Event | MachineInitialEvent
58
+ readonly transitions: ReadonlyArray<{
59
+ readonly source: string
60
+ readonly trigger: Machine.TransitionTrigger
61
+ readonly reenter: boolean
62
+ readonly target: string | undefined
63
+ readonly resolvedTarget: string | undefined
64
+ }>
65
+ readonly commands: ReadonlyArray<RuntimeCommand>
66
+ readonly raisedEvents: ReadonlyArray<Event>
67
+ readonly emittedEvents: ReadonlyArray<unknown>
68
+ readonly exitPaths: ReadonlyArray<string>
69
+ readonly entryPaths: ReadonlyArray<string>
70
+ readonly changed: boolean
71
+ }
72
+
73
+ export type MacrostepPlan<State, Event, E, R, Output> =
74
+ & {
75
+ readonly next: State
76
+ readonly commands: ReadonlyArray<RuntimeCommand>
77
+ readonly microsteps: ReadonlyArray<MicrostepPlan<State, Event, E, R>>
78
+ readonly emittedEvents: ReadonlyArray<unknown>
79
+ }
80
+ & (
81
+ | {
82
+ readonly done: true
83
+ readonly output: Output
84
+ }
85
+ | {
86
+ readonly done: false
87
+ readonly output: undefined
88
+ }
89
+ )
90
+
91
+ export type TransitionHandler<States extends Machine.StateSchemas, E, R, Context> = (
92
+ context: Context,
93
+ enqueue: Enqueue<any, any>
94
+ ) => Machine.HandlerResult<States, E, R>
95
+
96
+ type EventTransition<States extends Machine.StateSchemas, E, R, Context> =
97
+ | TransitionHandler<States, E, R, Context>
98
+ | {
99
+ readonly reenter?: boolean
100
+ readonly targets?: ReadonlyArray<string>
101
+ readonly transition: TransitionHandler<States, E, R, Context>
102
+ }
103
+
104
+ export type MicrostepTransition<States extends Machine.StateSchemas, E, R, Context> = {
105
+ readonly reenter: boolean
106
+ readonly targets: ReadonlyArray<string> | undefined
107
+ readonly transition: TransitionHandler<States, E, R, Context>
108
+ }
109
+
110
+ export const normalizeTransition = <States extends Machine.StateSchemas, E, R, Context>(
111
+ transition: EventTransition<States, E, R, Context> | undefined
112
+ ): MicrostepTransition<States, E, R, Context> | undefined => {
113
+ if (transition === undefined) {
114
+ return undefined
115
+ }
116
+ return typeof transition === "function"
117
+ ? { reenter: false, targets: undefined, transition }
118
+ : {
119
+ reenter: transition.reenter === true,
120
+ targets: transition.targets,
121
+ transition: transition.transition
122
+ }
123
+ }
124
+
125
+ const collectStateAction = <Context, Event, E, R>(
126
+ machine: Machine.Any,
127
+ handler: ((context: Context, enqueue: Enqueue<any, any>) => Machine.StateActionResult<E, R>) | undefined,
128
+ context: Context
129
+ ) => {
130
+ const collected = makeCollector<Event>(machine)
131
+ if (handler === undefined) {
132
+ return {
133
+ commands: collected.commands,
134
+ raisedEvents: collected.raisedEvents,
135
+ emittedEvents: collected.emittedEvents
136
+ }
137
+ }
138
+ handler(context, collected.enqueue)
139
+ return {
140
+ commands: collected.commands,
141
+ raisedEvents: collected.raisedEvents,
142
+ emittedEvents: collected.emittedEvents
143
+ }
144
+ }
145
+
146
+ const collectTransition = <
147
+ const States extends Machine.StateSchemas,
148
+ Event,
149
+ E,
150
+ R,
151
+ Context
152
+ >(
153
+ machine: Machine.Any,
154
+ transition: TransitionHandler<States, E, R, Context>,
155
+ context: Context
156
+ ) => {
157
+ const collected = makeCollector<Event>(machine)
158
+ const state = transition(context, collected.enqueue)
159
+ return {
160
+ state,
161
+ commands: collected.commands,
162
+ raisedEvents: collected.raisedEvents,
163
+ emittedEvents: collected.emittedEvents
164
+ }
165
+ }
166
+
167
+ const collectStateInitializer = (
168
+ machine: Machine.Any,
169
+ handler: (context: any, enqueue: Enqueue<any, any>) => unknown,
170
+ context: any
171
+ ) => {
172
+ const collected = makeCollector<unknown>(machine)
173
+ return {
174
+ value: handler(context, collected.enqueue),
175
+ commands: collected.commands,
176
+ raisedEvents: collected.raisedEvents,
177
+ emittedEvents: collected.emittedEvents
178
+ }
179
+ }
180
+
181
+ /** Completes the intentionally partial configuration held by shallow history.
182
+ * Only a compound node with no remembered child invokes an initializer; deep
183
+ * history never reaches this path. */
184
+ const completeHistoryConfiguration = (
185
+ machine: Machine.Any,
186
+ configuration: ActiveConfiguration,
187
+ event: unknown
188
+ ) => {
189
+ const active = new Set(configuration.active)
190
+ const values = new Map(configuration.values)
191
+ const commands: Array<RuntimeCommand> = []
192
+ const raisedEvents: Array<unknown> = []
193
+ const emittedEvents: Array<unknown> = []
194
+
195
+ let changed = true
196
+ while (changed) {
197
+ changed = false
198
+ for (const path of Array.from(active).sort((left, right) => compareDocumentOrder(machine, left, right))) {
199
+ const node = getNode(machine, path)
200
+ if (node.type === "compound" && !node.children.some((child) => active.has(child))) {
201
+ if (node.initial === undefined) {
202
+ throw new Error(`Machine shallow history expected compound state "${path}" to have an initial child`)
203
+ }
204
+ const initializer = machine.handlers[path]?.initial
205
+ if (initializer === undefined) {
206
+ throw new Error(`Machine shallow history requires an initial value implementation for state "${path}"`)
207
+ }
208
+ const current = {
209
+ active,
210
+ values,
211
+ outputs: configuration.outputs,
212
+ history: configuration.history
213
+ } as ActiveConfiguration
214
+ const initialized = collectStateInitializer(machine, initializer, {
215
+ state: getActiveValue(current, path),
216
+ parent: getParentValue(machine, current, path),
217
+ parents: getParentValues(machine, current, path),
218
+ event
219
+ })
220
+ const child = getNode(machine, node.initial)
221
+ active.add(child.path)
222
+ values.set(child.path, decodeStateValueSync(machine, child, initialized.value))
223
+ commands.push(...initialized.commands)
224
+ raisedEvents.push(...initialized.raisedEvents)
225
+ emittedEvents.push(...initialized.emittedEvents)
226
+ changed = true
227
+ }
228
+ if (node.type === "parallel") {
229
+ const missing = node.children.filter((childPath) => !active.has(childPath))
230
+ if (missing.length > 0) {
231
+ const initializer = machine.handlers[path]?.initial
232
+ if (initializer === undefined) {
233
+ throw new Error(`Machine shallow history requires an initial value implementation for state "${path}"`)
234
+ }
235
+ const current = {
236
+ active,
237
+ values,
238
+ outputs: configuration.outputs,
239
+ history: configuration.history
240
+ } as ActiveConfiguration
241
+ const initialized = collectStateInitializer(machine, initializer, {
242
+ state: getActiveValue(current, path),
243
+ parent: getParentValue(machine, current, path),
244
+ parents: getParentValues(machine, current, path),
245
+ event
246
+ })
247
+ if (typeof initialized.value !== "object" || initialized.value === null) {
248
+ throw new Error(`Machine parallel state initializer for "${path}" must return its region values`)
249
+ }
250
+ for (const childPath of missing) {
251
+ const child = getNode(machine, childPath)
252
+ if (!Object.prototype.hasOwnProperty.call(initialized.value, child.key)) {
253
+ throw new Error(`Machine parallel state initializer for "${path}" must return region "${child.key}"`)
254
+ }
255
+ active.add(child.path)
256
+ values.set(
257
+ child.path,
258
+ decodeStateValueSync(machine, child, (initialized.value as Record<string, unknown>)[child.key])
259
+ )
260
+ }
261
+ commands.push(...initialized.commands)
262
+ raisedEvents.push(...initialized.raisedEvents)
263
+ emittedEvents.push(...initialized.emittedEvents)
264
+ changed = true
265
+ }
266
+ }
267
+ }
268
+ }
269
+ return {
270
+ configuration: {
271
+ active,
272
+ values,
273
+ outputs: new Map<string, unknown>(),
274
+ history: configuration.history
275
+ } as ActiveConfiguration,
276
+ commands,
277
+ raisedEvents,
278
+ emittedEvents
279
+ }
280
+ }
281
+
282
+ const resolveHistoryTarget = (
283
+ machine: Machine.Any,
284
+ configuration: ActiveConfiguration,
285
+ target: { readonly path: string; readonly parent: string },
286
+ event: unknown
287
+ ) => {
288
+ const node = getNode(machine, target.path)
289
+ if (node.type !== "history" || node.parent !== target.parent) {
290
+ throw new Error(`Machine expected history target "${target.path}" to resolve to its declared parent`)
291
+ }
292
+ const record = getHistoryRecord(configuration, target.path)
293
+ if (record !== undefined) {
294
+ const restored = configurationFromHistoryRecord(machine, configuration, record)
295
+ // Deep records are already complete below the history parent. This pass is
296
+ // still required for parallel ancestors outside that parent whose other
297
+ // regions must be initialized when the ancestor is re-entered.
298
+ const completed = completeHistoryConfiguration(machine, restored, event)
299
+ const snapshot = snapshotFromConfigurationAtPath(machine, completed.configuration, target.parent)
300
+ const values = Object.fromEntries(completed.configuration.values)
301
+ return {
302
+ target: makeTarget(target.parent as any, snapshot.value as any, {
303
+ snapshot: snapshot as any,
304
+ values: values as any
305
+ }),
306
+ commands: completed.commands,
307
+ raisedEvents: completed.raisedEvents,
308
+ emittedEvents: completed.emittedEvents,
309
+ transitions: []
310
+ }
311
+ }
312
+
313
+ const key = node.key
314
+ const fallback = machine.handlers[target.parent]?.history?.[key]?.default
315
+ if (fallback === undefined) {
316
+ throw new Error(`Machine history state "${target.path}" requires a default implementation`)
317
+ }
318
+ const collected = collectTransition(machine, fallback, {
319
+ event,
320
+ target: getTargetBuilder(machine, target.parent).full,
321
+ parent: target.parent
322
+ })
323
+ if (collected.state === undefined || isHistoryTarget(collected.state) || !isSnapshot(collected.state)) {
324
+ throw new Error(`Machine history default for "${target.path}" must return a complete snapshot containing its owner`)
325
+ }
326
+ const fallbackChoice = choiceFromTarget(collected.state)
327
+ const choiceResolution = fallbackChoice === undefined
328
+ ? undefined
329
+ : resolveChoiceTarget(
330
+ machine,
331
+ {
332
+ active: new Set(),
333
+ values: new Map(),
334
+ outputs: new Map(),
335
+ history: configuration.history
336
+ },
337
+ collected.state,
338
+ event
339
+ )
340
+ let fallbackConfiguration = choiceResolution === undefined
341
+ ? normalizeConfigurationSync(machine, collected.state as any)
342
+ : normalizeTargetConfigurationSync(machine, {
343
+ active: new Set(),
344
+ values: new Map(),
345
+ outputs: new Map(),
346
+ history: configuration.history
347
+ }, choiceResolution.target as any)
348
+ for (const additionalTarget of choiceResolution?.additionalTargets ?? []) {
349
+ fallbackConfiguration = normalizeTargetConfigurationSync(
350
+ machine,
351
+ fallbackConfiguration,
352
+ additionalTarget as
353
+ | Machine.Snapshot<any>
354
+ | Machine.Target<any, string>
355
+ )
356
+ }
357
+ if (!fallbackConfiguration.active.has(target.parent)) {
358
+ throw new Error(
359
+ `Machine history default for "${target.path}" returned a configuration that does not contain owner state "${target.parent}"`
360
+ )
361
+ }
362
+ const snapshot = snapshotFromConfigurationAtPath(machine, fallbackConfiguration, target.parent)
363
+ const values = Object.fromEntries(fallbackConfiguration.values)
364
+ return {
365
+ target: makeTarget(target.parent as any, snapshot.value as any, {
366
+ snapshot: snapshot as any,
367
+ values: values as any
368
+ }),
369
+ commands: [...collected.commands, ...(choiceResolution?.commands ?? [])],
370
+ raisedEvents: [...collected.raisedEvents, ...(choiceResolution?.raisedEvents ?? [])],
371
+ emittedEvents: [...collected.emittedEvents, ...(choiceResolution?.emittedEvents ?? [])],
372
+ transitions: choiceResolution?.transitions ?? []
373
+ }
374
+ }
375
+
376
+ export type SelectedTransition<States extends Machine.StateSchemas, E, R, Context> = {
377
+ readonly sourcePath: string
378
+ readonly leafPath: string
379
+ readonly trigger: Machine.TransitionTrigger
380
+ readonly transition: MicrostepTransition<States, E, R, Context>
381
+ readonly context: Context
382
+ }
383
+
384
+ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R, Context> = {
385
+ readonly selection: SelectedTransition<States, E, R, Context>
386
+ readonly unresolvedTarget:
387
+ | Machine.Snapshot<States>
388
+ | Machine.Target<States, Machine.StateIdentifier<States>>
389
+ | Machine.HistoryTarget<States, Machine.HistoryIdentifier<States>>
390
+ | Machine.ChoiceTarget<States, Machine.ChoiceIdentifier<States>>
391
+ | undefined
392
+ readonly target:
393
+ | Machine.Snapshot<States>
394
+ | Machine.Target<States, Machine.StateIdentifier<States>>
395
+ | undefined
396
+ readonly commands: ReadonlyArray<RuntimeCommand>
397
+ readonly raisedEvents: ReadonlyArray<Event>
398
+ readonly emittedEvents: ReadonlyArray<unknown>
399
+ readonly changed: boolean
400
+ readonly exitPaths: ReadonlyArray<string>
401
+ readonly entryPaths: ReadonlyArray<string>
402
+ readonly choiceTransitions: ReadonlyArray<{
403
+ readonly source: string
404
+ readonly trigger: Machine.TransitionTrigger
405
+ readonly reenter: false
406
+ readonly target: string
407
+ readonly resolvedTarget: string
408
+ }>
409
+ }
410
+
411
+ const getCandidatePaths = (machine: Machine.Any, configuration: ActiveConfiguration): ReadonlyArray<string> =>
412
+ Array.from(configuration.active)
413
+ .sort((left, right) => {
414
+ const depth = pathDepth(machine, right) - pathDepth(machine, left)
415
+ return depth === 0 ? compareDocumentOrder(machine, left, right) : depth
416
+ })
417
+
418
+ const getLeafCandidatePaths = (machine: Machine.Any, leaf: string): ReadonlyArray<string> =>
419
+ [...getPathToRoot(machine, leaf)].reverse()
420
+
421
+ export const getLeastCommonAncestor = (
422
+ machine: Machine.Any,
423
+ left: string,
424
+ right: string
425
+ ): string | undefined => {
426
+ const leftPath = getPathToRoot(machine, left)
427
+ const rightPath = getPathToRoot(machine, right)
428
+ let ancestor: string | undefined = undefined
429
+ const length = Math.min(leftPath.length, rightPath.length)
430
+ for (let index = 0; index < length; index++) {
431
+ if (leftPath[index] !== rightPath[index]) {
432
+ break
433
+ }
434
+ ancestor = leftPath[index]
435
+ }
436
+ return ancestor
437
+ }
438
+
439
+ export const broadenTransitionBoundary = (
440
+ naturalBoundary: string | undefined,
441
+ reentryBoundary: string | undefined
442
+ ): string | undefined => {
443
+ if (naturalBoundary === undefined || reentryBoundary === undefined) return undefined
444
+ return naturalBoundary === reentryBoundary || isDescendantOf(naturalBoundary, reentryBoundary)
445
+ ? reentryBoundary
446
+ : naturalBoundary
447
+ }
448
+
449
+ export const getExitPaths = (
450
+ machine: Machine.Any,
451
+ configuration: ActiveConfiguration,
452
+ boundary: string | undefined
453
+ ): ReadonlyArray<string> =>
454
+ sortExitPaths(
455
+ machine,
456
+ Array.from(configuration.active)
457
+ .filter((path) => boundary === undefined || isDescendantOf(path, boundary))
458
+ )
459
+
460
+ export const getEntryPaths = (
461
+ machine: Machine.Any,
462
+ configuration: ActiveConfiguration,
463
+ boundary: string | undefined
464
+ ): ReadonlyArray<string> =>
465
+ sortEntryPaths(
466
+ machine,
467
+ Array.from(configuration.active).filter((path) => boundary === undefined || isDescendantOf(path, boundary))
468
+ )
469
+
470
+ const hasSameActivePaths = (left: ActiveConfiguration, right: ActiveConfiguration): boolean =>
471
+ left.active.size === right.active.size && Array.from(left.active).every((path) => right.active.has(path))
472
+
473
+ export const sortExitPaths = (machine: Machine.Any, paths: Iterable<string>): ReadonlyArray<string> =>
474
+ Array.from(new Set(paths))
475
+ .sort((left, right) => {
476
+ const depth = getPathToRoot(machine, right).length - getPathToRoot(machine, left).length
477
+ return depth === 0 ? getNode(machine, right).order - getNode(machine, left).order : depth
478
+ })
479
+
480
+ export const sortEntryPaths = (machine: Machine.Any, paths: Iterable<string>): ReadonlyArray<string> =>
481
+ Array.from(new Set(paths))
482
+ .sort((left, right) => {
483
+ const depth = getPathToRoot(machine, left).length - getPathToRoot(machine, right).length
484
+ return depth === 0 ? compareDocumentOrder(machine, left, right) : depth
485
+ })
486
+
487
+ const makeStateActionContext = <
488
+ const States extends Machine.StateSchemas,
489
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
490
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
491
+ StateId extends Machine.StateIdentifier<States>
492
+ >(
493
+ machine: Machine.Any,
494
+ configuration: ActiveConfiguration,
495
+ path: string,
496
+ event: Machine.LifecycleEvent<Events>
497
+ ): Machine.StateActionContext<States, Events, Emits, StateId> => ({
498
+ state: getActiveValue(configuration, path) as Machine.StateByIdentifier<States, StateId>,
499
+ parent: getParentValue(machine, configuration, path) as Machine.ParentStateValue<States, StateId>,
500
+ parents: getParentValues(machine, configuration, path) as Machine.ParentStateValues<States, StateId>,
501
+ event
502
+ })
503
+
504
+ const makeTransitionContext = <
505
+ const States extends Machine.StateSchemas,
506
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
507
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
508
+ StateId extends Machine.StateIdentifier<States>,
509
+ EventTag extends Machine.TagOf<Events[number]>
510
+ >(
511
+ machine: Machine<States, Events, any, any, any, any, any, any, any, any, Emits>,
512
+ configuration: ActiveConfiguration,
513
+ path: string,
514
+ event: Machine.EventByTag<Events, EventTag>,
515
+ snapshot: Machine.Snapshot<States>
516
+ ): Machine.HandlerContext<States, Events, Emits, StateId, EventTag, any, any> => ({
517
+ state: getActiveValue(configuration, path) as Machine.StateByIdentifier<States, StateId>,
518
+ parent: getParentValue(machine, configuration, path) as Machine.ParentStateValue<States, StateId>,
519
+ parents: getParentValues(machine, configuration, path) as Machine.ParentStateValues<States, StateId>,
520
+ event,
521
+ snapshot,
522
+ target: getTargetBuilder(machine, path)
523
+ })
524
+
525
+ const makeDoneContext = <
526
+ const States extends Machine.StateSchemas,
527
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
528
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
529
+ StateId extends Machine.StateIdentifier<States>
530
+ >(
531
+ machine: Machine.Any,
532
+ configuration: ActiveConfiguration,
533
+ path: string,
534
+ event: Machine.LifecycleEvent<Events>,
535
+ output: unknown,
536
+ snapshot: Machine.Snapshot<States>
537
+ ): Machine.DoneContext<States, Events, Emits, StateId> => ({
538
+ state: getActiveValue(configuration, path) as Machine.StateByIdentifier<States, StateId>,
539
+ parent: getParentValue(machine, configuration, path) as Machine.ParentStateValue<States, StateId>,
540
+ parents: getParentValues(machine, configuration, path) as Machine.ParentStateValues<States, StateId>,
541
+ event,
542
+ output: output as Machine.CompletionOutputByIdentifier<States, StateId>,
543
+ snapshot,
544
+ target: getTargetBuilder(machine, path)
545
+ })
546
+
547
+ const collectStateActions = <
548
+ const States extends Machine.StateSchemas,
549
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
550
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
551
+ E,
552
+ R
553
+ >(
554
+ machine: Machine.Any,
555
+ configuration: ActiveConfiguration,
556
+ paths: ReadonlyArray<string>,
557
+ event: Machine.LifecycleEvent<Events>,
558
+ key: "entry" | "exit"
559
+ ) => {
560
+ const commands: Array<RuntimeCommand> = []
561
+ const raisedEvents: Array<Machine.EventOf<Events>> = []
562
+ const emittedEvents: Array<Machine.EmitOf<Emits>> = []
563
+ for (const path of paths) {
564
+ const collected = collectStateAction<
565
+ Machine.StateActionContext<States, Events, Emits, Machine.StateIdentifier<States>>,
566
+ Machine.EventOf<Events>,
567
+ E,
568
+ R
569
+ >(
570
+ machine,
571
+ machine.handlers[path]?.[key],
572
+ makeStateActionContext<States, Events, Emits, Machine.StateIdentifier<States>>(
573
+ machine,
574
+ configuration,
575
+ path,
576
+ event
577
+ )
578
+ )
579
+ commands.push(...collected.commands)
580
+ raisedEvents.push(...collected.raisedEvents)
581
+ emittedEvents.push(...collected.emittedEvents as ReadonlyArray<Machine.EmitOf<Emits>>)
582
+ }
583
+ return { commands, emittedEvents, raisedEvents }
584
+ }
585
+
586
+ const selectAlwaysTransitions = <
587
+ const States extends Machine.StateSchemas,
588
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
589
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
590
+ E,
591
+ R
592
+ >(
593
+ machine: Machine.Any,
594
+ configuration: ActiveConfiguration,
595
+ event: Machine.LifecycleEvent<Events>
596
+ ): ReadonlyArray<
597
+ SelectedTransition<
598
+ States,
599
+ E,
600
+ R,
601
+ Machine.AlwaysContext<States, Events, Emits, Machine.StateIdentifier<States>>
602
+ >
603
+ > => {
604
+ const selected: Array<
605
+ SelectedTransition<
606
+ States,
607
+ E,
608
+ R,
609
+ Machine.AlwaysContext<States, Events, Emits, Machine.StateIdentifier<States>>
610
+ >
611
+ > = []
612
+ const selectedSources = new Set<string>()
613
+ let snapshot: Machine.Snapshot<States> | undefined
614
+ const capturedSnapshot = () => snapshot ??= snapshotFromConfiguration<States>(machine, configuration)
615
+ for (const leaf of getActiveLeafPaths(machine, configuration)) {
616
+ for (const path of getLeafCandidatePaths(machine, leaf)) {
617
+ const always = normalizeTransition(machine.handlers[path]?.always)
618
+ if (always !== undefined) {
619
+ if (!selectedSources.has(path)) {
620
+ selectedSources.add(path)
621
+ selected.push({
622
+ sourcePath: path,
623
+ leafPath: leaf,
624
+ trigger: { type: "always" },
625
+ transition: always as unknown as MicrostepTransition<
626
+ States,
627
+ E,
628
+ R,
629
+ Machine.AlwaysContext<States, Events, Emits, Machine.StateIdentifier<States>>
630
+ >,
631
+ context: {
632
+ state: getActiveValue(configuration, path) as Machine.StateByIdentifier<
633
+ States,
634
+ Machine.StateIdentifier<States>
635
+ >,
636
+ parent: getParentValue(machine, configuration, path) as Machine.ParentStateValue<
637
+ States,
638
+ Machine.StateIdentifier<States>
639
+ >,
640
+ parents: getParentValues(machine, configuration, path) as Machine.ParentStateValues<
641
+ States,
642
+ Machine.StateIdentifier<States>
643
+ >,
644
+ event,
645
+ snapshot: capturedSnapshot(),
646
+ target: getTargetBuilder(machine, path)
647
+ }
648
+ })
649
+ }
650
+ break
651
+ }
652
+ }
653
+ }
654
+ return selected
655
+ }
656
+
657
+ const selectDoneTransitions = <
658
+ const States extends Machine.StateSchemas,
659
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
660
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
661
+ E,
662
+ R
663
+ >(
664
+ machine: Machine.Any,
665
+ configuration: ActiveConfiguration,
666
+ event: Machine.LifecycleEvent<Events>,
667
+ completions: ReadonlyArray<{ readonly path: string; readonly output: unknown }>
668
+ ): ReadonlyArray<
669
+ SelectedTransition<
670
+ States,
671
+ E,
672
+ R,
673
+ Machine.DoneContext<States, Events, Emits, Machine.StateIdentifier<States>>
674
+ >
675
+ > => {
676
+ const selected: Array<
677
+ SelectedTransition<
678
+ States,
679
+ E,
680
+ R,
681
+ Machine.DoneContext<States, Events, Emits, Machine.StateIdentifier<States>>
682
+ >
683
+ > = []
684
+ const selectedSources = new Set<string>()
685
+ let snapshot: Machine.Snapshot<States> | undefined
686
+ const capturedSnapshot = () => snapshot ??= snapshotFromConfiguration<States>(machine, configuration)
687
+ for (const completion of completions) {
688
+ const onDone = normalizeTransition(machine.handlers[completion.path]?.onDone)
689
+ if (onDone !== undefined && !selectedSources.has(completion.path)) {
690
+ selectedSources.add(completion.path)
691
+ selected.push({
692
+ sourcePath: completion.path,
693
+ leafPath: getActiveLeafPathFrom(machine, configuration, completion.path),
694
+ trigger: { type: "done" },
695
+ transition: onDone as unknown as MicrostepTransition<
696
+ States,
697
+ E,
698
+ R,
699
+ Machine.DoneContext<States, Events, Emits, Machine.StateIdentifier<States>>
700
+ >,
701
+ context: makeDoneContext<States, Events, Emits, Machine.StateIdentifier<States>>(
702
+ machine,
703
+ configuration,
704
+ completion.path,
705
+ event,
706
+ completion.output,
707
+ capturedSnapshot()
708
+ )
709
+ })
710
+ }
711
+ }
712
+ return selected
713
+ }
714
+
715
+ const selectEventTransitions = <
716
+ const States extends Machine.StateSchemas,
717
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
718
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
719
+ E,
720
+ R
721
+ >(
722
+ machine: Machine.Any,
723
+ configuration: ActiveConfiguration,
724
+ event: Machine.EventByTag<Events, Machine.TagOf<Events[number]>>
725
+ ): ReadonlyArray<
726
+ SelectedTransition<
727
+ States,
728
+ E,
729
+ R,
730
+ Machine.HandlerContext<States, Events, Emits, Machine.StateIdentifier<States>, Machine.TagOf<Events[number]>, E, R>
731
+ >
732
+ > => {
733
+ const selected: Array<
734
+ SelectedTransition<
735
+ States,
736
+ E,
737
+ R,
738
+ Machine.HandlerContext<
739
+ States,
740
+ Events,
741
+ Emits,
742
+ Machine.StateIdentifier<States>,
743
+ Machine.TagOf<Events[number]>,
744
+ E,
745
+ R
746
+ >
747
+ >
748
+ > = []
749
+ const selectedSources = new Set<string>()
750
+ let snapshot: Machine.Snapshot<States> | undefined
751
+ const capturedSnapshot = () => snapshot ??= snapshotFromConfiguration<States>(machine, configuration)
752
+ for (const leaf of getActiveLeafPaths(machine, configuration)) {
753
+ for (const path of getLeafCandidatePaths(machine, leaf)) {
754
+ const transition = normalizeTransition(machine.handlers[path]?.on?.[event._tag])
755
+ if (transition !== undefined) {
756
+ if (!selectedSources.has(path)) {
757
+ selectedSources.add(path)
758
+ selected.push({
759
+ sourcePath: path,
760
+ leafPath: leaf,
761
+ trigger: { type: "event", event: event._tag },
762
+ transition: transition as unknown as MicrostepTransition<
763
+ States,
764
+ E,
765
+ R,
766
+ Machine.HandlerContext<
767
+ States,
768
+ Events,
769
+ Emits,
770
+ Machine.StateIdentifier<States>,
771
+ Machine.TagOf<Events[number]>,
772
+ E,
773
+ R
774
+ >
775
+ >,
776
+ context: makeTransitionContext<
777
+ States,
778
+ Events,
779
+ Emits,
780
+ Machine.StateIdentifier<States>,
781
+ Machine.TagOf<Events[number]>
782
+ >(machine as any, configuration, path, event, capturedSnapshot())
783
+ })
784
+ }
785
+ break
786
+ }
787
+ }
788
+ }
789
+ return selected
790
+ }
791
+
792
+ export const getTargetNodePath = <const States extends Machine.StateSchemas>(
793
+ target:
794
+ | Machine.Snapshot<States>
795
+ | Machine.Target<States, Machine.StateIdentifier<States>>
796
+ | Machine.HistoryTarget<States, Machine.HistoryIdentifier<States>>
797
+ | Machine.ChoiceTarget<States, Machine.ChoiceIdentifier<States>>
798
+ ): string => {
799
+ if (isHistoryTarget(target) || isChoiceTarget(target)) {
800
+ return String(target.path)
801
+ }
802
+ if (isTarget(target)) {
803
+ return String(target.path)
804
+ }
805
+ if (isSnapshot(target)) {
806
+ return String(target.path)
807
+ }
808
+ throw new Error("Machine expected transition target to be a snapshot or target builder result")
809
+ }
810
+
811
+ export const validateDeclaredTransitionTarget = (
812
+ sourcePath: string,
813
+ trigger: Machine.TransitionTrigger,
814
+ declaredTargets: ReadonlyArray<string> | undefined,
815
+ target: unknown
816
+ ): void => {
817
+ if (declaredTargets === undefined || target === undefined) {
818
+ return
819
+ }
820
+ const actual = typeof target === "object" && target !== null && "path" in target
821
+ ? String(target.path)
822
+ : "<unknown>"
823
+ if (!declaredTargets.some((path) => actual === path || actual.startsWith(`${path}.`))) {
824
+ const triggerLabel = trigger.type === "event" ? String(trigger.event) : trigger.type
825
+ throw new Error(
826
+ `Machine transition from "${sourcePath}" on "${triggerLabel}" returned target "${actual}" outside declared targets: ${
827
+ declaredTargets.length === 0 ? "none" : declaredTargets.map((path) => `"${path}"`).join(", ")
828
+ }`
829
+ )
830
+ }
831
+ }
832
+
833
+ const hasPathIntersection = (left: ReadonlyArray<string>, right: ReadonlyArray<string>): boolean => {
834
+ for (const path of left) {
835
+ if (right.includes(path)) {
836
+ return true
837
+ }
838
+ }
839
+ return false
840
+ }
841
+
842
+ export const sortEvaluatedTransitions = <
843
+ const States extends Machine.StateSchemas,
844
+ Event,
845
+ E,
846
+ R,
847
+ Context
848
+ >(
849
+ machine: Machine.Any,
850
+ transitions: Iterable<EvaluatedTransition<States, Event, E, R, Context>>
851
+ ): ReadonlyArray<EvaluatedTransition<States, Event, E, R, Context>> =>
852
+ Array.from(transitions)
853
+ .sort((left, right) => compareDocumentOrder(machine, left.selection.sourcePath, right.selection.sourcePath))
854
+
855
+ const removePreemptedAncestorSelections = <
856
+ const States extends Machine.StateSchemas,
857
+ E,
858
+ R,
859
+ Context
860
+ >(
861
+ selections: ReadonlyArray<SelectedTransition<States, E, R, Context>>
862
+ ): ReadonlyArray<SelectedTransition<States, E, R, Context>> =>
863
+ selections.filter((selection) =>
864
+ !selections.some((other) =>
865
+ other.sourcePath !== selection.sourcePath && isDescendantOf(other.sourcePath, selection.sourcePath)
866
+ )
867
+ )
868
+
869
+ export const removeConflictingTransitions = <
870
+ const States extends Machine.StateSchemas,
871
+ Event,
872
+ E,
873
+ R,
874
+ Context
875
+ >(
876
+ machine: Machine.Any,
877
+ transitions: ReadonlyArray<EvaluatedTransition<States, Event, E, R, Context>>
878
+ ): ReadonlyArray<EvaluatedTransition<States, Event, E, R, Context>> => {
879
+ const filtered: Array<EvaluatedTransition<States, Event, E, R, Context>> = []
880
+ for (const transition of sortEvaluatedTransitions(machine, transitions)) {
881
+ let preempted = false
882
+ const transitionsToRemove = new Set<EvaluatedTransition<States, Event, E, R, Context>>()
883
+ for (const selected of filtered) {
884
+ if (hasPathIntersection(transition.exitPaths, selected.exitPaths)) {
885
+ if (isDescendantOf(transition.selection.sourcePath, selected.selection.sourcePath)) {
886
+ transitionsToRemove.add(selected)
887
+ } else {
888
+ preempted = true
889
+ break
890
+ }
891
+ }
892
+ }
893
+ if (!preempted) {
894
+ for (const removed of transitionsToRemove) {
895
+ const index = filtered.indexOf(removed)
896
+ if (index >= 0) {
897
+ filtered.splice(index, 1)
898
+ }
899
+ }
900
+ filtered.push(transition)
901
+ }
902
+ }
903
+ return filtered
904
+ }
905
+
906
+ const choicesFromTarget = (
907
+ target: unknown,
908
+ inheritedValues: Readonly<Record<string, unknown>> = {}
909
+ ): ReadonlyArray<{
910
+ readonly target: ReturnType<typeof makeChoiceTarget>
911
+ readonly values: Readonly<Record<string, unknown>>
912
+ }> => {
913
+ const values: Record<string, unknown> = { ...inheritedValues }
914
+ const choices: Array<ReturnType<typeof makeChoiceTarget>> = []
915
+ const visit = (current: unknown): void => {
916
+ if (isChoiceTarget(current)) {
917
+ Object.assign(values, current.values ?? {})
918
+ choices.push(current)
919
+ return
920
+ }
921
+ if (typeof current !== "object" || current === null || !("path" in current) || !("value" in current)) return
922
+ values[String(current.path)] = current.value
923
+ if ("state" in current) visit(current.state)
924
+ if ("states" in current && typeof current.states === "object" && current.states !== null) {
925
+ for (const state of Object.values(current.states)) visit(state)
926
+ }
927
+ }
928
+ visit(target)
929
+ return choices.map((choice) => ({
930
+ target: makeChoiceTarget(choice.path, choice.parent, values),
931
+ values
932
+ }))
933
+ }
934
+
935
+ const choiceFromTarget = (
936
+ target: unknown,
937
+ inheritedValues: Readonly<Record<string, unknown>> = {}
938
+ ) => choicesFromTarget(target, inheritedValues)[0]
939
+
940
+ const withChoiceValues = (target: unknown, values: Readonly<Record<string, unknown>>): unknown => {
941
+ if (isChoiceTarget(target)) {
942
+ return makeChoiceTarget(target.path, target.parent, { ...values, ...(target.values ?? {}) })
943
+ }
944
+ if (!isTarget(target) || Object.keys(values).length === 0) return target
945
+ const snapshot = target[TargetSnapshotTypeId]
946
+ return makeTarget(target.path, target.value, {
947
+ ...(snapshot === undefined ? {} : { snapshot }),
948
+ values: { ...values, ...(target.values ?? {}) }
949
+ })
950
+ }
951
+
952
+ interface ResolvedChoiceTransition {
953
+ readonly source: string
954
+ readonly trigger: Machine.TransitionTrigger
955
+ readonly reenter: false
956
+ readonly target: string
957
+ readonly resolvedTarget: string
958
+ }
959
+
960
+ const resolveChoiceTarget = (
961
+ machine: Machine.Any,
962
+ configuration: ActiveConfiguration,
963
+ initialTarget: unknown,
964
+ event: unknown
965
+ ) => {
966
+ const pending: Array<unknown> = [initialTarget]
967
+ const resolvedTargets: Array<unknown> = []
968
+ const commands: Array<RuntimeCommand> = []
969
+ const raisedEvents: Array<unknown> = []
970
+ const emittedEvents: Array<unknown> = []
971
+ const transitions: Array<ResolvedChoiceTransition> = []
972
+ let iterations = 0
973
+
974
+ while (pending.length > 0) {
975
+ let current: unknown = pending.shift()
976
+ while (true) {
977
+ const extractedChoices = choicesFromTarget(current)
978
+ const extracted = extractedChoices[0]
979
+ if (extracted === undefined) break
980
+ pending.unshift(...extractedChoices.slice(1).map(({ target }) => target))
981
+ iterations += 1
982
+ if (iterations > MaxMacrostepIterations) {
983
+ throw new InfiniteTransitionError({
984
+ machineId: machine.id,
985
+ state: extracted.target.path,
986
+ maxIterations: MaxMacrostepIterations
987
+ })
988
+ }
989
+ const node = getNode(machine, extracted.target.path)
990
+ if (node.type !== "choice" || node.parent !== extracted.target.parent) {
991
+ throw new Error(`Machine expected choice target "${extracted.target.path}" to resolve to its declared parent`)
992
+ }
993
+ const choice = machine.handlers[node.path]?.choice
994
+ if (choice === undefined || typeof choice.transition !== "function") {
995
+ throw new Error(`Machine choice state "${node.path}" requires an implementation`)
996
+ }
997
+ const provisional: ActiveConfiguration = {
998
+ active: new Set([
999
+ ...configuration.active,
1000
+ ...Object.keys(extracted.values)
1001
+ ]),
1002
+ values: new Map([
1003
+ ...configuration.values,
1004
+ ...Object.entries(extracted.values)
1005
+ ]),
1006
+ outputs: configuration.outputs,
1007
+ history: configuration.history
1008
+ }
1009
+ const collected = collectTransition(machine, choice.transition, {
1010
+ parent: getParentValue(machine, provisional, node.path),
1011
+ parents: getParentValues(machine, provisional, node.path),
1012
+ event,
1013
+ target: getTargetBuilder(machine, node.path)
1014
+ })
1015
+ if (collected.state === undefined) {
1016
+ throw new Error(`Machine choice resolver for "${node.path}" must return a target`)
1017
+ }
1018
+ validateDeclaredTransitionTarget(node.path, { type: "choice" }, choice.targets, collected.state)
1019
+ const returnedPath = getTargetNodePath(collected.state as any)
1020
+ current = withChoiceValues(collected.state, extracted.values)
1021
+ const nested = choiceFromTarget(current)
1022
+ transitions.push({
1023
+ source: node.path,
1024
+ trigger: { type: "choice" },
1025
+ reenter: false,
1026
+ target: returnedPath,
1027
+ resolvedTarget: nested?.target.path ?? returnedPath
1028
+ })
1029
+ commands.push(...collected.commands)
1030
+ raisedEvents.push(...collected.raisedEvents)
1031
+ emittedEvents.push(...collected.emittedEvents)
1032
+ }
1033
+
1034
+ if (!isTarget(current) && !isSnapshot(current) && !isHistoryTarget(current)) {
1035
+ throw new Error("Machine choice resolver must return a concrete typed target")
1036
+ }
1037
+ resolvedTargets.push(current)
1038
+ }
1039
+
1040
+ return {
1041
+ target: resolvedTargets[0],
1042
+ additionalTargets: resolvedTargets.slice(1),
1043
+ commands,
1044
+ raisedEvents,
1045
+ emittedEvents,
1046
+ transitions
1047
+ }
1048
+ }
1049
+
1050
+ const collectEvaluatedTransition = <
1051
+ const States extends Machine.StateSchemas,
1052
+ Event,
1053
+ E,
1054
+ R,
1055
+ Context
1056
+ >(
1057
+ machine: Machine.Any,
1058
+ state: ActiveConfiguration,
1059
+ selection: SelectedTransition<States, E, R, Context>
1060
+ ) => {
1061
+ const stateIdentifier = selection.leafPath
1062
+ const transitionResult = collectTransition<States, Event, E, R, Context>(
1063
+ machine,
1064
+ selection.transition.transition,
1065
+ selection.context
1066
+ )
1067
+ const unresolvedTarget = transitionResult.state === undefined
1068
+ ? undefined
1069
+ : transitionResult.state as
1070
+ | Machine.Snapshot<States>
1071
+ | Machine.Target<States, Machine.StateIdentifier<States>>
1072
+ | Machine.HistoryTarget<States, Machine.HistoryIdentifier<States>>
1073
+ | Machine.ChoiceTarget<States, Machine.ChoiceIdentifier<States>>
1074
+ validateDeclaredTransitionTarget(
1075
+ selection.sourcePath,
1076
+ selection.trigger,
1077
+ selection.transition.targets,
1078
+ unresolvedTarget
1079
+ )
1080
+ const choiceResolution = unresolvedTarget === undefined
1081
+ ? undefined
1082
+ : resolveChoiceTarget(
1083
+ machine,
1084
+ state,
1085
+ unresolvedTarget,
1086
+ (selection.context as any).event
1087
+ )
1088
+ const choiceResolvedTarget = choiceResolution?.target ?? unresolvedTarget
1089
+ let historyResolution: {
1090
+ readonly target: unknown
1091
+ readonly commands: ReadonlyArray<RuntimeCommand>
1092
+ readonly raisedEvents: ReadonlyArray<unknown>
1093
+ readonly emittedEvents: ReadonlyArray<unknown>
1094
+ readonly transitions: ReadonlyArray<ResolvedChoiceTransition>
1095
+ } | undefined
1096
+ const reenteredHistoryTarget = isHistoryTarget(choiceResolvedTarget) && selection.transition.reenter &&
1097
+ state.active.has(choiceResolvedTarget.parent)
1098
+ ? choiceResolvedTarget
1099
+ : undefined
1100
+ if (choiceResolvedTarget !== undefined && isHistoryTarget(choiceResolvedTarget)) {
1101
+ // A reentering transition may exit the history node's own parent. SCXML
1102
+ // history observes that same exit, so resolve against a provisional
1103
+ // capture rather than an older record (or the default).
1104
+ const provisionalBoundary = selection.transition.reenter
1105
+ ? getNode(machine, selection.sourcePath).parent
1106
+ : getLeastCommonAncestor(machine, stateIdentifier, choiceResolvedTarget.parent)
1107
+ const provisionalExitPaths = reenteredHistoryTarget !== undefined
1108
+ ? sortExitPaths(
1109
+ machine,
1110
+ Array.from(state.active).filter((path) => isPathInSubtree(path, choiceResolvedTarget.parent))
1111
+ )
1112
+ : getExitPaths(machine, state, provisionalBoundary)
1113
+ const stateAtHistoryResolution = provisionalExitPaths.includes(choiceResolvedTarget.parent)
1114
+ ? captureHistory(machine, state, state, provisionalExitPaths)
1115
+ : state
1116
+ historyResolution = resolveHistoryTarget(
1117
+ machine,
1118
+ stateAtHistoryResolution,
1119
+ choiceResolvedTarget,
1120
+ (selection.context as any).event
1121
+ )
1122
+ }
1123
+ const target: Machine.Snapshot<States> | Machine.Target<States, Machine.StateIdentifier<States>> | undefined =
1124
+ historyResolution === undefined
1125
+ ? choiceResolvedTarget as
1126
+ | Machine.Snapshot<States>
1127
+ | Machine.Target<States, Machine.StateIdentifier<States>>
1128
+ | undefined
1129
+ : historyResolution.target as
1130
+ | Machine.Snapshot<States>
1131
+ | Machine.Target<States, Machine.StateIdentifier<States>>
1132
+ const additionalHistoryActions: Array<RuntimeCommand> = []
1133
+ const additionalHistoryRaisedEvents: Array<unknown> = []
1134
+ const additionalHistoryEmittedEvents: Array<unknown> = []
1135
+ const additionalHistoryChoiceTransitions: Array<ResolvedChoiceTransition> = []
1136
+ const additionalChoiceTargets: Array<
1137
+ Machine.Snapshot<States> | Machine.Target<States, Machine.StateIdentifier<States>>
1138
+ > = []
1139
+ for (const additionalTarget of choiceResolution?.additionalTargets ?? []) {
1140
+ if (!isHistoryTarget(additionalTarget)) {
1141
+ additionalChoiceTargets.push(additionalTarget as any)
1142
+ continue
1143
+ }
1144
+ const resolved = resolveHistoryTarget(
1145
+ machine,
1146
+ state,
1147
+ additionalTarget,
1148
+ (selection.context as any).event
1149
+ )
1150
+ additionalChoiceTargets.push(resolved.target as any)
1151
+ additionalHistoryActions.push(...resolved.commands)
1152
+ additionalHistoryRaisedEvents.push(...resolved.raisedEvents)
1153
+ additionalHistoryEmittedEvents.push(...resolved.emittedEvents)
1154
+ additionalHistoryChoiceTransitions.push(...resolved.transitions)
1155
+ }
1156
+ const targetPath = target === undefined
1157
+ ? undefined
1158
+ : additionalChoiceTargets.length === 0 || unresolvedTarget === undefined
1159
+ ? getTargetNodePath(target)
1160
+ : getTargetNodePath(unresolvedTarget)
1161
+ let stateAfterTransition = target === undefined
1162
+ ? state
1163
+ : normalizeTargetConfigurationSync<States>(machine, state, target)
1164
+ for (const additionalTarget of additionalChoiceTargets) {
1165
+ stateAfterTransition = normalizeTargetConfigurationSync<States>(
1166
+ machine,
1167
+ stateAfterTransition,
1168
+ additionalTarget
1169
+ )
1170
+ }
1171
+ const changed = selection.transition.reenter || !hasSameActivePaths(state, stateAfterTransition)
1172
+
1173
+ if (!changed) {
1174
+ return {
1175
+ selection,
1176
+ unresolvedTarget,
1177
+ target,
1178
+ commands: [
1179
+ ...transitionResult.commands,
1180
+ ...(choiceResolution?.commands ?? []),
1181
+ ...(historyResolution?.commands ?? []),
1182
+ ...additionalHistoryActions
1183
+ ],
1184
+ raisedEvents: [
1185
+ ...transitionResult.raisedEvents,
1186
+ ...(choiceResolution?.raisedEvents ?? []),
1187
+ ...(historyResolution?.raisedEvents ?? []),
1188
+ ...additionalHistoryRaisedEvents
1189
+ ],
1190
+ emittedEvents: [
1191
+ ...transitionResult.emittedEvents,
1192
+ ...(choiceResolution?.emittedEvents ?? []),
1193
+ ...(historyResolution?.emittedEvents ?? []),
1194
+ ...additionalHistoryEmittedEvents
1195
+ ],
1196
+ changed,
1197
+ exitPaths: [],
1198
+ entryPaths: [],
1199
+ choiceTransitions: [
1200
+ ...(choiceResolution?.transitions ?? []),
1201
+ ...(historyResolution?.transitions ?? []),
1202
+ ...additionalHistoryChoiceTransitions
1203
+ ]
1204
+ } as EvaluatedTransition<States, Event, E, R, Context>
1205
+ }
1206
+
1207
+ const naturalBoundary = targetPath === undefined
1208
+ ? getNode(machine, selection.sourcePath).parent
1209
+ : getLeastCommonAncestor(machine, stateIdentifier, targetPath)
1210
+ const reentryBoundary = getNode(machine, selection.sourcePath).parent
1211
+ const boundary = selection.transition.reenter
1212
+ ? broadenTransitionBoundary(naturalBoundary, reentryBoundary)
1213
+ : naturalBoundary
1214
+
1215
+ return {
1216
+ selection,
1217
+ unresolvedTarget,
1218
+ target,
1219
+ commands: [
1220
+ ...transitionResult.commands,
1221
+ ...(choiceResolution?.commands ?? []),
1222
+ ...(historyResolution?.commands ?? []),
1223
+ ...additionalHistoryActions
1224
+ ],
1225
+ raisedEvents: [
1226
+ ...transitionResult.raisedEvents,
1227
+ ...(choiceResolution?.raisedEvents ?? []),
1228
+ ...(historyResolution?.raisedEvents ?? []),
1229
+ ...additionalHistoryRaisedEvents
1230
+ ],
1231
+ emittedEvents: [
1232
+ ...transitionResult.emittedEvents,
1233
+ ...(choiceResolution?.emittedEvents ?? []),
1234
+ ...(historyResolution?.emittedEvents ?? []),
1235
+ ...additionalHistoryEmittedEvents
1236
+ ],
1237
+ changed,
1238
+ exitPaths: reenteredHistoryTarget !== undefined
1239
+ ? sortExitPaths(
1240
+ machine,
1241
+ Array.from(state.active).filter((path) => isPathInSubtree(path, reenteredHistoryTarget.parent))
1242
+ )
1243
+ : getExitPaths(machine, state, boundary),
1244
+ entryPaths: reenteredHistoryTarget !== undefined
1245
+ ? sortEntryPaths(
1246
+ machine,
1247
+ Array.from(stateAfterTransition.active).filter((path) => isPathInSubtree(path, reenteredHistoryTarget.parent))
1248
+ )
1249
+ : getEntryPaths(machine, stateAfterTransition, boundary),
1250
+ choiceTransitions: [
1251
+ ...(choiceResolution?.transitions ?? []),
1252
+ ...(historyResolution?.transitions ?? []),
1253
+ ...additionalHistoryChoiceTransitions
1254
+ ]
1255
+ } as EvaluatedTransition<States, Event, E, R, Context>
1256
+ }
1257
+
1258
+ export const MaxMacrostepIterations = 1000
1259
+ export const InitialEvent: MachineInitialEvent = { _tag: InitialEventTypeId }
1260
+
1261
+ export const isFinalState = (
1262
+ machine: Machine.Any,
1263
+ state: Machine.Snapshot<any>
1264
+ ): boolean => isActiveFinalConfiguration(machine, normalizeConfiguration(machine, state))
1265
+
1266
+ export const getFinalOutputEffect = <
1267
+ const States extends Machine.StateSchemas,
1268
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
1269
+ Output
1270
+ >(
1271
+ machine: Machine.Any,
1272
+ state: Machine.Snapshot<States>,
1273
+ event: Machine.LifecycleEvent<Events>
1274
+ ): Effect.Effect<Output, MachineSchemaDecodeError> =>
1275
+ normalizeConfigurationEffect(machine, state).pipe(
1276
+ Effect.flatMap((configuration) => completeConfigurationEffect(machine, configuration, event)),
1277
+ Effect.flatMap((completed): Effect.Effect<Output> => {
1278
+ const root = getRootPath(machine, completed.configuration)
1279
+ if (
1280
+ !isActiveFinalConfiguration(machine, completed.configuration)
1281
+ || !completed.configuration.outputs.has(root)
1282
+ ) {
1283
+ return Effect.die(
1284
+ new Error("Machine attempted to read terminal output from a non-terminal configuration")
1285
+ )
1286
+ }
1287
+ return Effect.succeed(completed.configuration.outputs.get(root) as Output)
1288
+ })
1289
+ )
1290
+
1291
+ export const isFinal = <
1292
+ const States extends Machine.StateSchemas,
1293
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
1294
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
1295
+ const Input extends Schema.Top = typeof Schema.Void,
1296
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
1297
+ E = never,
1298
+ R = never,
1299
+ InitialE = never,
1300
+ InitialR = never,
1301
+ FinalStates extends Machine.StateIdentifier<States> = never,
1302
+ Output = never,
1303
+ OutputStates extends Machine.StateIdentifier<States> = never
1304
+ >(
1305
+ machine: Machine<
1306
+ States,
1307
+ Events,
1308
+ Input,
1309
+ UnhandledStates,
1310
+ E,
1311
+ R,
1312
+ InitialE,
1313
+ InitialR,
1314
+ FinalStates,
1315
+ Output,
1316
+ Emits,
1317
+ OutputStates
1318
+ >,
1319
+ state: Machine.Snapshot<States>
1320
+ ): state is Machine.SnapshotContainingFinal<States, FinalStates> => isFinalState(machine, state)
1321
+
1322
+ export const planInitialSync = <
1323
+ const States extends Machine.StateSchemas,
1324
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
1325
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
1326
+ const Input extends Schema.Top = typeof Schema.Void,
1327
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
1328
+ E = never,
1329
+ R = never,
1330
+ InitialE = never,
1331
+ InitialR = never,
1332
+ FinalStates extends Machine.StateIdentifier<States> = never,
1333
+ Output = never
1334
+ >(
1335
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
1336
+ ...args: [...Machine.InputArgs<Input>]
1337
+ ) => {
1338
+ const inputArgs = machine.input === undefined
1339
+ ? args
1340
+ : args.length === 0
1341
+ ? (decodeInputSync(machine, machine.input, undefined), args)
1342
+ : [decodeInputSync(machine, machine.input, args[0])] as [...Machine.InputArgs<Input>]
1343
+ const state = machine.initial(...inputArgs)
1344
+ const emptyConfiguration: ActiveConfiguration = {
1345
+ active: new Set(),
1346
+ values: new Map(),
1347
+ outputs: new Map(),
1348
+ history: new Map()
1349
+ }
1350
+ const initialChoice = choiceFromTarget(state)
1351
+ const choiceResolution = initialChoice === undefined
1352
+ ? undefined
1353
+ : resolveChoiceTarget(
1354
+ machine,
1355
+ emptyConfiguration,
1356
+ state,
1357
+ InitialEvent
1358
+ )
1359
+ const initialHistoryActions: Array<RuntimeCommand> = []
1360
+ const initialHistoryRaisedEvents: Array<unknown> = []
1361
+ const initialHistoryEmittedEvents: Array<unknown> = []
1362
+ const initialHistoryChoiceTransitions: Array<ResolvedChoiceTransition> = []
1363
+ const resolvedInitialTargets: Array<unknown> = []
1364
+ for (
1365
+ const target of choiceResolution === undefined
1366
+ ? []
1367
+ : [choiceResolution.target, ...choiceResolution.additionalTargets]
1368
+ ) {
1369
+ if (!isHistoryTarget(target)) {
1370
+ resolvedInitialTargets.push(target)
1371
+ continue
1372
+ }
1373
+ const history = resolveHistoryTarget(machine, emptyConfiguration, target, InitialEvent)
1374
+ resolvedInitialTargets.push(history.target)
1375
+ initialHistoryActions.push(...history.commands)
1376
+ initialHistoryRaisedEvents.push(...history.raisedEvents)
1377
+ initialHistoryEmittedEvents.push(...history.emittedEvents)
1378
+ initialHistoryChoiceTransitions.push(...history.transitions)
1379
+ }
1380
+ let resolvedConfiguration = choiceResolution === undefined
1381
+ ? normalizeConfigurationSync<States>(machine, state as Machine.Snapshot<States>)
1382
+ : normalizeTargetConfigurationSync<States>(
1383
+ machine,
1384
+ emptyConfiguration,
1385
+ resolvedInitialTargets[0] as
1386
+ | Machine.Snapshot<States>
1387
+ | Machine.Target<States, Machine.StateIdentifier<States>>
1388
+ )
1389
+ for (const additionalTarget of resolvedInitialTargets.slice(1)) {
1390
+ resolvedConfiguration = normalizeTargetConfigurationSync<States>(
1391
+ machine,
1392
+ resolvedConfiguration,
1393
+ additionalTarget as Machine.Snapshot<States> | Machine.Target<States, Machine.StateIdentifier<States>>
1394
+ )
1395
+ }
1396
+ const configuration: ActiveConfiguration = resolvedConfiguration
1397
+ validateInitialConfiguration(machine, configuration)
1398
+ const startingState = snapshotFromConfiguration<States>(machine, configuration)
1399
+ const initialEntryPaths = getInitialEntryPaths(machine, configuration)
1400
+ const commands = [
1401
+ ...(choiceResolution?.commands ?? []),
1402
+ ...initialHistoryActions
1403
+ ]
1404
+ const raisedEvents = [
1405
+ ...(choiceResolution?.raisedEvents ?? []),
1406
+ ...initialHistoryRaisedEvents
1407
+ ]
1408
+ const emittedEvents = [
1409
+ ...(choiceResolution?.emittedEvents ?? []),
1410
+ ...initialHistoryEmittedEvents
1411
+ ]
1412
+ const entry = collectStateActions<States, Events, Emits, E, R>(
1413
+ machine,
1414
+ configuration,
1415
+ initialEntryPaths,
1416
+ InitialEvent,
1417
+ "entry"
1418
+ )
1419
+ const settled = settle(
1420
+ machine,
1421
+ configuration,
1422
+ InitialEvent,
1423
+ [...entry.commands],
1424
+ [...raisedEvents, ...entry.raisedEvents] as Array<Machine.EventOf<Events>>,
1425
+ [...emittedEvents, ...entry.emittedEvents],
1426
+ choiceResolution === undefined ? [] : [{
1427
+ next: configuration,
1428
+ event: InitialEvent,
1429
+ transitions: [...choiceResolution.transitions, ...initialHistoryChoiceTransitions],
1430
+ commands: [...choiceResolution.commands, ...initialHistoryActions],
1431
+ raisedEvents: [
1432
+ ...choiceResolution.raisedEvents,
1433
+ ...initialHistoryRaisedEvents
1434
+ ] as ReadonlyArray<Machine.EventOf<Events>>,
1435
+ emittedEvents: [...choiceResolution.emittedEvents, ...initialHistoryEmittedEvents],
1436
+ exitPaths: [],
1437
+ entryPaths: [],
1438
+ changed: false
1439
+ }]
1440
+ )
1441
+
1442
+ const planned = {
1443
+ startingState,
1444
+ initialEntryPaths,
1445
+ state: snapshotFromConfiguration<States>(machine, settled.next),
1446
+ commands: [
1447
+ ...commands,
1448
+ ...settled.commands
1449
+ ],
1450
+ emittedEvents: settled.emittedEvents as ReadonlyArray<Machine.EmitOf<Emits>>,
1451
+ microsteps: settled.microsteps.map((step) => ({
1452
+ next: snapshotFromConfiguration<States>(machine, step.next),
1453
+ event: step.event,
1454
+ transitions: step.transitions,
1455
+ commands: step.commands,
1456
+ raisedEvents: step.raisedEvents,
1457
+ emittedEvents: step.emittedEvents,
1458
+ exitPaths: step.exitPaths,
1459
+ entryPaths: step.entryPaths,
1460
+ changed: step.changed
1461
+ }))
1462
+ }
1463
+ return settled.done
1464
+ ? { ...planned, done: true as const, output: settled.output }
1465
+ : { ...planned, done: false as const, output: undefined }
1466
+ }
1467
+
1468
+ export const enabled = <
1469
+ const States extends Machine.StateSchemas,
1470
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
1471
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
1472
+ const Input extends Schema.Top = typeof Schema.Void,
1473
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
1474
+ E = never,
1475
+ R = never,
1476
+ InitialE = never,
1477
+ InitialR = never,
1478
+ FinalStates extends Machine.StateIdentifier<States> = never,
1479
+ Output = never,
1480
+ OutputStates extends Machine.StateIdentifier<States> = never
1481
+ >(
1482
+ machine: Machine<
1483
+ States,
1484
+ Events,
1485
+ Input,
1486
+ UnhandledStates,
1487
+ E,
1488
+ R,
1489
+ InitialE,
1490
+ InitialR,
1491
+ FinalStates,
1492
+ Output,
1493
+ Emits,
1494
+ OutputStates
1495
+ >,
1496
+ state: Machine.Snapshot<States>
1497
+ ): ReadonlyArray<Machine.TagOf<Events[number]>> => {
1498
+ if (isFinalState(machine, state)) {
1499
+ return []
1500
+ }
1501
+ const configuration = normalizeConfiguration(machine, state)
1502
+ const tags: Array<Machine.TagOf<Events[number]>> = []
1503
+ const seen = new Set<PropertyKey>()
1504
+ for (const path of getCandidatePaths(machine, configuration)) {
1505
+ for (const tag of Reflect.ownKeys(machine.handlers[path]?.on ?? {})) {
1506
+ if (!seen.has(tag)) {
1507
+ seen.add(tag)
1508
+ tags.push(tag as Machine.TagOf<Events[number]>)
1509
+ }
1510
+ }
1511
+ }
1512
+ return tags
1513
+ }
1514
+
1515
+ const microstep = <
1516
+ const States extends Machine.StateSchemas,
1517
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
1518
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
1519
+ const Input extends Schema.Top = typeof Schema.Void,
1520
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
1521
+ E = never,
1522
+ R = never,
1523
+ InitialE = never,
1524
+ InitialR = never,
1525
+ FinalStates extends Machine.StateIdentifier<States> = never,
1526
+ Output = never,
1527
+ Context = never
1528
+ >(
1529
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
1530
+ state: ActiveConfiguration,
1531
+ event: Machine.LifecycleEvent<Events>,
1532
+ selections: ReadonlyArray<SelectedTransition<States, E, R, Context>>
1533
+ ) => {
1534
+ if (selections.length === 0) {
1535
+ return {
1536
+ next: state,
1537
+ event,
1538
+ transitions: [],
1539
+ commands: [],
1540
+ raisedEvents: [],
1541
+ emittedEvents: [],
1542
+ exitPaths: [],
1543
+ entryPaths: [],
1544
+ changed: false
1545
+ }
1546
+ }
1547
+
1548
+ const activeSelections = removePreemptedAncestorSelections(selections)
1549
+ const evaluatedTransitions: Array<EvaluatedTransition<States, Machine.EventOf<Events>, E, R, Context>> = []
1550
+ for (const selection of activeSelections) {
1551
+ evaluatedTransitions.push(
1552
+ collectEvaluatedTransition<States, Machine.EventOf<Events>, E, R, Context>(
1553
+ machine,
1554
+ state,
1555
+ selection
1556
+ )
1557
+ )
1558
+ }
1559
+
1560
+ const transitions = removeConflictingTransitions(machine, evaluatedTransitions)
1561
+ const sortedTransitions = sortEvaluatedTransitions(machine, transitions)
1562
+ const retainedTransitions = sortedTransitions.flatMap((transition) => [
1563
+ {
1564
+ source: transition.selection.sourcePath,
1565
+ trigger: transition.selection.trigger,
1566
+ reenter: transition.selection.transition.reenter,
1567
+ target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
1568
+ resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
1569
+ },
1570
+ ...transition.choiceTransitions
1571
+ ])
1572
+ let stateAfterTransition = state
1573
+ // Value-only targets are evaluated against the original configuration. If
1574
+ // one is applied after a control-changing transition, it can resurrect a
1575
+ // branch that the changing transition exited. Apply value-only updates
1576
+ // first so later control targets remain authoritative while still
1577
+ // preserving updates made in unaffected parallel regions.
1578
+ const targetApplicationOrder = [
1579
+ ...sortedTransitions.filter((transition) => !transition.changed),
1580
+ ...sortedTransitions.filter((transition) => transition.changed)
1581
+ ]
1582
+ for (const transition of targetApplicationOrder) {
1583
+ if (transition.target !== undefined) {
1584
+ stateAfterTransition = normalizeTargetConfigurationSync<States>(
1585
+ machine,
1586
+ stateAfterTransition,
1587
+ transition.target
1588
+ )
1589
+ }
1590
+ }
1591
+
1592
+ const changed = transitions.some((transition) => transition.changed)
1593
+ const transitionActions = sortedTransitions
1594
+ .flatMap((transition) => transition.commands)
1595
+ const transitionRaisedEvents = sortedTransitions
1596
+ .flatMap((transition) => transition.raisedEvents)
1597
+ const transitionEmittedEvents = sortedTransitions
1598
+ .flatMap((transition) => transition.emittedEvents)
1599
+
1600
+ if (!changed) {
1601
+ return {
1602
+ next: stateAfterTransition,
1603
+ event,
1604
+ transitions: retainedTransitions,
1605
+ commands: transitionActions,
1606
+ raisedEvents: transitionRaisedEvents,
1607
+ emittedEvents: transitionEmittedEvents,
1608
+ exitPaths: [],
1609
+ entryPaths: [],
1610
+ changed: false
1611
+ }
1612
+ }
1613
+
1614
+ const exitPaths = sortExitPaths(machine, sortedTransitions.flatMap((transition) => transition.exitPaths))
1615
+ const entryPaths = sortEntryPaths(machine, sortedTransitions.flatMap((transition) => transition.entryPaths))
1616
+ stateAfterTransition = captureHistory(machine, state, stateAfterTransition, exitPaths)
1617
+ const exit = collectStateActions<States, Events, Emits, E, R>(
1618
+ machine,
1619
+ state,
1620
+ exitPaths,
1621
+ event,
1622
+ "exit"
1623
+ )
1624
+ const entry = collectStateActions<States, Events, Emits, E, R>(
1625
+ machine,
1626
+ stateAfterTransition,
1627
+ entryPaths,
1628
+ event,
1629
+ "entry"
1630
+ )
1631
+
1632
+ return {
1633
+ next: stateAfterTransition,
1634
+ event,
1635
+ transitions: retainedTransitions,
1636
+ commands: [...exit.commands, ...transitionActions, ...entry.commands],
1637
+ raisedEvents: [...exit.raisedEvents, ...transitionRaisedEvents, ...entry.raisedEvents] as ReadonlyArray<
1638
+ Machine.EventOf<Events>
1639
+ >,
1640
+ emittedEvents: [...exit.emittedEvents, ...transitionEmittedEvents, ...entry.emittedEvents],
1641
+ exitPaths,
1642
+ entryPaths,
1643
+ changed: true
1644
+ }
1645
+ }
1646
+
1647
+ const settle = <
1648
+ const States extends Machine.StateSchemas,
1649
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
1650
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
1651
+ const Input extends Schema.Top = typeof Schema.Void,
1652
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
1653
+ E = never,
1654
+ R = never,
1655
+ InitialE = never,
1656
+ InitialR = never,
1657
+ FinalStates extends Machine.StateIdentifier<States> = never,
1658
+ Output = never
1659
+ >(
1660
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
1661
+ state: ActiveConfiguration,
1662
+ event: Machine.LifecycleEvent<Events>,
1663
+ commands: Array<RuntimeCommand>,
1664
+ raisedEvents: Array<Machine.EventOf<Events>>,
1665
+ emittedEvents: Array<unknown>,
1666
+ microsteps: Array<MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R>>
1667
+ ) => {
1668
+ let currentState = state
1669
+ let currentEvent = event
1670
+ let shouldRunAlways = true
1671
+ let iterations = 0
1672
+ let raisedEventIndex = 0
1673
+ let completedTerminal = false
1674
+ let finalOutput: Output | undefined = undefined
1675
+ const pendingCompletions: Array<{ readonly path: string; readonly output: unknown }> = []
1676
+
1677
+ while (true) {
1678
+ iterations += 1
1679
+ if (iterations > MaxMacrostepIterations) {
1680
+ throw new InfiniteTransitionError({
1681
+ machineId: machine.id,
1682
+ state: String(getLeafPath(machine, currentState)),
1683
+ maxIterations: MaxMacrostepIterations
1684
+ })
1685
+ }
1686
+
1687
+ const completed = completeConfigurationSync(machine, currentState, currentEvent)
1688
+ currentState = completed.configuration
1689
+ pendingCompletions.push(
1690
+ ...completed.completions.filter((completion) => machine.handlers[completion.path]?.onDone !== undefined)
1691
+ )
1692
+ while (pendingCompletions.length > 0 && !currentState.active.has(pendingCompletions[0]!.path)) {
1693
+ pendingCompletions.shift()
1694
+ }
1695
+ const done = selectDoneTransitions<States, Events, Emits, E, R>(
1696
+ machine,
1697
+ currentState,
1698
+ currentEvent,
1699
+ pendingCompletions.length === 0 ? [] : [pendingCompletions.shift()!]
1700
+ )
1701
+ if (done.length > 0) {
1702
+ const doneStep: MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
1703
+ machine,
1704
+ currentState,
1705
+ currentEvent,
1706
+ done
1707
+ )
1708
+ commands.push(...doneStep.commands)
1709
+ raisedEvents.push(...doneStep.raisedEvents)
1710
+ emittedEvents.push(...doneStep.emittedEvents)
1711
+ microsteps.push(doneStep)
1712
+ currentState = doneStep.next
1713
+ shouldRunAlways = doneStep.changed
1714
+ continue
1715
+ }
1716
+ if (isActiveFinalConfiguration(machine, currentState)) {
1717
+ const root = getRootPath(machine, currentState)
1718
+ if (!currentState.outputs.has(root)) {
1719
+ throw new Error("Machine reached a terminal configuration without a completed root output")
1720
+ }
1721
+ completedTerminal = true
1722
+ finalOutput = currentState.outputs.get(root) as Output
1723
+ break
1724
+ }
1725
+
1726
+ const always = shouldRunAlways
1727
+ ? selectAlwaysTransitions<States, Events, Emits, E, R>(machine, currentState, currentEvent)
1728
+ : []
1729
+ if (always.length > 0) {
1730
+ const alwaysStep: MicrostepPlan<ActiveConfiguration, Machine.EventOf<Events>, E, R> = microstep(
1731
+ machine,
1732
+ currentState,
1733
+ currentEvent,
1734
+ always
1735
+ )
1736
+ commands.push(...alwaysStep.commands)
1737
+ raisedEvents.push(...alwaysStep.raisedEvents)
1738
+ emittedEvents.push(...alwaysStep.emittedEvents)
1739
+ microsteps.push(alwaysStep)
1740
+ currentState = alwaysStep.next
1741
+ shouldRunAlways = alwaysStep.changed
1742
+ continue
1743
+ }
1744
+
1745
+ const raisedEventValue = raisedEvents[raisedEventIndex]
1746
+ if (raisedEventValue === undefined) {
1747
+ break
1748
+ }
1749
+ raisedEventIndex += 1
1750
+
1751
+ // Planning runtime validates and normalizes every event before it enters this
1752
+ // internal queue, so decoding it again here only repeats schema work.
1753
+ const raisedEvent = raisedEventValue
1754
+ currentEvent = raisedEvent
1755
+ const raisedSelections = selectEventTransitions<States, Events, Emits, E, R>(
1756
+ machine,
1757
+ currentState,
1758
+ raisedEvent as Machine.EventByTag<Events, Machine.TagOf<Events[number]>>
1759
+ )
1760
+ if (raisedSelections.length === 0) {
1761
+ shouldRunAlways = true
1762
+ continue
1763
+ }
1764
+ const raisedStep = microstep(
1765
+ machine,
1766
+ currentState,
1767
+ raisedEvent,
1768
+ raisedSelections
1769
+ )
1770
+ commands.push(...raisedStep.commands)
1771
+ raisedEvents.push(...raisedStep.raisedEvents)
1772
+ emittedEvents.push(...raisedStep.emittedEvents)
1773
+ microsteps.push(raisedStep)
1774
+ currentState = raisedStep.next
1775
+ shouldRunAlways = true
1776
+ }
1777
+
1778
+ const result = {
1779
+ next: currentState,
1780
+ commands,
1781
+ emittedEvents,
1782
+ microsteps
1783
+ }
1784
+ return completedTerminal
1785
+ ? { ...result, done: true as const, output: finalOutput as Output }
1786
+ : { ...result, done: false as const, output: undefined }
1787
+ }
1788
+
1789
+ const macrostepConfiguration = <
1790
+ const States extends Machine.StateSchemas,
1791
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
1792
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
1793
+ const Input extends Schema.Top = typeof Schema.Void,
1794
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
1795
+ E = never,
1796
+ R = never,
1797
+ InitialE = never,
1798
+ InitialR = never,
1799
+ FinalStates extends Machine.StateIdentifier<States> = never,
1800
+ Output = never
1801
+ >(
1802
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
1803
+ configuration: ActiveConfiguration,
1804
+ event: Machine.EventOf<Events>
1805
+ ) => {
1806
+ const decodedEvent = decodeEventSync<Events>(machine, event)
1807
+ if (isActiveFinalConfiguration(machine, configuration)) {
1808
+ const completed = completeConfigurationSync(machine, configuration, decodedEvent)
1809
+ const root = getRootPath(machine, completed.configuration)
1810
+ if (!completed.configuration.outputs.has(root)) {
1811
+ throw new Error("Machine reached a terminal configuration without a completed root output")
1812
+ }
1813
+ return {
1814
+ next: completed.configuration,
1815
+ commands: [],
1816
+ emittedEvents: [],
1817
+ microsteps: [],
1818
+ done: true as const,
1819
+ output: completed.configuration.outputs.get(root) as Output
1820
+ }
1821
+ }
1822
+
1823
+ const selections = selectEventTransitions<States, Events, Emits, E, R>(
1824
+ machine,
1825
+ configuration,
1826
+ decodedEvent as Machine.EventByTag<Events, Machine.TagOf<Events[number]>>
1827
+ )
1828
+ if (selections.length === 0) {
1829
+ return {
1830
+ next: configuration,
1831
+ commands: [],
1832
+ emittedEvents: [],
1833
+ microsteps: [],
1834
+ done: false as const,
1835
+ output: undefined
1836
+ }
1837
+ }
1838
+ const step = microstep(
1839
+ machine,
1840
+ configuration,
1841
+ decodedEvent,
1842
+ selections
1843
+ )
1844
+ const commands = [...step.commands]
1845
+ const raisedEvents = [...step.raisedEvents]
1846
+ const emittedEvents = [...step.emittedEvents]
1847
+ const microsteps = [step]
1848
+ return settle(machine, step.next, decodedEvent, commands, raisedEvents, emittedEvents, microsteps)
1849
+ }
1850
+
1851
+ const snapshotMacrostep = <
1852
+ const States extends Machine.StateSchemas,
1853
+ Event,
1854
+ E,
1855
+ R,
1856
+ Output
1857
+ >(
1858
+ machine: Machine.Any,
1859
+ settled: MacrostepPlan<ActiveConfiguration, Event, E, R, Output>
1860
+ ): MacrostepPlan<Machine.Snapshot<States>, Event, E, R, Output> => {
1861
+ const planned = {
1862
+ next: snapshotFromConfiguration<States>(machine, settled.next),
1863
+ commands: settled.commands,
1864
+ emittedEvents: settled.emittedEvents,
1865
+ microsteps: settled.microsteps.map((step) => ({
1866
+ next: snapshotFromConfiguration<States>(machine, step.next),
1867
+ event: step.event,
1868
+ transitions: step.transitions,
1869
+ commands: step.commands,
1870
+ raisedEvents: step.raisedEvents,
1871
+ emittedEvents: step.emittedEvents,
1872
+ exitPaths: step.exitPaths,
1873
+ entryPaths: step.entryPaths,
1874
+ changed: step.changed
1875
+ }))
1876
+ }
1877
+ return settled.done
1878
+ ? { ...planned, done: true, output: settled.output }
1879
+ : { ...planned, done: false, output: undefined }
1880
+ }
1881
+
1882
+ const macrostep = <
1883
+ const States extends Machine.StateSchemas,
1884
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
1885
+ const Emits extends ReadonlyArray<Machine.TaggedSchema> = readonly [],
1886
+ const Input extends Schema.Top = typeof Schema.Void,
1887
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
1888
+ E = never,
1889
+ R = never,
1890
+ InitialE = never,
1891
+ InitialR = never,
1892
+ FinalStates extends Machine.StateIdentifier<States> = never,
1893
+ Output = never
1894
+ >(
1895
+ machine: Machine<States, Events, Input, UnhandledStates, E, R, InitialE, InitialR, FinalStates, Output, Emits>,
1896
+ state: Machine.Snapshot<States>,
1897
+ event: Machine.EventOf<Events>
1898
+ ) => {
1899
+ const configuration = normalizeConfigurationSync<States>(machine, state)
1900
+ const settled = macrostepConfiguration(machine, configuration, event)
1901
+ return snapshotMacrostep<States, Machine.EventOf<Events>, E, R, Output>(machine, settled)
1902
+ }
1903
+
1904
+ export const planSync = macrostep
1905
+
1906
+ export const planConfiguration = macrostepConfiguration
1907
+
1908
+ const planningEffect = <A>(thunk: () => A): Effect.Effect<A, InfiniteTransitionError | MachineSchemaDecodeError> =>
1909
+ Effect.suspend(() => {
1910
+ try {
1911
+ return Effect.succeed(thunk())
1912
+ } catch (error) {
1913
+ return error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError
1914
+ ? Effect.fail(error)
1915
+ : Effect.die(error)
1916
+ }
1917
+ })
1918
+
1919
+ export const plan = (machine: Machine.Any, state: Machine.Snapshot<any>, event: unknown) =>
1920
+ planningEffect(() => planSync(machine as any, state, event as any))
1921
+
1922
+ export const planInitial = (
1923
+ machine: Machine.Any,
1924
+ ...args: ReadonlyArray<unknown>
1925
+ ): Effect.Effect<any, InfiniteTransitionError | MachineSchemaDecodeError | StartupError> =>
1926
+ Effect.try({
1927
+ try: () => (planInitialSync as any)(machine, ...args),
1928
+ catch: (error) => {
1929
+ return error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError
1930
+ ? error
1931
+ : new StartupError({ cause: Cause.die(error) })
1932
+ }
1933
+ })