fix: add AbortController to fetchPredictions and handleFetchBatchPredictions
Prevent race conditions by aborting in-flight requests when a new request is triggered. Each function now: - Aborts the previous request via a stored AbortController ref - Passes signal to all fetch() calls - Silently discards AbortError in catch blocks Completes task 7.2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
838c063b5b
commit
45a23047dd
2 changed files with 39 additions and 12 deletions
|
|
@ -64,7 +64,7 @@
|
|||
## 7. Frontend — Stale Closures & Race Conditions
|
||||
|
||||
- [x] 7.1 `[opus]` Fix stale closure in `fetchPredictions`: extract `modelInfo` into a `useRef` that stays in sync with state, use ref in `generateCacheKey` (`src/app/page.tsx:489-552`)
|
||||
- [ ] 7.2 `[opus]` Add AbortController to `fetchPredictions` and `handleFetchBatchPredictions`: store controller in ref, abort previous on new request, discard stale responses (`src/app/page.tsx:494-663`)
|
||||
- [x] 7.2 `[opus]` Add AbortController to `fetchPredictions` and `handleFetchBatchPredictions`: store controller in ref, abort previous on new request, discard stale responses (`src/app/page.tsx:494-663`)
|
||||
- [ ] 7.3 `[opus]` Fix stale closure in CandleChart click handler: convert `drawingState`, `selectedLineId`, `dragState`, `annotations` to refs, update refs alongside setState, read refs in handler (`src/components/CandleChart.tsx:572-971`)
|
||||
- [ ] 7.4 `[opus]` Fix stale closure in SpanAnnotationManager keyboard handler: wrap `handleDeleteSpan` in `useCallback`, use ref for `selectedSpanId` (`src/components/SpanAnnotationManager.tsx:461-535`)
|
||||
|
||||
|
|
|
|||
|
|
@ -198,6 +198,14 @@ export default function Home() {
|
|||
modelVersion: string;
|
||||
}>>(new Map());
|
||||
|
||||
// Ref to avoid stale closure over modelInfo in fetchPredictions/generateCacheKey
|
||||
const modelInfoRef = useRef(predictionState.modelInfo);
|
||||
const fetchPredictionsAbortRef = useRef<AbortController | null>(null);
|
||||
const fetchBatchAbortRef = useRef<AbortController | null>(null);
|
||||
useEffect(() => {
|
||||
modelInfoRef.current = predictionState.modelInfo;
|
||||
}, [predictionState.modelInfo]);
|
||||
|
||||
// Model health state
|
||||
const [isModelOnline, setIsModelOnline] = useState(true);
|
||||
|
||||
|
|
@ -490,20 +498,21 @@ export default function Home() {
|
|||
// Generate cache key from chart, timerange, and model version
|
||||
const generateCacheKey = useCallback((chartId: number | null, modelVersion?: string | null) => {
|
||||
if (!chartId) return null;
|
||||
const version = modelVersion || predictionState.modelInfo?.model_version || 'unknown';
|
||||
const version = modelVersion || modelInfoRef.current?.model_version || 'unknown';
|
||||
return `${chartId}_${version}`;
|
||||
}, [predictionState.modelInfo]);
|
||||
}, []);
|
||||
|
||||
// Fetch predictions for visible candles
|
||||
const fetchPredictions = useCallback(async (candles: any[]) => {
|
||||
if (!activeChartId || candles.length === 0) return;
|
||||
|
||||
const cacheKey = generateCacheKey(activeChartId, predictionState.modelInfo?.model_version);
|
||||
|
||||
const currentModelInfo = modelInfoRef.current;
|
||||
const cacheKey = generateCacheKey(activeChartId, currentModelInfo?.model_version);
|
||||
|
||||
// Check cache first
|
||||
if (cacheKey && predictionCacheRef.current.has(cacheKey)) {
|
||||
const cached = predictionCacheRef.current.get(cacheKey)!;
|
||||
if (cached.modelVersion === predictionState.modelInfo?.model_version) {
|
||||
if (cached.modelVersion === currentModelInfo?.model_version) {
|
||||
setPredictionState((prev) => ({
|
||||
...prev,
|
||||
spans: cached.spans,
|
||||
|
|
@ -514,6 +523,11 @@ export default function Home() {
|
|||
}
|
||||
}
|
||||
|
||||
// Abort any in-flight prediction request
|
||||
fetchPredictionsAbortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
fetchPredictionsAbortRef.current = controller;
|
||||
|
||||
setPredictionState((prev) => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
|
|
@ -521,6 +535,7 @@ export default function Home() {
|
|||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ candles }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -528,7 +543,7 @@ export default function Home() {
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
// Cache the results
|
||||
if (cacheKey) {
|
||||
predictionCacheRef.current.set(cacheKey, {
|
||||
|
|
@ -546,6 +561,7 @@ export default function Home() {
|
|||
cacheKey,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') return;
|
||||
console.error('Failed to fetch predictions:', error);
|
||||
setPredictionState((prev) => ({
|
||||
...prev,
|
||||
|
|
@ -553,7 +569,7 @@ export default function Home() {
|
|||
error: error instanceof Error ? error.message : 'Failed to fetch predictions',
|
||||
}));
|
||||
}
|
||||
}, [activeChartId, predictionState.modelInfo, generateCacheKey]);
|
||||
}, [activeChartId, generateCacheKey]);
|
||||
|
||||
// Toggle prediction visibility
|
||||
const togglePredictionVisibility = useCallback(() => {
|
||||
|
|
@ -604,23 +620,32 @@ export default function Home() {
|
|||
const handleFetchBatchPredictions = useCallback(async () => {
|
||||
if (!activeChartId) return;
|
||||
|
||||
// Abort any in-flight batch prediction request
|
||||
fetchBatchAbortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
fetchBatchAbortRef.current = controller;
|
||||
|
||||
setPredictionState((prev) => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
// Fetch chart data to get pair/timeframe info
|
||||
const chartResponse = await fetch(`/api/charts/${activeChartId}`);
|
||||
const chartResponse = await fetch(`/api/charts/${activeChartId}`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!chartResponse.ok) {
|
||||
throw new Error('Failed to fetch chart info');
|
||||
}
|
||||
const chartData = await chartResponse.json();
|
||||
|
||||
// Fetch candles for the chart
|
||||
const candlesResponse = await fetch(`/api/candles?chartId=${activeChartId}`);
|
||||
const candlesResponse = await fetch(`/api/candles?chartId=${activeChartId}`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!candlesResponse.ok) {
|
||||
throw new Error('Failed to fetch candles');
|
||||
}
|
||||
const candlesData = await candlesResponse.json();
|
||||
|
||||
|
||||
if (candlesData.length === 0) {
|
||||
throw new Error('No candles found for this chart');
|
||||
}
|
||||
|
|
@ -632,6 +657,7 @@ export default function Home() {
|
|||
body: JSON.stringify({
|
||||
candles: candlesData,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -639,7 +665,7 @@ export default function Home() {
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
const cacheKey = generateCacheKey(activeChartId, data.model_info.model_version);
|
||||
if (cacheKey) {
|
||||
predictionCacheRef.current.set(cacheKey, {
|
||||
|
|
@ -657,6 +683,7 @@ export default function Home() {
|
|||
cacheKey,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') return;
|
||||
console.error('Failed to fetch batch predictions:', error);
|
||||
setPredictionState((prev) => ({
|
||||
...prev,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue