perf(monitor): aggregate realtime workspace
This commit is contained in:
@@ -91,6 +91,13 @@ test('monitor viewport requests forward AbortSignal so obsolete pans can be canc
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v2/monitor/map?zoom=13&bounds=113%2C22%2C114%2C23', { signal: controller.signal });
|
||||
});
|
||||
|
||||
test('monitor workspace aggregates the map screen behind one abortable request', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { summary: {}, vehicles: { items: [] }, map: { points: [], clusters: [] } } }) } as Response);
|
||||
const controller = new AbortController();
|
||||
await api.monitorWorkspace(new URLSearchParams({ zoom: '13', railLimit: '200' }), controller.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v2/monitor/workspace?zoom=13&railLimit=200', { signal: controller.signal });
|
||||
});
|
||||
|
||||
test('high-volume history, track, and mileage requests forward AbortSignal', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {} }) } as Response);
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
MileageStatistics,
|
||||
MapReverseGeocode,
|
||||
MonitorMapResponse,
|
||||
MonitorWorkspaceResponse,
|
||||
MonitorSummary,
|
||||
OnlineStatisticsSummary,
|
||||
OnlineVehicleStatusRow,
|
||||
@@ -131,6 +132,7 @@ export const api = {
|
||||
session: (signal?: AbortSignal) => request<SessionInfo>('/api/v2/session', withSignal(undefined, signal)),
|
||||
monitorSummary: (params = new URLSearchParams(), signal?: AbortSignal) => request<MonitorSummary>(`/api/v2/monitor/summary?${params.toString()}`, signal ? { signal } : undefined),
|
||||
monitorMap: (params = new URLSearchParams(), signal?: AbortSignal) => request<MonitorMapResponse>(`/api/v2/monitor/map?${params.toString()}`, signal ? { signal } : undefined),
|
||||
monitorWorkspace: (params = new URLSearchParams(), signal?: AbortSignal) => request<MonitorWorkspaceResponse>(`/api/v2/monitor/workspace?${params.toString()}`, signal ? { signal } : undefined),
|
||||
trackPlayback: (params = new URLSearchParams(), signal?: AbortSignal) => request<TrackPlaybackResponse>(`/api/v2/tracks?${params.toString()}`, signal ? { signal } : undefined),
|
||||
metricCatalog: (signal?: AbortSignal) => request<MetricCatalog>('/api/v2/metrics', withSignal(undefined, signal)),
|
||||
historyMetricCatalog: (signal?: AbortSignal) => request<HistoryMetricCatalog>('/api/v2/history/metrics', withSignal(undefined, signal)),
|
||||
|
||||
@@ -61,6 +61,12 @@ export interface MonitorMapResponse {
|
||||
asOf: string;
|
||||
}
|
||||
|
||||
export interface MonitorWorkspaceResponse {
|
||||
summary: MonitorSummary;
|
||||
vehicles: Page<VehicleRealtimeRow>;
|
||||
map: MonitorMapResponse;
|
||||
}
|
||||
|
||||
export interface TrackPlaybackResponse {
|
||||
vin: string;
|
||||
plate: string;
|
||||
|
||||
@@ -10,9 +10,14 @@ afterEach(() => {
|
||||
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: '' });
|
||||
test('rapid viewport changes retain one workspace payload and avoid duplicate fleet requests', async () => {
|
||||
const monitorSummary = 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 vehicleRealtime = vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [], total: 0, limit: 200, offset: 0 });
|
||||
const monitorWorkspace = vi.spyOn(api, 'monitorWorkspace').mockResolvedValue({
|
||||
summary: { totalVehicles: 0, onlineVehicles: 0, offlineVehicles: 0, drivingVehicles: 0, idleVehicles: 0, unknownVehicles: 0, alertVehicles: 0, activeToday: 0, frameToday: 0, alertDataAvailable: false, truncated: false, asOf: '' },
|
||||
vehicles: { items: [], total: 0, limit: 200, offset: 0 },
|
||||
map: { 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: '' };
|
||||
@@ -21,14 +26,16 @@ test('rapid viewport changes retain at most one monitor map payload', async () =
|
||||
initialProps: { bounds: '113.0000,22.0000,114.0000,23.0000' }
|
||||
});
|
||||
|
||||
await waitFor(() => expect(monitorMap).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(monitorWorkspace).toHaveBeenCalledTimes(1));
|
||||
expect(monitorSummary).not.toHaveBeenCalled();
|
||||
expect(vehicleRealtime).not.toHaveBeenCalled();
|
||||
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(() => expect(monitorWorkspace.mock.calls.length).toBeGreaterThanOrEqual(index + 1));
|
||||
}
|
||||
await waitFor(() => {
|
||||
const cachedMaps = client.getQueryCache().findAll({ queryKey: ['monitor', 'map'] });
|
||||
expect(cachedMaps).toHaveLength(1);
|
||||
const cachedWorkspaces = client.getQueryCache().findAll({ queryKey: ['monitor', 'workspace'] });
|
||||
expect(cachedWorkspaces).toHaveLength(1);
|
||||
});
|
||||
view.unmount();
|
||||
client.clear();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import type { VehicleRealtimeRow } from '../../api/types';
|
||||
import type { MonitorMapResponse, MonitorSummary, MonitorWorkspaceResponse, Page, VehicleRealtimeRow } from '../../api/types';
|
||||
import { QUERY_MEMORY } from '../queryPolicy';
|
||||
|
||||
export type MonitorFilters = {
|
||||
@@ -58,34 +58,43 @@ export function monitorMapQueryParams(filters: MonitorFilters, viewport: Monitor
|
||||
return params;
|
||||
}
|
||||
|
||||
function projectWorkspaceQuery<T>(query: UseQueryResult<MonitorWorkspaceResponse>, select: (workspace: MonitorWorkspaceResponse) => T): UseQueryResult<T> {
|
||||
return { ...query, data: query.data ? select(query.data) : undefined } as UseQueryResult<T>;
|
||||
}
|
||||
|
||||
export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewport, selectedVin: string, mapEnabled = true, vehicleEnabled = true) {
|
||||
const params = monitorQueryParams(filters, 200);
|
||||
const mapParams = monitorMapQueryParams(filters, viewport);
|
||||
mapParams.set('railLimit', '200');
|
||||
|
||||
const summary = useQuery({
|
||||
queryKey: ['monitor', 'summary', params.toString()],
|
||||
queryFn: ({ signal }) => api.monitorSummary(params, signal),
|
||||
refetchInterval: MONITOR_REFRESH.summary,
|
||||
gcTime: QUERY_MEMORY.summaryGcTime
|
||||
});
|
||||
const vehicles = useQuery({
|
||||
queryKey: ['monitor', 'vehicles', params.toString()],
|
||||
queryFn: ({ signal }) => api.vehicleRealtime(params, signal),
|
||||
enabled: vehicleEnabled,
|
||||
placeholderData: (previous) => previous,
|
||||
refetchInterval: MONITOR_REFRESH.fleet,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
});
|
||||
|
||||
const map = useQuery({
|
||||
queryKey: ['monitor', 'map', mapParams.toString()],
|
||||
queryFn: ({ signal }) => api.monitorMap(mapParams, signal),
|
||||
const workspace = useQuery({
|
||||
queryKey: ['monitor', 'workspace', mapParams.toString()],
|
||||
queryFn: ({ signal }) => api.monitorWorkspace(mapParams, signal),
|
||||
enabled: mapEnabled,
|
||||
placeholderData: (previous) => previous,
|
||||
staleTime: 5_000,
|
||||
gcTime: MONITOR_CACHE.mapGcTime,
|
||||
refetchInterval: MONITOR_REFRESH.fleet
|
||||
refetchInterval: mapEnabled ? MONITOR_REFRESH.fleet : false
|
||||
});
|
||||
const summaryQuery = useQuery({
|
||||
queryKey: ['monitor', 'summary', params.toString()],
|
||||
queryFn: ({ signal }) => api.monitorSummary(params, signal),
|
||||
enabled: !mapEnabled,
|
||||
placeholderData: workspace.data?.summary,
|
||||
refetchInterval: !mapEnabled ? MONITOR_REFRESH.summary : false,
|
||||
gcTime: QUERY_MEMORY.summaryGcTime
|
||||
});
|
||||
const vehiclesQuery = useQuery({
|
||||
queryKey: ['monitor', 'vehicles', params.toString()],
|
||||
queryFn: ({ signal }) => api.vehicleRealtime(params, signal),
|
||||
enabled: vehicleEnabled && !mapEnabled,
|
||||
placeholderData: workspace.data?.vehicles,
|
||||
refetchInterval: vehicleEnabled && !mapEnabled ? MONITOR_REFRESH.fleet : false,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
});
|
||||
const summary = mapEnabled ? projectWorkspaceQuery<MonitorSummary>(workspace, (data) => data.summary) : summaryQuery;
|
||||
const vehicles = mapEnabled ? projectWorkspaceQuery<Page<VehicleRealtimeRow>>(workspace, (data) => data.vehicles) : vehiclesQuery;
|
||||
const map = projectWorkspaceQuery<MonitorMapResponse>(workspace, (data) => data.map);
|
||||
|
||||
const selectedVehicle = useQuery({
|
||||
queryKey: ['monitor', 'selected-vehicle', selectedVin],
|
||||
|
||||
Reference in New Issue
Block a user