query prefetching

This commit is contained in:
Nate Kelley 2025-04-25 10:16:49 -06:00
parent ca9b816c5a
commit c57b5ce40e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
20 changed files with 149 additions and 37 deletions

View File

@ -93,7 +93,7 @@ describe('Chat Query Hooks', () => {
expect(requests.getListChats).toHaveBeenCalledWith({
admin_view: false,
page_token: 0,
page_size: 3000,
page_size: 3500,
search: 'test'
});
});

View File

@ -37,14 +37,14 @@ export const useGetListChats = (
filters?: Omit<Parameters<typeof getListChats>[0], 'page_token' | 'page_size'>
) => {
const filtersCompiled: Parameters<typeof getListChats>[0] = useMemo(
() => ({ admin_view: false, page_token: 0, page_size: 3000, ...filters }),
() => ({ admin_view: false, page_token: 0, page_size: 3500, ...filters }),
[filters]
);
const queryFn = useMemoizedFn(() => getListChats(filtersCompiled));
return useQuery({
...chatQueryKeys.chatsGetList(filters),
...chatQueryKeys.chatsGetList(filtersCompiled),
queryFn
});
};
@ -67,14 +67,14 @@ export const useGetListLogs = (
filters?: Omit<Parameters<typeof getListLogs>[0], 'page_token' | 'page_size'>
) => {
const filtersCompiled: Parameters<typeof getListLogs>[0] = useMemo(
() => ({ page_token: 0, page_size: 3000, ...filters }),
() => ({ page_token: 0, page_size: 3500, ...filters }),
[filters]
);
const queryFn = useMemoizedFn(() => getListLogs(filtersCompiled));
return useQuery({
...chatQueryKeys.logsGetList(filters),
...chatQueryKeys.logsGetList(filtersCompiled),
queryFn
});
};

View File

@ -44,7 +44,7 @@ describe('Chat API Requests', () => {
// Verify the API was called with correct parameters
expect(mainApi.get).toHaveBeenCalledWith('/chats', {
params: { page_token: 0, page_size: 3000 }
params: { page_token: 0, page_size: 3500 }
});
// Verify the result matches the mock data

View File

@ -9,7 +9,7 @@ export const getListChats = async (params?: {
page_token: number;
page_size: number;
}): Promise<BusterChatListItem[]> => {
const { page_token = 0, page_size = 3000 } = params || {};
const { page_token = 0, page_size = 3500 } = params || {};
return mainApi
.get<BusterChatListItem[]>(`${CHATS_BASE}`, {
params: { page_token, page_size }
@ -20,7 +20,7 @@ export const getListChats = async (params?: {
export const getListLogs = async (
params?: Parameters<typeof getListChats>[0]
): Promise<BusterChatListItem[]> => {
const { page_token = 0, page_size = 3000 } = params || {};
const { page_token = 0, page_size = 3500 } = params || {};
return mainApi
.get<BusterChatListItem[]>(`/logs`, {
params: { page_token, page_size }

View File

@ -1,4 +1,10 @@
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query';
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
UseQueryOptions
} from '@tanstack/react-query';
import { collectionQueryKeys } from '@/api/query_keys/collection';
import {
collectionsGetList,
@ -19,25 +25,45 @@ import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsPro
import { create } from 'mutative';
import type { BusterCollection } from '@/api/asset_interfaces/collection';
import { RustApiError } from '../errors';
import { isQueryStale } from '@/lib';
export const useGetCollectionsList = (
filters: Omit<Parameters<typeof collectionsGetList>[0], 'page' | 'page_size'>,
filters: Omit<Parameters<typeof collectionsGetList>[0], 'page_token' | 'page_size'>,
options?: Omit<
UseQueryOptions<Awaited<ReturnType<typeof collectionsGetList>>, RustApiError>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const payload = useMemo(() => {
return { page: 0, page_size: 3000, ...filters };
return { ...filters, page_token: 0, page_size: 3500 };
}, [filters]);
return useQuery({
...collectionQueryKeys.collectionsGetList(filters),
...collectionQueryKeys.collectionsGetList(payload),
queryFn: () => collectionsGetList(payload),
...options
});
};
export const prefetchGetCollectionsList = async (
queryClient: QueryClient,
params?: Parameters<typeof collectionsGetList>[0]
) => {
const options = collectionQueryKeys.collectionsGetList(params);
const isStale = isQueryStale(options, queryClient);
if (!isStale) return queryClient;
const lastQueryKey = options.queryKey[options.queryKey.length - 1];
const compiledParams = lastQueryKey as Parameters<typeof collectionsGetList>[0];
await queryClient.prefetchQuery({
...options,
queryFn: () => collectionsGetList(compiledParams)
});
return queryClient;
};
const useFetchCollection = () => {
const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword);

View File

@ -1,4 +1,3 @@
import { ShareRole } from '@/api/asset_interfaces';
import type { BusterCollection, BusterCollectionListItem } from '@/api/asset_interfaces/collection';
import {
ShareDeleteRequest,
@ -10,7 +9,7 @@ import type { ShareAssetType } from '@/api/asset_interfaces';
export const collectionsGetList = async (params: {
/** Current page number (1-based indexing) */
page: number;
page_token: number;
/** Number of items to display per page */
page_size: number;
/** When true, returns only collections shared with the current user */

View File

@ -1,4 +1,10 @@
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query';
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
UseQueryOptions
} from '@tanstack/react-query';
import {
dashboardsGetList,
dashboardsCreateDashboard,
@ -37,6 +43,7 @@ import { useGetLatestMetricVersionMemoized } from '../metrics';
import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider';
import last from 'lodash/last';
import { createDashboardFullConfirmModal } from './confirmModals';
import { isQueryStale } from '@/lib';
export const useGetDashboard = <TData = BusterDashboardResponse>(
{
@ -700,13 +707,29 @@ export const useGetDashboardsList = (
return {
...params,
page_token: 0,
page_size: 3000
page_size: 3500
};
}, [params]);
return useQuery({
...dashboardQueryKeys.dashboardGetList(params),
...dashboardQueryKeys.dashboardGetList(filters),
queryFn: () => dashboardsGetList(filters),
...options
});
};
export const prefetchGetDashboardsList = async (
queryClient: QueryClient,
params?: Parameters<typeof dashboardsGetList>[0]
) => {
const options = dashboardQueryKeys.dashboardGetList(params);
const isStale = isQueryStale(options, queryClient);
if (!isStale) return queryClient;
const lastQueryKey = options.queryKey[options.queryKey.length - 1];
const compiledParams = lastQueryKey as Parameters<typeof dashboardsGetList>[0];
await queryClient.prefetchQuery({ ...options, queryFn: () => dashboardsGetList(compiledParams) });
return queryClient;
};

View File

@ -1,9 +1,10 @@
import { useMemo } from 'react';
import { listMetrics } from './requests';
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query';
import { useMemoizedFn } from '@/hooks';
import { metricsQueryKeys } from '@/api/query_keys/metric';
import { RustApiError } from '../errors';
import { isQueryStale } from '@/lib';
export const useGetMetricsList = (
params: Omit<Parameters<typeof listMetrics>[0], 'page_token' | 'page_size'>,
@ -13,16 +14,36 @@ export const useGetMetricsList = (
>
) => {
const compiledParams: Parameters<typeof listMetrics>[0] = useMemo(
() => ({ ...params, page_token: 0, page_size: 3500 }),
() => ({ status: [], ...params, page_token: 0, page_size: 3500 }),
[params]
);
const queryFn = useMemoizedFn(() => listMetrics(compiledParams));
return useQuery({
...metricsQueryKeys.metricsGetList(params),
...metricsQueryKeys.metricsGetList(compiledParams),
queryFn,
select: options?.select,
...options
});
};
export const prefetchGetMetricsList = async (
queryClient: QueryClient,
params?: Parameters<typeof listMetrics>[0]
) => {
const options = metricsQueryKeys.metricsGetList(params);
const isStale = isQueryStale(options, queryClient);
if (!isStale) return queryClient;
const lastQueryKey = options.queryKey[options.queryKey.length - 1];
const compiledParams = lastQueryKey as Parameters<typeof listMetrics>[0];
await queryClient.prefetchQuery({
...options,
queryFn: async () => {
return await listMetrics(compiledParams);
}
});
return queryClient;
};

View File

@ -7,10 +7,10 @@ import { useMemoizedFn } from '@/hooks';
import { useBusterNotifications } from '@/context/BusterNotifications';
export const useGetTermsList = (
params?: Omit<Parameters<typeof getTermsList>[0], 'page' | 'page_size'>
params?: Omit<Parameters<typeof getTermsList>[0], 'page_token' | 'page_size'>
) => {
const compiledParams: Parameters<typeof getTermsList>[0] = useMemo(
() => ({ page: 0, page_size: 3000, ...params }),
() => ({ page_token: 0, page_size: 3500, ...params }),
[params]
);

View File

@ -3,7 +3,7 @@ import { BusterTerm, BusterTermListItem } from '@/api/asset_interfaces/terms';
export const getTermsList = async (params: {
/** The page number to retrieve */
page: number;
page_token: number;
/** The number of items per page */
page_size: number;
}) => {

View File

@ -29,7 +29,7 @@ const chatsGetList = (
filters?: Omit<Parameters<typeof getListChats>[0], 'page_token' | 'page_size'>
) =>
queryOptions<BusterChatListItem[]>({
queryKey: ['chats', 'list', filters || {}] as const,
queryKey: ['chats', 'list', filters || { page_token: 0, page_size: 3500 }] as const,
staleTime: 60 * 1000, // 1 minute
initialData: [],
initialDataUpdatedAt: 0
@ -47,7 +47,7 @@ const logsGetList = (
filters?: Omit<Parameters<typeof getListLogs>[0], 'page_token' | 'page_size'>
) =>
queryOptions<BusterChatListItem[]>({
queryKey: ['logs', 'list', filters || {}] as const,
queryKey: ['logs', 'list', filters || { page_token: 0, page_size: 3500 }] as const,
staleTime: 60 * 1000, // 1 minute
initialData: [],
initialDataUpdatedAt: 0

View File

@ -3,10 +3,10 @@ import type { BusterCollectionListItem, BusterCollection } from '@/api/asset_int
import { collectionsGetList as collectionsGetListRequest } from '@/api/buster_rest/collections/requests';
const collectionsGetList = (
filters?: Omit<Parameters<typeof collectionsGetListRequest>[0], 'page' | 'page_size'>
filters?: Omit<Parameters<typeof collectionsGetListRequest>[0], 'page_token' | 'page_size'>
) =>
queryOptions<BusterCollectionListItem[]>({
queryKey: ['collections', 'list', filters || {}] as const,
queryKey: ['collections', 'list', filters || { page_token: 0, page_size: 3500 }] as const,
staleTime: 60 * 1000,
initialData: [],
initialDataUpdatedAt: 0

View File

@ -9,7 +9,7 @@ const dashboardGetList = (
filters?: Omit<Parameters<typeof dashboardsGetList>[0], 'page_token' | 'page_size'>
) =>
queryOptions<BusterDashboardListItem[]>({
queryKey: ['dashboard', 'list', filters || {}] as const,
queryKey: ['dashboard', 'list', filters || { page_token: 0, page_size: 3500 }] as const,
initialData: [],
staleTime: 60 * 1000,
initialDataUpdatedAt: 0

View File

@ -17,7 +17,11 @@ export const metricsGetList = (
filters?: Omit<Parameters<typeof listMetrics>[0], 'page_token' | 'page_size'>
) =>
queryOptions<BusterMetricListItem[]>({
queryKey: ['metrics', 'list', filters || { page_token: 0, page_size: 3500 }] as const,
queryKey: [
'metrics',
'list',
filters || { status: [], page_token: 0, page_size: 3500 }
] as const,
initialData: [],
initialDataUpdatedAt: 0
});

View File

@ -1,5 +1,4 @@
import { Dropdown, type DropdownItem, type DropdownProps } from '@/components/ui/dropdown';
import { AppTooltip } from '@/components/ui/tooltip';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes';
import { useMemoizedFn } from '@/hooks';

View File

@ -6,6 +6,10 @@ import { BusterRoutes, createBusterRoute } from '@/routes';
import { BusterAppRoutes } from '@/routes/busterRoutes/busterAppRoutes';
import { useAsyncEffect } from '@/hooks';
import { timeout } from '@/lib';
import { prefetchGetMetricsList } from '@/api/buster_rest/metrics';
import { QueryClient, useQueryClient } from '@tanstack/react-query';
import { prefetchGetDashboardsList } from '@/api/buster_rest/dashboards';
import { prefetchGetCollectionsList } from '@/api/buster_rest/collections';
const HIGH_PRIORITY_ROUTES = [
BusterRoutes.APP_HOME,
@ -25,14 +29,25 @@ const LOW_PRIORITY_ROUTES = [
BusterRoutes.APP_CHAT_ID_METRIC_ID_CHART
];
const LOW_PRIORITY_PREFETCH: ((queryClient: QueryClient) => Promise<QueryClient>)[] = [
(queryClient) => prefetchGetMetricsList(queryClient),
(queryClient) => prefetchGetDashboardsList(queryClient),
(queryClient) => prefetchGetCollectionsList(queryClient)
];
export const RoutePrefetcher: React.FC<{}> = React.memo(() => {
const router = useRouter();
const queryClient = useQueryClient();
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const isPreFetchedHighPriorityRef = useRef(false);
const isPreFetchedLowPriorityRef = useRef(false);
useAsyncEffect(async () => {
const prefetchRoutes = (routes: BusterRoutes[], priority: 'high' | 'low') => {
const prefetchRoutes = (
routes: BusterRoutes[],
prefetchFns: typeof LOW_PRIORITY_PREFETCH,
priority: 'high' | 'low'
) => {
if (priority === 'high' && isPreFetchedHighPriorityRef.current) return;
if (priority === 'low' && isPreFetchedLowPriorityRef.current) return;
@ -41,6 +56,10 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => {
router.prefetch(path);
});
prefetchFns.forEach((prefetchFn) => {
prefetchFn(queryClient);
});
if (priority === 'high') {
isPreFetchedHighPriorityRef.current = true;
} else {
@ -49,7 +68,7 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => {
};
if (!isPreFetchedHighPriorityRef.current) {
prefetchRoutes(HIGH_PRIORITY_ROUTES, 'high');
prefetchRoutes(HIGH_PRIORITY_ROUTES, [], 'high');
}
// Wait for page load
@ -72,7 +91,7 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => {
// Set a new debounce timer - will trigger if no network activity for 1500ms
debounceTimerRef.current = setTimeout(() => {
prefetchRoutes(LOW_PRIORITY_ROUTES, 'low');
prefetchRoutes(LOW_PRIORITY_ROUTES, LOW_PRIORITY_PREFETCH, 'low');
observer.disconnect();
}, 1000);
});
@ -82,7 +101,7 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => {
// Fallback - ensure prefetch happens even if network is already quiet
fallbackTimer = setTimeout(() => {
prefetchRoutes(LOW_PRIORITY_ROUTES, 'low');
prefetchRoutes(LOW_PRIORITY_ROUTES, LOW_PRIORITY_PREFETCH, 'low');
observer.disconnect();
}, 3000);
} catch (error) {
@ -92,7 +111,7 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => {
clearTimeout(debounceTimerRef.current);
}
// Still prefetch low priority routes as fallback
prefetchRoutes(LOW_PRIORITY_ROUTES, 'low');
prefetchRoutes(LOW_PRIORITY_ROUTES, LOW_PRIORITY_PREFETCH, 'low');
}
return () => {

View File

@ -10,7 +10,7 @@ import { collectionsGetList } from '@/api/buster_rest/collections/requests';
export const CollectionListController: React.FC = () => {
const [openNewCollectionModal, setOpenNewCollectionModal] = useState(false);
const [collectionListFilters, setCollectionListFilters] = useState<
Omit<Parameters<typeof collectionsGetList>[0], 'page' | 'page_size'>
Omit<Parameters<typeof collectionsGetList>[0], 'page_token' | 'page_size'>
>({});
const { data: collectionsList, isFetched: isCollectionListFetched } =

View File

@ -16,7 +16,10 @@ import { BusterCollectionListItem } from '@/api/asset_interfaces/collection';
import { useGetCollection } from '@/api/buster_rest/collections';
import { collectionsGetList } from '@/api/buster_rest/collections/requests';
type CollectionListFilters = Omit<Parameters<typeof collectionsGetList>[0], 'page' | 'page_size'>;
type CollectionListFilters = Omit<
Parameters<typeof collectionsGetList>[0],
'page_token' | 'page_size'
>;
type SetCollectionListFilters = (filters: CollectionListFilters) => void;
export const CollectionListHeader: React.FC<{

View File

@ -13,3 +13,4 @@ export * from './columnFormatter';
export * from './colors';
export * from './regression';
export * from './canvas';
export * from './query';

17
web/src/lib/query.ts Normal file
View File

@ -0,0 +1,17 @@
import { RustApiError } from '@/api/buster_rest/errors';
import { QueryClient, queryOptions } from '@tanstack/react-query';
export const isQueryStale = (
options: ReturnType<typeof queryOptions<any, RustApiError, any>>,
queryClient: QueryClient
): boolean => {
const queryState = queryClient.getQueryState(options.queryKey);
const updatedAt = queryState?.dataUpdatedAt;
const staleTime =
(options.staleTime as number) ||
(queryClient.getDefaultOptions().queries?.staleTime as number) ||
0;
const isStale = updatedAt ? Date.now() - updatedAt > staleTime : true;
return isStale;
};