perf(web): release inactive address queries

This commit is contained in:
lingniu
2026-07-16 02:14:40 +08:00
parent 85e92e62da
commit d995024e06
5 changed files with 20 additions and 3 deletions

View File

@@ -116,7 +116,7 @@ test('switches to a lightweight realtime list and resolves addresses only on dem
vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [row], total: 1, limit: 50, offset: 0 });
const reverseGeocode = vi.spyOn(api, 'reverseGeocode').mockResolvedValue({ provider: 'AMap', longitude: 113.26, latitude: 23.13, formattedAddress: '广东省广州市天河区测试路' });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
const view = render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
fireEvent.click(screen.getByRole('button', { name: /列表/ }));
expect(await screen.findByText('车辆实时列表')).toBeInTheDocument();
expect(screen.getByText('速度、里程和坐标实时刷新,文字地址按需解析')).toBeInTheDocument();
@@ -129,9 +129,14 @@ test('switches to a lightweight realtime list and resolves addresses only on dem
fireEvent.click(screen.getByRole('button', { name: '解析粤A12345位置' }));
expect(await screen.findByText('广东省广州市天河区测试路')).toBeInTheDocument();
expect(reverseGeocode).toHaveBeenCalledTimes(1);
expect(queryClient.getQueryCache().findAll({ queryKey: ['monitor', 'list-address'] })).toHaveLength(1);
expect(screen.queryByText('综合状态')).not.toBeInTheDocument();
expect(screen.queryByText('三路正常')).not.toBeInTheDocument();
expect(screen.queryByTestId('fleet-map')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /地图/ }));
await waitFor(() => expect(queryClient.getQueryCache().findAll({ queryKey: ['monitor', 'list-address'] })).toHaveLength(0));
view.unmount();
});
test('mounts only the mobile list representation and removes its viewport listener', async () => {

View File

@@ -70,7 +70,7 @@ const MonitorAddressCell = memo(function MonitorAddressCell({ vehicle }: { vehic
}), signal),
enabled: Boolean(requested),
staleTime: 6 * 60 * 60_000,
gcTime: 24 * 60 * 60_000,
gcTime: QUERY_MEMORY.highVolumeGcTime,
refetchOnWindowFocus: false,
retry: 1
});

View File

@@ -46,10 +46,14 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '0');
expect(screen.getByText('停留 00:05:00 · 1 个点')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: '暂停轨迹播放' })).not.toBeInTheDocument();
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(1));
expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1);
fireEvent.click(screen.getByRole('button', { name: /事件点/ }));
fireEvent.click(screen.getByRole('button', { name: /急加速/ }));
expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '1');
await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(2));
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1));
fireEvent.click(screen.getByRole('button', { name: '收起查询面板' }));
expect(screen.getByRole('button', { name: /查询与明细/ })).toBeInTheDocument();
@@ -62,4 +66,5 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
expect(client.getQueryCache().findAll({ queryKey: ['track-playback'] })).toHaveLength(1);
view.unmount();
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-playback'] })).toHaveLength(0));
await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(0));
});

View File

@@ -218,7 +218,8 @@ export default function TrackPage() {
const addressQuery = useQuery({
queryKey: ['track-address', addressPoint?.longitude.toFixed(6), addressPoint?.latitude.toFixed(6)],
enabled: Boolean(addressPoint), staleTime: 60 * 60 * 1000,
queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({ longitude: addressPoint!.longitude.toFixed(6), latitude: addressPoint!.latitude.toFixed(6) }), signal)
queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({ longitude: addressPoint!.longitude.toFixed(6), latitude: addressPoint!.latitude.toFixed(6) }), signal),
gcTime: QUERY_MEMORY.highVolumeGcTime
});
useEffect(() => { setActiveIndex(0); setPlaying(false); setFollow(true); if (track?.points.length) setRailCollapsed(window.matchMedia('(max-width: 700px)').matches); }, [track?.asOf]);

View File

@@ -2,6 +2,12 @@
This document records verified risks, the production controls that address them, and the next evidence to collect. It is intentionally operational: a passing build alone is not proof that the browser application is production-ready.
## 2026-07-16: high-cardinality address cache lifecycle
The monitor list previously retained every manually resolved coordinate for 24 hours, and track replay inherited the global five-minute cache for every paused point. Long-running dispatch sessions could therefore accumulate address payloads across pages and arbitrary playback coordinates even after their components disappeared.
Both query families now use zero inactive-cache retention: the visible list row or current paused track point keeps its address while mounted, but switching monitor mode, selecting another point, or leaving the route immediately releases the previous browser entry. This does not cause repeated AMap quota consumption because the authenticated server adapter independently owns the one-hour, 4,096-entry LRU cache and collapses concurrent misses. Component tests prove list addresses reach zero after returning to map mode, track address churn retains only one active key, and route unmount leaves no track address entry.
## 2026-07-16: complete release-asset compatibility gate
Release `web-release-smoke-20260716015150` installs `deploy/verify-web-release.sh` as a production release gate. It verifies the exact root document, runtime configuration object, every original asset in the current `.release-assets` manifest and every original asset in the immediately previous manifest. Asset responses are compared byte-for-byte with the active release files; HTTP 200 with an SPA HTML fallback therefore fails instead of masking a missing lazy route chunk.