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