type safety updates for mutation

This commit is contained in:
Nate Kelley 2025-02-12 14:19:51 -07:00
parent ad02a021ac
commit e91e0d9f86
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
12 changed files with 327 additions and 115 deletions

View File

@ -18,7 +18,7 @@ export const useUpdateMetricAssosciations = ({
}) => {
const busterSocket = useBusterWebSocket();
const userFavorites = useUserConfigContextSelector((state) => state.userFavorites);
const forceGetFavoritesList = useUserConfigContextSelector((x) => x.forceGetFavoritesList);
const refreshFavoritesList = useUserConfigContextSelector((x) => x.refreshFavoritesList);
const removeItemFromIndividualDashboard = useBusterDashboardContextSelector(
(state) => state.removeItemFromIndividualDashboard
);
@ -108,7 +108,7 @@ export const useUpdateMetricAssosciations = ({
if (addToPromises.length) await Promise.all(addToPromises);
if (collectionIsInFavorites) {
await forceGetFavoritesList();
await refreshFavoritesList();
}
}
);
@ -205,7 +205,7 @@ export const useUpdateMetricAssosciations = ({
}
});
if (collectionIsInFavorites && ignoreFavoriteUpdates !== true) {
await forceGetFavoritesList();
await refreshFavoritesList();
}
}
);

View File

@ -1,39 +1,31 @@
import React, { useRef } from 'react';
import { useBusterWebSocket } from '../BusterWebSocket';
import { useMemoizedFn, useMount } from 'ahooks';
import type { BusterUserFavorite } from '@/api/asset_interfaces';
import { ShareAssetType } from '@/api/asset_interfaces';
import { useMemoizedFn } from 'ahooks';
import { BusterUserFavorite, ShareAssetType } from '@/api/asset_interfaces';
import { createQueryKey, useSocketQueryEmitOn } from '@/hooks';
import { useQueryClient } from '@tanstack/react-query';
import { useHotkeys } from 'react-hotkeys-hook';
export const useFavoriteProvider = () => {
const busterSocket = useBusterWebSocket();
const useMountedUserFavorites = useRef(false);
const [userFavorites, setUserFavorites] = React.useState<BusterUserFavorite[]>([]);
const queryClient = useQueryClient();
const _onSetInitialFavoritesList = useMemoizedFn((favorites: BusterUserFavorite[]) => {
setUserFavorites(favorites);
});
const favoritesQueryKey = createQueryKey(
{ route: '/users/favorites/list:listFavorites' },
{ route: '/users/favorites/list', payload: {} }
);
const forceGetFavoritesList = useMemoizedFn(() => {
useMountedUserFavorites.current = false;
busterSocket.off({
route: '/users/favorites/list:listFavorites',
callback: _onSetInitialFavoritesList
});
return _onGetFavoritesList();
});
const { data: userFavorites, refetch: refreshFavoritesList } = useSocketQueryEmitOn(
{ route: '/users/favorites/list', payload: {} },
{ route: '/users/favorites/list:listFavorites' }
);
const _onGetFavoritesList = useMemoizedFn(() => {
if (useMountedUserFavorites.current) return;
useMountedUserFavorites.current = true;
busterSocket.emit({
route: '/users/favorites/list',
payload: {}
});
busterSocket.on({
route: '/users/favorites/list:listFavorites',
callback: _onSetInitialFavoritesList
});
});
const setUserFavorites = useMemoizedFn(
(updater: (v: BusterUserFavorite[]) => BusterUserFavorite[]) => {
queryClient.setQueryData(favoritesQueryKey, (v: BusterUserFavorite[] | undefined) => {
return updater(v || []);
});
}
);
const addItemToFavorite = useMemoizedFn(
async ({
@ -48,77 +40,81 @@ export const useFavoriteProvider = () => {
}) => {
setUserFavorites((v) => [{ id, type: asset_type, name }, ...v]);
await busterSocket.emitAndOnce({
emitEvent: {
route: '/users/favorites/post',
payload: {
id,
asset_type
}
},
responseEvent: {
route: '/users/favorites/post:createFavorite',
callback: _onSetInitialFavoritesList
}
});
// busterSocket.emit({
// route: '/users/favorites/post',
// payload: {
// id,
// asset_type
// }
// });
// await busterSocket.emitAndOnce({
// emitEvent: {
// route: '/users/favorites/post',
// payload: {
// id,
// asset_type
// }
// },
// responseEvent: {
// route: '/users/favorites/post:createFavorite',
// callback: _onSetInitialFavoritesList
// }
// });
}
);
const removeItemFromFavorite = useMemoizedFn(
async ({ id, asset_type }: { id: string; asset_type: ShareAssetType }) => {
setUserFavorites(userFavorites.filter((f) => f.id !== id));
await busterSocket.emitAndOnce({
emitEvent: {
route: '/users/favorites/delete',
payload: {
id,
asset_type
}
},
responseEvent: {
route: '/users/favorites/post:createFavorite',
callback: _onSetInitialFavoritesList
}
});
// setUserFavorites(userFavorites.filter((f) => f.id !== id));
// await busterSocket.emitAndOnce({
// emitEvent: {
// route: '/users/favorites/delete',
// payload: {
// id,
// asset_type
// }
// },
// responseEvent: {
// route: '/users/favorites/post:createFavorite',
// callback: _onSetInitialFavoritesList
// }
// });
}
);
const reorderFavorites = useMemoizedFn(async (favorites: string[]) => {
requestAnimationFrame(() => {
setUserFavorites((v) => {
return favorites.map((id, index) => {
let favorite = v.find((f) => f.id === id || f.collection_id === id)!;
return { ...favorite, index };
});
});
});
await busterSocket.emitAndOnce({
emitEvent: {
route: '/users/favorites/update',
payload: {
favorites
}
},
responseEvent: {
route: '/users/favorites/update:updateFavorite',
callback: _onSetInitialFavoritesList
}
});
// requestAnimationFrame(() => {
// setUserFavorites((v) => {
// return favorites.map((id, index) => {
// let favorite = v.find((f) => f.id === id || f.collection_id === id)!;
// return { ...favorite, index };
// });
// });
// });
// await busterSocket.emitAndOnce({
// emitEvent: {
// route: '/users/favorites/update',
// payload: {
// favorites
// }
// },
// responseEvent: {
// route: '/users/favorites/update:updateFavorite',
// callback: _onSetInitialFavoritesList
// }
// });
});
const bulkEditFavorites = useMemoizedFn(async (favorites: string[]) => {
return reorderFavorites(favorites);
});
useMount(async () => {
_onGetFavoritesList();
});
return {
bulkEditFavorites,
forceGetFavoritesList,
refreshFavoritesList,
reorderFavorites,
userFavorites,
userFavorites: userFavorites || [],
addItemToFavorite,
removeItemFromFavorite
};

View File

@ -1,4 +1,4 @@
export * from './react';
export * from './dom';
export * from './useDebounceSearch';
export * from './useBusterWebSocketQuery';
export * from './useSocketQuery';

View File

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

View File

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

View File

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

View File

@ -11,18 +11,24 @@ import type {
BusterSocketResponseConfig
} from './types';
import { useCreateReactQuery } from '@/api/createReactQuery';
import { createQueryKey } from './helpers';
import { useMemo } from 'react';
export function useBusterWebSocketQuery<TRoute extends BusterSocketResponseRoute, TError = unknown>(
queryKey: QueryKey,
export function useSocketQueryEmitAndOnce<
TRoute extends BusterSocketResponseRoute,
TError = unknown
>(
socketRequest: BusterSocketRequest,
socketResponse: BusterSocketResponseConfig<TRoute>,
options?: Omit<
UseQueryOptions<InferBusterSocketResponseData<TRoute>, TError>,
'queryKey' | 'queryFn'
>
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({
@ -44,15 +50,8 @@ export function useBusterWebSocketQuery<TRoute extends BusterSocketResponseRoute
queryKey,
queryFn,
isUseSession: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
options
});
}
// Example usage with automatic type inference
const ExampleUsage = () => {
const { data, isLoading, error } = useBusterWebSocketQuery(
['chats', 'get', '123'],
{ route: '/chats/get', payload: { id: '123' } },
{ route: '/chats/get:getChat' }
);
};

View File

@ -0,0 +1,63 @@
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';
/**
* 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>,
options?: Omit<
UseQueryOptions<InferBusterSocketResponseData<TRoute>, TError>,
'queryKey' | 'queryFn'
>
): UseBusterSocketQueryResult<InferBusterSocketResponseData<TRoute>, TError> => {
const busterSocket = useBusterWebSocket();
const queryClient = useQueryClient();
const queryKey = createQueryKey(socketResponse, socketRequest);
const emitEvent = useMemoizedFn(async () => {
const res = await busterSocket.emitAndOnce({
emitEvent: socketRequest,
responseEvent: {
...socketResponse,
callback: (d: unknown) => d
} as BusterSocketResponse
});
return res;
}) as () => Promise<InferBusterSocketResponseData<TRoute>>;
useMount(() => {
emitEvent();
});
return useSockeQueryOn(socketResponse, {
...options,
queryKey,
queryFn: emitEvent
});
};

View File

@ -0,0 +1,130 @@
import {
BusterSocketRequest,
BusterSocketResponse,
BusterSocketResponseRoute
} from '@/api/buster_socket';
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
import { useBusterWebSocket } from '@/context/BusterWebSocket';
import { useMemoizedFn } from 'ahooks';
import { BusterSocketResponseConfig, InferBusterSocketResponseData } from './types';
import { ShareAssetType } from '@/api/asset_interfaces';
import { createQueryKey } from './helpers';
/**
* A hook that creates a mutation for emitting socket requests and handling responses.
*
* @template TRoute - The type of socket response route
* @template TData - The type of data returned by the socket response
* @template TVariables - The type of variables passed to the mutation function
* @template TError - The type of error that can occur
*
* @param socketRequest - The base socket request to emit (variables will be merged with this)
* @param socketResponse - Configuration for the socket response including route and error handler
* @param options - Additional options for the React Query mutation hook
*
* @returns A React Query mutation result for handling socket requests
*/
export const useSocketQueryMutation = <
TRoute extends BusterSocketResponseRoute,
TVariables = void,
TError = unknown
>(
socketRequest: BusterSocketRequest,
socketResponse: BusterSocketResponseConfig<TRoute> & {
callback?: (d: unknown) => InferBusterSocketResponseData<TRoute>;
},
optionsProps?: Omit<
UseMutationOptions<InferBusterSocketResponseData<TRoute>, TError, TVariables>,
'mutationFn'
> & {
preSetQueryData?: (
d: InferBusterSocketResponseData<TRoute> | undefined,
variables: TVariables
) => InferBusterSocketResponseData<TRoute>;
queryDataStrategy?: 'replace' | 'append' | 'prepend' | 'merge' | 'ignore';
}
) => {
const busterSocket = useBusterWebSocket();
const queryClient = useQueryClient();
const { preSetQueryData, queryDataStrategy = 'ignore', ...options } = optionsProps || {};
const mutationFn = useMemoizedFn(async (variables: TVariables) => {
const queryKey = createQueryKey(socketResponse, socketRequest);
if (preSetQueryData) {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>>(queryKey, (d) => {
return preSetQueryData(d, variables);
});
}
const res = await busterSocket.emitAndOnce({
emitEvent: socketRequest,
responseEvent: {
...socketResponse,
callback: (d: unknown) => {
if (socketResponse.callback) {
socketResponse.callback(d);
}
return d;
}
} as BusterSocketResponse
});
if (queryDataStrategy === 'replace') {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>>(queryKey, () => {
return res as InferBusterSocketResponseData<TRoute>;
});
} else if (queryDataStrategy === 'append') {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>[]>(queryKey, (d) => {
return [...(Array.isArray(d) ? d : []), res as InferBusterSocketResponseData<TRoute>];
});
} else if (queryDataStrategy === 'prepend') {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>[]>(queryKey, (d) => {
return [res as InferBusterSocketResponseData<TRoute>, ...(Array.isArray(d) ? d : [])];
});
} else if (queryDataStrategy === 'merge') {
await queryClient.setQueryData<Record<string, InferBusterSocketResponseData<TRoute>>>(
queryKey,
(d) => {
if (typeof res === 'object' && res !== null && 'id' in res) {
const typedRes = res as InferBusterSocketResponseData<TRoute> & { id: string };
return {
...(d || {}),
[typedRes.id]: typedRes
};
} else {
console.warn('response is not an object with an id', res);
}
return d;
}
);
}
return res as InferBusterSocketResponseData<TRoute>;
});
return useMutation<InferBusterSocketResponseData<TRoute>, TError, TVariables>({
...options,
mutationFn
});
};
const Example = () => {
const { mutate, data, ...rest } = useSocketQueryMutation(
{
route: '/users/favorites/post',
payload: {
id: '1',
asset_type: ShareAssetType.DASHBOARD
}
},
{
route: '/users/favorites/post:createFavorite'
},
{
preSetQueryData: (d) => {
return d || [];
}
}
);
};

View File

@ -1,7 +1,11 @@
'use client';
import { QueryKey, useQuery, useQueryClient } from '@tanstack/react-query';
import type { BusterSocketResponse, BusterSocketResponseRoute } from '@/api/buster_socket';
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,
@ -9,32 +13,34 @@ import type {
BusterSocketResponseConfig
} from './types';
import { useMount } from 'ahooks';
import { createQueryKey } from './helpers';
import { useMemo } from 'react';
export const useBusterWebSocketOn = <TRoute extends BusterSocketResponseRoute, TError = unknown>(
queryKey: QueryKey,
socketResponse: BusterSocketResponseConfig<TRoute>
export const useSockeQueryOn = <TRoute extends BusterSocketResponseRoute, TError = unknown>(
socketResponse: BusterSocketResponseConfig<TRoute>,
options?: Partial<UseQueryOptions<InferBusterSocketResponseData<TRoute>, TError>>
): 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>);
queryClient.invalidateQueries({ queryKey });
}
} as BusterSocketResponse);
});
return useQuery({
queryKey
});
};
const ExampleUsage = () => {
const { data, isFetched } = useBusterWebSocketOn(['chats', 'get', '123'], {
route: '/chats/get:getChat'
queryKey,
...options,
enabled: false
});
};