mirror of https://github.com/buster-so/buster.git
type safety updates for mutation
This commit is contained in:
parent
ad02a021ac
commit
e91e0d9f86
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export * from './react';
|
||||
export * from './dom';
|
||||
export * from './useDebounceSearch';
|
||||
export * from './useBusterWebSocketQuery';
|
||||
export * from './useSocketQuery';
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from './useBusterWebSocketQuery';
|
|
@ -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];
|
||||
};
|
|
@ -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 };
|
|
@ -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' }
|
||||
);
|
||||
};
|
|
@ -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
|
||||
});
|
||||
};
|
|
@ -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 || [];
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue