( project: string, maxNodes = GRAPH_RENDER_NODE_LIMIT, onProgress?: (progress: LoadProgress) => void, graph: GraphVariant = "code", )
| 41 | export type GraphVariant = "code" | "missed"; |
| 42 | |
| 43 | export async function fetchLayout( |
| 44 | project: string, |
| 45 | maxNodes = GRAPH_RENDER_NODE_LIMIT, |
| 46 | onProgress?: (progress: LoadProgress) => void, |
| 47 | graph: GraphVariant = "code", |
| 48 | ): Promise<GraphData> { |
| 49 | const params = new URLSearchParams({ project, max_nodes: String(maxNodes) }); |
| 50 | if (graph === "missed") params.set("graph", "missed"); |
| 51 | const res = await fetch(`/api/layout?${params}`); |
| 52 | |
| 53 | if (!res.ok) { |
| 54 | const body = await res.json().catch(() => ({ error: res.statusText })); |
| 55 | throw new Error(body.error ?? `HTTP ${res.status}`); |
| 56 | } |
| 57 | |
| 58 | /* Stream the body when possible so large budgets show live download |
| 59 | * progress instead of a silent stall. */ |
| 60 | if (!res.body || !onProgress) { |
| 61 | return res.json(); |
| 62 | } |
| 63 | |
| 64 | const lengthHeader = res.headers.get("content-length"); |
| 65 | const totalBytes = lengthHeader ? parseInt(lengthHeader, 10) || null : null; |
| 66 | const reader = res.body.getReader(); |
| 67 | const chunks: Uint8Array[] = []; |
| 68 | let receivedBytes = 0; |
| 69 | |
| 70 | for (;;) { |
| 71 | const { done, value } = await reader.read(); |
| 72 | if (done) break; |
| 73 | chunks.push(value); |
| 74 | receivedBytes += value.length; |
| 75 | onProgress({ receivedBytes, totalBytes }); |
| 76 | } |
| 77 | |
| 78 | const merged = new Uint8Array(receivedBytes); |
| 79 | let offset = 0; |
| 80 | for (const chunk of chunks) { |
| 81 | merged.set(chunk, offset); |
| 82 | offset += chunk.length; |
| 83 | } |
| 84 | return JSON.parse(new TextDecoder().decode(merged)); |
| 85 | } |
| 86 | |
| 87 | const NO_PROGRESS: LoadProgress = { receivedBytes: 0, totalBytes: null }; |
| 88 |
no outgoing calls
no test coverage detected