Conversation
… into feat/lab-query-plan
There was a problem hiding this comment.
Code Review
This pull request introduces Hive Router query plan rendering to the Hive Laboratory, featuring a new visual graph component and a text-based viewer. It also expands the laboratory's configuration options for fetch operations and subscription protocols. Feedback highlights the need to adhere to the repository's style guide by using fetchWithRetry for introspection, improving the retry logic to handle HTTP errors, securing unsafe JSON parsing in the UI, and optimizing performance in the graph rendering logic.
| import { toast } from 'sonner'; | ||
| import z from 'zod'; | ||
| import { asyncInterval } from '@/lib/utils'; | ||
| import { createRequestSignal } from './request'; |
There was a problem hiding this comment.
Import fetchWithRetry to adhere to the repository style guide regarding network calls and built-in retries.
| import { createRequestSignal } from './request'; | |
| import { createRequestSignal, fetchWithRetry } from './request'; |
References
- Avoid using fetch or node-fetch directly, as it does not have built-in retries. (link)
| const response = await fetch(requestUrl, { | ||
| signal: requestSignal, | ||
| method: introspectionRequest.method, | ||
| body: | ||
| introspectionRequest.method === 'POST' | ||
| ? JSON.stringify({ | ||
| query: introspectionRequest.query, | ||
| }) | ||
| : undefined, | ||
| headers: | ||
| introspectionRequest.method === 'POST' | ||
| ? { | ||
| 'Content-Type': 'application/json', | ||
| } | ||
| : undefined, | ||
| }).then(r => r.json()); |
There was a problem hiding this comment.
Use fetchWithRetry instead of direct fetch to ensure introspection requests have built-in retries as required by the style guide. This also helps in handling transient network failures during schema polling.
const response = await fetchWithRetry(
requestUrl,
{
signal: requestSignal,
method: introspectionRequest.method,
body:
introspectionRequest.method === 'POST'
? JSON.stringify({
query: introspectionRequest.query,
})
: undefined,
headers:
introspectionRequest.method === 'POST'
? {
'Content-Type': 'application/json',
}
: undefined,
},
props.settingsApi?.settings.fetch.retry,
).then(r => r.json());References
- Avoid using fetch or node-fetch directly, as it does not have built-in retries. (link)
| export async function fetchWithRetry( | ||
| input: RequestInfo | URL, | ||
| init: RequestInit, | ||
| retryCount = 0, | ||
| ): Promise<Response> { | ||
| let attempt = 0; | ||
|
|
||
| while (true) { | ||
| try { | ||
| return await fetch(input, init); | ||
| } catch (error) { | ||
| if (attempt >= retryCount) { | ||
| throw error; | ||
| } | ||
|
|
||
| attempt++; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The fetchWithRetry implementation only retries on network errors (exceptions thrown by fetch). It should also consider retrying on transient HTTP error status codes (e.g., 502, 503, 504) by checking response.ok. Additionally, implementing a delay or exponential backoff between retries is recommended to avoid overwhelming the server during transient failures.
| const queryPlan = useMemo(() => { | ||
| const queryPlan = | ||
| JSON.parse((historyItem as LaboratoryHistoryRequest)?.response ?? '{}').extensions | ||
| ?.queryPlan ?? {}; | ||
|
|
||
| if (!queryPlan) { | ||
| return null; | ||
| } | ||
|
|
||
| return QueryPlanSchema.safeParse(queryPlan).success ? queryPlan : null; | ||
| }, [historyItem]); |
There was a problem hiding this comment.
Directly calling JSON.parse on the response string is unsafe. If the response is not valid JSON (e.g., a network error returning HTML or plain text), this will throw and potentially crash the component. Consider wrapping this in a try-catch block to handle parsing errors gracefully.
const queryPlan = useMemo(() => {
try {
const queryPlan =
JSON.parse((historyItem as LaboratoryHistoryRequest)?.response ?? '{}').extensions
?.queryPlan;
if (!queryPlan) {
return null;
}
return QueryPlanSchema.safeParse(queryPlan).success ? queryPlan : null;
} catch {
return null;
}
}, [historyItem]);
| const hasValidQueryPlan = useMemo(() => { | ||
| if (!historyItem) { | ||
| return false; | ||
| } | ||
|
|
||
| const queryPlan = JSON.parse(historyItem.response).extensions?.queryPlan; | ||
|
|
||
| if (!queryPlan) { | ||
| return false; | ||
| } | ||
|
|
||
| console.log(QueryPlanSchema.safeParse(queryPlan).error?.message); | ||
|
|
||
| return QueryPlanSchema.safeParse(queryPlan).success; | ||
| }, [historyItem?.response]); |
There was a problem hiding this comment.
The JSON.parse call is unsafe and could crash the UI if the response is not valid JSON. Also, ensure debug console.log statements are removed before merging.
const hasValidQueryPlan = useMemo(() => {
if (!historyItem) {
return false;
}
try {
const queryPlan = JSON.parse(historyItem.response).extensions?.queryPlan;
return !!queryPlan && QueryPlanSchema.safeParse(queryPlan).success;
} catch {
return false;
}
}, [historyItem?.response]);
| ref={ref => { | ||
| if (ref && !nodeSizes[node.id]) { | ||
| setNodeSizes(prev => ({ | ||
| ...prev, | ||
| [node.id]: { width: ref.clientWidth, height: ref.clientHeight }, | ||
| })); | ||
| } | ||
| }} |
There was a problem hiding this comment.
Updating state (setNodeSizes) within a ref callback inside a loop (nodes.map) can lead to performance issues, as it triggers a re-render for every node being measured. For large graphs, this will cause excessive churn. Consider using a ResizeObserver to measure elements or batching these updates in a useEffect hook after the initial render.
Background
Description
Checklist