@typeonce/effect-machine-devtools 0.25.0 → 0.26.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/README.md +2 -2
- package/dist/client/assets/index-BsorPSDS.css +1 -0
- package/dist/client/assets/index-eiptehRd.js +37 -0
- package/dist/client/index.html +2 -2
- package/package.json +6 -6
- package/src/internal/browser/chart-layout-policy.ts +206 -0
- package/src/internal/browser/chart-layout.ts +334 -81
- package/src/internal/browser/chart-model.ts +10 -0
- package/src/internal/browser/chart-renderer.ts +127 -14
- package/src/internal/browser/styles.css +188 -10
- package/src/internal/browser/transition-semantics-example.ts +10 -1
- package/src/internal/browser/visualizer-analysis.ts +52 -0
- package/src/internal/browser/visualizer-app.ts +110 -10
- package/dist/client/assets/index-BTOAhezX.js +0 -37
- package/dist/client/assets/index-Cg7xzSkz.css +0 -1
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
renderChart
|
|
20
20
|
} from "./chart-renderer.js"
|
|
21
21
|
import { type InputField, projectInputSchema } from "./input-form.js"
|
|
22
|
+
import { analyzeVisualization, type VisualizerAnalysis } from "./visualizer-analysis.js"
|
|
22
23
|
import {
|
|
23
24
|
branchTargetApi,
|
|
24
25
|
type IncomingTransition,
|
|
@@ -186,7 +187,7 @@ const renderActivity = (activity: VisualizationActivity, navigate: StateNavigato
|
|
|
186
187
|
const title = createElement("div", "card-title")
|
|
187
188
|
title.append(createElement("strong", undefined, activityTitle(activity)))
|
|
188
189
|
const flags = createElement("div", "card-flags")
|
|
189
|
-
flags.append(badge(activity.type,
|
|
190
|
+
flags.append(badge(activity.type, `activity badge-activity-${activity.type}`))
|
|
190
191
|
header.append(title, flags)
|
|
191
192
|
card.append(header)
|
|
192
193
|
|
|
@@ -394,6 +395,8 @@ export const renderVisualizer = (
|
|
|
394
395
|
diagnostics: ReadonlyArray<Diagnostic> = []
|
|
395
396
|
): void => {
|
|
396
397
|
const model = makeVisualizerModel(visualization)
|
|
398
|
+
const analysis = analyzeVisualization(visualization)
|
|
399
|
+
const analysisCount = analysis.warnings.length + analysis.topologyNotes.length
|
|
397
400
|
const transitionsById = new Map(visualization.transitions.map((transition) => [transition.id, transition]))
|
|
398
401
|
const eventSchemas = new Map(visualization.inputs.events.map(({ event, schema }) => [event, schema]))
|
|
399
402
|
const relatedFrom = new Set<string>()
|
|
@@ -405,6 +408,7 @@ export const renderVisualizer = (
|
|
|
405
408
|
let selectedFrame: MachineWalkthrough.Frame | undefined
|
|
406
409
|
let walkthrough: MachineWalkthrough.Session | undefined
|
|
407
410
|
let chartView: ChartView | undefined
|
|
411
|
+
let inspectorView: "selection" | "analysis" | undefined
|
|
408
412
|
|
|
409
413
|
const activePaths = (): ReadonlyArray<string> =>
|
|
410
414
|
walkthrough === undefined ? model.activePaths : MachineWalkthrough.current(walkthrough).after.activePaths
|
|
@@ -417,7 +421,7 @@ export const renderVisualizer = (
|
|
|
417
421
|
chartPanel.setAttribute("aria-label", `${model.machineId} topology`)
|
|
418
422
|
const inspector = createElement("aside", "inspector")
|
|
419
423
|
inspector.setAttribute("aria-live", "polite")
|
|
420
|
-
inspector.setAttribute("aria-label", "
|
|
424
|
+
inspector.setAttribute("aria-label", "Machine inspector")
|
|
421
425
|
inspector.hidden = true
|
|
422
426
|
const inspectorClose = createElement("button", "inspector-close", "Close")
|
|
423
427
|
inspectorClose.type = "button"
|
|
@@ -448,16 +452,28 @@ export const renderVisualizer = (
|
|
|
448
452
|
const walkthroughButton = createElement("button", "toolbar-button", "Start simulation")
|
|
449
453
|
walkthroughButton.type = "button"
|
|
450
454
|
walkthroughButton.disabled = model.roots.length === 0
|
|
455
|
+
const analysisButton = createElement("button", "toolbar-button analysis-button")
|
|
456
|
+
analysisButton.type = "button"
|
|
457
|
+
analysisButton.hidden = analysisCount === 0
|
|
458
|
+
analysisButton.setAttribute("aria-pressed", "false")
|
|
459
|
+
analysisButton.append(
|
|
460
|
+
createElement("span", undefined, "Analysis"),
|
|
461
|
+
createElement("span", "analysis-button-count", String(analysisCount))
|
|
462
|
+
)
|
|
451
463
|
|
|
452
464
|
const hideInspector = (): void => {
|
|
453
465
|
inspectorContent.replaceChildren()
|
|
454
466
|
inspector.hidden = true
|
|
467
|
+
inspectorView = undefined
|
|
455
468
|
detailsButton.textContent = "View details"
|
|
469
|
+
analysisButton.setAttribute("aria-pressed", "false")
|
|
456
470
|
}
|
|
457
471
|
|
|
458
|
-
const showInspector = (): void => {
|
|
472
|
+
const showInspector = (view: "selection" | "analysis"): void => {
|
|
459
473
|
inspector.hidden = false
|
|
460
|
-
|
|
474
|
+
inspectorView = view
|
|
475
|
+
detailsButton.textContent = view === "selection" ? "Hide details" : "View details"
|
|
476
|
+
analysisButton.setAttribute("aria-pressed", view === "analysis" ? "true" : "false")
|
|
461
477
|
}
|
|
462
478
|
|
|
463
479
|
const closeChoicePicker = (): void => {
|
|
@@ -467,7 +483,7 @@ export const renderVisualizer = (
|
|
|
467
483
|
|
|
468
484
|
const renderInspection = (inspection: StateInspection): void => {
|
|
469
485
|
inspectorContent.replaceChildren()
|
|
470
|
-
showInspector()
|
|
486
|
+
showInspector("selection")
|
|
471
487
|
const header = createElement("header", "inspector-header")
|
|
472
488
|
const breadcrumbs = createElement("nav", "breadcrumbs")
|
|
473
489
|
breadcrumbs.setAttribute("aria-label", "State path")
|
|
@@ -528,7 +544,7 @@ export const renderVisualizer = (
|
|
|
528
544
|
|
|
529
545
|
const renderTransitionInspection = (transition: VisualizationTransition): void => {
|
|
530
546
|
inspectorContent.replaceChildren()
|
|
531
|
-
showInspector()
|
|
547
|
+
showInspector("selection")
|
|
532
548
|
const header = createElement("header", "inspector-header")
|
|
533
549
|
const titleRow = createElement("div", "inspector-title-row")
|
|
534
550
|
const flags = createElement("div", "card-flags")
|
|
@@ -560,6 +576,82 @@ export const renderVisualizer = (
|
|
|
560
576
|
}
|
|
561
577
|
}
|
|
562
578
|
|
|
579
|
+
const renderAnalysis = (findings: VisualizerAnalysis): void => {
|
|
580
|
+
inspectorContent.replaceChildren()
|
|
581
|
+
showInspector("analysis")
|
|
582
|
+
|
|
583
|
+
const header = createElement("header", "inspector-header analysis-header")
|
|
584
|
+
const titleRow = createElement("div", "inspector-title-row")
|
|
585
|
+
titleRow.append(
|
|
586
|
+
createElement("h2", undefined, "Analysis"),
|
|
587
|
+
badge(String(findings.warnings.length + findings.topologyNotes.length), "count")
|
|
588
|
+
)
|
|
589
|
+
header.append(
|
|
590
|
+
titleRow,
|
|
591
|
+
createElement(
|
|
592
|
+
"p",
|
|
593
|
+
"analysis-summary",
|
|
594
|
+
"Static checks from the captured machine definition. Machine callbacks are not executed."
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
inspectorContent.append(header)
|
|
598
|
+
|
|
599
|
+
const warnings = createElement("section", "inspector-section analysis-section")
|
|
600
|
+
warnings.append(inspectionSection("Unhandled events", findings.warnings.length))
|
|
601
|
+
if (findings.warnings.length === 0) {
|
|
602
|
+
warnings.append(createElement("p", "section-empty", "Every declared public event has a registered handler."))
|
|
603
|
+
} else {
|
|
604
|
+
for (const warning of findings.warnings) {
|
|
605
|
+
const card = createElement("article", "inspection-card analysis-card")
|
|
606
|
+
const cardHeader = createElement("div", "card-header")
|
|
607
|
+
const title = createElement("div", "card-title")
|
|
608
|
+
title.append(createElement("strong", undefined, warning.event))
|
|
609
|
+
cardHeader.append(title, badge("no handler", "warning"))
|
|
610
|
+
card.append(
|
|
611
|
+
cardHeader,
|
|
612
|
+
createElement(
|
|
613
|
+
"p",
|
|
614
|
+
"analysis-message",
|
|
615
|
+
"This public event is declared, but no state registers a handler for it."
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
warnings.append(card)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
inspectorContent.append(warnings)
|
|
622
|
+
|
|
623
|
+
const topology = createElement("section", "inspector-section analysis-section")
|
|
624
|
+
topology.append(inspectionSection("Topology notes", findings.topologyNotes.length))
|
|
625
|
+
if (findings.topologyNotes.length === 0) {
|
|
626
|
+
topology.append(createElement("p", "section-empty", "Every state appears on a static path from initial."))
|
|
627
|
+
} else {
|
|
628
|
+
for (const note of findings.topologyNotes) {
|
|
629
|
+
const card = createElement("article", "inspection-card analysis-card")
|
|
630
|
+
const cardHeader = createElement("div", "card-header")
|
|
631
|
+
const title = createElement("div", "card-title")
|
|
632
|
+
title.append(stateLink(note.path, note.label, navigateToState))
|
|
633
|
+
cardHeader.append(title, badge(note.type, "state"))
|
|
634
|
+
card.append(
|
|
635
|
+
cardHeader,
|
|
636
|
+
createElement(
|
|
637
|
+
"p",
|
|
638
|
+
"analysis-message",
|
|
639
|
+
note.descendantCount === 0
|
|
640
|
+
? "No statically known path reaches this state from the machine's initial configuration."
|
|
641
|
+
: `No statically known path reaches this state or its ${note.descendantCount} descendants from the machine's initial configuration.`
|
|
642
|
+
),
|
|
643
|
+
createElement(
|
|
644
|
+
"p",
|
|
645
|
+
"analysis-qualification",
|
|
646
|
+
"Runtime-resolved targets or a resumed snapshot may still reach it."
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
topology.append(card)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
inspectorContent.append(topology)
|
|
653
|
+
}
|
|
654
|
+
|
|
563
655
|
const renderPathGroup = (label: string, paths: ReadonlyArray<string>): HTMLElement => {
|
|
564
656
|
const group = createElement("div", "trace-path-group")
|
|
565
657
|
group.append(createElement("span", "trace-label", label))
|
|
@@ -572,7 +664,7 @@ export const renderVisualizer = (
|
|
|
572
664
|
|
|
573
665
|
const renderWalkthroughTrace = (frame: MachineWalkthrough.Frame): void => {
|
|
574
666
|
inspectorContent.replaceChildren()
|
|
575
|
-
showInspector()
|
|
667
|
+
showInspector("selection")
|
|
576
668
|
const header = createElement("header", "inspector-header trace-header")
|
|
577
669
|
const eyebrow = createElement("div", "inspector-eyebrow")
|
|
578
670
|
eyebrow.append(badge("walkthrough", "active"))
|
|
@@ -916,7 +1008,7 @@ export const renderVisualizer = (
|
|
|
916
1008
|
inspectorClose.addEventListener("click", hideInspector)
|
|
917
1009
|
choicePickerClose.addEventListener("click", closeChoicePicker)
|
|
918
1010
|
detailsButton.addEventListener("click", () => {
|
|
919
|
-
if (!inspector.hidden) {
|
|
1011
|
+
if (!inspector.hidden && inspectorView === "selection") {
|
|
920
1012
|
hideInspector()
|
|
921
1013
|
} else if (selectedFrame !== undefined) {
|
|
922
1014
|
renderWalkthroughTrace(selectedFrame)
|
|
@@ -928,6 +1020,13 @@ export const renderVisualizer = (
|
|
|
928
1020
|
if (transition !== undefined) renderTransitionInspection(transition)
|
|
929
1021
|
}
|
|
930
1022
|
})
|
|
1023
|
+
analysisButton.addEventListener("click", () => {
|
|
1024
|
+
if (!inspector.hidden && inspectorView === "analysis") {
|
|
1025
|
+
hideInspector()
|
|
1026
|
+
} else {
|
|
1027
|
+
renderAnalysis(analysis)
|
|
1028
|
+
}
|
|
1029
|
+
})
|
|
931
1030
|
revealActiveButton.addEventListener("click", () => chartView?.revealStates(activePaths()))
|
|
932
1031
|
|
|
933
1032
|
const zoomOutButton = createElement("button", "toolbar-button zoom-button", "−")
|
|
@@ -971,7 +1070,7 @@ export const renderVisualizer = (
|
|
|
971
1070
|
const runtimeText = createElement("span")
|
|
972
1071
|
runtime.append(runtimeDot, runtimeText)
|
|
973
1072
|
const toolbarActions = createElement("div", "toolbar-actions")
|
|
974
|
-
toolbarActions.append(clearButton, detailsButton, walkthroughButton, revealActiveButton)
|
|
1073
|
+
toolbarActions.append(clearButton, detailsButton, walkthroughButton, revealActiveButton, analysisButton)
|
|
975
1074
|
const zoomControls = createElement("div", "zoom-controls")
|
|
976
1075
|
zoomControls.setAttribute("role", "group")
|
|
977
1076
|
zoomControls.setAttribute("aria-label", "Chart zoom")
|
|
@@ -1002,10 +1101,11 @@ export const renderVisualizer = (
|
|
|
1002
1101
|
revealActiveButton.disabled = active.size === 0
|
|
1003
1102
|
clearButton.hidden = simulating
|
|
1004
1103
|
detailsButton.hidden = simulating
|
|
1104
|
+
analysisButton.hidden = simulating || analysisCount === 0
|
|
1005
1105
|
chartPanel.classList.toggle("is-simulating", simulating)
|
|
1006
1106
|
if (simulating) hideInspector()
|
|
1007
1107
|
renderWalkthroughDock()
|
|
1008
|
-
if (!simulating && !inspector.hidden) {
|
|
1108
|
+
if (!simulating && !inspector.hidden && inspectorView === "selection") {
|
|
1009
1109
|
if (selectedFrame !== undefined) {
|
|
1010
1110
|
renderWalkthroughTrace(selectedFrame)
|
|
1011
1111
|
} else if (selectedPath !== undefined) {
|