From 4844871b93b96bb725292c203979d21bc1154188 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 16 Apr 2026 19:44:49 +0200 Subject: [PATCH 1/3] chore: fix time range change race condition --- .../UI/Charts/AdvancedChart/AdvancedChart.tsx | 31 ++++++++++++++++--- .../__tests__/AdvancedChart.test.tsx | 30 +++++++++++++++++- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx b/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx index 1397eb9149b..3dd11febc3f 100644 --- a/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx +++ b/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx @@ -120,6 +120,8 @@ const AdvancedChart = forwardRef( const prevChartTypeRef = useRef(chartType); const prevOhlcvDataRef = useRef([]); const prevOhlcvSeriesKeyRef = useRef(undefined); + /** When non-null, `ohlcvData` is still the previous series' array; skip sync until the hook replaces it. */ + const ohlcvSeriesStaleSnapshotRef = useRef(null); const tradingViewOpenInterceptRef = useRef(0); const htmlContent = useMemo( @@ -141,6 +143,7 @@ const AdvancedChart = forwardRef( prevChartTypeRef.current = undefined; prevOhlcvDataRef.current = []; prevOhlcvSeriesKeyRef.current = undefined; + ohlcvSeriesStaleSnapshotRef.current = null; }, [htmlContent]); // eslint-disable-line react-hooks/exhaustive-deps // ---- Helpers ---- @@ -171,6 +174,19 @@ const AdvancedChart = forwardRef( }, LAYOUT_SETTLE_FALLBACK_MS); }, [isChartReady, clearLayoutSettleTimeout]); + // WebView remounts when `key` changes; parent state would otherwise still look "ready". + // `CHART_READY` clears indicator/position/chart-type refs once the new instance loads. + useEffect(() => { + if (ohlcvSeriesKey === undefined) { + return; + } + setChartReadyCount(0); + setWebViewLoaded(false); + setLayoutSettling(false); + clearLayoutSettleTimeout(); + ohlcvSeriesStaleSnapshotRef.current = null; + }, [ohlcvSeriesKey, clearLayoutSettleTimeout]); + useEffect( () => () => { clearLayoutSettleTimeout(); @@ -397,6 +413,14 @@ const AdvancedChart = forwardRef( useEffect(() => { if (ohlcvData.length === 0 || !webViewLoaded) return; + if (ohlcvSeriesStaleSnapshotRef.current !== null) { + if (ohlcvData !== ohlcvSeriesStaleSnapshotRef.current) { + ohlcvSeriesStaleSnapshotRef.current = null; + } else { + return; + } + } + const prevData = prevOhlcvDataRef.current; if ( @@ -404,12 +428,8 @@ const AdvancedChart = forwardRef( ohlcvSeriesKey !== prevOhlcvSeriesKeyRef.current ) { if (prevOhlcvSeriesKeyRef.current !== undefined) { - // Time range switch: ohlcvData is still stale from the previous - // period (fetch is in progress). Show skeleton, mark the key, and - // clear prevData so the fresh data triggers the length-diff branch - // on arrival — avoiding sending stale data to the WebView which - // causes a resolution race condition in TradingView. beginFullOhlcvLayoutSettle(); + ohlcvSeriesStaleSnapshotRef.current = ohlcvData; prevOhlcvSeriesKeyRef.current = ohlcvSeriesKey; prevOhlcvDataRef.current = []; return; @@ -561,6 +581,7 @@ const AdvancedChart = forwardRef( { expect(getByTestId('advanced-chart-skeleton')).toBeOnTheScreen(); + const webViewAfterRerender = getByTestId('mock-webview'); act(() => { - webView.props.onMessage({ + webViewAfterRerender.props.onLoadEnd(); + }); + act(() => { + webViewAfterRerender.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ type: 'CHART_READY', payload: {} }), + }, + }); + }); + + act(() => { + webViewAfterRerender.props.onMessage({ nativeEvent: { data: JSON.stringify({ type: 'CHART_LAYOUT_SETTLED', payload: {} }), }, @@ -201,6 +213,22 @@ describe('AdvancedChart', () => { ); expect(setOhlcvCallsAfterKeyChange).toHaveLength(0); + // Series key remounts the WebView; load must finish before sync runs. Stale wait still applies. + const webViewAfterKeyChange = getByTestId('mock-webview'); + act(() => { + webViewAfterKeyChange.props.onLoadEnd(); + }); + + expect( + mockPostMessage.mock.calls.filter((call) => { + try { + return JSON.parse(call[0] as string).type === 'SET_OHLCV_DATA'; + } catch { + return false; + } + }), + ).toHaveLength(0); + // Fresh data arrives (same key, different bars) — NOW it should send mockPostMessage.mockClear(); rerender(); From f7a52c7a839681ababa09f92c982883d5e40705c Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 16 Apr 2026 21:52:19 +0200 Subject: [PATCH 2/3] chore: fix --- app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx b/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx index 3dd11febc3f..2c65fba444a 100644 --- a/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx +++ b/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx @@ -185,6 +185,9 @@ const AdvancedChart = forwardRef( setLayoutSettling(false); clearLayoutSettleTimeout(); ohlcvSeriesStaleSnapshotRef.current = null; + activeIndicatorsRef.current.clear(); + prevPositionLinesRef.current = undefined; + prevChartTypeRef.current = undefined; }, [ohlcvSeriesKey, clearLayoutSettleTimeout]); useEffect( From ccd3cffae1157111b1aa9428a9072de5f240835c Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 16 Apr 2026 23:49:01 +0200 Subject: [PATCH 3/3] chore: fix --- .../UI/Charts/AdvancedChart/AdvancedChart.tsx | 1 + .../__tests__/AdvancedChart.test.tsx | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx b/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx index 2c65fba444a..64a62234b80 100644 --- a/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx +++ b/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx @@ -400,6 +400,7 @@ const AdvancedChart = forwardRef( prevChartTypeRef.current = undefined; prevOhlcvDataRef.current = []; prevOhlcvSeriesKeyRef.current = undefined; + ohlcvSeriesStaleSnapshotRef.current = null; webViewRef.current?.reload(); }, }), diff --git a/app/components/UI/Charts/AdvancedChart/__tests__/AdvancedChart.test.tsx b/app/components/UI/Charts/AdvancedChart/__tests__/AdvancedChart.test.tsx index 6b755b1a065..814d6778285 100644 --- a/app/components/UI/Charts/AdvancedChart/__tests__/AdvancedChart.test.tsx +++ b/app/components/UI/Charts/AdvancedChart/__tests__/AdvancedChart.test.tsx @@ -250,6 +250,59 @@ describe('AdvancedChart', () => { expect(realtimeCalls).toHaveLength(0); }); + it('reset() clears stale series snapshot so OHLCV sync runs after reload with the same data ref', () => { + const staleBars: OHLCVBar[] = [ + { time: 1000000, open: 10, high: 12, low: 9, close: 11, volume: 100 }, + ]; + const ref = React.createRef(); + const { getByTestId, rerender } = render( + , + ); + + const webViewInitial = getByTestId('mock-webview'); + act(() => { + webViewInitial.props.onLoadEnd(); + }); + + rerender( + , + ); + + const webViewAfterKeyChange = getByTestId('mock-webview'); + act(() => { + webViewAfterKeyChange.props.onLoadEnd(); + }); + + mockPostMessage.mockClear(); + + act(() => { + ref.current?.reset(); + }); + + const webViewAfterReset = getByTestId('mock-webview'); + act(() => { + webViewAfterReset.props.onLoadEnd(); + }); + + expect( + mockPostMessage.mock.calls.some((call) => { + try { + return JSON.parse(call[0] as string).type === 'SET_OHLCV_DATA'; + } catch { + return false; + } + }), + ).toBe(true); + }); + it('exposes addIndicator via ref', () => { const ref = React.createRef(); render();