@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.
- package/NOTICE +4 -0
- package/README.md +11 -11
- package/dist/DevToolsProtocol.d.ts +66 -630
- package/dist/DevToolsProtocol.d.ts.map +1 -1
- package/dist/DevToolsProtocol.js +0 -190
- package/dist/DevToolsProtocol.js.map +1 -1
- package/dist/MachineDocument.d.ts +38 -18
- package/dist/MachineDocument.d.ts.map +1 -1
- package/dist/MachineDocument.js +14 -12
- package/dist/MachineDocument.js.map +1 -1
- package/dist/MachineRegistry.d.ts +22 -2
- package/dist/MachineRegistry.d.ts.map +1 -1
- package/dist/MachineWalkthrough.d.ts +171 -0
- package/dist/MachineWalkthrough.d.ts.map +1 -0
- package/dist/MachineWalkthrough.js +98 -0
- package/dist/MachineWalkthrough.js.map +1 -0
- package/dist/ProjectInspector.d.ts +0 -2
- package/dist/ProjectInspector.d.ts.map +1 -1
- package/dist/ProjectInspector.js.map +1 -1
- package/dist/client/assets/index-BTOAhezX.js +37 -0
- package/dist/client/assets/index-Cg7xzSkz.css +1 -0
- package/dist/client/index.html +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/devServer.d.ts.map +1 -1
- package/dist/internal/devServer.js +4 -64
- package/dist/internal/devServer.js.map +1 -1
- package/dist/internal/evaluationWorker.d.ts.map +1 -1
- package/dist/internal/evaluationWorker.js +7 -263
- package/dist/internal/evaluationWorker.js.map +1 -1
- package/dist/internal/machineDocument.d.ts.map +1 -1
- package/dist/internal/machineDocument.js +3 -1
- package/dist/internal/machineDocument.js.map +1 -1
- package/dist/internal/machineWalkthrough.d.ts +26 -0
- package/dist/internal/machineWalkthrough.d.ts.map +1 -0
- package/dist/internal/machineWalkthrough.js +256 -0
- package/dist/internal/machineWalkthrough.js.map +1 -0
- package/dist/internal/projectInspector.d.ts.map +1 -1
- package/dist/internal/projectInspector.js +1 -11
- package/dist/internal/projectInspector.js.map +1 -1
- package/index.html +2 -2
- package/package.json +4 -3
- package/src/DevToolsProtocol.ts +0 -285
- package/src/MachineDocument.ts +21 -19
- package/src/MachineWalkthrough.ts +199 -0
- package/src/ProjectInspector.ts +0 -5
- package/src/index.ts +2 -2
- package/src/internal/browser/chart-layout.ts +433 -0
- package/src/internal/browser/chart-model.ts +234 -0
- package/src/internal/browser/chart-renderer.ts +661 -0
- package/src/internal/browser/example-machine.ts +15 -6
- package/src/internal/browser/input-form.ts +1 -418
- package/src/internal/browser/invoke-outcomes-example.ts +231 -0
- package/src/internal/browser/parallel-completion-example.ts +199 -0
- package/src/internal/browser/planner-example.ts +2 -0
- package/src/internal/browser/protocol-events-example.ts +200 -0
- package/src/internal/browser/styles.css +795 -341
- package/src/internal/browser/transition-semantics-example.ts +236 -0
- package/src/internal/browser/visualizer-app.ts +781 -713
- package/src/internal/browser/visualizer-model.ts +104 -0
- package/src/internal/devServer.ts +4 -82
- package/src/internal/evaluationWorker.ts +7 -371
- package/src/internal/machineDocument.ts +3 -1
- package/src/internal/machineWalkthrough.ts +355 -0
- package/src/internal/projectInspector.ts +3 -31
- package/dist/MachineSimulator.d.ts +0 -169
- package/dist/MachineSimulator.d.ts.map +0 -1
- package/dist/MachineSimulator.js +0 -98
- package/dist/MachineSimulator.js.map +0 -1
- package/dist/client/assets/index-B49Tjawc.js +0 -14
- package/dist/client/assets/index-BKH0cOG2.css +0 -1
- package/dist/internal/machineSimulator.d.ts +0 -5
- package/dist/internal/machineSimulator.d.ts.map +0 -1
- package/dist/internal/machineSimulator.js +0 -156
- package/dist/internal/machineSimulator.js.map +0 -1
- package/src/MachineSimulator.ts +0 -154
- package/src/internal/browser/simulation-client.ts +0 -20
- 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
|
+
}
|