@typeonce/effect-machine 0.12.0 → 0.14.0

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 (103) hide show
  1. package/README.md +130 -30
  2. package/dist/Machine.d.ts +722 -262
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +130 -62
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/atom.d.ts +1 -0
  7. package/dist/internal/machine/atom.d.ts.map +1 -1
  8. package/dist/internal/machine/atom.js +23 -0
  9. package/dist/internal/machine/atom.js.map +1 -1
  10. package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
  11. package/dist/internal/machine/commandRuntime.js +2 -5
  12. package/dist/internal/machine/commandRuntime.js.map +1 -1
  13. package/dist/internal/machine/configuration.d.ts +5 -0
  14. package/dist/internal/machine/configuration.d.ts.map +1 -1
  15. package/dist/internal/machine/configuration.js +6 -2
  16. package/dist/internal/machine/configuration.js.map +1 -1
  17. package/dist/internal/machine/executionPlan.d.ts +1 -0
  18. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  19. package/dist/internal/machine/executionPlan.js +57 -24
  20. package/dist/internal/machine/executionPlan.js.map +1 -1
  21. package/dist/internal/machine/initialization.d.ts +5 -0
  22. package/dist/internal/machine/initialization.d.ts.map +1 -0
  23. package/dist/internal/machine/initialization.js +61 -0
  24. package/dist/internal/machine/initialization.js.map +1 -0
  25. package/dist/internal/machine/inspectionRuntime.d.ts +25 -0
  26. package/dist/internal/machine/inspectionRuntime.d.ts.map +1 -0
  27. package/dist/internal/machine/inspectionRuntime.js +79 -0
  28. package/dist/internal/machine/inspectionRuntime.js.map +1 -0
  29. package/dist/internal/machine/invocation.d.ts +2 -1
  30. package/dist/internal/machine/invocation.d.ts.map +1 -1
  31. package/dist/internal/machine/invocation.js +17 -10
  32. package/dist/internal/machine/invocation.js.map +1 -1
  33. package/dist/internal/machine/machine.d.ts +4 -3
  34. package/dist/internal/machine/machine.d.ts.map +1 -1
  35. package/dist/internal/machine/machine.js +327 -32
  36. package/dist/internal/machine/machine.js.map +1 -1
  37. package/dist/internal/machine/planner.d.ts +28 -0
  38. package/dist/internal/machine/planner.d.ts.map +1 -1
  39. package/dist/internal/machine/planner.js +132 -51
  40. package/dist/internal/machine/planner.js.map +1 -1
  41. package/dist/internal/machine/process.d.ts.map +1 -1
  42. package/dist/internal/machine/process.js +4 -2
  43. package/dist/internal/machine/process.js.map +1 -1
  44. package/dist/internal/machine/runtime.d.ts +27 -3
  45. package/dist/internal/machine/runtime.d.ts.map +1 -1
  46. package/dist/internal/machine/runtime.js +440 -64
  47. package/dist/internal/machine/runtime.js.map +1 -1
  48. package/dist/internal/machine/symbols.d.ts +2 -0
  49. package/dist/internal/machine/symbols.d.ts.map +1 -1
  50. package/dist/internal/machine/symbols.js +2 -0
  51. package/dist/internal/machine/symbols.js.map +1 -1
  52. package/dist/internal/machine/topology.d.ts +25 -0
  53. package/dist/internal/machine/topology.d.ts.map +1 -1
  54. package/dist/internal/machine/topology.js +26 -10
  55. package/dist/internal/machine/topology.js.map +1 -1
  56. package/dist/internal/testing/machine/exploration.d.ts.map +1 -1
  57. package/dist/internal/testing/machine/exploration.js +11 -2
  58. package/dist/internal/testing/machine/exploration.js.map +1 -1
  59. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
  60. package/dist/internal/testing/machine/finiteModel.js +75 -69
  61. package/dist/internal/testing/machine/finiteModel.js.map +1 -1
  62. package/dist/internal/testing/machine/transitionCoverage.d.ts +20 -0
  63. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -0
  64. package/dist/internal/testing/machine/transitionCoverage.js +80 -0
  65. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -0
  66. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  67. package/dist/internal/testing/machine/verification.js +171 -33
  68. package/dist/internal/testing/machine/verification.js.map +1 -1
  69. package/dist/testing/MachineTest.d.ts +97 -22
  70. package/dist/testing/MachineTest.d.ts.map +1 -1
  71. package/dist/testing/MachineTest.js +58 -16
  72. package/dist/testing/MachineTest.js.map +1 -1
  73. package/dist/unstable/cluster/ClusterMachine.d.ts +4 -1
  74. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
  75. package/dist/unstable/cluster/ClusterMachine.js +4 -1
  76. package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
  77. package/dist/unstable/reactivity/AtomMachine.d.ts +21 -3
  78. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  79. package/dist/unstable/reactivity/AtomMachine.js +21 -3
  80. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  81. package/docs/agent-guide.md +240 -65
  82. package/package.json +5 -5
  83. package/src/Machine.ts +1240 -495
  84. package/src/internal/machine/atom.ts +26 -0
  85. package/src/internal/machine/commandRuntime.ts +2 -9
  86. package/src/internal/machine/configuration.ts +28 -3
  87. package/src/internal/machine/executionPlan.ts +72 -24
  88. package/src/internal/machine/initialization.ts +73 -0
  89. package/src/internal/machine/inspectionRuntime.ts +102 -0
  90. package/src/internal/machine/invocation.ts +37 -12
  91. package/src/internal/machine/machine.ts +438 -39
  92. package/src/internal/machine/planner.ts +180 -55
  93. package/src/internal/machine/process.ts +4 -2
  94. package/src/internal/machine/runtime.ts +661 -89
  95. package/src/internal/machine/symbols.ts +3 -0
  96. package/src/internal/machine/topology.ts +65 -12
  97. package/src/internal/testing/machine/exploration.ts +10 -2
  98. package/src/internal/testing/machine/finiteModel.ts +114 -80
  99. package/src/internal/testing/machine/transitionCoverage.ts +116 -0
  100. package/src/internal/testing/machine/verification.ts +216 -58
  101. package/src/testing/MachineTest.ts +118 -28
  102. package/src/unstable/cluster/ClusterMachine.ts +4 -1
  103. package/src/unstable/reactivity/AtomMachine.ts +24 -3
@@ -1,5 +1,8 @@
1
1
  /** @internal */
2
2
  export const InitialEventTypeId: unique symbol = Symbol("effect/Machine/InitialEvent")
3
3
 
4
+ /** @internal */
5
+ export const SnapshotBuilderStateTypeId: unique symbol = Symbol("effect/Machine/SnapshotBuilderState")
6
+
4
7
  /** @internal Returns process logic for a child descriptor and optional input. */
5
8
  export const ChildMachineLogicTypeId: unique symbol = Symbol("effect/Machine/ChildMachineLogic")
@@ -19,10 +19,14 @@ export const StateConstructionTypeId: unique symbol = Symbol("effect/Machine/Sta
19
19
 
20
20
  export const HistoryTargetTypeId: unique symbol = Symbol("effect/Machine/HistoryTarget")
21
21
 
22
+ export const InitialTargetTypeId: unique symbol = Symbol("effect/Machine/InitialTarget")
23
+
22
24
  export const ChoiceTargetTypeId: unique symbol = Symbol("effect/Machine/ChoiceTarget")
23
25
 
24
26
  export const NoTargetTypeId: unique symbol = Symbol("effect/Machine/NoTarget")
25
27
 
28
+ export const TargetSelectionTypeId: unique symbol = Symbol("effect/Machine/TargetSelection")
29
+
26
30
  interface StateInput {
27
31
  readonly [StateInputTypeId]: typeof StateInputTypeId
28
32
  readonly input: unknown
@@ -36,6 +40,17 @@ export interface HistoryTarget {
36
40
  readonly parent: string
37
41
  }
38
42
 
43
+ /** Internal instruction that enters the selected state's declared initial
44
+ * configuration. The planner supplies implicit child values through the
45
+ * owning states' `initialize` handlers. */
46
+ export interface InitialTarget {
47
+ readonly [InitialTargetTypeId]: typeof InitialTargetTypeId
48
+ readonly _tag: "InitialTarget"
49
+ readonly path: string
50
+ readonly value: unknown
51
+ readonly values?: Readonly<Record<string, unknown>>
52
+ }
53
+
39
54
  /** Internal target produced by a choice target builder. */
40
55
  export interface ChoiceTarget {
41
56
  readonly [ChoiceTargetTypeId]: typeof ChoiceTargetTypeId
@@ -49,6 +64,32 @@ export interface NoTarget {
49
64
  readonly [NoTargetTypeId]: typeof NoTargetTypeId
50
65
  }
51
66
 
67
+ export type TargetSelectionKind = "state" | "initial" | "history" | "choice" | "none"
68
+
69
+ export type TargetSelectionScope = "local" | "branch" | "full" | "initial"
70
+
71
+ /** Immutable destination selected while a machine definition is captured. */
72
+ export interface TargetSelection {
73
+ readonly [TargetSelectionTypeId]: typeof TargetSelectionTypeId
74
+ readonly kind: TargetSelectionKind
75
+ readonly scope: TargetSelectionScope | undefined
76
+ readonly path: string | undefined
77
+ }
78
+
79
+ export const makeTargetSelection = (
80
+ kind: TargetSelectionKind,
81
+ path?: string,
82
+ scope?: TargetSelectionScope
83
+ ): TargetSelection =>
84
+ Object.freeze({
85
+ [TargetSelectionTypeId]: TargetSelectionTypeId as typeof TargetSelectionTypeId,
86
+ kind,
87
+ scope,
88
+ path
89
+ })
90
+
91
+ export const isTargetSelection = (u: unknown): u is TargetSelection => hasProperty(u, TargetSelectionTypeId)
92
+
52
93
  const noTarget = Object.freeze({
53
94
  [NoTargetTypeId]: NoTargetTypeId
54
95
  }) as NoTarget
@@ -65,6 +106,20 @@ export const makeHistoryTarget = (path: string, parent: string): HistoryTarget =
65
106
 
66
107
  export const isHistoryTarget = (u: unknown): u is HistoryTarget => hasProperty(u, HistoryTargetTypeId)
67
108
 
109
+ export const makeInitialTarget = (
110
+ path: string,
111
+ value: unknown,
112
+ values?: Readonly<Record<string, unknown>>
113
+ ): InitialTarget => ({
114
+ [InitialTargetTypeId]: InitialTargetTypeId,
115
+ _tag: "InitialTarget",
116
+ path,
117
+ value,
118
+ ...(values === undefined ? {} : { values })
119
+ })
120
+
121
+ export const isInitialTarget = (u: unknown): u is InitialTarget => hasProperty(u, InitialTargetTypeId)
122
+
68
123
  export const makeChoiceTarget = (
69
124
  path: string,
70
125
  parent: string,
@@ -354,12 +409,10 @@ export const compileStateNodes = (states: Machine.StateSchemas): Machine.StateNo
354
409
  } as Machine.StateNodes
355
410
  }
356
411
 
357
- const dynamicTransitionTargets = { type: "dynamic" } as const
358
-
359
- const transitionTargets = (handler: unknown): Machine.TransitionTargets =>
360
- typeof handler === "object" && handler !== null && "targets" in handler && handler.targets !== undefined
361
- ? { type: "declared", paths: Array.from(handler.targets as ReadonlyArray<string>) }
362
- : dynamicTransitionTargets
412
+ const transitionBranches = (handler: unknown): ReadonlyArray<Machine.TransitionBranch> =>
413
+ typeof handler === "object" && handler !== null && "branches" in handler && Array.isArray(handler.branches)
414
+ ? Array.from(handler.branches as ReadonlyArray<Machine.TransitionBranch>)
415
+ : []
363
416
 
364
417
  export const transitionDefinitions = (
365
418
  machine: Machine.Any
@@ -377,7 +430,7 @@ export const transitionDefinitions = (
377
430
  source: node.path,
378
431
  trigger: { type: "choice" },
379
432
  reenter: false,
380
- targets: transitionTargets(choice)
433
+ branches: transitionBranches(choice)
381
434
  })
382
435
  }
383
436
  continue
@@ -387,8 +440,8 @@ export const transitionDefinitions = (
387
440
  definitions.push({
388
441
  source: node.path,
389
442
  trigger: { type: "event", event },
390
- reenter: typeof handler === "object" && handler !== null && handler.reenter === true,
391
- targets: transitionTargets(handler)
443
+ reenter: hasProperty(handler, "reenter") && handler.reenter === true,
444
+ branches: transitionBranches(handler)
392
445
  })
393
446
  }
394
447
  if (config.always !== undefined) {
@@ -396,7 +449,7 @@ export const transitionDefinitions = (
396
449
  source: node.path,
397
450
  trigger: { type: "always" },
398
451
  reenter: false,
399
- targets: transitionTargets(config.always)
452
+ branches: transitionBranches(config.always)
400
453
  })
401
454
  }
402
455
  if (config.onDone !== undefined) {
@@ -404,7 +457,7 @@ export const transitionDefinitions = (
404
457
  source: node.path,
405
458
  trigger: { type: "done" },
406
459
  reenter: false,
407
- targets: transitionTargets(config.onDone)
460
+ branches: transitionBranches(config.onDone)
408
461
  })
409
462
  }
410
463
  const invokes = config.invoke === undefined
@@ -425,7 +478,7 @@ export const transitionDefinitions = (
425
478
  source: node.path,
426
479
  trigger: { type: "invoke", id, outcome },
427
480
  reenter: typeof handler === "object" && handler !== null && handler.reenter === true,
428
- targets: transitionTargets(handler)
481
+ branches: transitionBranches(handler)
429
482
  })
430
483
  }
431
484
  }
@@ -30,6 +30,7 @@ import type {
30
30
  import type { EnsureExecutable } from "../../machine/readiness.js"
31
31
  import { assertInvariants, type InvariantError } from "./invariant.js"
32
32
  import { appendTrace, run } from "./trace.js"
33
+ import { makeTransitionCoverageCollector } from "./transitionCoverage.js"
33
34
 
34
35
  type AnyMachine = Machine.Machine.Any
35
36
 
@@ -119,6 +120,10 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
119
120
 
120
121
  const initialContext = stateContext(machine, initialTrace)
121
122
  const initialKey = validateKey(options.stateKey(initialContext))
123
+ const transitionCoverage = makeTransitionCoverageCollector<M>(machine)
124
+ for (const microstep of initialTrace.initial.plan.microsteps) {
125
+ transitionCoverage.observeMicrostep(microstep)
126
+ }
122
127
  const nodes: Array<ExplorationNode<M, Key>> = [{ ...initialContext, key: initialKey }]
123
128
  const nodeDraftsByKey = new Map<Key, number>([[initialKey, 0]])
124
129
  const edges: Array<ExplorationEdgeDraft<M>> = []
@@ -165,6 +170,8 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
165
170
  const scenario = scenarioWithEvent(source.trace, event)
166
171
  const targetTrace = yield* appendTrace(machine, source.trace, event, scenario)
167
172
  plannedTransitions += 1
173
+ const step = targetTrace.steps[targetTrace.steps.length - 1]!
174
+ for (const microstep of step.plan.microsteps) transitionCoverage.observeMicrostep(microstep)
168
175
  if (invariants.length > 0) {
169
176
  yield* assertInvariants(machine, targetTrace, invariants)
170
177
  }
@@ -178,7 +185,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
178
185
  target: existing,
179
186
  edge: {
180
187
  event,
181
- step: targetTrace.steps[targetTrace.steps.length - 1]!,
188
+ step,
182
189
  discovered: false
183
190
  }
184
191
  })
@@ -207,7 +214,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
207
214
  target: targetIndex,
208
215
  edge: {
209
216
  event,
210
- step: targetTrace.steps[targetTrace.steps.length - 1]!,
217
+ step,
211
218
  discovered: true
212
219
  }
213
220
  })
@@ -243,6 +250,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
243
250
  retainedEdges: edges.length,
244
251
  maxDepth: nodes.reduce((maximum, node) => Math.max(maximum, node.depth), 0)
245
252
  },
253
+ transitionCoverage: transitionCoverage.summary(),
246
254
  completeness
247
255
  }
248
256
  })
@@ -1177,44 +1177,6 @@ const stateValue = (
1177
1177
  return { _tag: stateTag(state.path), value: value ?? state.node.value }
1178
1178
  }
1179
1179
 
1180
- const runtimeTargetPath = (
1181
- byPath: ReadonlyMap<string, FlatFiniteState>,
1182
- sourcePath: string,
1183
- requestedPath: string
1184
- ): string => {
1185
- const source = byPath.get(sourcePath)!
1186
- const requested = byPath.get(requestedPath)!
1187
- if (requested.node._tag === "History") return requested.parent!
1188
- if (requested.node._tag === "Choice") return runtimeTargetPath(byPath, sourcePath, requested.node.selected)
1189
- if (source.root !== requested.root) return requested.path
1190
-
1191
- const initial = (path: string): string => {
1192
- const current = byPath.get(path)!
1193
- if (current.node._tag === "Parallel") {
1194
- if (sourcePath !== current.path && !sourcePath.startsWith(`${current.path}.`)) {
1195
- return current.path
1196
- }
1197
- const child = sourcePath === current.path
1198
- ? current.node.states[0]!.key
1199
- : sourcePath.slice(current.path.length + 1).split(".")[0]!
1200
- return initial(`${current.path}.${child}`)
1201
- }
1202
- return current.node._tag === "Compound" ? initial(`${current.path}.${current.node.initial}`) : current.path
1203
- }
1204
- const inspect = (path: string): string => {
1205
- const current = byPath.get(path)!
1206
- if (
1207
- current.node._tag === "Parallel" && sourcePath !== current.path && !sourcePath.startsWith(`${current.path}.`)
1208
- ) {
1209
- return current.path
1210
- }
1211
- if (current.path === requestedPath) return initial(current.path)
1212
- const next = requestedPath.slice(current.path.length + 1).split(".")[0]!
1213
- return inspect(`${current.path}.${next}`)
1214
- }
1215
- return inspect(source.root)
1216
- }
1217
-
1218
1180
  const makeStateTree = (
1219
1181
  states: ReadonlyArray<FiniteState>,
1220
1182
  parent: string | undefined
@@ -1357,10 +1319,88 @@ const selectHistoryTarget = (builder: Record<string, any>, path: string): unknow
1357
1319
  return current[parts[parts.length - 1]!]()
1358
1320
  }
1359
1321
 
1360
- type TargetBuilder = {
1361
- readonly branch: Record<string, any>
1362
- readonly full: Record<string, any>
1363
- readonly history: Record<string, any>
1322
+ const selectableDefinitionTarget = (
1323
+ source: string,
1324
+ target: string,
1325
+ byPath: ReadonlyMap<string, FlatFiniteState>
1326
+ ): string => {
1327
+ const parts = target.split(".")
1328
+ for (let index = 0; index < parts.length; index++) {
1329
+ const path = parts.slice(0, index + 1).join(".")
1330
+ const node = byPath.get(path)?.node
1331
+ if (
1332
+ (node?._tag === "Compound" || node?._tag === "Parallel") &&
1333
+ source !== path && !source.startsWith(`${path}.`)
1334
+ ) return path
1335
+ }
1336
+ return target
1337
+ }
1338
+
1339
+ const selectDefinitionTarget = (
1340
+ selector: Record<string, any>,
1341
+ source: string,
1342
+ target: string,
1343
+ byPath: ReadonlyMap<string, FlatFiniteState>
1344
+ ): unknown => {
1345
+ const selected = byPath.get(target)!
1346
+ if (selected.node._tag === "History") {
1347
+ return selectHistoryTarget(selector.history, target)
1348
+ }
1349
+ const sourceRoot = byPath.get(source)!.root
1350
+ if (selected.root !== sourceRoot) {
1351
+ const root = target.split(".")[0]!
1352
+ return selector.full[root]()
1353
+ }
1354
+ const selectable = selectableDefinitionTarget(source, target, byPath)
1355
+ let current = selector.branch
1356
+ const parts = selectable.split(".")
1357
+ for (const part of parts) current = current[part]
1358
+ if (typeof current === "function") return current()
1359
+ return current.initial()
1360
+ }
1361
+
1362
+ const resolveDefinitionTarget = (
1363
+ builder: any,
1364
+ source: string,
1365
+ target: string,
1366
+ byPath: ReadonlyMap<string, FlatFiniteState>,
1367
+ value?: number
1368
+ ): unknown => {
1369
+ const selected = byPath.get(target)!
1370
+ if (selected.node._tag === "History") return builder()
1371
+ if (selected.root !== byPath.get(source)!.root) {
1372
+ const parts = target.split(".")
1373
+ return selectSnapshot({ [parts[0]!]: builder }, parts[0]!, byPath, parts, 0, source, value)
1374
+ }
1375
+ const selectable = selectableDefinitionTarget(source, target, byPath)
1376
+ if (selectable !== target) {
1377
+ const bound = byPath.get(selectable)!
1378
+ const parts = target.split(".")
1379
+ return selectSnapshot(
1380
+ { [bound.node.key]: builder },
1381
+ selectable,
1382
+ byPath,
1383
+ parts,
1384
+ selectable.split(".").length - 1,
1385
+ source,
1386
+ value
1387
+ )
1388
+ }
1389
+ if (selected.node._tag === "Choice") return builder()
1390
+ const input = { value: value ?? selected.node.value }
1391
+ if (selected.node._tag === "Compound" || selected.node._tag === "Parallel") {
1392
+ const parts = target.split(".")
1393
+ return selectSnapshot(
1394
+ { [selected.node.key]: builder },
1395
+ target,
1396
+ byPath,
1397
+ parts,
1398
+ parts.length - 1,
1399
+ source,
1400
+ value
1401
+ )
1402
+ }
1403
+ return builder.from(input)
1364
1404
  }
1365
1405
 
1366
1406
  const makeHandlers = (
@@ -1375,15 +1415,11 @@ const makeHandlers = (
1375
1415
  if (node._tag === "History") continue
1376
1416
  if (node._tag === "Choice") {
1377
1417
  handlers[node.key] = {
1378
- choice: {
1379
- targets: node.targets,
1380
- transition: ({ target }: { readonly target: TargetBuilder }) => {
1381
- const selected = byPath.get(node.selected)!
1382
- const parts = selected.path.split(".")
1383
- const builder = selected.root === byPath.get(path)!.root ? target.branch : target.full
1384
- return selectSnapshot(builder, parts[0]!, byPath, parts, 0, path)
1385
- }
1386
- }
1418
+ choice: (Machine.transition as any)({
1419
+ target: (to: Record<string, any>) => selectDefinitionTarget(to, path, node.selected, byPath) as any,
1420
+ resolve: ({ target }: { readonly target: any }) =>
1421
+ resolveDefinitionTarget(target, path, node.selected, byPath)
1422
+ } as any)
1387
1423
  }
1388
1424
  continue
1389
1425
  }
@@ -1396,27 +1432,17 @@ const makeHandlers = (
1396
1432
  let onDone: unknown
1397
1433
  for (const transition of transitions.values()) {
1398
1434
  if (transition.source !== path) continue
1399
- const config = {
1435
+ const config = (Machine.transition as any)({
1400
1436
  ...("reenter" in transition ? { reenter: transition.reenter } : {}),
1401
- ...(transition.target === undefined
1402
- ? {}
1403
- : {
1404
- targets: [
1405
- byPath.get(transition.target)!.node._tag === "History" ||
1406
- byPath.get(transition.target)!.node._tag === "Choice"
1407
- ? transition.target
1408
- : runtimeTargetPath(byPath, path, transition.target)
1409
- ]
1410
- }),
1411
- transition: ({ target }: { readonly target: TargetBuilder }) => {
1412
- if (transition.target === undefined) return undefined
1413
- const targetState = byPath.get(transition.target)!
1414
- if (targetState.node._tag === "History") return selectHistoryTarget(target.history, targetState.path)
1415
- const parts = transition.target.split(".")
1416
- const builder = targetState.root === byPath.get(path)!.root ? target.branch : target.full
1417
- return selectSnapshot(builder, parts[0]!, byPath, parts, 0, path, transition.targetValue)
1418
- }
1419
- }
1437
+ target: (to: Record<string, any>) =>
1438
+ transition.target === undefined
1439
+ ? to.none()
1440
+ : selectDefinitionTarget(to, path, transition.target, byPath),
1441
+ resolve: transition.target === undefined
1442
+ ? () => undefined
1443
+ : ({ target }: { readonly target: any }) =>
1444
+ resolveDefinitionTarget(target, path, transition.target!, byPath, transition.targetValue)
1445
+ } as any)
1420
1446
  if (transition.trigger.type === "event") on[transition.trigger.event] = config
1421
1447
  else if (transition.trigger.type === "always") always = config
1422
1448
  else onDone = config
@@ -1449,15 +1475,16 @@ const makeHandlers = (
1449
1475
  ...(Object.keys(history).length === 0 ? {} : { history }),
1450
1476
  ...(node._tag === "Compound"
1451
1477
  ? {
1452
- initial: () => stateValue(byPath.get(`${path}.${node.initial}`)!)
1478
+ initialize: ({ builder }: any) => builder(stateValue(byPath.get(`${path}.${node.initial}`)!))
1453
1479
  }
1454
1480
  : {
1455
- initial: () =>
1456
- Object.fromEntries(
1457
- node.states
1458
- .filter((child) => child._tag !== "History" && child._tag !== "Choice")
1459
- .map((child) => [child.key, stateValue(byPath.get(`${path}.${child.key}`)!)])
1460
- )
1481
+ initialize: ({ builder }: any) =>
1482
+ node.states
1483
+ .filter((child) => child._tag !== "History" && child._tag !== "Choice")
1484
+ .reduce(
1485
+ (current: any, child) => current[child.key](stateValue(byPath.get(`${path}.${child.key}`)!)),
1486
+ builder
1487
+ )
1461
1488
  }),
1462
1489
  states: makeHandlers(node.states, path, byPath, transitions)
1463
1490
  }
@@ -1488,8 +1515,15 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => {
1488
1515
  const machine = Machine.make({
1489
1516
  states: defined.states as any,
1490
1517
  events: Machine.events(...eventSchemas) as any,
1491
- initial: () => selectSnapshot(defined.initial as any, initial.path, byPath, [initial.path], 0) as any
1492
- })
1518
+ initial: {
1519
+ target: (to: Record<string, any>) => {
1520
+ const selected = to[initial.path]
1521
+ return typeof selected === "function" ? selected() : selected.initial()
1522
+ },
1523
+ resolve: ({ target }: { readonly target: any }) =>
1524
+ selectSnapshot({ [initial.path]: target }, initial.path, byPath, [initial.path], 0) as any
1525
+ }
1526
+ } as any)
1493
1527
  const transitions = new Map(model.transitions.map((transition) => [
1494
1528
  `${transition.source}\u0000${triggerKey(transition.trigger)}`,
1495
1529
  transition
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Exact transition-definition and branch coverage collected from retained
3
+ * planner microsteps.
4
+ *
5
+ * @since 0.14.0
6
+ */
7
+
8
+ import * as Machine from "../../../Machine.js"
9
+ import type {
10
+ CoverageSummary,
11
+ Microstep,
12
+ TransitionBranchCoverageItem,
13
+ TransitionCoverage,
14
+ TransitionDefinitionCoverageItem
15
+ } from "../../../testing/MachineTest.js"
16
+
17
+ type AnyMachine = Machine.Machine.Any
18
+
19
+ type StateNodePath<M extends AnyMachine> = Machine.Machine.StateNodeIdentifier<Machine.Machine.States<M>>
20
+
21
+ type EventTag<M extends AnyMachine> = Machine.Machine.TagOf<Machine.Machine.Events<M>[number]>
22
+
23
+ type MachineTransitionCoverage<M extends AnyMachine> = TransitionCoverage<
24
+ StateNodePath<M>,
25
+ EventTag<M>,
26
+ StateNodePath<M>
27
+ >
28
+
29
+ const coverageSummary = <Item>(declared: ReadonlyArray<Item>, hit: ReadonlySet<number>): CoverageSummary<Item> => {
30
+ const hits: Array<Item> = []
31
+ const misses: Array<Item> = []
32
+ declared.forEach((item, index) => (hit.has(index) ? hits : misses).push(item))
33
+ return {
34
+ total: declared.length,
35
+ hit: hits.length,
36
+ missing: misses.length,
37
+ hits,
38
+ misses
39
+ }
40
+ }
41
+
42
+ export const sameTransitionTrigger = (
43
+ left: Machine.Machine.TransitionTrigger,
44
+ right: Machine.Machine.TransitionTrigger
45
+ ): boolean => {
46
+ if (left.type !== right.type) return false
47
+ if (left.type === "event") return right.type === "event" && left.event === right.event
48
+ if (left.type === "invoke") {
49
+ return right.type === "invoke" && left.id === right.id && left.outcome === right.outcome
50
+ }
51
+ return true
52
+ }
53
+
54
+ export interface TransitionCoverageCollector<M extends AnyMachine> {
55
+ readonly observeMicrostep: (microstep: Microstep<M, unknown>) => void
56
+ readonly summary: () => MachineTransitionCoverage<M>
57
+ }
58
+
59
+ export const makeTransitionCoverageCollector = <M extends AnyMachine>(
60
+ machine: M
61
+ ): TransitionCoverageCollector<M> => {
62
+ const definitions = Machine.transitionDefinitions(machine).map(
63
+ (definition, index): TransitionDefinitionCoverageItem<StateNodePath<M>, EventTag<M>, StateNodePath<M>> => ({
64
+ id: `transition:${index}`,
65
+ index,
66
+ source: definition.source,
67
+ trigger: definition.trigger,
68
+ reenter: definition.reenter,
69
+ branches: definition.branches
70
+ })
71
+ )
72
+ const definitionHits = new Set<number>()
73
+ const branchOffsets: Array<number> = []
74
+ const branches: Array<TransitionBranchCoverageItem<StateNodePath<M>, EventTag<M>, StateNodePath<M>>> = []
75
+ for (let definitionIndex = 0; definitionIndex < definitions.length; definitionIndex++) {
76
+ branchOffsets.push(branches.length)
77
+ const definition = definitions[definitionIndex]!
78
+ definition.branches.forEach((branch, branchIndex) => {
79
+ branches.push({
80
+ id: `transition:${definitionIndex}:branch:${branchIndex}`,
81
+ definitionIndex,
82
+ branchIndex,
83
+ source: definition.source,
84
+ trigger: definition.trigger,
85
+ reenter: definition.reenter,
86
+ branch
87
+ })
88
+ })
89
+ }
90
+ const branchHits = new Set<number>()
91
+
92
+ return {
93
+ observeMicrostep(microstep) {
94
+ for (const retained of microstep.transitions) {
95
+ const definitionIndex = definitions.findIndex((definition) =>
96
+ definition.source === retained.source &&
97
+ definition.reenter === retained.reenter &&
98
+ sameTransitionTrigger(definition.trigger, retained.trigger)
99
+ )
100
+ if (definitionIndex === -1) continue
101
+ definitionHits.add(definitionIndex)
102
+ const definition = definitions[definitionIndex]!
103
+ if (
104
+ Number.isSafeInteger(retained.branchIndex) && retained.branchIndex >= 0 &&
105
+ retained.branchIndex < definition.branches.length
106
+ ) {
107
+ branchHits.add(branchOffsets[definitionIndex]! + retained.branchIndex)
108
+ }
109
+ }
110
+ },
111
+ summary: () => ({
112
+ definitions: coverageSummary(definitions, definitionHits),
113
+ branches: coverageSummary(branches, branchHits)
114
+ })
115
+ }
116
+ }