fix: Analyst ratings abort without reason on overview page
- trpcQuery/trpcMutate now throw 'Request timed out (8s)' instead of generic 'The operation was aborted.' so users know WHY the request failed - AnalystRatings adds cancelled guard (race condition fix) so stale promise results from symbol changes are ignored (matches OverviewPanel pattern) - Add Retry button on error state for timeout or other failures - useEffect cleanup returns cancelled flag setter
This commit is contained in:
@@ -52,17 +52,25 @@ export function AnalystRatings({ symbol }: Props) {
|
|||||||
const [consensus, setConsensus] = useState<{ strongBuy: number; buy: number; hold: number; sell: number; strongSell: number } | null>(null);
|
const [consensus, setConsensus] = useState<{ strongBuy: number; buy: number; hold: number; sell: number; strongSell: number } | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [retryKey, setRetryKey] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!symbol) return;
|
if (!symbol) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
let cancelled = false;
|
||||||
api.institutional.analystRatings(symbol).then((res) => {
|
api.institutional.analystRatings(symbol).then((res) => {
|
||||||
|
if (cancelled) return;
|
||||||
if (res.error) { setError(res.error); setRatings(null); }
|
if (res.error) { setError(res.error); setRatings(null); }
|
||||||
else { setRatings(res.ratings); setConsensus(res.consensus); }
|
else { setRatings(res.ratings); setConsensus(res.consensus); }
|
||||||
}).catch((e: Error) => setError(e.message))
|
}).catch((e: Error) => {
|
||||||
.finally(() => setLoading(false));
|
if (cancelled) return;
|
||||||
}, [symbol]);
|
setError(e.message);
|
||||||
|
}).finally(() => {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [symbol, retryKey]);
|
||||||
|
|
||||||
const topFirms = useMemo(() => {
|
const topFirms = useMemo(() => {
|
||||||
if (!ratings) return [];
|
if (!ratings) return [];
|
||||||
@@ -76,7 +84,12 @@ export function AnalystRatings({ symbol }: Props) {
|
|||||||
|
|
||||||
if (!symbol) return null;
|
if (!symbol) return null;
|
||||||
if (loading) return <div className="text-xs text-fg-muted py-2">Loading analyst ratings...</div>;
|
if (loading) return <div className="text-xs text-fg-muted py-2">Loading analyst ratings...</div>;
|
||||||
if (error) return <div className="text-xs text-danger py-2">Analyst ratings: {error}</div>;
|
if (error) return (
|
||||||
|
<div className="text-xs text-danger py-2 flex items-center gap-2">
|
||||||
|
<span>Analyst ratings: {error}</span>
|
||||||
|
<button className="text-up hover:underline" onClick={() => setRetryKey((k) => k + 1)}>Retry</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
if (!ratings || ratings.length === 0) return <div className="text-xs text-fg-muted py-2">No analyst ratings available for {symbol}.</div>;
|
if (!ratings || ratings.length === 0) return <div className="text-xs text-fg-muted py-2">No analyst ratings available for {symbol}.</div>;
|
||||||
|
|
||||||
const consensusText = consensus
|
const consensusText = consensus
|
||||||
|
|||||||
@@ -396,6 +396,11 @@ async function trpcQuery<T>(procedure: string, input?: unknown): Promise<T> {
|
|||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
return parse<T>(res);
|
return parse<T>(res);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof DOMException && e.name === "AbortError") {
|
||||||
|
throw new Error(`Request timed out (${TRPC_TIMEOUT_MS / 1000}s)`);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
@@ -413,6 +418,11 @@ async function trpcMutate<T>(procedure: string, input: unknown): Promise<T> {
|
|||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
return parse<T>(res);
|
return parse<T>(res);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof DOMException && e.name === "AbortError") {
|
||||||
|
throw new Error(`Request timed out (${TRPC_TIMEOUT_MS / 1000}s)`);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user