diff --git a/web/src/context/Metrics/BusterMetricsIndividualProvider/useMetricUpdateAssosciations.ts b/web/src/context/Metrics/BusterMetricsIndividualProvider/useMetricUpdateAssosciations.ts index d9976dd7b..33cd78f6e 100644 --- a/web/src/context/Metrics/BusterMetricsIndividualProvider/useMetricUpdateAssosciations.ts +++ b/web/src/context/Metrics/BusterMetricsIndividualProvider/useMetricUpdateAssosciations.ts @@ -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(); } } ); diff --git a/web/src/context/Users/useFavoriteProvider.tsx b/web/src/context/Users/useFavoriteProvider.tsx index 69de8ce40..1a7f429be 100644 --- a/web/src/context/Users/useFavoriteProvider.tsx +++ b/web/src/context/Users/useFavoriteProvider.tsx @@ -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([]); + 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 }; diff --git a/web/src/hooks/index.ts b/web/src/hooks/index.ts index aee9b89fd..8bdda625e 100644 --- a/web/src/hooks/index.ts +++ b/web/src/hooks/index.ts @@ -1,4 +1,4 @@ export * from './react'; export * from './dom'; export * from './useDebounceSearch'; -export * from './useBusterWebSocketQuery'; +export * from './useSocketQuery'; diff --git a/web/src/hooks/useBusterWebSocketQuery/index.ts b/web/src/hooks/useBusterWebSocketQuery/index.ts deleted file mode 100644 index dcc130f75..000000000 --- a/web/src/hooks/useBusterWebSocketQuery/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useBusterWebSocketQuery'; diff --git a/web/src/hooks/useBusterWebSocketQuery/config.ts b/web/src/hooks/useSocketQuery/config.ts similarity index 100% rename from web/src/hooks/useBusterWebSocketQuery/config.ts rename to web/src/hooks/useSocketQuery/config.ts diff --git a/web/src/hooks/useSocketQuery/helpers.ts b/web/src/hooks/useSocketQuery/helpers.ts new file mode 100644 index 000000000..f5d648978 --- /dev/null +++ b/web/src/hooks/useSocketQuery/helpers.ts @@ -0,0 +1,11 @@ +import { BusterSocketRequest, BusterSocketResponseRoute } from '@/api/buster_socket'; +import { BusterSocketResponseConfig } from './types'; +import { QueryKey } from '@tanstack/react-query'; + +export const createQueryKey: ( + socketResponse: BusterSocketResponseConfig, + socketRequest?: BusterSocketRequest +) => QueryKey = (socketResponse, socketRequest) => { + if (socketRequest) return [socketResponse.route, socketRequest.route, socketRequest.payload]; + return [socketResponse.route]; +}; diff --git a/web/src/hooks/useSocketQuery/index.ts b/web/src/hooks/useSocketQuery/index.ts new file mode 100644 index 000000000..d1b575736 --- /dev/null +++ b/web/src/hooks/useSocketQuery/index.ts @@ -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 }; diff --git a/web/src/hooks/useBusterWebSocketQuery/types.ts b/web/src/hooks/useSocketQuery/types.ts similarity index 100% rename from web/src/hooks/useBusterWebSocketQuery/types.ts rename to web/src/hooks/useSocketQuery/types.ts diff --git a/web/src/hooks/useBusterWebSocketQuery/useBusterWebSocketQuery.tsx b/web/src/hooks/useSocketQuery/useSocketQueryEmitAndOnce.tsx similarity index 71% rename from web/src/hooks/useBusterWebSocketQuery/useBusterWebSocketQuery.tsx rename to web/src/hooks/useSocketQuery/useSocketQueryEmitAndOnce.tsx index 1c1335eae..5e750af9e 100644 --- a/web/src/hooks/useBusterWebSocketQuery/useBusterWebSocketQuery.tsx +++ b/web/src/hooks/useSocketQuery/useSocketQueryEmitAndOnce.tsx @@ -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( - queryKey: QueryKey, +export function useSocketQueryEmitAndOnce< + TRoute extends BusterSocketResponseRoute, + TError = unknown +>( socketRequest: BusterSocketRequest, socketResponse: BusterSocketResponseConfig, - options?: Omit< - UseQueryOptions, TError>, - 'queryKey' | 'queryFn' - > + options?: Partial, TError>, 'queryFn'>> ): UseBusterSocketQueryResult, TError> { const busterSocket = useBusterWebSocket(); + const queryKey = useMemo( + () => options?.queryKey || createQueryKey(socketResponse, socketRequest), + [options?.queryKey, socketResponse?.route, socketRequest?.route] + ); + const queryFn = async (): Promise> => { try { const result = await busterSocket.emitAndOnce({ @@ -44,15 +50,8 @@ export function useBusterWebSocketQuery { - const { data, isLoading, error } = useBusterWebSocketQuery( - ['chats', 'get', '123'], - { route: '/chats/get', payload: { id: '123' } }, - { route: '/chats/get:getChat' } - ); -}; diff --git a/web/src/hooks/useSocketQuery/useSocketQueryEmitOn.tsx b/web/src/hooks/useSocketQuery/useSocketQueryEmitOn.tsx new file mode 100644 index 000000000..a253a5b26 --- /dev/null +++ b/web/src/hooks/useSocketQuery/useSocketQueryEmitOn.tsx @@ -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 = ( + socketRequest: BusterSocketRequest, + socketResponse: BusterSocketResponseConfig, + options?: Omit< + UseQueryOptions, TError>, + 'queryKey' | 'queryFn' + > +): UseBusterSocketQueryResult, 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>; + + useMount(() => { + emitEvent(); + }); + + return useSockeQueryOn(socketResponse, { + ...options, + queryKey, + queryFn: emitEvent + }); +}; diff --git a/web/src/hooks/useSocketQuery/useSocketQueryMutation.tsx b/web/src/hooks/useSocketQuery/useSocketQueryMutation.tsx new file mode 100644 index 000000000..f890227c7 --- /dev/null +++ b/web/src/hooks/useSocketQuery/useSocketQueryMutation.tsx @@ -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 & { + callback?: (d: unknown) => InferBusterSocketResponseData; + }, + optionsProps?: Omit< + UseMutationOptions, TError, TVariables>, + 'mutationFn' + > & { + preSetQueryData?: ( + d: InferBusterSocketResponseData | undefined, + variables: TVariables + ) => InferBusterSocketResponseData; + 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>(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>(queryKey, () => { + return res as InferBusterSocketResponseData; + }); + } else if (queryDataStrategy === 'append') { + await queryClient.setQueryData[]>(queryKey, (d) => { + return [...(Array.isArray(d) ? d : []), res as InferBusterSocketResponseData]; + }); + } else if (queryDataStrategy === 'prepend') { + await queryClient.setQueryData[]>(queryKey, (d) => { + return [res as InferBusterSocketResponseData, ...(Array.isArray(d) ? d : [])]; + }); + } else if (queryDataStrategy === 'merge') { + await queryClient.setQueryData>>( + queryKey, + (d) => { + if (typeof res === 'object' && res !== null && 'id' in res) { + const typedRes = res as InferBusterSocketResponseData & { 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; + }); + + return useMutation, 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 || []; + } + } + ); +}; diff --git a/web/src/hooks/useBusterWebSocketQuery/useBusterWebSocketOn.tsx b/web/src/hooks/useSocketQuery/useSocketQueryOn.tsx similarity index 50% rename from web/src/hooks/useBusterWebSocketQuery/useBusterWebSocketOn.tsx rename to web/src/hooks/useSocketQuery/useSocketQueryOn.tsx index a7b33d6aa..62f61f348 100644 --- a/web/src/hooks/useBusterWebSocketQuery/useBusterWebSocketOn.tsx +++ b/web/src/hooks/useSocketQuery/useSocketQueryOn.tsx @@ -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 = ( - queryKey: QueryKey, - socketResponse: BusterSocketResponseConfig +export const useSockeQueryOn = ( + socketResponse: BusterSocketResponseConfig, + options?: Partial, TError>> ): UseBusterSocketQueryResult, 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); - 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 }); };