chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { GraphData } from "../lib/types";
|
||||
|
||||
export interface LoadProgress {
|
||||
receivedBytes: number;
|
||||
totalBytes: number | null;
|
||||
}
|
||||
|
||||
interface UseGraphDataResult {
|
||||
data: GraphData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
progress: LoadProgress;
|
||||
fetchOverview: (
|
||||
project: string,
|
||||
maxNodes?: number,
|
||||
graph?: "code" | "missed",
|
||||
) => void;
|
||||
fetchDetail: (project: string, centerNode: string) => void;
|
||||
}
|
||||
|
||||
/* Node budget: how many nodes the layout endpoint is asked for. The default
|
||||
* keeps first paint fast; the user can raise it in 5k steps up to the hard
|
||||
* ceiling (mirrors HARD_MAX_NODES in src/ui/layout3d.c). Edges always follow
|
||||
* the budget — the server returns every edge between the loaded nodes. */
|
||||
export const GRAPH_RENDER_NODE_LIMIT = 5000;
|
||||
export const GRAPH_NODE_BUDGET_STEP = 5000;
|
||||
export const GRAPH_NODE_BUDGET_MAX = 10_000_000;
|
||||
|
||||
export function clampNodeBudget(value: number): number {
|
||||
if (!Number.isFinite(value)) return GRAPH_RENDER_NODE_LIMIT;
|
||||
const stepped =
|
||||
Math.round(value / GRAPH_NODE_BUDGET_STEP) * GRAPH_NODE_BUDGET_STEP;
|
||||
if (stepped < GRAPH_NODE_BUDGET_STEP) return GRAPH_NODE_BUDGET_STEP;
|
||||
if (stepped > GRAPH_NODE_BUDGET_MAX) return GRAPH_NODE_BUDGET_MAX;
|
||||
return stepped;
|
||||
}
|
||||
|
||||
/** Which graph to lay out: the code graph (default) or the missed graph —
|
||||
* only files the indexer could not fully cover, as their file structure. */
|
||||
export type GraphVariant = "code" | "missed";
|
||||
|
||||
export async function fetchLayout(
|
||||
project: string,
|
||||
maxNodes = GRAPH_RENDER_NODE_LIMIT,
|
||||
onProgress?: (progress: LoadProgress) => void,
|
||||
graph: GraphVariant = "code",
|
||||
): Promise<GraphData> {
|
||||
const params = new URLSearchParams({ project, max_nodes: String(maxNodes) });
|
||||
if (graph === "missed") params.set("graph", "missed");
|
||||
const res = await fetch(`/api/layout?${params}`);
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ error: res.statusText }));
|
||||
throw new Error(body.error ?? `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
/* Stream the body when possible so large budgets show live download
|
||||
* progress instead of a silent stall. */
|
||||
if (!res.body || !onProgress) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
const lengthHeader = res.headers.get("content-length");
|
||||
const totalBytes = lengthHeader ? parseInt(lengthHeader, 10) || null : null;
|
||||
const reader = res.body.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
let receivedBytes = 0;
|
||||
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
receivedBytes += value.length;
|
||||
onProgress({ receivedBytes, totalBytes });
|
||||
}
|
||||
|
||||
const merged = new Uint8Array(receivedBytes);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
merged.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return JSON.parse(new TextDecoder().decode(merged));
|
||||
}
|
||||
|
||||
const NO_PROGRESS: LoadProgress = { receivedBytes: 0, totalBytes: null };
|
||||
|
||||
export function useGraphData(): UseGraphDataResult {
|
||||
const [data, setData] = useState<GraphData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [progress, setProgress] = useState<LoadProgress>(NO_PROGRESS);
|
||||
|
||||
const fetchOverview = useCallback(
|
||||
async (project: string, maxNodes?: number, graph: GraphVariant = "code") => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setProgress(NO_PROGRESS);
|
||||
try {
|
||||
const result = await fetchLayout(project, maxNodes, setProgress, graph);
|
||||
setData(result);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to fetch layout");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
async (project: string, _centerNode: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setProgress(NO_PROGRESS);
|
||||
try {
|
||||
/* TODO: detail level with center_node filtering */
|
||||
const result = await fetchLayout(project, undefined, setProgress);
|
||||
setData(result);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to fetch layout");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { data, loading, error, progress, fetchOverview, fetchDetail };
|
||||
}
|
||||
Reference in New Issue
Block a user