request queries

This commit is contained in:
Nate Kelley 2025-02-13 12:59:50 -07:00
parent 8ec0137fbc
commit ba80aaf41d
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
27 changed files with 54 additions and 509 deletions

View File

@ -1,7 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { BusterChat, BusterChatListItem } from '@/api/asset_interfaces/chat';
import { queryOptions } from '@tanstack/react-query';
import { ChatListEmitPayload } from '@/api/buster_socket/chats';
import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';
import type { BusterChat, BusterChatListItem } from './chatInterfaces';
import type { GetChatListParams } from '@/api/request_interfaces/chats';
const chatsGetChat = (chatId: string) =>
queryOptions<BusterChat>({
@ -9,19 +8,19 @@ const chatsGetChat = (chatId: string) =>
staleTime: 10 * 1000
});
const chatsGetList = (filters?: ChatListEmitPayload) =>
const chatsGetList = (filters?: GetChatListParams) =>
queryOptions<BusterChatListItem[]>({
queryKey: ['chats', 'list', filters] as const
});
const deleteChat = (chatId: string) => {
const deleteChat = () => {
const queryKey = ['chats', 'list'] as const;
return queryOptions<BusterChatListItem[]>({
queryKey
});
};
export const queryOptionsConfig = {
export const chatQueryKeys = {
'/chats/get:getChat': chatsGetChat,
'/chats/list:getChatsList': chatsGetList,
'/chats/delete:deleteChat': deleteChat
@ -29,7 +28,7 @@ export const queryOptionsConfig = {
const ExampleComponent = () => {
const queryClient = useQueryClient();
const options = queryOptionsConfig['/chats/get:getChat']!('123');
const options = chatQueryKeys['/chats/get:getChat']!('123');
const queryKey = options.queryKey;
const data = queryClient.getQueryData(queryKey);
@ -40,7 +39,7 @@ const ExampleComponent = () => {
return d;
});
const options2 = queryOptionsConfig['/chats/delete:deleteChat']!('123');
const options2 = chatQueryKeys['/chats/delete:deleteChat']!();
const queryKey2 = options2.queryKey;
const data3 = queryClient.getQueryData(queryKey2);

View File

@ -1,2 +0,0 @@
import type { BusterChat } from './chatInterfaces';
import type { BusterChatMessage } from './chatMessageInterfaces';

View File

@ -0,0 +1,11 @@
import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';
import { BusterCollectionListItem } from './interfaces';
const collectionsGetList = (filters?: GetCollectionListParams) =>
queryOptions<BusterCollectionListItem[]>({
queryKey: ['collections', 'list', filters] as const
});
export const collectionQueryKeys = {
'/collections/list:getCollectionsList': collectionsGetList
};

View File

@ -14,3 +14,4 @@ export * from './sql';
export * from './permission_groups';
export * from './dataset_groups';
export * from './api_keys';
export * from './queryKeys';

View File

@ -0,0 +1,7 @@
import { chatQueryKeys } from './chat/chatQueryKeys';
import { collectionQueryKeys } from './collection/collectionQueryKeys';
export const queryKeys = {
...chatQueryKeys,
...collectionQueryKeys
};

View File

@ -1,12 +1,12 @@
import { mainApi } from '../instances';
import { serverFetch } from '../../createServerInstance';
import type { BusterChatListItem, BusterChat } from '@/api/asset_interfaces';
import type { ChatListParams, GetChatParams } from './interfaces';
import type { GetChatListParams, GetChatParams } from '../../request_interfaces/chats';
const CHATS_BASE = '/chats';
// Client-side fetch version
export const getListChats = async (params?: ChatListParams): Promise<BusterChatListItem[]> => {
export const getListChats = async (params?: GetChatListParams): Promise<BusterChatListItem[]> => {
const { page_token = 0, page_size = 1000, admin_view = false } = params || {};
return mainApi
.get<BusterChatListItem[]>(`${CHATS_BASE}/list`, {
@ -17,7 +17,7 @@ export const getListChats = async (params?: ChatListParams): Promise<BusterChatL
// Server-side fetch version
export const getListChats_server = async (
params?: ChatListParams
params?: GetChatListParams
): Promise<BusterChatListItem[]> => {
const { page_token = 0, page_size = 1000, admin_view = false } = params || {};
return await serverFetch<BusterChatListItem[]>(`${CHATS_BASE}/list`, {

View File

@ -1,3 +1,4 @@
import { GetChatParams, GetChatListParams } from '../../request_interfaces/chats';
import type { BusterSocketRequestBase } from '../base_interfaces';
/**
@ -45,13 +46,7 @@ export type ChatStopChat = BusterSocketRequestBase<
* @interface ChatGetChat
* @extends BusterSocketRequestBase
*/
export type ChatGetChat = BusterSocketRequestBase<
'/chats/get',
{
/** The unique identifier of the chat to retrieve */
id: string;
}
>;
export type ChatGetChat = BusterSocketRequestBase<'/chats/get', GetChatParams>;
/**
* Request type for unsubscribing from real-time updates of a specific chat.
@ -71,17 +66,7 @@ export type ChatUnsubscribeFromChat = BusterSocketRequestBase<
* @interface ChatListEmitPayload
* @extends BusterSocketRequestBase
*/
export type ChatListEmitPayload = BusterSocketRequestBase<
'/chats/list',
{
/** Pagination token indicating the page number */
page_token: number;
/** Number of chat items to return per page */
page_size: number;
/** When true, shows all organization chats (admin only). When false, shows only user's chats */
admin_view: boolean;
}
>;
export type ChatListEmitPayload = BusterSocketRequestBase<'/chats/list', GetChatListParams>;
/**
* Request type for deleting a specific chat.

View File

@ -0,0 +1,3 @@
export * from './useSocketQueryMutation';
export * from './useSocketQueryOn';
export * from './useSocketQueryEmitAndOnce';

View File

@ -25,7 +25,7 @@ import type {
} from './types';
import type { BusterChatListItem } from '@/api/asset_interfaces/chat';
export function useSocketMutation<
export function useSocketQueryMutation<
TRequestRoute extends BusterSocketRequestRoute,
TRoute extends BusterSocketResponseRoute,
TError = unknown,
@ -92,7 +92,7 @@ const ExampleComponent = () => {
const data = queryClient.getQueryData(options.queryKey);
data?.[0].created_by_avatar;
const { mutate } = useSocketMutation<
const { mutate } = useSocketQueryMutation<
'/chats/delete',
'/chats/delete:deleteChat',
unknown,

View File

@ -1 +0,0 @@
export * from './buster_rest';

View File

@ -0,0 +1 @@
export * from './interfaces';

View File

@ -1,4 +1,4 @@
export interface ChatListParams {
export interface GetChatListParams {
/** Pagination token indicating the page number */
page_token: number;
/** Number of chat items to return per page */

View File

@ -1,7 +1,8 @@
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useSocketQueryMutation } from '@/hooks';
import { useSocketQueryMutation } from '@/api/buster_socket_query';
import { timeout } from '@/utils';
import { useMemoizedFn } from 'ahooks';
import { queryKeys } from '@/api/asset_interfaces';
export const useCollectionCreate = () => {
const { openConfirmModal } = useBusterNotifications();
@ -30,16 +31,18 @@ export const useCollectionCreate = () => {
useSocketQueryMutation(
{ route: '/collections/delete' },
{ route: '/collections/delete:deleteCollections' },
{
preSetQueryData: [
{
responseRoute: '/collections/list:listCollections',
callback: (data, variables) => {
return data?.filter((collection) => !variables.ids.includes(collection.id)) || [];
}
}
]
}
queryKeys
// {
// preSetQueryData: [
// {
// responseRoute: '/collections/list:listCollections',
// callback: (data, variables) => {
// return data?.filter((collection) => !variables.ids.includes(collection.id)) || [];
// }
// }
// ]
// }
);
const deleteCollection = useMemoizedFn(async (id: string | string[], useConfirmModal = true) => {

View File

@ -1,12 +0,0 @@
import { BusterSocketRequest, BusterSocketResponseRoute } from '@/api/buster_socket';
import { BusterSocketResponseConfig, InferBusterSocketResponseData } from './types';
import { QueryKey } from '@tanstack/react-query';
export const createQueryKey: <TRoute extends BusterSocketResponseRoute>(
socketResponse: BusterSocketResponseConfig<TRoute>['route'],
callbackResult: InferBusterSocketResponseData<TRoute>,
socketRequest?: BusterSocketRequest
) => QueryKey = (socketResponse, callbackResult, socketRequest) => {
if (socketRequest) return [socketResponse, socketRequest.route, socketRequest.payload];
return [socketResponse];
};

View File

@ -1,9 +0,0 @@
import { createQueryKey } from './helpers';
import { BusterSocketResponseConfig } from './types';
export * from './useSocketQueryEmitAndOnce';
export * from './useSocketQueryEmitOn';
export * from './useSocketQueryOn';
export * from './useSocketQueryMutation';
export { createQueryKey, type BusterSocketResponseConfig };

View File

@ -1,59 +0,0 @@
import { UseMutationOptions } from '@tanstack/react-query';
import { BusterSocketResponseRoute } from '@/api/buster_socket';
import type { InferBusterSocketResponseData, BusterSocketRequestRoute } from './types';
export type QueryDataStrategy = 'replace' | 'append' | 'prepend' | 'merge' | 'ignore';
export type PreSetQueryDataItem<TVariables> = {
[Route in BusterSocketResponseRoute]: {
responseRoute: Route;
requestRoute?: BusterSocketRequestRoute;
callback: (
data: InferBusterSocketResponseData<Route>,
variables: TVariables
) => InferBusterSocketResponseData<Route>;
};
}[BusterSocketResponseRoute];
export type SinglePreSetQueryDataItem<TRoute extends BusterSocketResponseRoute, TVariables> = {
requestRoute?: BusterSocketRequestRoute;
callback: (
data: InferBusterSocketResponseData<TRoute>,
variables: TVariables
) => InferBusterSocketResponseData<TRoute>;
};
export type SocketQueryMutationOptions<
TRoute extends BusterSocketResponseRoute,
TError,
TVariables
> = Omit<
UseMutationOptions<InferBusterSocketResponseData<TRoute>, TError, TVariables>,
'mutationFn'
> & {
/**
* Configuration for optimistically updating query data before the mutation completes.
* Can be either a single item or an array of items.
*/
preSetQueryData?:
| Array<PreSetQueryDataItem<TVariables>>
| SinglePreSetQueryDataItem<TRoute, TVariables>;
/**
* When true, adds a small delay before applying preSetQueryData to ensure React Query's cache
* is properly initialized.
* @default false
*/
awaitPrefetchQueryData?: boolean;
/**
* Strategy for integrating mutation response data into existing query data.
* @property 'replace' - Replace existing data
* @property 'append' - Add to end of array
* @property 'prepend' - Add to start of array
* @property 'merge' - Merge objects (requires ID field)
* @property 'ignore' - No automatic update
* @default 'ignore'
*/
queryDataStrategy?: QueryDataStrategy;
};

View File

@ -1,48 +0,0 @@
import { QueryClient } from '@tanstack/react-query';
import { QueryDataStrategy } from './mutationTypes';
import { BusterSocketResponseRoute } from '@/api/buster_socket';
import { InferBusterSocketResponseData } from './types';
export const executeQueryDataStrategy = async <TRoute extends BusterSocketResponseRoute>(
queryClient: QueryClient,
queryKey: unknown[],
data: InferBusterSocketResponseData<TRoute>,
strategy: QueryDataStrategy
) => {
if (strategy === 'ignore') return;
const strategies: Record<Exclude<QueryDataStrategy, 'ignore'>, () => Promise<void>> = {
replace: async () => {
await queryClient.setQueryData(queryKey, data);
},
append: async () => {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>[]>(queryKey, (prev) => [
...(Array.isArray(prev) ? prev : []),
data
]);
},
prepend: async () => {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>[]>(queryKey, (prev) => [
data,
...(Array.isArray(prev) ? prev : [])
]);
},
merge: async () => {
if (typeof data === 'object' && data !== null && 'id' in data) {
await queryClient.setQueryData<Record<string, InferBusterSocketResponseData<TRoute>>>(
queryKey,
(prev) => ({
...(prev || {}),
[(data as { id: string }).id]: data
})
);
}
}
};
const updateStrategy = strategies[strategy as Exclude<QueryDataStrategy, 'ignore'>];
if (updateStrategy) {
await updateStrategy();
}
};

View File

@ -1,40 +0,0 @@
import type {
BusterSocketRequest,
BusterSocketResponse,
BusterSocketResponseRoute
} from '@/api/buster_socket';
import { UseQueryResult } from '@tanstack/react-query';
/**
* Infers the response data type from a BusterSocket route
*/
export type InferBusterSocketResponseData<TRoute extends BusterSocketResponseRoute> = Extract<
BusterSocketResponse,
{ route: TRoute }
>['callback'] extends (d: infer D) => void
? D
: never;
/**
* Socket response configuration with optional error handler
*/
export type BusterSocketResponseConfig<TRoute extends BusterSocketResponseRoute> = {
route: TRoute;
onError?: (d: unknown) => void;
};
export type UseBusterSocketQueryResult<TData, TError = unknown> = UseQueryResult<TData, TError>;
/**
* Extract the route type from BusterSocketRequest
*/
export type BusterSocketRequestRoute = BusterSocketRequest['route'];
export type BusterSocketRequestConfig<TRoute extends BusterSocketRequestRoute> = {
route: TRoute;
};
export type InferBusterSocketRequestPayload<TRoute extends BusterSocketRequestRoute> = Extract<
BusterSocketRequest,
{ route: TRoute }
>['payload'];

View File

@ -1,57 +0,0 @@
import { QueryKey, UseQueryOptions } from '@tanstack/react-query';
import type {
BusterSocketRequest,
BusterSocketResponse,
BusterSocketResponseRoute
} from '@/api/buster_socket';
import { useBusterWebSocket } from '@/context/BusterWebSocket';
import type {
UseBusterSocketQueryResult,
InferBusterSocketResponseData,
BusterSocketResponseConfig
} from './types';
import { useCreateReactQuery } from '@/api/createReactQuery';
import { createQueryKey } from './helpers';
import { useMemo } from 'react';
export function useSocketQueryEmitAndOnce<
TRoute extends BusterSocketResponseRoute,
TError = unknown
>(
socketRequest: BusterSocketRequest,
socketResponse: BusterSocketResponseConfig<TRoute>,
options?: Partial<Omit<UseQueryOptions<InferBusterSocketResponseData<TRoute>, TError>, 'queryFn'>>
): UseBusterSocketQueryResult<InferBusterSocketResponseData<TRoute>, TError> {
const busterSocket = useBusterWebSocket();
const queryKey = useMemo(
() => options?.queryKey || createQueryKey(socketResponse, socketRequest),
[options?.queryKey, socketResponse?.route, socketRequest?.route]
);
const queryFn = async (): Promise<InferBusterSocketResponseData<TRoute>> => {
try {
const result = await busterSocket.emitAndOnce({
emitEvent: socketRequest,
responseEvent: {
route: socketResponse.route,
onError: socketResponse.onError,
callback: (d: unknown) => d
} as BusterSocketResponse
});
return result as InferBusterSocketResponseData<TRoute>;
} catch (error) {
throw error;
}
};
return useCreateReactQuery<InferBusterSocketResponseData<TRoute>, TError>({
queryKey,
queryFn,
isUseSession: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
options
});
}

View File

@ -1,66 +0,0 @@
import {
BusterSocketRequest,
BusterSocketResponse,
BusterSocketResponseRoute
} from '@/api/buster_socket';
import { useQueryClient, UseQueryOptions } from '@tanstack/react-query';
import {
BusterSocketResponseConfig,
InferBusterSocketResponseData,
UseBusterSocketQueryResult
} from './types';
import { useSockeQueryOn } from './useSocketQueryOn';
import { useBusterWebSocket } from '@/context/BusterWebSocket';
import { useMemoizedFn, useMount } from 'ahooks';
import { createQueryKey } from './helpers';
import { useEffect } from 'react';
/**
* A hook that emits a socket request on mount and listens for responses.
*
* @template TRoute - The type of socket response route
* @template TError - The type of error that can occur
*
* @param socketRequest - The socket request to emit
* @param socketResponse - Configuration for the socket response including route and error handler
* @param options - Additional options for the React Query hook
*
* @returns A React Query result containing the response data and status
*/
export const useSocketQueryEmitOn = <TRoute extends BusterSocketResponseRoute, TError = unknown>(
socketRequest: BusterSocketRequest,
socketResponse: BusterSocketResponseConfig<TRoute>,
optionsProps?: Omit<
UseQueryOptions<InferBusterSocketResponseData<TRoute>, TError>,
'queryKey' | 'queryFn'
> & { enabled?: boolean | string }
): UseBusterSocketQueryResult<InferBusterSocketResponseData<TRoute>, TError> => {
const busterSocket = useBusterWebSocket();
const { enabled = true, ...options } = optionsProps || {};
const queryKey = createQueryKey(socketResponse, socketRequest);
const queryFn = useMemoizedFn(async () => {
const res = await busterSocket.emitAndOnce({
emitEvent: socketRequest,
responseEvent: {
...socketResponse,
callback: (d: unknown) => d
} as BusterSocketResponse
});
return res;
}) as () => Promise<InferBusterSocketResponseData<TRoute>>;
useEffect(() => {
if (enabled) {
queryFn();
}
}, [enabled]);
return useSockeQueryOn(socketResponse, {
...options,
queryKey,
queryFn
});
};

View File

@ -1,123 +0,0 @@
import {
BusterSocketRequest,
BusterSocketResponse,
BusterSocketResponseRoute
} from '@/api/buster_socket';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useBusterWebSocket } from '@/context/BusterWebSocket';
import { useMemoizedFn } from 'ahooks';
import {
BusterSocketResponseConfig,
InferBusterSocketResponseData,
InferBusterSocketRequestPayload,
BusterSocketRequestConfig,
BusterSocketRequestRoute
} from './types';
import { SocketQueryMutationOptions } from './mutationTypes';
import { executeQueryDataStrategy } from './queryDataStrategies';
import { createQueryKey } from './helpers';
/**
* A hook that creates a mutation for emitting socket requests and handling responses.
* Supports optimistic updates and various strategies for updating the query cache.
*
* @template TRequestRoute - The socket request route type
* @template TRoute - The socket response route type
* @template TError - The error type that can occur
* @template TVariables - The variables type passed to the mutation function
*
* @param socketRequest - The base socket request configuration
* @param socketResponse - The socket response configuration with optional error handler
* @param options - Additional options for configuring the mutation behavior
*
* @example
* ```tsx
* const { mutate } = useSocketQueryMutation(
* { route: '/users/favorites/post' },
* { route: '/users/favorites/post:createFavorite' },
* {
* preSetQueryData: [
* {
* responseRoute: '/users/favorites/list:listFavorites',
* callback: (data, variables) => [...(data || []), variables]
* }
* ],
* queryDataStrategy: 'append'
* }
* );
* ```
*/
export const useSocketQueryMutation = <
TRequestRoute extends BusterSocketRequestRoute,
TRoute extends BusterSocketResponseRoute,
TError = unknown,
TVariables = InferBusterSocketRequestPayload<TRequestRoute>
>(
socketRequest: BusterSocketRequestConfig<TRequestRoute>,
socketResponse: BusterSocketResponseConfig<TRoute> & {
callback?: (data: unknown) => InferBusterSocketResponseData<TRoute>;
},
options?: SocketQueryMutationOptions<TRoute, TError, TVariables>
) => {
const busterSocket = useBusterWebSocket();
const queryClient = useQueryClient();
const { preSetQueryData, queryDataStrategy = 'ignore', ...mutationOptions } = options || {};
const handlePreSetQueryData = useMemoizedFn(async (variables: TVariables) => {
if (!preSetQueryData) return;
if (options?.awaitPrefetchQueryData) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
const arrayOfPreSetQueryData = Array.isArray(preSetQueryData)
? preSetQueryData
: [{ ...preSetQueryData, responseRoute: socketResponse.route }];
for (const item of arrayOfPreSetQueryData) {
const { responseRoute, requestRoute, callback } = item!;
const requestPayload: undefined | BusterSocketRequest = requestRoute
? ({ route: requestRoute, payload: variables } as BusterSocketRequest)
: undefined;
const presetQueryKey = createQueryKey({ route: responseRoute! }, requestPayload);
await queryClient.setQueryData(presetQueryKey, (prev: any) => callback(prev, variables));
}
});
const mutationFn = useMemoizedFn(async (variables: TVariables) => {
const request = {
...socketRequest,
payload: variables
} as BusterSocketRequest;
const queryKey = createQueryKey(socketResponse, request);
await handlePreSetQueryData(variables);
const response = await busterSocket.emitAndOnce({
emitEvent: request,
responseEvent: {
...socketResponse,
callback: (data: unknown) => {
socketResponse.callback?.(data);
return data;
}
} as BusterSocketResponse
});
if (response !== undefined) {
await executeQueryDataStrategy(
queryClient,
queryKey as unknown[],
response as InferBusterSocketResponseData<TRoute>,
queryDataStrategy
);
}
return response as InferBusterSocketResponseData<TRoute>;
});
return useMutation({
...mutationOptions,
mutationFn
});
};

View File

@ -1,48 +0,0 @@
'use client';
import { QueryKey, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query';
import type {
BusterSocketRequest,
BusterSocketResponse,
BusterSocketResponseRoute
} from '@/api/buster_socket';
import { useBusterWebSocket } from '@/context/BusterWebSocket';
import type {
UseBusterSocketQueryResult,
InferBusterSocketResponseData,
BusterSocketResponseConfig
} from './types';
import { useMount } from 'ahooks';
import { createQueryKey } from './helpers';
import { useMemo } from 'react';
export const useSockeQueryOn = <TRoute extends BusterSocketResponseRoute, TError = unknown>(
socketResponse: BusterSocketResponseConfig<TRoute>,
options?: {
queryKey?: QueryKey;
}
): UseBusterSocketQueryResult<InferBusterSocketResponseData<TRoute>, TError> => {
const busterSocket = useBusterWebSocket();
const queryClient = useQueryClient();
// const queryKey = useMemo(
// () => options?.queryKey || createQueryKey(socketResponse),
// [options?.queryKey, socketResponse?.route]
// );
useMount(() => {
busterSocket.on({
route: socketResponse.route,
onError: socketResponse.onError,
callback: (d: unknown) => {
queryClient.setQueryData(queryKey, d as InferBusterSocketResponseData<TRoute>);
}
} as BusterSocketResponse);
});
return useQuery({
queryKey,
...options,
enabled: false //must be disabled to prevent automatic fetching
});
};