Skip to content

Commit 4844871

Browse files
committed
chore: fix time range change race condition
1 parent 5e9026a commit 4844871

2 files changed

Lines changed: 55 additions & 6 deletions

File tree

app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
120120
const prevChartTypeRef = useRef(chartType);
121121
const prevOhlcvDataRef = useRef<OHLCVBar[]>([]);
122122
const prevOhlcvSeriesKeyRef = useRef<string | undefined>(undefined);
123+
/** When non-null, `ohlcvData` is still the previous series' array; skip sync until the hook replaces it. */
124+
const ohlcvSeriesStaleSnapshotRef = useRef<OHLCVBar[] | null>(null);
123125
const tradingViewOpenInterceptRef = useRef(0);
124126

125127
const htmlContent = useMemo(
@@ -141,6 +143,7 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
141143
prevChartTypeRef.current = undefined;
142144
prevOhlcvDataRef.current = [];
143145
prevOhlcvSeriesKeyRef.current = undefined;
146+
ohlcvSeriesStaleSnapshotRef.current = null;
144147
}, [htmlContent]); // eslint-disable-line react-hooks/exhaustive-deps
145148

146149
// ---- Helpers ----
@@ -171,6 +174,19 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
171174
}, LAYOUT_SETTLE_FALLBACK_MS);
172175
}, [isChartReady, clearLayoutSettleTimeout]);
173176

177+
// WebView remounts when `key` changes; parent state would otherwise still look "ready".
178+
// `CHART_READY` clears indicator/position/chart-type refs once the new instance loads.
179+
useEffect(() => {
180+
if (ohlcvSeriesKey === undefined) {
181+
return;
182+
}
183+
setChartReadyCount(0);
184+
setWebViewLoaded(false);
185+
setLayoutSettling(false);
186+
clearLayoutSettleTimeout();
187+
ohlcvSeriesStaleSnapshotRef.current = null;
188+
}, [ohlcvSeriesKey, clearLayoutSettleTimeout]);
189+
174190
useEffect(
175191
() => () => {
176192
clearLayoutSettleTimeout();
@@ -397,19 +413,23 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
397413
useEffect(() => {
398414
if (ohlcvData.length === 0 || !webViewLoaded) return;
399415

416+
if (ohlcvSeriesStaleSnapshotRef.current !== null) {
417+
if (ohlcvData !== ohlcvSeriesStaleSnapshotRef.current) {
418+
ohlcvSeriesStaleSnapshotRef.current = null;
419+
} else {
420+
return;
421+
}
422+
}
423+
400424
const prevData = prevOhlcvDataRef.current;
401425

402426
if (
403427
ohlcvSeriesKey !== undefined &&
404428
ohlcvSeriesKey !== prevOhlcvSeriesKeyRef.current
405429
) {
406430
if (prevOhlcvSeriesKeyRef.current !== undefined) {
407-
// Time range switch: ohlcvData is still stale from the previous
408-
// period (fetch is in progress). Show skeleton, mark the key, and
409-
// clear prevData so the fresh data triggers the length-diff branch
410-
// on arrival — avoiding sending stale data to the WebView which
411-
// causes a resolution race condition in TradingView.
412431
beginFullOhlcvLayoutSettle();
432+
ohlcvSeriesStaleSnapshotRef.current = ohlcvData;
413433
prevOhlcvSeriesKeyRef.current = ohlcvSeriesKey;
414434
prevOhlcvDataRef.current = [];
415435
return;
@@ -561,6 +581,7 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
561581
<View style={styles.container}>
562582
<View style={styles.chartSurface}>
563583
<WebView
584+
key={`advanced-chart-${ohlcvSeriesKey ?? ''}`}
564585
ref={webViewRef}
565586
source={{ html: htmlContent, baseUrl: CHARTING_LIBRARY_BASE_URL }}
566587
style={styles.webview}

app/components/UI/Charts/AdvancedChart/__tests__/AdvancedChart.test.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,20 @@ describe('AdvancedChart', () => {
115115

116116
expect(getByTestId('advanced-chart-skeleton')).toBeOnTheScreen();
117117

118+
const webViewAfterRerender = getByTestId('mock-webview');
118119
act(() => {
119-
webView.props.onMessage({
120+
webViewAfterRerender.props.onLoadEnd();
121+
});
122+
act(() => {
123+
webViewAfterRerender.props.onMessage({
124+
nativeEvent: {
125+
data: JSON.stringify({ type: 'CHART_READY', payload: {} }),
126+
},
127+
});
128+
});
129+
130+
act(() => {
131+
webViewAfterRerender.props.onMessage({
120132
nativeEvent: {
121133
data: JSON.stringify({ type: 'CHART_LAYOUT_SETTLED', payload: {} }),
122134
},
@@ -201,6 +213,22 @@ describe('AdvancedChart', () => {
201213
);
202214
expect(setOhlcvCallsAfterKeyChange).toHaveLength(0);
203215

216+
// Series key remounts the WebView; load must finish before sync runs. Stale wait still applies.
217+
const webViewAfterKeyChange = getByTestId('mock-webview');
218+
act(() => {
219+
webViewAfterKeyChange.props.onLoadEnd();
220+
});
221+
222+
expect(
223+
mockPostMessage.mock.calls.filter((call) => {
224+
try {
225+
return JSON.parse(call[0] as string).type === 'SET_OHLCV_DATA';
226+
} catch {
227+
return false;
228+
}
229+
}),
230+
).toHaveLength(0);
231+
204232
// Fresh data arrives (same key, different bars) — NOW it should send
205233
mockPostMessage.mockClear();
206234
rerender(<AdvancedChart ohlcvData={freshBars} ohlcvSeriesKey="range-b" />);

0 commit comments

Comments
 (0)