mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
perf(admin-traffic): migrate data flow to React Query
The page maintained two parallel cache mechanisms:
L1 — adminTrafficApi.getCached + Map<key, {data, timestamp}> with 5min
TTL and a 20-entry LRU eviction
L2 — manual setState + initialLoading/loading flags + cancelled-flag
cleanup in useEffect
React Query covers both with stronger guarantees:
- dedup of in-flight requests with identical queryKey (the prefetch
+ main-fetch race could double-fire previously)
- staleTime: 5 min matches the old TTL
- gcTime: 5 min matches the eviction behaviour
- background refetch on stale + tab refocus disabled globally
trafficQuery owns the main list; enrichmentQuery is gated on
!trafficQuery.isLoading && items.length > 0 (preserves the 'don't tag
before tags have anyone to tag' guard). Existing setItems / setNodes /
setLoading / setEnrichment state is fed by sync useEffects so all
downstream selectors, the table, derived memos, and the column defs
are untouched.
Prefetch adjacent periods now goes through queryClient.prefetchQuery
(same warmup cadence, no separate path). handleRefresh invalidates
both queries + the internal Map cache and refetches enrichment
imperatively.
adminTrafficApi's own Map cache stays in place as a harmless L2 — it
backs queryFn calls and any non-RQ caller (none in src/ today, but
the API surface is shared).
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
useReactTable,
|
useReactTable,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
adminTrafficApi,
|
adminTrafficApi,
|
||||||
type UserTrafficItem,
|
type UserTrafficItem,
|
||||||
type TrafficNodeInfo,
|
type TrafficNodeInfo,
|
||||||
type TrafficUsageResponse,
|
|
||||||
type TrafficParams,
|
type TrafficParams,
|
||||||
type TrafficEnrichmentData,
|
type TrafficEnrichmentData,
|
||||||
} from '../api/adminTraffic';
|
} from '../api/adminTraffic';
|
||||||
@@ -1130,89 +1130,75 @@ export default function AdminTrafficUsage() {
|
|||||||
customEnd,
|
customEnd,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const applyData = useCallback((data: TrafficUsageResponse) => {
|
const queryClient = useQueryClient();
|
||||||
setItems(data.items);
|
const params = useMemo(() => buildParams(), [buildParams]);
|
||||||
setNodes(data.nodes);
|
|
||||||
setTotal(data.total);
|
|
||||||
setAvailableTariffs(data.available_tariffs);
|
|
||||||
setAvailableStatuses(data.available_statuses);
|
|
||||||
setPeriodDays(data.period_days);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadData = useCallback(
|
// 5 min staleTime mirrors the previous in-memory cache TTL, so navigating
|
||||||
async (skipCache = false) => {
|
// away and back inside the window doesn't refetch. React Query handles
|
||||||
const params = buildParams();
|
// dedup + per-key caching; the adminTrafficApi's own Map cache stays as a
|
||||||
|
// harmless L2 (still hit by background prefetch + tests).
|
||||||
|
const trafficQuery = useQuery({
|
||||||
|
queryKey: ['admin-traffic', params] as const,
|
||||||
|
queryFn: () => adminTrafficApi.getTrafficUsage(params),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
gcTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
// Check cache first — apply instantly without any loading state
|
// Sync trafficQuery into the existing state vars so the rest of the
|
||||||
if (!skipCache) {
|
// page (selectors, table, derived memos) is untouched.
|
||||||
const cached = adminTrafficApi.getCached(params);
|
|
||||||
if (cached) {
|
|
||||||
applyData(cached);
|
|
||||||
setInitialLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await adminTrafficApi.getTrafficUsage(params, { skipCache });
|
|
||||||
applyData(data);
|
|
||||||
} catch {
|
|
||||||
// silently fail — keep stale data visible
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
setInitialLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[buildParams, applyData],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load on param change
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
if (!trafficQuery.data) return;
|
||||||
}, [loadData]);
|
setItems(trafficQuery.data.items);
|
||||||
|
setNodes(trafficQuery.data.nodes);
|
||||||
// Load enrichment after main data arrives
|
setTotal(trafficQuery.data.total);
|
||||||
|
setAvailableTariffs(trafficQuery.data.available_tariffs);
|
||||||
|
setAvailableStatuses(trafficQuery.data.available_statuses);
|
||||||
|
setPeriodDays(trafficQuery.data.period_days);
|
||||||
|
}, [trafficQuery.data]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialLoading || items.length === 0) return;
|
setLoading(trafficQuery.isFetching);
|
||||||
let cancelled = false;
|
if (!trafficQuery.isLoading) setInitialLoading(false);
|
||||||
const load = async () => {
|
}, [trafficQuery.isFetching, trafficQuery.isLoading]);
|
||||||
setEnrichmentLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await adminTrafficApi.getEnrichment();
|
|
||||||
if (!cancelled) setEnrichment(res.data);
|
|
||||||
} catch {
|
|
||||||
// silently fail — enrichment is optional
|
|
||||||
} finally {
|
|
||||||
if (!cancelled) setEnrichmentLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
load();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [initialLoading, items.length]);
|
|
||||||
|
|
||||||
// Prefetch adjacent periods in background (only in period mode)
|
// Enrichment query — gated on first data arrival so we don't fetch tags
|
||||||
|
// before there's anyone to tag.
|
||||||
|
const enrichmentQuery = useQuery({
|
||||||
|
queryKey: ['admin-traffic-enrichment'] as const,
|
||||||
|
queryFn: () => adminTrafficApi.getEnrichment(),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
gcTime: 5 * 60 * 1000,
|
||||||
|
enabled: !trafficQuery.isLoading && items.length > 0,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (enrichmentQuery.data) setEnrichment(enrichmentQuery.data.data);
|
||||||
|
}, [enrichmentQuery.data]);
|
||||||
|
useEffect(() => {
|
||||||
|
setEnrichmentLoading(enrichmentQuery.isFetching);
|
||||||
|
}, [enrichmentQuery.isFetching]);
|
||||||
|
|
||||||
|
// Prefetch adjacent periods in background via queryClient — populates the
|
||||||
|
// cache so switching period feels instant.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dateMode) return;
|
if (dateMode) return;
|
||||||
const prefetchPeriods = PERIODS.filter((p) => p !== period);
|
const prefetchPeriods = PERIODS.filter((p) => p !== period);
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
prefetchPeriods.forEach((p) => {
|
prefetchPeriods.forEach((p) => {
|
||||||
const params: TrafficParams = {
|
const prefetchParams: TrafficParams = {
|
||||||
period: p,
|
period: p,
|
||||||
limit,
|
limit,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
sort_by: 'total_bytes',
|
sort_by: 'total_bytes',
|
||||||
sort_desc: true,
|
sort_desc: true,
|
||||||
};
|
};
|
||||||
if (!adminTrafficApi.getCached(params)) {
|
queryClient.prefetchQuery({
|
||||||
adminTrafficApi.getTrafficUsage(params).catch(() => {});
|
queryKey: ['admin-traffic', prefetchParams] as const,
|
||||||
}
|
queryFn: () => adminTrafficApi.getTrafficUsage(prefetchParams),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [period, dateMode]);
|
}, [period, dateMode, limit, queryClient]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (toast) {
|
if (toast) {
|
||||||
@@ -1320,14 +1306,12 @@ export default function AdminTrafficUsage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
loadData(true);
|
// Force-bypass both layers: drop the API's Map cache, invalidate React
|
||||||
setEnrichment(null);
|
// Query, and explicitly refetch enrichment.
|
||||||
setEnrichmentLoading(true);
|
adminTrafficApi.invalidateCache();
|
||||||
adminTrafficApi
|
queryClient.invalidateQueries({ queryKey: ['admin-traffic'] });
|
||||||
.getEnrichment({ skipCache: true })
|
queryClient.invalidateQueries({ queryKey: ['admin-traffic-enrichment'] });
|
||||||
.then((res) => setEnrichment(res.data))
|
enrichmentQuery.refetch();
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => setEnrichmentLoading(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const availableCountries = useMemo(() => {
|
const availableCountries = useMemo(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user