perf(monitor): throttle selected address lookups
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { cleanup, render, renderHook, waitFor } from '@testing-library/react';
|
||||
import { act, cleanup, render, renderHook, waitFor } from '@testing-library/react';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
import type { ReactNode } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
@@ -7,6 +7,7 @@ import { useMonitorData, useMonitorVehicleCard } from './useMonitorData';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -126,3 +127,47 @@ test('collapsed vehicle card does not issue or retain detail-only queries', asyn
|
||||
view.unmount();
|
||||
client.clear();
|
||||
});
|
||||
|
||||
test('throttles moving vehicle address lookups, publishes the latest point, and switches vehicles immediately', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.spyOn(api, 'vehicleDetail').mockResolvedValue({} as never);
|
||||
vi.spyOn(api, 'alertEventsV2').mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 });
|
||||
const address = vi.spyOn(api, 'reverseGeocode').mockResolvedValue({ provider: 'AMap', longitude: 113.26, latitude: 23.13, formattedAddress: '测试地址' });
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
function ActiveCard({ vehicle }: { vehicle: { vin: string; longitude: number; latitude: number } }) {
|
||||
useMonitorVehicleCard(vehicle.vin, vehicle as never, true);
|
||||
return null;
|
||||
}
|
||||
const view = render(<QueryClientProvider client={client}><ActiveCard vehicle={{ vin: 'VIN-001', longitude: 113.260041, latitude: 23.130041 }} /></QueryClientProvider>);
|
||||
|
||||
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
|
||||
expect(address).toHaveBeenCalledTimes(1);
|
||||
expect(address.mock.calls[0]?.[0]?.get('longitude')).toBe('113.2600');
|
||||
expect(address.mock.calls[0]?.[0]?.get('latitude')).toBe('23.1300');
|
||||
|
||||
view.rerender(<QueryClientProvider client={client}><ActiveCard vehicle={{ vin: 'VIN-001', longitude: 113.260049, latitude: 23.130049 }} /></QueryClientProvider>);
|
||||
await act(async () => { await Promise.resolve(); });
|
||||
expect(address).toHaveBeenCalledTimes(1);
|
||||
|
||||
view.rerender(<QueryClientProvider client={client}><ActiveCard vehicle={{ vin: 'VIN-001', longitude: 113.270041, latitude: 23.140041 }} /></QueryClientProvider>);
|
||||
await act(async () => { vi.advanceTimersByTime(30_000); await Promise.resolve(); });
|
||||
view.rerender(<QueryClientProvider client={client}><ActiveCard vehicle={{ vin: 'VIN-001', longitude: 113.280041, latitude: 23.150041 }} /></QueryClientProvider>);
|
||||
await act(async () => { vi.advanceTimersByTime(29_999); await Promise.resolve(); });
|
||||
expect(address).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => { vi.advanceTimersByTime(1); await Promise.resolve(); await Promise.resolve(); });
|
||||
expect(address).toHaveBeenCalledTimes(2);
|
||||
expect(address.mock.calls[1]?.[0]?.get('longitude')).toBe('113.2800');
|
||||
expect(address.mock.calls[1]?.[0]?.get('latitude')).toBe('23.1500');
|
||||
|
||||
view.rerender(<QueryClientProvider client={client}><ActiveCard vehicle={{ vin: 'VIN-002', longitude: 114.310041, latitude: 22.910041 }} /></QueryClientProvider>);
|
||||
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
|
||||
expect(address).toHaveBeenCalledTimes(3);
|
||||
expect(address.mock.calls[2]?.[0]?.get('longitude')).toBe('114.3100');
|
||||
|
||||
view.rerender(<QueryClientProvider client={client}><ActiveCard vehicle={{ vin: 'VIN-002', longitude: 114.320041, latitude: 22.920041 }} /></QueryClientProvider>);
|
||||
view.unmount();
|
||||
await act(async () => { vi.advanceTimersByTime(60_000); await Promise.resolve(); });
|
||||
expect(address).toHaveBeenCalledTimes(3);
|
||||
client.clear();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
import type { MonitorMapResponse, MonitorSummary, MonitorWorkspaceResponse, Page, VehicleRealtimeRow } from '../../api/types';
|
||||
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||
@@ -24,6 +25,67 @@ export const MONITOR_REFRESH = {
|
||||
export const MONITOR_CACHE = { mapGcTime: 0 } as const;
|
||||
|
||||
export const MAX_MONITOR_SEARCH_TERMS = 100;
|
||||
export const MONITOR_ADDRESS_REFRESH_MS = 60_000;
|
||||
|
||||
type MonitorAddressPoint = {
|
||||
vin: string;
|
||||
key: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
};
|
||||
|
||||
function monitorAddressPoint(vin: string, longitude?: number, latitude?: number): MonitorAddressPoint | undefined {
|
||||
if (!vin || !Number.isFinite(longitude) || !Number.isFinite(latitude)
|
||||
|| Math.abs(longitude ?? 0) > 180 || Math.abs(latitude ?? 0) > 90
|
||||
|| (longitude === 0 && latitude === 0)) return undefined;
|
||||
const roundedLongitude = Number(longitude!.toFixed(4));
|
||||
const roundedLatitude = Number(latitude!.toFixed(4));
|
||||
return {
|
||||
vin,
|
||||
key: `${roundedLongitude.toFixed(4)},${roundedLatitude.toFixed(4)}`,
|
||||
longitude: roundedLongitude,
|
||||
latitude: roundedLatitude
|
||||
};
|
||||
}
|
||||
|
||||
function useMonitorAddressPoint(vin: string, longitude?: number, latitude?: number) {
|
||||
const next = monitorAddressPoint(vin, longitude, latitude);
|
||||
const [published, setPublished] = useState<MonitorAddressPoint>();
|
||||
const pendingRef = useRef<MonitorAddressPoint>();
|
||||
const lastPublishedAtRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!next) {
|
||||
pendingRef.current = undefined;
|
||||
lastPublishedAtRef.current = 0;
|
||||
setPublished((current) => current === undefined ? current : undefined);
|
||||
return;
|
||||
}
|
||||
if (!published || published.vin !== next.vin) {
|
||||
pendingRef.current = undefined;
|
||||
lastPublishedAtRef.current = Date.now();
|
||||
setPublished(next);
|
||||
return;
|
||||
}
|
||||
if (published.key === next.key) {
|
||||
pendingRef.current = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
pendingRef.current = next;
|
||||
const elapsed = Date.now() - lastPublishedAtRef.current;
|
||||
const timer = window.setTimeout(() => {
|
||||
const pending = pendingRef.current;
|
||||
if (!pending) return;
|
||||
pendingRef.current = undefined;
|
||||
lastPublishedAtRef.current = Date.now();
|
||||
setPublished(pending);
|
||||
}, Math.max(0, MONITOR_ADDRESS_REFRESH_MS - elapsed));
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [next?.key, next?.vin, published?.key, published?.vin]);
|
||||
|
||||
return published;
|
||||
}
|
||||
|
||||
export function parseMonitorSearchTerms(value: string) {
|
||||
const terms: string[] = [];
|
||||
@@ -119,8 +181,7 @@ export function useMonitorVehicleCard(vin: string, vehicle?: VehicleRealtimeRow,
|
||||
const enabled = Boolean(vin);
|
||||
const longitude = vehicle?.longitude;
|
||||
const latitude = vehicle?.latitude;
|
||||
const hasCoordinate = Number.isFinite(longitude) && Number.isFinite(latitude)
|
||||
&& Math.abs(longitude ?? 0) <= 180 && Math.abs(latitude ?? 0) <= 90;
|
||||
const addressPoint = useMonitorAddressPoint(vin, longitude, latitude);
|
||||
|
||||
const detail = useQuery({
|
||||
queryKey: ['monitor', 'vehicle-card', 'detail', vin],
|
||||
@@ -138,12 +199,12 @@ export function useMonitorVehicleCard(vin: string, vehicle?: VehicleRealtimeRow,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
});
|
||||
const address = useQuery({
|
||||
queryKey: ['monitor', 'vehicle-card', 'address', longitude, latitude],
|
||||
queryKey: ['monitor', 'vehicle-card', 'address', addressPoint?.vin, addressPoint?.key],
|
||||
queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({
|
||||
longitude: longitude!.toFixed(6),
|
||||
latitude: latitude!.toFixed(6)
|
||||
longitude: addressPoint!.longitude.toFixed(4),
|
||||
latitude: addressPoint!.latitude.toFixed(4)
|
||||
}), signal),
|
||||
enabled: enabled && hasCoordinate,
|
||||
enabled: enabled && addressPoint?.vin === vin,
|
||||
staleTime: 60 * 60_000,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user