@typeonce/effect-machine 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +5 -5
  2. package/src/Machine.ts +6873 -0
  3. package/src/index.ts +1 -0
  4. package/src/internal/machine/activities.ts +108 -0
  5. package/src/internal/machine/atom.ts +636 -0
  6. package/src/internal/machine/cluster.ts +394 -0
  7. package/src/internal/machine/command.ts +58 -0
  8. package/src/internal/machine/commandRuntime.ts +43 -0
  9. package/src/internal/machine/configuration.ts +1331 -0
  10. package/src/internal/machine/errors.ts +87 -0
  11. package/src/internal/machine/executionPlan.ts +996 -0
  12. package/src/internal/machine/invocation.ts +119 -0
  13. package/src/internal/machine/machine.ts +1747 -0
  14. package/src/internal/machine/planner.ts +1933 -0
  15. package/src/internal/machine/process.ts +906 -0
  16. package/src/internal/machine/protocol.ts +322 -0
  17. package/src/internal/machine/readiness.ts +10 -0
  18. package/src/internal/machine/runtime.ts +2512 -0
  19. package/src/internal/machine/serialization.ts +498 -0
  20. package/src/internal/machine/stateDefinition.ts +270 -0
  21. package/src/internal/machine/symbols.ts +2 -0
  22. package/src/internal/machine/topology.ts +479 -0
  23. package/src/internal/testing/machine/arbitrary.ts +102 -0
  24. package/src/internal/testing/machine/exploration.ts +331 -0
  25. package/src/internal/testing/machine/finiteModel.ts +1498 -0
  26. package/src/internal/testing/machine/invariant.ts +372 -0
  27. package/src/internal/testing/machine/probe.ts +79 -0
  28. package/src/internal/testing/machine/referenceModel.ts +1505 -0
  29. package/src/internal/testing/machine/runtime.ts +1710 -0
  30. package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
  31. package/src/internal/testing/machine/trace.ts +150 -0
  32. package/src/internal/testing/machine/verification.ts +1890 -0
  33. package/src/testing/MachineTest.ts +2067 -0
  34. package/src/testing/index.ts +7 -0
  35. package/src/unstable/cluster/ClusterMachine.ts +390 -0
  36. package/src/unstable/cluster/index.ts +1 -0
  37. package/src/unstable/reactivity/AtomMachine.ts +649 -0
  38. package/src/unstable/reactivity/index.ts +1 -0
@@ -0,0 +1,1331 @@
1
+ /**
2
+ * Internal active-configuration, history, and completion 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 * as Option from "effect/Option"
10
+ import { hasProperty } from "effect/Predicate"
11
+ import type { Machine } from "../../Machine.js"
12
+ import { MachineSchemaDecodeError } from "./errors.js"
13
+ import {
14
+ decodeBoundary,
15
+ decodeOutputValue,
16
+ decodeOutputValueSync,
17
+ decodeStateValue,
18
+ decodeStateValueSync
19
+ } from "./protocol.js"
20
+ import { getNode, getStateNodeSchema, isSnapshot, isTarget, TargetSnapshotTypeId } from "./topology.js"
21
+
22
+ export interface HistoryRecord {
23
+ readonly mode: "shallow" | "deep"
24
+ readonly parent: string
25
+ readonly active: ReadonlySet<string>
26
+ readonly values: ReadonlyMap<string, unknown>
27
+ }
28
+
29
+ export const validateHistoryRecordControl = (machine: Machine.Any, record: HistoryRecord): void => {
30
+ const ancestry = new Set(getPathToRoot(machine, record.parent))
31
+ const visit = (path: string): void => {
32
+ const node = getNode(machine, path)
33
+ if (node.type === "compound") {
34
+ const children = node.children.filter((child) => record.active.has(child))
35
+ if (children.length !== 1) {
36
+ throw new Error(`Machine history expected compound state "${path}" to retain one active child`)
37
+ }
38
+ if (record.mode === "deep") visit(children[0]!)
39
+ return
40
+ }
41
+ if (node.type === "parallel") {
42
+ for (const child of node.children) {
43
+ if (!record.active.has(child)) {
44
+ throw new Error(`Machine history expected parallel state "${path}" to retain region "${child}"`)
45
+ }
46
+ if (record.mode === "deep") visit(child)
47
+ }
48
+ }
49
+ }
50
+ visit(record.parent)
51
+ for (const path of record.active) {
52
+ if (!ancestry.has(path) && !isPathInSubtree(path, record.parent)) {
53
+ throw new Error(`Machine history contains state "${path}" outside parent "${record.parent}"`)
54
+ }
55
+ if (
56
+ record.mode === "shallow" && isDescendantOf(path, record.parent) &&
57
+ getNode(machine, path).parent !== record.parent
58
+ ) {
59
+ throw new Error(`Machine shallow history contains deep descendant "${path}"`)
60
+ }
61
+ }
62
+ }
63
+
64
+ type SnapshotWithHistory = Machine.AtomicSnapshot<string, unknown> & {
65
+ readonly history?: Readonly<
66
+ Record<string, {
67
+ readonly mode: "shallow" | "deep"
68
+ readonly active: ReadonlyArray<string>
69
+ readonly values: Readonly<Record<string, unknown>>
70
+ }>
71
+ >
72
+ }
73
+
74
+ const historyFromSnapshot = (
75
+ machine: Machine.Any,
76
+ snapshot: SnapshotWithHistory
77
+ ): ReadonlyMap<string, HistoryRecord> => {
78
+ const history = new Map<string, HistoryRecord>()
79
+ for (const [path, entry] of Object.entries(snapshot.history ?? {})) {
80
+ const historyNode = getNode(machine, path)
81
+ if (historyNode.type !== "history" || historyNode.parent === undefined || historyNode.history !== entry.mode) {
82
+ throw new Error(`Machine snapshot contains invalid history record "${path}"`)
83
+ }
84
+ const active = new Set<string>()
85
+ const values = new Map<string, unknown>()
86
+ for (const activePath of entry.active) {
87
+ if (active.has(activePath) || !Object.prototype.hasOwnProperty.call(entry.values, activePath)) {
88
+ throw new Error(`Machine snapshot contains invalid remembered state "${activePath}"`)
89
+ }
90
+ const node = getNode(machine, activePath)
91
+ if (
92
+ node.type === "history" || node.type === "choice" ||
93
+ !(isPathInSubtree(activePath, historyNode.parent) ||
94
+ getPathToRoot(machine, historyNode.parent).includes(activePath))
95
+ ) {
96
+ throw new Error(`Machine snapshot contains invalid remembered value for "${activePath}"`)
97
+ }
98
+ active.add(activePath)
99
+ values.set(activePath, decodeStateValueSync(machine, node, entry.values[activePath]))
100
+ }
101
+ if (!active.has(historyNode.parent) || Object.keys(entry.values).length !== active.size) {
102
+ throw new Error(`Machine snapshot contains incomplete history record "${path}"`)
103
+ }
104
+ history.set(path, {
105
+ mode: entry.mode,
106
+ parent: historyNode.parent,
107
+ active,
108
+ values
109
+ })
110
+ validateHistoryRecordControl(machine, history.get(path)!)
111
+ }
112
+ return history
113
+ }
114
+
115
+ const historyFromSnapshotEffect = Effect.fnUntraced(function*(
116
+ machine: Machine.Any,
117
+ snapshot: SnapshotWithHistory
118
+ ) {
119
+ const history = new Map<string, HistoryRecord>()
120
+ for (const [path, entry] of Object.entries(snapshot.history ?? {})) {
121
+ const historyNode = machine.stateNodes.byPath.get(path)
122
+ if (
123
+ historyNode === undefined || historyNode.type !== "history" || historyNode.parent === undefined ||
124
+ historyNode.history !== entry.mode
125
+ ) {
126
+ return yield* Effect.fail(
127
+ new MachineSchemaDecodeError({
128
+ machineId: machine.id,
129
+ boundary: "history",
130
+ state: path,
131
+ cause: Cause.die(new Error(`Machine snapshot contains invalid history record "${path}"`))
132
+ })
133
+ )
134
+ }
135
+ const active = new Set<string>()
136
+ const values = new Map<string, unknown>()
137
+ for (const activePath of entry.active) {
138
+ const node = machine.stateNodes.byPath.get(activePath)
139
+ if (
140
+ active.has(activePath) || node === undefined || node.type === "history" || node.type === "choice" ||
141
+ !Object.prototype.hasOwnProperty.call(entry.values, activePath) ||
142
+ !(isPathInSubtree(activePath, historyNode.parent) ||
143
+ getPathToRoot(machine, historyNode.parent).includes(activePath))
144
+ ) {
145
+ return yield* Effect.fail(
146
+ new MachineSchemaDecodeError({
147
+ machineId: machine.id,
148
+ boundary: "history",
149
+ state: activePath,
150
+ cause: Cause.die(new Error(`Machine snapshot contains invalid remembered state "${activePath}"`))
151
+ })
152
+ )
153
+ }
154
+ active.add(activePath)
155
+ values.set(
156
+ activePath,
157
+ yield* decodeBoundary(machine, getStateNodeSchema(node), entry.values[activePath], {
158
+ boundary: "history",
159
+ state: activePath
160
+ })
161
+ )
162
+ }
163
+ if (!active.has(historyNode.parent) || Object.keys(entry.values).length !== active.size) {
164
+ return yield* Effect.fail(
165
+ new MachineSchemaDecodeError({
166
+ machineId: machine.id,
167
+ boundary: "history",
168
+ state: path,
169
+ cause: Cause.die(new Error(`Machine snapshot contains incomplete history record "${path}"`))
170
+ })
171
+ )
172
+ }
173
+ history.set(path, {
174
+ mode: entry.mode,
175
+ parent: historyNode.parent,
176
+ active,
177
+ values
178
+ })
179
+ try {
180
+ validateHistoryRecordControl(machine, history.get(path)!)
181
+ } catch (cause) {
182
+ return yield* Effect.fail(
183
+ new MachineSchemaDecodeError({
184
+ machineId: machine.id,
185
+ boundary: "history",
186
+ state: path,
187
+ cause: Cause.die(cause)
188
+ })
189
+ )
190
+ }
191
+ }
192
+ return history
193
+ })
194
+
195
+ const historyToSnapshot = (
196
+ machine: Machine.Any,
197
+ history: ReadonlyMap<string, HistoryRecord>
198
+ ): Readonly<
199
+ Record<string, {
200
+ readonly mode: "shallow" | "deep"
201
+ readonly active: ReadonlyArray<string>
202
+ readonly values: Readonly<Record<string, unknown>>
203
+ }>
204
+ > => {
205
+ const entries: Record<string, {
206
+ readonly mode: "shallow" | "deep"
207
+ readonly active: ReadonlyArray<string>
208
+ readonly values: Readonly<Record<string, unknown>>
209
+ }> = {}
210
+ for (const [path, record] of history) {
211
+ const active = Array.from(record.active).sort((left, right) => compareDocumentOrder(machine, left, right))
212
+ entries[path] = {
213
+ mode: record.mode,
214
+ active,
215
+ values: Object.fromEntries(active.map((path) => [path, record.values.get(path)]))
216
+ }
217
+ }
218
+ return entries
219
+ }
220
+
221
+ export interface ActiveConfiguration {
222
+ readonly active: ReadonlySet<string>
223
+ readonly values: ReadonlyMap<string, unknown>
224
+ readonly outputs: ReadonlyMap<string, unknown>
225
+ readonly history: ReadonlyMap<string, HistoryRecord>
226
+ }
227
+
228
+ export interface FinalCompletion {
229
+ readonly path: string
230
+ readonly output: unknown
231
+ }
232
+
233
+ interface CompletionResult extends FinalCompletion {
234
+ readonly isNew: boolean
235
+ }
236
+
237
+ const pathToRootCache = new WeakMap<Machine.StateNodes, Map<string, ReadonlyArray<string>>>()
238
+
239
+ export const hasOwn = (u: object, key: string): boolean => Object.prototype.hasOwnProperty.call(u, key)
240
+
241
+ export const isDescendantOf = (path: string, ancestor: string): boolean => path.startsWith(`${ancestor}.`)
242
+
243
+ export const isPathInSubtree = (path: string, ancestor: string): boolean =>
244
+ path === ancestor || isDescendantOf(path, ancestor)
245
+
246
+ export const getPathToRoot = (machine: Machine.Any, path: string): ReadonlyArray<string> => {
247
+ let pathsByLeaf = pathToRootCache.get(machine.stateNodes)
248
+ if (pathsByLeaf === undefined) {
249
+ pathsByLeaf = new Map()
250
+ pathToRootCache.set(machine.stateNodes, pathsByLeaf)
251
+ }
252
+ const cached = pathsByLeaf.get(path)
253
+ if (cached !== undefined) {
254
+ return cached
255
+ }
256
+ const paths: Array<string> = []
257
+ let current: string | undefined = path
258
+ while (current !== undefined) {
259
+ paths.unshift(current)
260
+ current = getNode(machine, current).parent
261
+ }
262
+ pathsByLeaf.set(path, paths)
263
+ return paths
264
+ }
265
+
266
+ export const pathDepth = (machine: Machine.Any, path: string): number => getPathToRoot(machine, path).length
267
+
268
+ export const compareDocumentOrder = (machine: Machine.Any, left: string, right: string): number =>
269
+ getNode(machine, left).order - getNode(machine, right).order
270
+
271
+ export const hasActiveChild = (machine: Machine.Any, configuration: ActiveConfiguration, path: string): boolean =>
272
+ getNode(machine, path).children.some((child) => configuration.active.has(child))
273
+
274
+ export const getActiveLeafPaths = (machine: Machine.Any, configuration: ActiveConfiguration): ReadonlyArray<string> => {
275
+ const leaves = Array.from(configuration.active)
276
+ .filter((path) => !hasActiveChild(machine, configuration, path))
277
+ .sort((left, right) => compareDocumentOrder(machine, left, right))
278
+ if (leaves.length === 0) {
279
+ throw new Error("Machine expected an active leaf state")
280
+ }
281
+ return leaves
282
+ }
283
+
284
+ export const getLeafPath = (machine: Machine.Any, configuration: ActiveConfiguration): string =>
285
+ getActiveLeafPaths(
286
+ machine,
287
+ configuration
288
+ )[0]!
289
+
290
+ export const getActiveLeafPathFrom = (
291
+ machine: Machine.Any,
292
+ configuration: ActiveConfiguration,
293
+ path: string
294
+ ): string => {
295
+ const leaves = getActiveLeafPaths(machine, configuration)
296
+ .filter((leaf) => isPathInSubtree(leaf, path))
297
+ if (leaves.length === 0) {
298
+ throw new Error(`Machine expected state "${path}" to have an active leaf state`)
299
+ }
300
+ return leaves[0]!
301
+ }
302
+
303
+ export const getRootPath = (machine: Machine.Any, configuration: ActiveConfiguration): string => {
304
+ for (const path of configuration.active) {
305
+ if (getNode(machine, path).parent === undefined) {
306
+ return path
307
+ }
308
+ }
309
+ throw new Error("Machine expected an active root state")
310
+ }
311
+
312
+ export const getActiveValue = (configuration: ActiveConfiguration, path: string): unknown => {
313
+ if (!configuration.values.has(path)) {
314
+ throw new Error(
315
+ `Machine expected active state "${path}" to have a value (available: ${
316
+ Array.from(configuration.values.keys()).join(", ")
317
+ })`
318
+ )
319
+ }
320
+ return configuration.values.get(path)
321
+ }
322
+
323
+ export const getParentValues = (
324
+ machine: Machine.Any,
325
+ configuration: ActiveConfiguration,
326
+ path: string
327
+ ): Readonly<Record<string, unknown>> => {
328
+ const parents: Record<string, unknown> = {}
329
+ const paths = getPathToRoot(machine, path)
330
+ for (let index = 0; index < paths.length - 1; index++) {
331
+ const parent = paths[index]!
332
+ parents[parent] = getActiveValue(configuration, parent)
333
+ }
334
+ return parents
335
+ }
336
+
337
+ export const getParentValue = (
338
+ machine: Machine.Any,
339
+ configuration: ActiveConfiguration,
340
+ path: string
341
+ ): unknown => {
342
+ const parent = getNode(machine, path).parent
343
+ return parent === undefined ? undefined : getActiveValue(configuration, parent)
344
+ }
345
+
346
+ export const getInitialEntryPaths = (
347
+ machine: Machine.Any,
348
+ configuration: ActiveConfiguration
349
+ ): ReadonlyArray<string> => {
350
+ const visit = (path: string): ReadonlyArray<string> => {
351
+ if (!configuration.active.has(path)) {
352
+ return []
353
+ }
354
+ const node = getNode(machine, path)
355
+ return [
356
+ path,
357
+ ...node.children.flatMap(visit)
358
+ ]
359
+ }
360
+ return machine.stateNodes.roots.flatMap(visit)
361
+ }
362
+
363
+ export const snapshotFromPath = <const States extends Machine.StateSchemas>(
364
+ machine: Machine.Any,
365
+ configuration: ActiveConfiguration,
366
+ path: string
367
+ ): Machine.SnapshotByIdentifier<States, Machine.StateIdentifier<States>> => {
368
+ const node = getNode(machine, path)
369
+ const snapshot: Record<string, unknown> = {
370
+ path,
371
+ value: getActiveValue(configuration, path)
372
+ }
373
+ if (node.type === "compound") {
374
+ const child = node.children.find((child) => configuration.active.has(child))
375
+ if (child === undefined) {
376
+ throw new Error(`Machine expected compound state "${path}" to have an active child`)
377
+ }
378
+ snapshot.state = snapshotFromPath(machine, configuration, child)
379
+ }
380
+ if (node.type === "parallel") {
381
+ const states: Record<string, unknown> = {}
382
+ for (const child of node.children) {
383
+ if (!configuration.active.has(child)) {
384
+ throw new Error(`Machine expected parallel state "${path}" to have active child region "${child}"`)
385
+ }
386
+ const childNode = getNode(machine, child)
387
+ states[childNode.key] = snapshotFromPath(machine, configuration, child)
388
+ }
389
+ snapshot.states = states
390
+ }
391
+ return snapshot as unknown as Machine.SnapshotByIdentifier<States, Machine.StateIdentifier<States>>
392
+ }
393
+
394
+ export const snapshotFromConfiguration = <const States extends Machine.StateSchemas>(
395
+ machine: Machine.Any,
396
+ configuration: ActiveConfiguration
397
+ ): Machine.Snapshot<States> => {
398
+ const snapshot = snapshotFromPath<States>(
399
+ machine,
400
+ configuration,
401
+ getRootPath(machine, configuration)
402
+ ) as Machine.Snapshot<States>
403
+ const completed = Array.from(configuration.outputs)
404
+ .map(([path, output]) => ({ path, output }))
405
+ if (completed.length > 0) {
406
+ ;(snapshot as Machine.AtomicSnapshot<string, unknown> & {
407
+ completed: ReadonlyArray<Machine.SnapshotCompletion>
408
+ }).completed = completed
409
+ }
410
+ if (configuration.history.size > 0) {
411
+ Object.assign(snapshot, { history: historyToSnapshot(machine, configuration.history) })
412
+ }
413
+ return snapshot
414
+ }
415
+
416
+ /** Creates a targetable subtree snapshot while carrying machine-level history
417
+ * metadata on that subtree root. This is used when a nested history target
418
+ * must preserve active ancestors and unaffected parallel regions. */
419
+ export const snapshotFromConfigurationAtPath = <const States extends Machine.StateSchemas>(
420
+ machine: Machine.Any,
421
+ configuration: ActiveConfiguration,
422
+ path: string
423
+ ): Machine.SnapshotByIdentifier<States, Machine.StateIdentifier<States>> => {
424
+ const snapshot = snapshotFromPath<States>(machine, configuration, path)
425
+ if (configuration.history.size > 0) {
426
+ Object.assign(snapshot, { history: historyToSnapshot(machine, configuration.history) })
427
+ }
428
+ return snapshot
429
+ }
430
+
431
+ export const configurationFromSnapshot = (
432
+ machine: Machine.Any,
433
+ snapshot: Machine.AtomicSnapshot<string, unknown>
434
+ ): ActiveConfiguration => {
435
+ const active = new Set<string>()
436
+ const values = new Map<string, unknown>()
437
+ const snapshotOutputs = snapshot.completed
438
+
439
+ const visit = (current: Machine.AtomicSnapshot<string, unknown>): void => {
440
+ const node = getNode(machine, String(current.path))
441
+ const value = decodeStateValueSync(machine, node, current.value)
442
+ active.add(node.path)
443
+ values.set(node.path, value)
444
+ if (node.type === "compound") {
445
+ if (!hasProperty(current, "state") || !isSnapshot(current.state)) {
446
+ throw new Error(`Machine expected compound snapshot "${node.path}" to include an active child state`)
447
+ }
448
+ const child = getNode(machine, String(current.state.path))
449
+ if (child.parent !== node.path) {
450
+ throw new Error(`Machine expected snapshot "${child.path}" to be a child of "${node.path}"`)
451
+ }
452
+ visit(current.state)
453
+ }
454
+ if (node.type === "parallel") {
455
+ if (!hasProperty(current, "states") || typeof current.states !== "object" || current.states === null) {
456
+ throw new Error(`Machine expected parallel snapshot "${node.path}" to include active child regions`)
457
+ }
458
+ const states = current.states as Readonly<Record<string, unknown>>
459
+ for (const childPath of node.children) {
460
+ const child = getNode(machine, childPath)
461
+ const childSnapshot = states[child.key]
462
+ if (!hasOwn(states, child.key) || !isSnapshot(childSnapshot)) {
463
+ throw new Error(`Machine expected parallel snapshot "${node.path}" to include region "${child.key}"`)
464
+ }
465
+ const snapshotChild = getNode(machine, String(childSnapshot.path))
466
+ if (snapshotChild.path !== child.path) {
467
+ throw new Error(`Machine expected snapshot "${snapshotChild.path}" to be region "${child.path}"`)
468
+ }
469
+ visit(childSnapshot)
470
+ }
471
+ }
472
+ }
473
+
474
+ visit(snapshot)
475
+ const outputs = new Map<string, unknown>()
476
+ if (snapshotOutputs !== undefined) {
477
+ for (const { output, path } of snapshotOutputs) {
478
+ if (active.has(path)) {
479
+ outputs.set(path, output)
480
+ }
481
+ }
482
+ }
483
+ return { active, values, outputs, history: historyFromSnapshot(machine, snapshot) }
484
+ }
485
+
486
+ export const normalizeConfiguration = <const States extends Machine.StateSchemas>(
487
+ machine: Machine.Any,
488
+ state: Machine.Snapshot<States>
489
+ ): ActiveConfiguration => configurationFromSnapshot(machine, state)
490
+
491
+ export const normalizeConfigurationSync = <const States extends Machine.StateSchemas>(
492
+ machine: Machine.Any,
493
+ state: Machine.Snapshot<States>
494
+ ): ActiveConfiguration => {
495
+ try {
496
+ return configurationFromSnapshot(machine, state)
497
+ } catch (cause) {
498
+ if (cause instanceof MachineSchemaDecodeError) {
499
+ throw cause
500
+ }
501
+ throw new MachineSchemaDecodeError({
502
+ machineId: machine.id,
503
+ boundary: "configuration",
504
+ cause: Cause.die(cause)
505
+ })
506
+ }
507
+ }
508
+
509
+ export const configurationFromSnapshotEffect = Effect.fnUntraced(function*(
510
+ machine: Machine.Any,
511
+ snapshot: Machine.AtomicSnapshot<string, unknown>
512
+ ) {
513
+ const active = new Set<string>()
514
+ const values = new Map<string, unknown>()
515
+ const snapshotOutputs = snapshot.completed
516
+
517
+ const visit: (
518
+ current: Machine.AtomicSnapshot<string, unknown>
519
+ ) => Effect.Effect<void, MachineSchemaDecodeError> = Effect.fnUntraced(function*(
520
+ current: Machine.AtomicSnapshot<string, unknown>
521
+ ) {
522
+ const node = getNode(machine, String(current.path))
523
+ const value = yield* decodeStateValue(machine, node, current.value)
524
+ active.add(node.path)
525
+ values.set(node.path, value)
526
+ if (node.type === "compound") {
527
+ if (!hasProperty(current, "state") || !isSnapshot(current.state)) {
528
+ throw new Error(`Machine expected compound snapshot "${node.path}" to include an active child state`)
529
+ }
530
+ const child = getNode(machine, String(current.state.path))
531
+ if (child.parent !== node.path) {
532
+ throw new Error(`Machine expected snapshot "${child.path}" to be a child of "${node.path}"`)
533
+ }
534
+ yield* visit(current.state)
535
+ }
536
+ if (node.type === "parallel") {
537
+ if (!hasProperty(current, "states") || typeof current.states !== "object" || current.states === null) {
538
+ throw new Error(`Machine expected parallel snapshot "${node.path}" to include active child regions`)
539
+ }
540
+ const states = current.states as Readonly<Record<string, unknown>>
541
+ for (const childPath of node.children) {
542
+ const child = getNode(machine, childPath)
543
+ const childSnapshot = states[child.key]
544
+ if (!hasOwn(states, child.key) || !isSnapshot(childSnapshot)) {
545
+ throw new Error(`Machine expected parallel snapshot "${node.path}" to include region "${child.key}"`)
546
+ }
547
+ const snapshotChild = getNode(machine, String(childSnapshot.path))
548
+ if (snapshotChild.path !== child.path) {
549
+ throw new Error(`Machine expected snapshot "${snapshotChild.path}" to be region "${child.path}"`)
550
+ }
551
+ yield* visit(childSnapshot)
552
+ }
553
+ }
554
+ })
555
+
556
+ yield* visit(snapshot)
557
+ const outputs = new Map<string, unknown>()
558
+ if (snapshotOutputs !== undefined) {
559
+ for (const { output, path } of snapshotOutputs) {
560
+ if (active.has(path)) {
561
+ outputs.set(path, output)
562
+ }
563
+ }
564
+ }
565
+ return {
566
+ active,
567
+ values,
568
+ outputs,
569
+ history: yield* historyFromSnapshotEffect(machine, snapshot)
570
+ } as ActiveConfiguration
571
+ })
572
+
573
+ export const normalizeConfigurationEffect = <const States extends Machine.StateSchemas>(
574
+ machine: Machine.Any,
575
+ state: Machine.Snapshot<States>
576
+ ): Effect.Effect<ActiveConfiguration, MachineSchemaDecodeError> =>
577
+ configurationFromSnapshotEffect(machine, state).pipe(
578
+ Effect.catchCause((cause) => {
579
+ const error = Cause.findErrorOption(cause)
580
+ return Option.isSome(error) && error.value instanceof MachineSchemaDecodeError
581
+ ? Effect.fail(error.value)
582
+ : Effect.fail(
583
+ new MachineSchemaDecodeError({
584
+ machineId: machine.id,
585
+ boundary: "configuration",
586
+ cause
587
+ })
588
+ )
589
+ })
590
+ )
591
+
592
+ export const validateInitialConfiguration = (machine: Machine.Any, configuration: ActiveConfiguration): void => {
593
+ for (const path of configuration.active) {
594
+ const node = getNode(machine, path)
595
+ if (node.type === "compound") {
596
+ const child = node.children.find((child) => configuration.active.has(child))
597
+ const initialNode = node.initial === undefined ? undefined : getNode(machine, node.initial)
598
+ if (initialNode?.type === "choice" ? child === undefined : child !== node.initial) {
599
+ throw new Error(`Machine initial state "${node.path}" must enter initial child "${node.initial}"`)
600
+ }
601
+ }
602
+ if (node.type === "parallel") {
603
+ for (const child of node.children) {
604
+ if (!configuration.active.has(child)) {
605
+ throw new Error(`Machine initial state "${node.path}" must enter child region "${child}"`)
606
+ }
607
+ }
608
+ }
609
+ }
610
+ }
611
+
612
+ /** Capture every history register whose owning parent exits in this microstep.
613
+ * The control record is deliberately independent from effects/actions: it is
614
+ * part of the logical snapshot and is therefore preserved by pure planning. */
615
+ export const captureHistory = (
616
+ machine: Machine.Any,
617
+ current: ActiveConfiguration,
618
+ next: ActiveConfiguration,
619
+ exitPaths: ReadonlyArray<string>
620
+ ): ActiveConfiguration => {
621
+ if (exitPaths.length === 0) {
622
+ return next
623
+ }
624
+ const exited = new Set(exitPaths)
625
+ const history = new Map(next.history)
626
+ for (const node of machine.stateNodes.byPath.values() as Iterable<Machine.StateNode>) {
627
+ if (node.type !== "history" || node.parent === undefined || !exited.has(node.parent)) {
628
+ continue
629
+ }
630
+ const mode = node.history === "deep" ? "deep" : "shallow"
631
+ const active = new Set<string>()
632
+ for (const ancestor of getPathToRoot(machine, node.parent)) {
633
+ if (current.active.has(ancestor)) {
634
+ active.add(ancestor)
635
+ }
636
+ }
637
+ for (const path of current.active) {
638
+ if (
639
+ path === node.parent ||
640
+ (mode === "deep" && isDescendantOf(path, node.parent)) ||
641
+ (mode === "shallow" && getNode(machine, path).parent === node.parent)
642
+ ) {
643
+ active.add(path)
644
+ }
645
+ }
646
+ const values = new Map<string, unknown>()
647
+ for (const path of active) {
648
+ values.set(path, getActiveValue(current, path))
649
+ }
650
+ history.set(node.path, {
651
+ mode,
652
+ parent: node.parent,
653
+ active,
654
+ values
655
+ })
656
+ }
657
+ return {
658
+ active: next.active,
659
+ values: next.values,
660
+ outputs: next.outputs,
661
+ history
662
+ }
663
+ }
664
+
665
+ export const getHistoryRecord = (
666
+ configuration: ActiveConfiguration,
667
+ path: string
668
+ ): HistoryRecord | undefined => configuration.history.get(path)
669
+
670
+ /** Builds the remembered portion of a configuration. Shallow records are
671
+ * intentionally incomplete below their direct child; the planner completes
672
+ * them by invoking only the required typed initializers. */
673
+ export const configurationFromHistoryRecord = (
674
+ machine: Machine.Any,
675
+ current: ActiveConfiguration,
676
+ record: HistoryRecord
677
+ ): ActiveConfiguration => {
678
+ const active = new Set(record.active)
679
+ const values = new Map(record.values)
680
+ const outputs = new Map<string, unknown>()
681
+ const ancestors = getPathToRoot(machine, record.parent)
682
+ const ancestorSet = new Set(ancestors)
683
+
684
+ // A history transition can occur while an ancestor parallel state remains
685
+ // active. Its unaffected regions retain their current configuration.
686
+ for (const ancestor of ancestors) {
687
+ const node = getNode(machine, ancestor)
688
+ if (node.type !== "parallel") {
689
+ continue
690
+ }
691
+ for (const child of node.children) {
692
+ if (ancestorSet.has(child) || record.active.has(child) || !current.active.has(child)) {
693
+ continue
694
+ }
695
+ for (const path of current.active) {
696
+ if (isPathInSubtree(path, child)) {
697
+ active.add(path)
698
+ if (current.values.has(path)) values.set(path, current.values.get(path))
699
+ if (current.outputs.has(path)) outputs.set(path, current.outputs.get(path))
700
+ }
701
+ }
702
+ }
703
+ }
704
+
705
+ return { active, values, outputs, history: current.history }
706
+ }
707
+
708
+ export const configurationFromTargetPathEffect = Effect.fnUntraced(function*(
709
+ machine: Machine.Any,
710
+ current: ActiveConfiguration,
711
+ path: string,
712
+ value: unknown,
713
+ providedValues: Readonly<Record<string, unknown>> | undefined
714
+ ) {
715
+ const node = getNode(machine, path)
716
+ const active = new Set<string>()
717
+ const values = new Map<string, unknown>()
718
+ const outputs = new Map<string, unknown>()
719
+ const paths = getPathToRoot(machine, node.path)
720
+ const pathSet = new Set(paths)
721
+
722
+ for (const currentPath of paths) {
723
+ const currentNode = getNode(machine, currentPath)
724
+ active.add(currentPath)
725
+ if (currentPath === node.path) {
726
+ values.set(currentPath, yield* decodeStateValue(machine, currentNode, value))
727
+ } else if (providedValues !== undefined && hasOwn(providedValues, currentPath)) {
728
+ values.set(currentPath, yield* decodeStateValue(machine, currentNode, providedValues[currentPath]))
729
+ } else if (current.values.has(currentPath)) {
730
+ values.set(currentPath, current.values.get(currentPath))
731
+ } else {
732
+ throw new Error(`Machine target "${node.path}" requires a value for ancestor state "${currentPath}"`)
733
+ }
734
+ }
735
+
736
+ for (const ancestor of paths) {
737
+ const ancestorNode = getNode(machine, ancestor)
738
+ if (ancestorNode.type === "parallel") {
739
+ for (const child of ancestorNode.children) {
740
+ if (pathSet.has(child) || !current.active.has(child)) {
741
+ continue
742
+ }
743
+ for (const activePath of current.active) {
744
+ if (isPathInSubtree(activePath, child)) {
745
+ active.add(activePath)
746
+ if (current.values.has(activePath)) {
747
+ values.set(activePath, current.values.get(activePath))
748
+ }
749
+ if (current.outputs.has(activePath)) {
750
+ outputs.set(activePath, current.outputs.get(activePath))
751
+ }
752
+ }
753
+ }
754
+ }
755
+ }
756
+ }
757
+
758
+ if (node.type === "compound" || node.type === "parallel") {
759
+ throw new Error(`Machine target "${node.path}" must include an active child state`)
760
+ }
761
+
762
+ return {
763
+ active,
764
+ values,
765
+ outputs,
766
+ history: current.history
767
+ } as ActiveConfiguration
768
+ })
769
+
770
+ export const configurationFromTargetSnapshotEffect = Effect.fnUntraced(function*(
771
+ machine: Machine.Any,
772
+ current: ActiveConfiguration,
773
+ snapshot: Machine.AtomicSnapshot<string, unknown>,
774
+ providedValues: Readonly<Record<string, unknown>> | undefined
775
+ ) {
776
+ const subtree = yield* configurationFromSnapshotEffect(machine, snapshot)
777
+ const active = new Set(subtree.active)
778
+ const values = new Map(subtree.values)
779
+ const outputs = new Map(subtree.outputs)
780
+ const paths = getPathToRoot(machine, String(snapshot.path))
781
+ const pathSet = new Set(paths)
782
+
783
+ for (const ancestor of paths.slice(0, -1)) {
784
+ const node = getNode(machine, ancestor)
785
+ active.add(ancestor)
786
+ if (providedValues !== undefined && hasOwn(providedValues, ancestor)) {
787
+ values.set(ancestor, yield* decodeStateValue(machine, node, providedValues[ancestor]))
788
+ } else if (current.values.has(ancestor)) {
789
+ values.set(ancestor, current.values.get(ancestor))
790
+ } else {
791
+ throw new Error(`Machine target "${snapshot.path}" requires a value for ancestor state "${ancestor}"`)
792
+ }
793
+ }
794
+
795
+ for (const ancestor of paths.slice(0, -1)) {
796
+ const ancestorNode = getNode(machine, ancestor)
797
+ if (ancestorNode.type === "parallel") {
798
+ for (const child of ancestorNode.children) {
799
+ if (pathSet.has(child)) continue
800
+ if (current.active.has(child)) {
801
+ for (const activePath of current.active) {
802
+ if (isPathInSubtree(activePath, child)) {
803
+ active.add(activePath)
804
+ if (current.values.has(activePath)) {
805
+ values.set(activePath, current.values.get(activePath))
806
+ }
807
+ if (current.outputs.has(activePath)) {
808
+ outputs.set(activePath, current.outputs.get(activePath))
809
+ }
810
+ }
811
+ }
812
+ continue
813
+ }
814
+ if (providedValues !== undefined && hasOwn(providedValues, child)) {
815
+ for (const [providedPath, providedValue] of Object.entries(providedValues)) {
816
+ if (!isPathInSubtree(providedPath, child)) continue
817
+ const providedNode = getNode(machine, providedPath)
818
+ active.add(providedPath)
819
+ values.set(providedPath, yield* decodeStateValue(machine, providedNode, providedValue))
820
+ }
821
+ }
822
+ }
823
+ }
824
+ }
825
+
826
+ return {
827
+ active,
828
+ values,
829
+ outputs,
830
+ history: new Map([...current.history, ...subtree.history])
831
+ } as ActiveConfiguration
832
+ })
833
+
834
+ export const normalizeTargetConfigurationEffect = <const States extends Machine.StateSchemas>(
835
+ machine: Machine.Any,
836
+ current: ActiveConfiguration,
837
+ target: Machine.Snapshot<States> | Machine.Target<States, Machine.StateIdentifier<States>>
838
+ ): Effect.Effect<ActiveConfiguration, MachineSchemaDecodeError> => {
839
+ if (isTarget(target)) {
840
+ const snapshot = target[TargetSnapshotTypeId]
841
+ if (snapshot !== undefined) {
842
+ if (String(snapshot.path) !== String(target.path)) {
843
+ throw new Error(`Machine expected target snapshot path to be "${target.path}"`)
844
+ }
845
+ return configurationFromTargetSnapshotEffect(
846
+ machine,
847
+ current,
848
+ snapshot,
849
+ target.values as Readonly<Record<string, unknown>> | undefined
850
+ )
851
+ }
852
+ return configurationFromTargetPathEffect(
853
+ machine,
854
+ current,
855
+ target.path,
856
+ target.value,
857
+ target.values as Readonly<Record<string, unknown>> | undefined
858
+ )
859
+ }
860
+ if (isSnapshot(target)) {
861
+ return normalizeConfigurationEffect(machine, target).pipe(
862
+ Effect.map((configuration) => ({
863
+ ...configuration,
864
+ history: new Map([...current.history, ...configuration.history])
865
+ }))
866
+ )
867
+ }
868
+ throw new Error("Machine expected transition target to be a snapshot or target builder result")
869
+ }
870
+
871
+ const configurationFromTargetPathSync = (
872
+ machine: Machine.Any,
873
+ current: ActiveConfiguration,
874
+ path: string,
875
+ value: unknown,
876
+ providedValues: Readonly<Record<string, unknown>> | undefined
877
+ ): ActiveConfiguration => {
878
+ const node = getNode(machine, path)
879
+ const active = new Set<string>()
880
+ const values = new Map<string, unknown>()
881
+ const outputs = new Map<string, unknown>()
882
+ const paths = getPathToRoot(machine, node.path)
883
+ const pathSet = new Set(paths)
884
+
885
+ for (const currentPath of paths) {
886
+ const currentNode = getNode(machine, currentPath)
887
+ active.add(currentPath)
888
+ if (currentPath === node.path) {
889
+ values.set(currentPath, decodeStateValueSync(machine, currentNode, value))
890
+ } else if (providedValues !== undefined && hasOwn(providedValues, currentPath)) {
891
+ values.set(currentPath, decodeStateValueSync(machine, currentNode, providedValues[currentPath]))
892
+ } else if (current.values.has(currentPath)) {
893
+ values.set(currentPath, current.values.get(currentPath))
894
+ } else {
895
+ throw new Error(`Machine target "${node.path}" requires a value for ancestor state "${currentPath}"`)
896
+ }
897
+ }
898
+
899
+ for (const ancestor of paths) {
900
+ const ancestorNode = getNode(machine, ancestor)
901
+ if (ancestorNode.type !== "parallel") continue
902
+ for (const child of ancestorNode.children) {
903
+ if (pathSet.has(child) || !current.active.has(child)) continue
904
+ for (const activePath of current.active) {
905
+ if (!isPathInSubtree(activePath, child)) continue
906
+ active.add(activePath)
907
+ if (current.values.has(activePath)) values.set(activePath, current.values.get(activePath))
908
+ if (current.outputs.has(activePath)) outputs.set(activePath, current.outputs.get(activePath))
909
+ }
910
+ }
911
+ }
912
+
913
+ if (node.type === "compound" || node.type === "parallel") {
914
+ throw new Error(`Machine target "${node.path}" must include an active child state`)
915
+ }
916
+ return { active, values, outputs, history: current.history }
917
+ }
918
+
919
+ const configurationFromTargetSnapshotSync = (
920
+ machine: Machine.Any,
921
+ current: ActiveConfiguration,
922
+ snapshot: Machine.AtomicSnapshot<string, unknown>,
923
+ providedValues: Readonly<Record<string, unknown>> | undefined
924
+ ): ActiveConfiguration => {
925
+ const subtree = configurationFromSnapshot(machine, snapshot)
926
+ const active = new Set(subtree.active)
927
+ const values = new Map(subtree.values)
928
+ const outputs = new Map(subtree.outputs)
929
+ const paths = getPathToRoot(machine, String(snapshot.path))
930
+ const pathSet = new Set(paths)
931
+
932
+ for (const ancestor of paths.slice(0, -1)) {
933
+ const node = getNode(machine, ancestor)
934
+ active.add(ancestor)
935
+ if (providedValues !== undefined && hasOwn(providedValues, ancestor)) {
936
+ values.set(ancestor, decodeStateValueSync(machine, node, providedValues[ancestor]))
937
+ } else if (current.values.has(ancestor)) {
938
+ values.set(ancestor, current.values.get(ancestor))
939
+ } else {
940
+ throw new Error(`Machine target "${snapshot.path}" requires a value for ancestor state "${ancestor}"`)
941
+ }
942
+ }
943
+
944
+ for (const ancestor of paths.slice(0, -1)) {
945
+ const ancestorNode = getNode(machine, ancestor)
946
+ if (ancestorNode.type !== "parallel") continue
947
+ for (const child of ancestorNode.children) {
948
+ if (pathSet.has(child)) continue
949
+ if (current.active.has(child)) {
950
+ for (const activePath of current.active) {
951
+ if (!isPathInSubtree(activePath, child)) continue
952
+ active.add(activePath)
953
+ if (current.values.has(activePath)) values.set(activePath, current.values.get(activePath))
954
+ if (current.outputs.has(activePath)) outputs.set(activePath, current.outputs.get(activePath))
955
+ }
956
+ continue
957
+ }
958
+ if (providedValues !== undefined && hasOwn(providedValues, child)) {
959
+ for (const [providedPath, providedValue] of Object.entries(providedValues)) {
960
+ if (!isPathInSubtree(providedPath, child)) continue
961
+ const providedNode = getNode(machine, providedPath)
962
+ active.add(providedPath)
963
+ values.set(providedPath, decodeStateValueSync(machine, providedNode, providedValue))
964
+ }
965
+ }
966
+ }
967
+ }
968
+ return {
969
+ active,
970
+ values,
971
+ outputs,
972
+ history: new Map([...current.history, ...subtree.history])
973
+ }
974
+ }
975
+
976
+ export const normalizeTargetConfigurationSync = <const States extends Machine.StateSchemas>(
977
+ machine: Machine.Any,
978
+ current: ActiveConfiguration,
979
+ target: Machine.Snapshot<States> | Machine.Target<States, Machine.StateIdentifier<States>>
980
+ ): ActiveConfiguration => {
981
+ if (isTarget(target)) {
982
+ const snapshot = target[TargetSnapshotTypeId]
983
+ if (snapshot !== undefined) {
984
+ if (String(snapshot.path) !== String(target.path)) {
985
+ throw new Error(`Machine expected target snapshot path to be "${target.path}"`)
986
+ }
987
+ return configurationFromTargetSnapshotSync(
988
+ machine,
989
+ current,
990
+ snapshot,
991
+ target.values as Readonly<Record<string, unknown>> | undefined
992
+ )
993
+ }
994
+ return configurationFromTargetPathSync(
995
+ machine,
996
+ current,
997
+ target.path,
998
+ target.value,
999
+ target.values as Readonly<Record<string, unknown>> | undefined
1000
+ )
1001
+ }
1002
+ if (isSnapshot(target)) {
1003
+ const configuration = normalizeConfigurationSync(machine, target)
1004
+ return { ...configuration, history: new Map([...current.history, ...configuration.history]) }
1005
+ }
1006
+ throw new Error("Machine expected transition target to be a snapshot or target builder result")
1007
+ }
1008
+
1009
+ export const getStateConfigByPath = (
1010
+ machine: Machine.Any,
1011
+ path: string
1012
+ ): Machine.AnyStateConfig | undefined => machine.handlers[path]
1013
+
1014
+ export const getActiveChildPath = (
1015
+ machine: Machine.Any,
1016
+ configuration: ActiveConfiguration,
1017
+ path: string
1018
+ ): string | undefined => getNode(machine, path).children.find((child) => configuration.active.has(child))
1019
+
1020
+ export const isDirectFinalPath = (
1021
+ machine: Machine.Any,
1022
+ path: string
1023
+ ): boolean => getNode(machine, path).type === "final"
1024
+
1025
+ export const hasCompletionHandler = (
1026
+ machine: Machine.Any,
1027
+ path: string
1028
+ ): boolean => getStateConfigByPath(machine, path)?.onDone !== undefined
1029
+
1030
+ export const isActiveFinalNode = (
1031
+ machine: Machine.Any,
1032
+ configuration: ActiveConfiguration,
1033
+ path: string
1034
+ ): boolean => {
1035
+ if (!configuration.active.has(path)) {
1036
+ return false
1037
+ }
1038
+ const node = getNode(machine, path)
1039
+ if (node.type === "compound") {
1040
+ const child = getActiveChildPath(machine, configuration, path)
1041
+ return child !== undefined && isDirectFinalPath(machine, child)
1042
+ }
1043
+ if (node.type === "parallel") {
1044
+ for (const child of node.children) {
1045
+ if (!isActiveFinalNode(machine, configuration, child)) {
1046
+ return false
1047
+ }
1048
+ }
1049
+ return true
1050
+ }
1051
+ return isDirectFinalPath(machine, path)
1052
+ }
1053
+
1054
+ export const isActiveFinalConfiguration = (
1055
+ machine: Machine.Any,
1056
+ configuration: ActiveConfiguration
1057
+ ): boolean => {
1058
+ const root = getRootPath(machine, configuration)
1059
+ return isActiveFinalNode(machine, configuration, root) && !hasCompletionHandler(machine, root)
1060
+ }
1061
+
1062
+ export const setCompletedOutput = (
1063
+ outputs: Map<string, unknown>,
1064
+ path: string,
1065
+ output: unknown
1066
+ ): CompletionResult => {
1067
+ if (outputs.has(path)) {
1068
+ return {
1069
+ path,
1070
+ output: outputs.get(path),
1071
+ isNew: false
1072
+ }
1073
+ }
1074
+ outputs.set(path, output)
1075
+ return {
1076
+ path,
1077
+ output,
1078
+ isNew: true
1079
+ }
1080
+ }
1081
+
1082
+ export const resolveFinalOutputEffect: <
1083
+ const Events extends ReadonlyArray<Machine.TaggedSchema>
1084
+ >(
1085
+ machine: Machine.Any,
1086
+ configuration: ActiveConfiguration,
1087
+ path: string,
1088
+ event: Machine.LifecycleEvent<Events>,
1089
+ outputs?: Readonly<Record<string, unknown>>
1090
+ ) => Effect.Effect<unknown, MachineSchemaDecodeError> = Effect.fnUntraced(function*<
1091
+ const Events extends ReadonlyArray<Machine.TaggedSchema>
1092
+ >(
1093
+ machine: Machine.Any,
1094
+ configuration: ActiveConfiguration,
1095
+ path: string,
1096
+ event: Machine.LifecycleEvent<Events>,
1097
+ outputs?: Readonly<Record<string, unknown>>
1098
+ ) {
1099
+ const node = getNode(machine, path)
1100
+ const output = getStateConfigByPath(machine, path)?.output?.({
1101
+ state: getActiveValue(configuration, path),
1102
+ parent: getParentValue(machine, configuration, path),
1103
+ parents: getParentValues(machine, configuration, path),
1104
+ event,
1105
+ outputs
1106
+ } as any)
1107
+ return yield* decodeOutputValue(machine, node, output)
1108
+ })
1109
+
1110
+ export const completeActiveFinalNodeEffect: <
1111
+ const Events extends ReadonlyArray<Machine.TaggedSchema>
1112
+ >(
1113
+ machine: Machine.Any,
1114
+ configuration: ActiveConfiguration,
1115
+ path: string,
1116
+ event: Machine.LifecycleEvent<Events>,
1117
+ outputs: Map<string, unknown>,
1118
+ completions: Array<FinalCompletion>
1119
+ ) => Effect.Effect<CompletionResult | undefined, MachineSchemaDecodeError> = Effect.fnUntraced(function*<
1120
+ const Events extends ReadonlyArray<Machine.TaggedSchema>
1121
+ >(
1122
+ machine: Machine.Any,
1123
+ configuration: ActiveConfiguration,
1124
+ path: string,
1125
+ event: Machine.LifecycleEvent<Events>,
1126
+ outputs: Map<string, unknown>,
1127
+ completions: Array<FinalCompletion>
1128
+ ) {
1129
+ if (!configuration.active.has(path)) {
1130
+ return undefined
1131
+ }
1132
+ if (outputs.has(path)) {
1133
+ return {
1134
+ path,
1135
+ output: outputs.get(path),
1136
+ isNew: false
1137
+ }
1138
+ }
1139
+ const node = getNode(machine, path)
1140
+ if (node.type === "compound") {
1141
+ const child = getActiveChildPath(machine, configuration, path)
1142
+ if (child === undefined || !isDirectFinalPath(machine, child)) {
1143
+ return undefined
1144
+ }
1145
+ const childCompletion = yield* completeActiveFinalNodeEffect(
1146
+ machine,
1147
+ configuration,
1148
+ child,
1149
+ event,
1150
+ outputs,
1151
+ completions
1152
+ )
1153
+ if (childCompletion === undefined) {
1154
+ return undefined
1155
+ }
1156
+ const completion = setCompletedOutput(outputs, path, childCompletion.output)
1157
+ if (completion.isNew) {
1158
+ completions.push(completion)
1159
+ }
1160
+ return completion
1161
+ }
1162
+ if (node.type === "parallel") {
1163
+ const regionOutputs: Record<string, unknown> = {}
1164
+ let completed = true
1165
+ for (const child of node.children) {
1166
+ const childCompletion = yield* completeActiveFinalNodeEffect(
1167
+ machine,
1168
+ configuration,
1169
+ child,
1170
+ event,
1171
+ outputs,
1172
+ completions
1173
+ )
1174
+ if (childCompletion === undefined) {
1175
+ completed = false
1176
+ } else {
1177
+ regionOutputs[getNode(machine, child).key] = childCompletion.output
1178
+ }
1179
+ }
1180
+ if (!completed) {
1181
+ return undefined
1182
+ }
1183
+ const completion = setCompletedOutput(
1184
+ outputs,
1185
+ path,
1186
+ yield* resolveFinalOutputEffect(machine, configuration, path, event, regionOutputs)
1187
+ )
1188
+ if (completion.isNew) {
1189
+ completions.push(completion)
1190
+ }
1191
+ return completion
1192
+ }
1193
+ if (!isDirectFinalPath(machine, path)) {
1194
+ return undefined
1195
+ }
1196
+ const completion = setCompletedOutput(
1197
+ outputs,
1198
+ path,
1199
+ yield* resolveFinalOutputEffect(machine, configuration, path, event)
1200
+ )
1201
+ if (completion.isNew) {
1202
+ completions.push(completion)
1203
+ }
1204
+ return completion
1205
+ })
1206
+
1207
+ export const completeConfigurationEffect: <
1208
+ const Events extends ReadonlyArray<Machine.TaggedSchema>
1209
+ >(
1210
+ machine: Machine.Any,
1211
+ configuration: ActiveConfiguration,
1212
+ event: Machine.LifecycleEvent<Events>
1213
+ ) => Effect.Effect<{
1214
+ readonly configuration: ActiveConfiguration
1215
+ readonly completions: ReadonlyArray<FinalCompletion>
1216
+ }, MachineSchemaDecodeError> = Effect.fnUntraced(function*<
1217
+ const Events extends ReadonlyArray<Machine.TaggedSchema>
1218
+ >(
1219
+ machine: Machine.Any,
1220
+ configuration: ActiveConfiguration,
1221
+ event: Machine.LifecycleEvent<Events>
1222
+ ) {
1223
+ const outputs = new Map(configuration.outputs)
1224
+ const completions: Array<FinalCompletion> = []
1225
+ const completed = {
1226
+ active: configuration.active,
1227
+ values: configuration.values,
1228
+ outputs,
1229
+ history: configuration.history
1230
+ }
1231
+ for (
1232
+ const path of Array.from(completed.active).sort((left, right) => {
1233
+ const depth = pathDepth(machine, right) - pathDepth(machine, left)
1234
+ return depth === 0 ? compareDocumentOrder(machine, left, right) : depth
1235
+ })
1236
+ ) {
1237
+ yield* completeActiveFinalNodeEffect(machine, completed, path, event, outputs, completions)
1238
+ }
1239
+ return { configuration: completed, completions } as {
1240
+ readonly configuration: ActiveConfiguration
1241
+ readonly completions: ReadonlyArray<FinalCompletion>
1242
+ }
1243
+ })
1244
+
1245
+ const resolveFinalOutputSync = <const Events extends ReadonlyArray<Machine.TaggedSchema>>(
1246
+ machine: Machine.Any,
1247
+ configuration: ActiveConfiguration,
1248
+ path: string,
1249
+ event: Machine.LifecycleEvent<Events>,
1250
+ outputs?: Readonly<Record<string, unknown>>
1251
+ ): unknown => {
1252
+ const node = getNode(machine, path)
1253
+ const output = getStateConfigByPath(machine, path)?.output?.({
1254
+ state: getActiveValue(configuration, path),
1255
+ parent: getParentValue(machine, configuration, path),
1256
+ parents: getParentValues(machine, configuration, path),
1257
+ event,
1258
+ outputs
1259
+ } as any)
1260
+ return decodeOutputValueSync(machine, node, output)
1261
+ }
1262
+
1263
+ const completeActiveFinalNodeSync = <const Events extends ReadonlyArray<Machine.TaggedSchema>>(
1264
+ machine: Machine.Any,
1265
+ configuration: ActiveConfiguration,
1266
+ path: string,
1267
+ event: Machine.LifecycleEvent<Events>,
1268
+ outputs: Map<string, unknown>,
1269
+ completions: Array<FinalCompletion>
1270
+ ): CompletionResult | undefined => {
1271
+ if (!configuration.active.has(path)) return undefined
1272
+ if (outputs.has(path)) return { path, output: outputs.get(path), isNew: false }
1273
+ const node = getNode(machine, path)
1274
+ if (node.type === "compound") {
1275
+ const child = getActiveChildPath(machine, configuration, path)
1276
+ if (child === undefined || !isDirectFinalPath(machine, child)) return undefined
1277
+ const childCompletion = completeActiveFinalNodeSync(machine, configuration, child, event, outputs, completions)
1278
+ if (childCompletion === undefined) return undefined
1279
+ const completion = setCompletedOutput(outputs, path, childCompletion.output)
1280
+ if (completion.isNew) completions.push(completion)
1281
+ return completion
1282
+ }
1283
+ if (node.type === "parallel") {
1284
+ const regionOutputs: Record<string, unknown> = {}
1285
+ let completed = true
1286
+ for (const child of node.children) {
1287
+ const childCompletion = completeActiveFinalNodeSync(machine, configuration, child, event, outputs, completions)
1288
+ if (childCompletion === undefined) completed = false
1289
+ else regionOutputs[getNode(machine, child).key] = childCompletion.output
1290
+ }
1291
+ if (!completed) return undefined
1292
+ const completion = setCompletedOutput(
1293
+ outputs,
1294
+ path,
1295
+ resolveFinalOutputSync(machine, configuration, path, event, regionOutputs)
1296
+ )
1297
+ if (completion.isNew) completions.push(completion)
1298
+ return completion
1299
+ }
1300
+ if (!isDirectFinalPath(machine, path)) return undefined
1301
+ const completion = setCompletedOutput(outputs, path, resolveFinalOutputSync(machine, configuration, path, event))
1302
+ if (completion.isNew) completions.push(completion)
1303
+ return completion
1304
+ }
1305
+
1306
+ export const completeConfigurationSync = <const Events extends ReadonlyArray<Machine.TaggedSchema>>(
1307
+ machine: Machine.Any,
1308
+ configuration: ActiveConfiguration,
1309
+ event: Machine.LifecycleEvent<Events>
1310
+ ): {
1311
+ readonly configuration: ActiveConfiguration
1312
+ readonly completions: ReadonlyArray<FinalCompletion>
1313
+ } => {
1314
+ const outputs = new Map(configuration.outputs)
1315
+ const completions: Array<FinalCompletion> = []
1316
+ const completed: ActiveConfiguration = {
1317
+ active: configuration.active,
1318
+ values: configuration.values,
1319
+ outputs,
1320
+ history: configuration.history
1321
+ }
1322
+ for (
1323
+ const path of Array.from(completed.active).sort((left, right) => {
1324
+ const depth = pathDepth(machine, right) - pathDepth(machine, left)
1325
+ return depth === 0 ? compareDocumentOrder(machine, left, right) : depth
1326
+ })
1327
+ ) {
1328
+ completeActiveFinalNodeSync(machine, completed, path, event, outputs, completions)
1329
+ }
1330
+ return { configuration: completed, completions }
1331
+ }