fix(web): harden route transitions and monitor memory

This commit is contained in:
lingniu
2026-07-15 23:54:21 +08:00
parent 3fabcf181a
commit c29ccdf2da
17 changed files with 332 additions and 36 deletions

View File

@@ -0,0 +1,35 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cleanup, renderHook, waitFor } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import type { ReactNode } from 'react';
import { api } from '../../api/client';
import { useMonitorData } from './useMonitorData';
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
test('rapid viewport changes retain at most one monitor map payload', async () => {
vi.spyOn(api, 'monitorSummary').mockResolvedValue({ totalVehicles: 0, onlineVehicles: 0, offlineVehicles: 0, drivingVehicles: 0, idleVehicles: 0, unknownVehicles: 0, alertVehicles: 0, activeToday: 0, frameToday: 0, alertDataAvailable: false, truncated: false, asOf: '' });
const monitorMap = vi.spyOn(api, 'monitorMap').mockResolvedValue({ mode: 'points', zoom: 13, total: 0, truncated: false, points: [], clusters: [], asOf: '' });
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const wrapper = ({ children }: { children: ReactNode }) => <QueryClientProvider client={client}>{children}</QueryClientProvider>;
const filters = { keyword: '', protocol: '', status: '' };
const view = renderHook(({ bounds }) => useMonitorData(filters, { zoom: 13, bounds }, '', true, false), {
wrapper,
initialProps: { bounds: '113.0000,22.0000,114.0000,23.0000' }
});
await waitFor(() => expect(monitorMap).toHaveBeenCalledTimes(1));
for (let index = 1; index <= 8; index += 1) {
view.rerender({ bounds: `${113 + index / 1000},22,${114 + index / 1000},23` });
await waitFor(() => expect(monitorMap.mock.calls.length).toBeGreaterThanOrEqual(index + 1));
}
await waitFor(() => {
const cachedMaps = client.getQueryCache().findAll({ queryKey: ['monitor', 'map'] });
expect(cachedMaps).toHaveLength(1);
});
view.unmount();
client.clear();
});

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorMapQueryParams, monitorQueryParams, parseMonitorSearchTerms } from './useMonitorData';
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_CACHE, MONITOR_REFRESH, monitorMapQueryParams, monitorQueryParams, parseMonitorSearchTerms } from './useMonitorData';
describe('monitor query params', () => {
it('keeps server-owned status filters and bounded list size', () => {
@@ -36,4 +36,8 @@ describe('monitor refresh cadence', () => {
expect(MONITOR_REFRESH.selected).toBeLessThan(MONITOR_REFRESH.fleet);
expect(MONITOR_REFRESH.fleet).toBeLessThan(MONITOR_REFRESH.summary);
});
it('does not retain inactive viewport payloads in the query cache', () => {
expect(MONITOR_CACHE.mapGcTime).toBe(0);
});
});

View File

@@ -20,6 +20,8 @@ export const MONITOR_REFRESH = {
alerts: 15_000
} as const;
export const MONITOR_CACHE = { mapGcTime: 0 } as const;
export const MAX_MONITOR_SEARCH_TERMS = 100;
export function parseMonitorSearchTerms(value: string) {
@@ -61,12 +63,12 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor
const summary = useQuery({
queryKey: ['monitor', 'summary', params.toString()],
queryFn: () => api.monitorSummary(params),
queryFn: ({ signal }) => api.monitorSummary(params, signal),
refetchInterval: MONITOR_REFRESH.summary
});
const vehicles = useQuery({
queryKey: ['monitor', 'vehicles', params.toString()],
queryFn: () => api.vehicleRealtime(params),
queryFn: ({ signal }) => api.vehicleRealtime(params, signal),
enabled: vehicleEnabled,
placeholderData: (previous) => previous,
refetchInterval: MONITOR_REFRESH.fleet
@@ -74,16 +76,17 @@ export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewpor
const map = useQuery({
queryKey: ['monitor', 'map', mapParams.toString()],
queryFn: () => api.monitorMap(mapParams),
queryFn: ({ signal }) => api.monitorMap(mapParams, signal),
enabled: mapEnabled,
placeholderData: (previous) => previous,
staleTime: 5_000,
gcTime: MONITOR_CACHE.mapGcTime,
refetchInterval: MONITOR_REFRESH.fleet
});
const selectedVehicle = useQuery({
queryKey: ['monitor', 'selected-vehicle', selectedVin],
queryFn: () => api.vehicleRealtime(new URLSearchParams({ keyword: selectedVin, limit: '1', offset: '0' })),
queryFn: ({ signal }) => api.vehicleRealtime(new URLSearchParams({ keyword: selectedVin, limit: '1', offset: '0' }), signal),
enabled: Boolean(selectedVin),
staleTime: 5_000,
refetchInterval: selectedVin ? MONITOR_REFRESH.selected : false