@typeonce/effect-machine-devtools 0.24.0 → 0.25.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 (80) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +11 -11
  3. package/dist/DevToolsProtocol.d.ts +66 -630
  4. package/dist/DevToolsProtocol.d.ts.map +1 -1
  5. package/dist/DevToolsProtocol.js +0 -190
  6. package/dist/DevToolsProtocol.js.map +1 -1
  7. package/dist/MachineDocument.d.ts +38 -18
  8. package/dist/MachineDocument.d.ts.map +1 -1
  9. package/dist/MachineDocument.js +14 -12
  10. package/dist/MachineDocument.js.map +1 -1
  11. package/dist/MachineRegistry.d.ts +22 -2
  12. package/dist/MachineRegistry.d.ts.map +1 -1
  13. package/dist/MachineWalkthrough.d.ts +171 -0
  14. package/dist/MachineWalkthrough.d.ts.map +1 -0
  15. package/dist/MachineWalkthrough.js +98 -0
  16. package/dist/MachineWalkthrough.js.map +1 -0
  17. package/dist/ProjectInspector.d.ts +0 -2
  18. package/dist/ProjectInspector.d.ts.map +1 -1
  19. package/dist/ProjectInspector.js.map +1 -1
  20. package/dist/client/assets/index-BTOAhezX.js +37 -0
  21. package/dist/client/assets/index-Cg7xzSkz.css +1 -0
  22. package/dist/client/index.html +4 -4
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal/devServer.d.ts.map +1 -1
  28. package/dist/internal/devServer.js +4 -64
  29. package/dist/internal/devServer.js.map +1 -1
  30. package/dist/internal/evaluationWorker.d.ts.map +1 -1
  31. package/dist/internal/evaluationWorker.js +7 -263
  32. package/dist/internal/evaluationWorker.js.map +1 -1
  33. package/dist/internal/machineDocument.d.ts.map +1 -1
  34. package/dist/internal/machineDocument.js +3 -1
  35. package/dist/internal/machineDocument.js.map +1 -1
  36. package/dist/internal/machineWalkthrough.d.ts +26 -0
  37. package/dist/internal/machineWalkthrough.d.ts.map +1 -0
  38. package/dist/internal/machineWalkthrough.js +256 -0
  39. package/dist/internal/machineWalkthrough.js.map +1 -0
  40. package/dist/internal/projectInspector.d.ts.map +1 -1
  41. package/dist/internal/projectInspector.js +1 -11
  42. package/dist/internal/projectInspector.js.map +1 -1
  43. package/index.html +2 -2
  44. package/package.json +4 -3
  45. package/src/DevToolsProtocol.ts +0 -285
  46. package/src/MachineDocument.ts +21 -19
  47. package/src/MachineWalkthrough.ts +199 -0
  48. package/src/ProjectInspector.ts +0 -5
  49. package/src/index.ts +2 -2
  50. package/src/internal/browser/chart-layout.ts +433 -0
  51. package/src/internal/browser/chart-model.ts +234 -0
  52. package/src/internal/browser/chart-renderer.ts +661 -0
  53. package/src/internal/browser/example-machine.ts +15 -6
  54. package/src/internal/browser/input-form.ts +1 -418
  55. package/src/internal/browser/invoke-outcomes-example.ts +231 -0
  56. package/src/internal/browser/parallel-completion-example.ts +199 -0
  57. package/src/internal/browser/planner-example.ts +2 -0
  58. package/src/internal/browser/protocol-events-example.ts +200 -0
  59. package/src/internal/browser/styles.css +795 -341
  60. package/src/internal/browser/transition-semantics-example.ts +236 -0
  61. package/src/internal/browser/visualizer-app.ts +781 -713
  62. package/src/internal/browser/visualizer-model.ts +104 -0
  63. package/src/internal/devServer.ts +4 -82
  64. package/src/internal/evaluationWorker.ts +7 -371
  65. package/src/internal/machineDocument.ts +3 -1
  66. package/src/internal/machineWalkthrough.ts +355 -0
  67. package/src/internal/projectInspector.ts +3 -31
  68. package/dist/MachineSimulator.d.ts +0 -169
  69. package/dist/MachineSimulator.d.ts.map +0 -1
  70. package/dist/MachineSimulator.js +0 -98
  71. package/dist/MachineSimulator.js.map +0 -1
  72. package/dist/client/assets/index-B49Tjawc.js +0 -14
  73. package/dist/client/assets/index-BKH0cOG2.css +0 -1
  74. package/dist/internal/machineSimulator.d.ts +0 -5
  75. package/dist/internal/machineSimulator.d.ts.map +0 -1
  76. package/dist/internal/machineSimulator.js +0 -156
  77. package/dist/internal/machineSimulator.js.map +0 -1
  78. package/src/MachineSimulator.ts +0 -154
  79. package/src/internal/browser/simulation-client.ts +0 -20
  80. package/src/internal/machineSimulator.ts +0 -199
@@ -0,0 +1,433 @@
1
+ import * as Data from "effect/Data"
2
+ import * as Effect from "effect/Effect"
3
+ import type {
4
+ ELK as ElkApi,
5
+ ELKConstructorArguments,
6
+ ElkExtendedEdge,
7
+ ElkNode,
8
+ ElkPoint,
9
+ ElkPort
10
+ } from "elkjs/lib/elk-api.js"
11
+ import ELKBundle from "elkjs/lib/elk.bundled.js"
12
+ import type { ChartEdge, ChartInitial, ChartModel, ChartNode, ChartRuntimeTarget } from "./chart-model.js"
13
+
14
+ export const maxVisibleFields = 4
15
+ export const maxVisibleActivities = 3
16
+
17
+ export interface ChartPoint {
18
+ readonly x: number
19
+ readonly y: number
20
+ }
21
+
22
+ export interface LaidOutChartNode {
23
+ readonly node: ChartNode
24
+ readonly x: number
25
+ readonly y: number
26
+ readonly width: number
27
+ readonly height: number
28
+ readonly headerHeight: number
29
+ }
30
+
31
+ export interface LaidOutChartInitial {
32
+ readonly initial: ChartInitial
33
+ readonly x: number
34
+ readonly y: number
35
+ readonly width: number
36
+ readonly height: number
37
+ }
38
+
39
+ export interface LaidOutChartRuntimeTarget {
40
+ readonly target: ChartRuntimeTarget
41
+ readonly x: number
42
+ readonly y: number
43
+ readonly width: number
44
+ readonly height: number
45
+ }
46
+
47
+ export interface LaidOutChartTransition {
48
+ readonly kind: "transition"
49
+ readonly edge: ChartEdge
50
+ readonly points: ReadonlyArray<ChartPoint>
51
+ readonly label: ChartPoint
52
+ readonly labelWidth: number
53
+ readonly labelHeight: number
54
+ }
55
+
56
+ export interface LaidOutChartInitialEdge {
57
+ readonly kind: "initial"
58
+ readonly initial: ChartInitial
59
+ readonly points: ReadonlyArray<ChartPoint>
60
+ }
61
+
62
+ export interface LaidOutChart {
63
+ readonly width: number
64
+ readonly height: number
65
+ readonly nodes: ReadonlyArray<LaidOutChartNode>
66
+ readonly initials: ReadonlyArray<LaidOutChartInitial>
67
+ readonly runtimeTargets: ReadonlyArray<LaidOutChartRuntimeTarget>
68
+ readonly edges: ReadonlyArray<LaidOutChartTransition | LaidOutChartInitialEdge>
69
+ }
70
+
71
+ export class ChartLayoutError extends Data.TaggedError("ChartLayoutError")<{
72
+ readonly cause: unknown
73
+ }> {}
74
+
75
+ interface NodeMetric {
76
+ readonly width: number
77
+ readonly height: number
78
+ readonly headerHeight: number
79
+ }
80
+
81
+ const sectionHeight = (length: number, limit: number): number => {
82
+ if (length === 0) return 0
83
+ const visible = Math.min(length, limit)
84
+ return 24 + visible * 22 + (length > limit ? 18 : 0)
85
+ }
86
+
87
+ const nodeMetric = (node: ChartNode): NodeMetric => {
88
+ const headerHeight = Math.max(
89
+ 78,
90
+ 76 +
91
+ sectionHeight(node.fields.length, maxVisibleFields) +
92
+ sectionHeight(node.activities.length, maxVisibleActivities)
93
+ )
94
+ const width = node.type === "choice" || node.type === "history" ? 176 : node.children.length > 0 ? 340 : 276
95
+ return {
96
+ width,
97
+ height: node.children.length === 0 ? headerHeight : Math.max(240, headerHeight + 104),
98
+ headerHeight
99
+ }
100
+ }
101
+
102
+ const ELK = ELKBundle as unknown as new(args?: ELKConstructorArguments) => ElkApi
103
+ const elk = new ELK()
104
+
105
+ const sourcePortId = (edge: ChartEdge): string => `port:${edge.id}:source`
106
+ const targetPortId = (edge: ChartEdge): string => `port:${edge.id}:target`
107
+ const initialNodeId = (initial: ChartInitial): string => `node:${initial.id}`
108
+ const initialTargetPortId = (initial: ChartInitial): string => `port:${initial.id}:target`
109
+ const runtimeNodeId = (target: ChartRuntimeTarget): string => `node:${target.id}`
110
+ const runtimeTargetPortId = (target: ChartRuntimeTarget): string => `port:${target.edgeId}:target`
111
+
112
+ const portsByState = (model: ChartModel): ReadonlyMap<string, ReadonlyArray<ElkPort>> => {
113
+ const ports = new Map<string, Array<ElkPort>>()
114
+ const add = (path: string, id: string, side: "EAST" | "WEST"): void => {
115
+ const statePorts = ports.get(path) ?? []
116
+ statePorts.push({
117
+ id,
118
+ width: 6,
119
+ height: 6,
120
+ layoutOptions: {
121
+ "elk.port.side": side,
122
+ "elk.port.index": String(statePorts.length)
123
+ }
124
+ })
125
+ ports.set(path, statePorts)
126
+ }
127
+
128
+ for (const edge of model.edges) {
129
+ add(edge.source, sourcePortId(edge), "EAST")
130
+ if (edge.kind === "target" && edge.target !== null) {
131
+ add(edge.target, targetPortId(edge), "WEST")
132
+ } else if (edge.kind === "targetless") {
133
+ add(edge.source, targetPortId(edge), "EAST")
134
+ }
135
+ }
136
+ for (const initial of model.initials) add(initial.target, initialTargetPortId(initial), "WEST")
137
+ return ports
138
+ }
139
+
140
+ const labelMetric = (label: string): { readonly width: number; readonly height: number } => ({
141
+ width: Math.min(230, Math.max(72, label.length * 7 + 20)),
142
+ height: 26
143
+ })
144
+
145
+ const makeGraph = (model: ChartModel): ElkNode => {
146
+ const nodesByParent = new Map<string | null, Array<ChartNode>>()
147
+ for (const node of model.nodes) {
148
+ const siblings = nodesByParent.get(node.parent) ?? []
149
+ siblings.push(node)
150
+ nodesByParent.set(node.parent, siblings)
151
+ }
152
+ const initialsByParent = new Map<string | null, Array<ChartInitial>>()
153
+ for (const initial of model.initials) {
154
+ const siblings = initialsByParent.get(initial.parent) ?? []
155
+ siblings.push(initial)
156
+ initialsByParent.set(initial.parent, siblings)
157
+ }
158
+ const runtimeTargetsByParent = new Map<string | null, Array<ChartRuntimeTarget>>()
159
+ for (const target of model.runtimeTargets) {
160
+ const siblings = runtimeTargetsByParent.get(target.parent) ?? []
161
+ siblings.push(target)
162
+ runtimeTargetsByParent.set(target.parent, siblings)
163
+ }
164
+ const ports = portsByState(model)
165
+
166
+ const children = (parent: string | null): Array<ElkNode> => [
167
+ ...(initialsByParent.get(parent) ?? []).map((initial): ElkNode => ({
168
+ id: initialNodeId(initial),
169
+ width: 14,
170
+ height: 14
171
+ })),
172
+ ...(nodesByParent.get(parent) ?? []).map((node): ElkNode => {
173
+ const metric = nodeMetric(node)
174
+ const descendants = children(node.path)
175
+ const common = {
176
+ id: node.path,
177
+ ports: [...ports.get(node.path) ?? []]
178
+ }
179
+ if (descendants.length === 0) {
180
+ return {
181
+ ...common,
182
+ width: metric.width,
183
+ height: metric.height,
184
+ layoutOptions: {
185
+ "elk.portConstraints": "FIXED_ORDER",
186
+ "elk.spacing.portPort": "22"
187
+ }
188
+ }
189
+ }
190
+ return {
191
+ ...common,
192
+ children: descendants,
193
+ layoutOptions: {
194
+ "elk.algorithm": "layered",
195
+ "elk.direction": "RIGHT",
196
+ "elk.padding": `[top=${metric.headerHeight + 28},left=28,bottom=28,right=28]`,
197
+ "elk.nodeSize.constraints": "MINIMUM_SIZE",
198
+ "elk.nodeSize.minimum": `(${metric.width}, ${metric.height})`,
199
+ "elk.portConstraints": "FIXED_ORDER",
200
+ "elk.spacing.portPort": "22",
201
+ "elk.spacing.nodeNode": "44",
202
+ "elk.layered.spacing.nodeNodeBetweenLayers": "108"
203
+ }
204
+ }
205
+ }),
206
+ ...(runtimeTargetsByParent.get(parent) ?? []).map((target): ElkNode => ({
207
+ id: runtimeNodeId(target),
208
+ width: 118,
209
+ height: 34,
210
+ ports: [{
211
+ id: runtimeTargetPortId(target),
212
+ width: 6,
213
+ height: 6,
214
+ layoutOptions: { "elk.port.side": "WEST" }
215
+ }],
216
+ layoutOptions: { "elk.portConstraints": "FIXED_SIDE" }
217
+ }))
218
+ ]
219
+
220
+ return {
221
+ id: "chart-root",
222
+ children: children(null),
223
+ edges: [
224
+ ...model.edges.map((edge): ElkExtendedEdge => {
225
+ const label = labelMetric(edge.label)
226
+ return {
227
+ id: edge.id,
228
+ sources: [sourcePortId(edge)],
229
+ targets: [targetPortId(edge)],
230
+ labels: [{ text: edge.label, width: label.width, height: label.height }]
231
+ }
232
+ }),
233
+ ...model.initials.map((initial): ElkExtendedEdge => ({
234
+ id: initial.id,
235
+ sources: [initialNodeId(initial)],
236
+ targets: [initialTargetPortId(initial)]
237
+ }))
238
+ ],
239
+ layoutOptions: {
240
+ "elk.algorithm": "layered",
241
+ "elk.direction": "RIGHT",
242
+ "elk.hierarchyHandling": "INCLUDE_CHILDREN",
243
+ "elk.edgeRouting": "ORTHOGONAL",
244
+ "elk.padding": "[top=44,left=44,bottom=44,right=44]",
245
+ "elk.spacing.nodeNode": "64",
246
+ "elk.layered.spacing.nodeNodeBetweenLayers": "148",
247
+ "elk.layered.spacing.edgeNodeBetweenLayers": "42",
248
+ "elk.layered.spacing.edgeEdgeBetweenLayers": "26",
249
+ "elk.spacing.edgeNode": "28",
250
+ "elk.spacing.edgeEdge": "20",
251
+ "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
252
+ "elk.layered.crossingMinimization.greedySwitchHierarchical.type": "TWO_SIDED",
253
+ "elk.layered.nodePlacement.favorStraightEdges": "true",
254
+ "elk.layered.mergeHierarchyEdges": "false",
255
+ "elk.layered.thoroughness": "12",
256
+ "elk.randomSeed": "1"
257
+ }
258
+ }
259
+ }
260
+
261
+ const add = (left: ChartPoint, right: ChartPoint): ChartPoint => ({
262
+ x: left.x + right.x,
263
+ y: left.y + right.y
264
+ })
265
+
266
+ const compactPoints = (points: ReadonlyArray<ChartPoint>): ReadonlyArray<ChartPoint> => {
267
+ const result: Array<ChartPoint> = []
268
+ for (const point of points) {
269
+ const previous = result.at(-1)
270
+ if (previous !== undefined && previous.x === point.x && previous.y === point.y) continue
271
+ const beforePrevious = result.at(-2)
272
+ if (
273
+ beforePrevious !== undefined && previous !== undefined &&
274
+ (beforePrevious.x === previous.x && previous.x === point.x ||
275
+ beforePrevious.y === previous.y && previous.y === point.y)
276
+ ) {
277
+ result[result.length - 1] = point
278
+ } else {
279
+ result.push(point)
280
+ }
281
+ }
282
+ return result
283
+ }
284
+
285
+ const edgePoints = (edge: ElkExtendedEdge, offset: ChartPoint): ReadonlyArray<ChartPoint> | undefined => {
286
+ const sections = edge.sections
287
+ if (sections === undefined || sections.length === 0) return undefined
288
+ const byId = new Map(sections.map((section) => [section.id, section]))
289
+ let section =
290
+ sections.find((candidate) =>
291
+ candidate.incomingShape !== undefined || candidate.incomingSections === undefined ||
292
+ candidate.incomingSections.length === 0
293
+ ) ?? sections[0]
294
+ const points: Array<ElkPoint> = []
295
+ const visited = new Set<string>()
296
+ while (section !== undefined && !visited.has(section.id)) {
297
+ visited.add(section.id)
298
+ if (points.length === 0) points.push(section.startPoint)
299
+ points.push(...(section.bendPoints ?? []), section.endPoint)
300
+ const next = section.outgoingSections?.[0]
301
+ section = next === undefined ? undefined : byId.get(next)
302
+ }
303
+ return points.length < 2 ? undefined : compactPoints(points.map((point) => add(point, offset)))
304
+ }
305
+
306
+ const midpoint = (points: ReadonlyArray<ChartPoint>): ChartPoint => {
307
+ const lengths = points.slice(1).map((point, index) => {
308
+ const previous = points[index]!
309
+ return Math.abs(point.x - previous.x) + Math.abs(point.y - previous.y)
310
+ })
311
+ const total = lengths.reduce((sum, length) => sum + length, 0)
312
+ let remaining = total / 2
313
+ for (let index = 0; index < lengths.length; index++) {
314
+ const length = lengths[index]!
315
+ const start = points[index]!
316
+ const end = points[index + 1]!
317
+ if (remaining <= length) {
318
+ const ratio = length === 0 ? 0 : remaining / length
319
+ return {
320
+ x: start.x + (end.x - start.x) * ratio,
321
+ y: start.y + (end.y - start.y) * ratio
322
+ }
323
+ }
324
+ remaining -= length
325
+ }
326
+ return points.at(-1)!
327
+ }
328
+
329
+ const expandTargetlessLoop = (points: ReadonlyArray<ChartPoint>): ReadonlyArray<ChartPoint> => {
330
+ const start = points[0]
331
+ const end = points.at(-1)
332
+ if (start === undefined || end === undefined) return points
333
+ const outerX = Math.max(...points.map(({ x }) => x)) + 30
334
+ return compactPoints([
335
+ start,
336
+ { x: outerX, y: start.y },
337
+ { x: outerX, y: end.y },
338
+ end
339
+ ])
340
+ }
341
+
342
+ const collectLayout = (model: ChartModel, graph: ElkNode): LaidOutChart => {
343
+ const chartNodes = new Map(model.nodes.map((node) => [node.path, node]))
344
+ const chartInitials = new Map(model.initials.map((initial) => [initialNodeId(initial), initial]))
345
+ const chartRuntimeTargets = new Map(model.runtimeTargets.map((target) => [runtimeNodeId(target), target]))
346
+ const offsets = new Map<string, ChartPoint>([[graph.id, { x: 0, y: 0 }]])
347
+ const nodes: Array<LaidOutChartNode> = []
348
+ const initials: Array<LaidOutChartInitial> = []
349
+ const runtimeTargets: Array<LaidOutChartRuntimeTarget> = []
350
+
351
+ const visit = (node: ElkNode, parentOffset: ChartPoint): void => {
352
+ const absolute = add(parentOffset, { x: node.x ?? 0, y: node.y ?? 0 })
353
+ offsets.set(node.id, absolute)
354
+ const chartNode = chartNodes.get(node.id)
355
+ if (chartNode !== undefined) {
356
+ const metric = nodeMetric(chartNode)
357
+ nodes.push({
358
+ node: chartNode,
359
+ x: absolute.x,
360
+ y: absolute.y,
361
+ width: node.width ?? metric.width,
362
+ height: node.height ?? metric.height,
363
+ headerHeight: metric.headerHeight
364
+ })
365
+ }
366
+ const initial = chartInitials.get(node.id)
367
+ if (initial !== undefined) {
368
+ initials.push({
369
+ initial,
370
+ x: absolute.x,
371
+ y: absolute.y,
372
+ width: node.width ?? 14,
373
+ height: node.height ?? 14
374
+ })
375
+ }
376
+ const runtimeTarget = chartRuntimeTargets.get(node.id)
377
+ if (runtimeTarget !== undefined) {
378
+ runtimeTargets.push({
379
+ target: runtimeTarget,
380
+ x: absolute.x,
381
+ y: absolute.y,
382
+ width: node.width ?? 118,
383
+ height: node.height ?? 34
384
+ })
385
+ }
386
+ for (const child of node.children ?? []) visit(child, absolute)
387
+ }
388
+ for (const child of graph.children ?? []) visit(child, { x: 0, y: 0 })
389
+
390
+ const chartEdges = new Map(model.edges.map((edge) => [edge.id, edge]))
391
+ const initialEdges = new Map(model.initials.map((initial) => [initial.id, initial]))
392
+ const edges = (graph.edges ?? []).flatMap((edge): ReadonlyArray<LaidOutChartTransition | LaidOutChartInitialEdge> => {
393
+ const offset = offsets.get(edge.container ?? graph.id) ?? { x: 0, y: 0 }
394
+ const points = edgePoints(edge, offset)
395
+ if (points === undefined) return []
396
+ const chartEdge = chartEdges.get(edge.id)
397
+ if (chartEdge !== undefined) {
398
+ const metric = labelMetric(chartEdge.label)
399
+ const label = edge.labels?.[0]
400
+ const transitionPoints = chartEdge.kind === "targetless" ? expandTargetlessLoop(points) : points
401
+ return [{
402
+ kind: "transition",
403
+ edge: chartEdge,
404
+ points: transitionPoints,
405
+ label: label?.x === undefined || label.y === undefined
406
+ ? midpoint(transitionPoints)
407
+ : add(offset, {
408
+ x: label.x + (label.width ?? metric.width) / 2,
409
+ y: label.y + (label.height ?? metric.height) / 2
410
+ }),
411
+ labelWidth: label?.width ?? metric.width,
412
+ labelHeight: label?.height ?? metric.height
413
+ }]
414
+ }
415
+ const initial = initialEdges.get(edge.id)
416
+ return initial === undefined ? [] : [{ kind: "initial", initial, points }]
417
+ })
418
+
419
+ return {
420
+ width: Math.max(360, graph.width ?? 0),
421
+ height: Math.max(280, graph.height ?? 0),
422
+ nodes,
423
+ initials,
424
+ runtimeTargets,
425
+ edges
426
+ }
427
+ }
428
+
429
+ export const layoutChart = (model: ChartModel): Effect.Effect<LaidOutChart, ChartLayoutError> =>
430
+ Effect.tryPromise({
431
+ try: () => elk.layout(makeGraph(model)),
432
+ catch: (cause) => new ChartLayoutError({ cause })
433
+ }).pipe(Effect.map((graph) => collectLayout(model, graph)))
@@ -0,0 +1,234 @@
1
+ import type {
2
+ Activity as VisualizationActivity,
3
+ Branch as VisualizationBranch,
4
+ MachineDocument as VisualizationDocument,
5
+ State as VisualizationState,
6
+ Transition as VisualizationTransition
7
+ } from "../../MachineDocument.js"
8
+ import { type InputField, projectInputSchema } from "./input-form.js"
9
+ import { stateLabel, triggerLabel } from "./visualizer-model.js"
10
+
11
+ export interface ChartField {
12
+ readonly key: string
13
+ readonly label: string
14
+ readonly type: string
15
+ readonly required: boolean
16
+ }
17
+
18
+ export interface ChartActivity {
19
+ readonly id: string
20
+ readonly kind: VisualizationActivity["type"]
21
+ readonly label: string
22
+ }
23
+
24
+ export interface ChartNode {
25
+ readonly path: string
26
+ readonly label: string
27
+ readonly type: VisualizationState["type"]
28
+ readonly parent: string | null
29
+ readonly children: ReadonlyArray<string>
30
+ readonly active: boolean
31
+ readonly initial: boolean
32
+ readonly fields: ReadonlyArray<ChartField>
33
+ readonly activities: ReadonlyArray<ChartActivity>
34
+ }
35
+
36
+ export interface ChartEdge {
37
+ readonly id: string
38
+ readonly transitionId: string
39
+ readonly branchIds: ReadonlyArray<string>
40
+ readonly kind: "target" | "targetless" | "runtime"
41
+ readonly source: string
42
+ readonly target: string | null
43
+ readonly label: string
44
+ readonly trigger: VisualizationTransition["trigger"]
45
+ readonly reenter: boolean
46
+ readonly acceptance: VisualizationTransition["acceptance"]
47
+ }
48
+
49
+ export interface ChartRuntimeTarget {
50
+ readonly id: string
51
+ readonly edgeId: string
52
+ readonly parent: string | null
53
+ readonly label: string
54
+ }
55
+
56
+ export interface ChartInitial {
57
+ readonly id: string
58
+ readonly target: string
59
+ readonly parent: string | null
60
+ }
61
+
62
+ export interface ChartModel {
63
+ readonly machineId: string
64
+ readonly roots: ReadonlyArray<string>
65
+ readonly nodes: ReadonlyArray<ChartNode>
66
+ readonly edges: ReadonlyArray<ChartEdge>
67
+ readonly runtimeTargets: ReadonlyArray<ChartRuntimeTarget>
68
+ readonly initials: ReadonlyArray<ChartInitial>
69
+ }
70
+
71
+ const literalType = (value: string | number | boolean | null): string =>
72
+ typeof value === "string" ? JSON.stringify(value) : String(value)
73
+
74
+ const fieldType = (field: InputField): string => {
75
+ switch (field._tag) {
76
+ case "String":
77
+ return field.format ?? "string"
78
+ case "Number":
79
+ return field.integer ? "integer" : "number"
80
+ case "Boolean":
81
+ return "boolean"
82
+ case "Enum":
83
+ return field.values.map(literalType).join(" | ")
84
+ case "Literal":
85
+ return literalType(field.value)
86
+ case "Object":
87
+ return "object"
88
+ case "Array":
89
+ return `${fieldType(field.item)}[]`
90
+ case "Union":
91
+ return field.alternatives.map(fieldType).join(" | ")
92
+ case "Unsupported":
93
+ return "unknown"
94
+ }
95
+ }
96
+
97
+ const stateFields = (state: VisualizationState): ReadonlyArray<ChartField> => {
98
+ if (state.valueSchema === null) return []
99
+ const projected = projectInputSchema(state.valueSchema)
100
+ if (projected._tag !== "Object") {
101
+ return [{
102
+ key: "value",
103
+ label: projected.title ?? "value",
104
+ type: fieldType(projected),
105
+ required: true
106
+ }]
107
+ }
108
+ return projected.fields
109
+ .filter(({ key }) => key !== "_tag")
110
+ .map(({ field, key, required }) => ({
111
+ key,
112
+ label: field.title ?? key,
113
+ type: fieldType(field),
114
+ required
115
+ }))
116
+ }
117
+
118
+ const activityLabel = (activity: VisualizationActivity): string => {
119
+ switch (activity.type) {
120
+ case "machine":
121
+ return `${activity.lifecycleId} → ${activity.child.machineId ?? activity.child.id}`
122
+ case "timer":
123
+ return `${activity.lifecycleId} · ${activity.duration}`
124
+ case "process":
125
+ case "effect":
126
+ case "stream":
127
+ return activity.lifecycleId
128
+ }
129
+ }
130
+
131
+ const transitionLabel = (transition: VisualizationTransition, branch: VisualizationBranch): string => {
132
+ const trigger = triggerLabel(transition)
133
+ return branch.type === "branch" ? `${trigger} · ${branch.title}` : trigger
134
+ }
135
+
136
+ interface EdgeGroup {
137
+ readonly kind: ChartEdge["kind"]
138
+ readonly target: string | null
139
+ readonly branches: Array<VisualizationBranch>
140
+ }
141
+
142
+ const edgeGroup = (
143
+ branch: VisualizationBranch,
144
+ states: ReadonlyMap<string, VisualizationState>
145
+ ): { readonly key: string; readonly kind: ChartEdge["kind"]; readonly target: string | null } => {
146
+ if (branch.target !== null && states.has(branch.target)) {
147
+ return { key: `target:${branch.target}`, kind: "target", target: branch.target }
148
+ }
149
+ if (branch.selection.kind === "none" || branch.selection.kind === "update") {
150
+ return { key: "targetless", kind: "targetless", target: null }
151
+ }
152
+ return { key: "runtime", kind: "runtime", target: null }
153
+ }
154
+
155
+ export const makeChartModel = (document: VisualizationDocument): ChartModel => {
156
+ const active = new Set(document.snapshot?.activePaths ?? [])
157
+ const initialPaths = new Set([document.initial.target])
158
+ const activities = new Map(document.activities.map((activity) => [activity.id, activity]))
159
+ const states = new Map(document.states.map((state) => [state.path, state]))
160
+
161
+ for (const state of document.states) {
162
+ if (state.initial !== null) initialPaths.add(state.initial)
163
+ }
164
+
165
+ const nodes = document.states.map((state): ChartNode => ({
166
+ path: state.path,
167
+ label: stateLabel(state),
168
+ type: state.type,
169
+ parent: state.parent,
170
+ children: [...state.children],
171
+ active: active.has(state.path),
172
+ initial: initialPaths.has(state.path),
173
+ fields: stateFields(state),
174
+ activities: state.activityIds.flatMap((id): ReadonlyArray<ChartActivity> => {
175
+ const activity = activities.get(id)
176
+ return activity === undefined ? [] : [{ id, kind: activity.type, label: activityLabel(activity) }]
177
+ })
178
+ }))
179
+
180
+ const edges = document.transitions.flatMap((transition): ReadonlyArray<ChartEdge> => {
181
+ if (!states.has(transition.source)) return []
182
+ const groups = new Map<string, EdgeGroup>()
183
+ for (const branch of transition.branches) {
184
+ const group = edgeGroup(branch, states)
185
+ const current = groups.get(group.key)
186
+ if (current === undefined) {
187
+ groups.set(group.key, { kind: group.kind, target: group.target, branches: [branch] })
188
+ } else {
189
+ current.branches.push(branch)
190
+ }
191
+ }
192
+ return [...groups].map(([key, { branches, kind, target }]): ChartEdge => ({
193
+ id: branches.length === 1 ? branches[0]!.id : `${transition.id}:${key}`,
194
+ transitionId: transition.id,
195
+ branchIds: branches.map(({ id }) => id),
196
+ kind,
197
+ source: transition.source,
198
+ target,
199
+ label: branches.length === 1
200
+ ? transitionLabel(transition, branches[0]!)
201
+ : `${triggerLabel(transition)} · ${branches.length} branches`,
202
+ trigger: transition.trigger,
203
+ reenter: transition.reenter,
204
+ acceptance: transition.acceptance
205
+ }))
206
+ })
207
+
208
+ const runtimeTargets = edges.flatMap((edge): ReadonlyArray<ChartRuntimeTarget> => {
209
+ if (edge.kind !== "runtime") return []
210
+ const source = states.get(edge.source)
211
+ return source === undefined
212
+ ? []
213
+ : [{
214
+ id: `runtime:${edge.id}`,
215
+ edgeId: edge.id,
216
+ parent: source.parent,
217
+ label: "runtime target"
218
+ }]
219
+ })
220
+
221
+ const initials = [...initialPaths].flatMap((target): ReadonlyArray<ChartInitial> => {
222
+ const state = states.get(target)
223
+ return state === undefined ? [] : [{ id: `initial:${target}`, target, parent: state.parent }]
224
+ })
225
+
226
+ return {
227
+ machineId: document.machineId,
228
+ roots: [...document.roots],
229
+ nodes,
230
+ edges,
231
+ runtimeTargets,
232
+ initials
233
+ }
234
+ }