Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
const prevChartTypeRef = useRef(chartType);
const prevOhlcvDataRef = useRef<OHLCVBar[]>([]);
const prevOhlcvSeriesKeyRef = useRef<string | undefined>(undefined);
/** When non-null, `ohlcvData` is still the previous series' array; skip sync until the hook replaces it. */
const ohlcvSeriesStaleSnapshotRef = useRef<OHLCVBar[] | null>(null);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holds the previous series' ohlcvData array reference after ohlcvSeriesKey changes (see
guard below). useOHLCVChart does not clear data until the new fetch resolves.

const tradingViewOpenInterceptRef = useRef(0);

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

// ---- Helpers ----
Expand Down Expand Up @@ -171,6 +174,22 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
}, 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;
activeIndicatorsRef.current.clear();
prevPositionLinesRef.current = undefined;
prevChartTypeRef.current = undefined;
}, [ohlcvSeriesKey, clearLayoutSettleTimeout]);

useEffect(
() => () => {
clearLayoutSettleTimeout();
Expand Down Expand Up @@ -381,6 +400,7 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
prevChartTypeRef.current = undefined;
prevOhlcvDataRef.current = [];
prevOhlcvSeriesKeyRef.current = undefined;
ohlcvSeriesStaleSnapshotRef.current = null;
webViewRef.current?.reload();
},
}),
Expand All @@ -397,19 +417,23 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
useEffect(() => {
if (ohlcvData.length === 0 || !webViewLoaded) return;

if (ohlcvSeriesStaleSnapshotRef.current !== null) {
if (ohlcvData !== ohlcvSeriesStaleSnapshotRef.current) {
ohlcvSeriesStaleSnapshotRef.current = null;
} else {
return;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check to make sure we do not send stale data when ohlcvSeriesKey changes, but should wait for fresh data. If it is stale, it returns and skips sendOHLCVData

const prevData = prevOhlcvDataRef.current;

if (
ohlcvSeriesKey !== undefined &&
ohlcvSeriesKey !== prevOhlcvSeriesKeyRef.current
) {
if (prevOhlcvSeriesKeyRef.current !== undefined) {
// Time range switch: ohlcvData is still stale from the previous
Comment thread
sahar-fehri marked this conversation as resolved.
// 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;
Expand Down Expand Up @@ -561,6 +585,7 @@ const AdvancedChart = forwardRef<AdvancedChartRef, AdvancedChartProps>(
<View style={styles.container}>
<View style={styles.chartSurface}>
<WebView
key={`advanced-chart-${ohlcvSeriesKey ?? ''}`}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When ohlcvSeriesKey changes, the key string changes and react destroys the old WebView and creates a new one.

ref={webViewRef}
source={{ html: htmlContent, baseUrl: CHARTING_LIBRARY_BASE_URL }}
style={styles.webview}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,20 @@ describe('AdvancedChart', () => {

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: {} }),
},
Expand Down Expand Up @@ -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(<AdvancedChart ohlcvData={freshBars} ohlcvSeriesKey="range-b" />);
Expand All @@ -222,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<AdvancedChartRef>();
const { getByTestId, rerender } = render(
<AdvancedChart
ref={ref}
ohlcvData={staleBars}
ohlcvSeriesKey="range-a"
/>,
);

const webViewInitial = getByTestId('mock-webview');
act(() => {
webViewInitial.props.onLoadEnd();
});

rerender(
<AdvancedChart
ref={ref}
ohlcvData={staleBars}
ohlcvSeriesKey="range-b"
/>,
);

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<AdvancedChartRef>();
render(<AdvancedChart ref={ref} ohlcvData={MOCK_BARS} />);
Expand Down
Loading