Packages

Providing a web interface to interact with mix xref graph output

Current section

Files

Jump to
ex_compile_graph lib ex_compile_graph_web assets src index.tsx
Raw

lib/ex_compile_graph_web/assets/src/index.tsx

import { createRoot } from "react-dom/client";
import React, { useEffect, useRef, useState, createContext } from "react";
import SidePanel from "./SidePanel";
import GraphView from "./GraphView";
import "./index.css";
type ApiRequest =
| { type: "getGraph" }
| {
type: "getDependencyExplanation";
payload: {
source: VertexId;
sink: VertexId;
reason: RecompileDepedencyReason;
};
};
type ApiResponseMap = {
getGraph: Graph;
getDependencyExplanation: RecompileDepedencyExplanation[];
};
type ApiResponse = unknown;
export type Graph = Vertex[];
export type VertexId = string;
export type DependencyType = "runtime" | "exports" | "compile";
export type RecompileDepedencyReason =
| "compile"
| "exports_then_compile"
| "exports"
| "compile_then_runtime";
export type RecompileDenpendency = {
id: VertexId;
reason: RecompileDepedencyReason;
};
export type Vertex = {
id: VertexId;
edges: Edge[];
recompile_dependencies: RecompileDenpendency[];
};
type Edge = {
from: VertexId;
to: VertexId;
dependency_type: DependencyType;
};
type LinesSpan = [from: number, to: number];
export type CodeSnippet = {
content: string;
highlight: LinesSpan;
lines_span: LinesSpan;
};
export type RecompileDepedencyExplanation = {
intermediates: VertexId[];
source: VertexId;
type: DependencyType;
snippets: CodeSnippet[];
};
export class ApiBase {
socket: WebSocket | null;
// Should not worry about overflow
private sequence: number;
private pendingRequests: Map<
number,
[type: ApiRequest["type"], successCb: (response: ApiResponse) => void]
>;
constructor() {
this.socket = null;
this.pendingRequests = new Map();
this.sequence = 0;
}
connect(cb: () => void) {
const ws = new WebSocket("ws://localhost:4040/ws");
ws.addEventListener("open", () => {
this.socket = ws;
cb();
});
ws.addEventListener("message", (event) => {
const { sequence, payload } = JSON.parse(event.data);
const entry = this.pendingRequests.get(sequence);
if (entry) {
const [type, successCb] = entry;
switch (type) {
case "getGraph": {
successCb(payload as ApiResponseMap["getGraph"]);
break;
}
case "getDependencyExplanation": {
successCb(payload as ApiResponseMap["getDependencyExplanation"]);
break;
}
}
successCb(payload);
this.pendingRequests.delete(sequence);
} else {
console.warn(
"Can't find matching a request for the response with sequence number #{sequence}"
);
}
});
}
disconnect() {
if (!this.socket) {
console.warn("Socket hasn't connected or its already closed");
return;
}
this.socket.close();
}
request<T extends ApiRequest>(
request: T
): Promise<ApiResponseMap[T["type"]]> {
return new Promise((resolve, reject) => {
if (!this.socket) {
return reject("Socket hasn't connected or its already closed");
}
if (this.socket.readyState !== 1) {
return reject("Socket is not ready");
}
const currentSequence = this.sequence++;
this.socket.send(JSON.stringify({ request, sequence: currentSequence }));
this.pendingRequests.set(currentSequence, [request.type, resolve]);
});
}
}
// Perform a DFS on graph but only follow edges satisfy the filter
function getConnectedVertices(
graph: Graph,
vertex: Vertex,
dependencyTypeFilter: Record<DependencyType, boolean>
) {
const dictionary = new Map<VertexId, Vertex>();
for (const vertex of graph) {
dictionary.set(vertex.id, vertex);
}
const visited = new Set();
const dfs = (vertexId: VertexId): Set<VertexId> => {
const result = new Set<VertexId>();
if (visited.has(vertexId)) return result;
visited.add(vertexId);
result.add(vertexId);
const vertex = dictionary.get(vertexId) as Vertex;
for (const edge of vertex.edges) {
if (dependencyTypeFilter[edge.dependency_type]) {
dfs(edge.to).forEach((i) => result.add(i));
}
}
return result;
};
return Array.from(dfs(vertex.id));
}
export function recompileDenpendencies(vertex: Vertex) {
return vertex.recompile_dependencies.filter((i) =>
["compile", "compile_then_runtme"].includes(i.reason)
);
}
export const ApiContext = createContext(new ApiBase());
const App = () => {
const [connected, setConnected] = useState(false);
const { current: api } = useRef(new ApiBase());
const [isLoading, setLoading] = useState(false);
const [graph, setGraph] = useState<Graph>();
const [focusedVertex, setFocusedVertex] = useState<VertexId>();
const [selectedVertex, setSelectedVertex] = useState<Vertex>();
const [dependencyTypeFilter, setDependencyTypeFilter] = useState<
Record<DependencyType, boolean>
>({ compile: true, exports: true, runtime: true });
useEffect(() => {
api.connect(() => {
setConnected(true);
api.request({ type: "getGraph" }).then((graph) => {
setLoading(false);
setGraph(graph);
});
});
return api.disconnect;
}, []);
console.log("graph", graph);
return connected ? (
<ApiContext.Provider value={api}>
<GraphView
loading={isLoading}
graph={graph}
focusedVertex={focusedVertex}
onSelectVertex={(vertexId) => {
if (graph) {
const vertex = graph.find((v) => v.id === vertexId);
if (!vertex)
throw new Error(
`Unexpected vertex ${vertexId} is selected in GraphView`
);
setFocusedVertex(vertex.id);
setSelectedVertex(vertex);
}
}}
highlightVercies={
graph && selectedVertex
? getConnectedVertices(graph, selectedVertex, dependencyTypeFilter)
: undefined
}
onDependencyFiltersChange={(value, type) => {
const clone = { ...dependencyTypeFilter };
clone[type] = value;
setDependencyTypeFilter(clone);
}}
dependencyTypeFilter={dependencyTypeFilter}
/>
<SidePanel
loading={isLoading}
graph={graph}
selectedVertex={selectedVertex}
onSelectVertex={(vertexId) => {
if (!vertexId) {
setFocusedVertex(undefined);
return setSelectedVertex(undefined);
}
if (graph) {
const vertex = graph.find((v) => v.id === vertexId);
if (!vertex)
throw new Error(
`Unexpected vertex ${vertexId} is selected in SidePanel`
);
setFocusedVertex(vertex.id);
setSelectedVertex(vertex);
}
}}
onHoverVertex={(vertexId) => setFocusedVertex(vertexId)}
onUnhoverVertex={() => setFocusedVertex(undefined)}
/>
</ApiContext.Provider>
) : null;
};
// Inject your application into the an element with the id `app`.
// Make sure that such an element exists in the dom ;)
const container = document.getElementById("app") as HTMLElement;
const root = createRoot(container);
root.render(<App />);