@oxyhq/services
Version:
216 lines (188 loc) • 6.97 kB
text/typescript
/**
* Mutation Factory - Creates standardized mutations with optimistic updates
*
* This factory reduces boilerplate code for mutations that follow the common pattern:
* 1. Cancel outgoing queries
* 2. Snapshot previous data
* 3. Apply optimistic update
* 4. On error: rollback and show toast
* 5. On success: update cache, stores, and invalidate queries
*/
import { QueryClient, UseMutationOptions } from '@tanstack/react-query';
import type { User } from '@oxyhq/core';
import { queryKeys, invalidateAccountQueries, invalidateUserQueries } from '../queries/queryKeys';
import { toast } from '../../../lib/sonner';
import { useAuthStore } from '../../stores/authStore';
/**
* Configuration for creating a standard profile mutation
*/
export interface ProfileMutationConfig<TData, TVariables> {
/** The mutation function that makes the API call */
mutationFn: (variables: TVariables) => Promise<TData>;
/** Query keys to cancel before mutation */
cancelQueryKeys?: unknown[][];
/** Function to apply optimistic update to the user data */
optimisticUpdate?: (previousUser: User, variables: TVariables) => Partial<User>;
/** Error message to show on failure */
errorMessage?: string | ((error: Error) => string);
/** Success message to show (optional) */
successMessage?: string;
/** Whether to update authStore on success (default: true) */
updateAuthStore?: boolean;
/** Whether to invalidate user queries on success (default: true) */
invalidateUserQueries?: boolean;
/** Whether to invalidate account queries on success (default: true) */
invalidateAccountQueries?: boolean;
/** Custom onSuccess handler */
onSuccess?: (data: TData, variables: TVariables, queryClient: QueryClient) => void;
}
/**
* Creates a standard profile mutation with optimistic updates
*
* @example
* ```ts
* const updateProfile = createProfileMutation({
* mutationFn: (updates) => oxyServices.updateProfile(updates),
* optimisticUpdate: (user, updates) => updates,
* errorMessage: 'Failed to update profile',
* });
* ```
*/
export function createProfileMutation<TVariables>(
config: ProfileMutationConfig<User, TVariables>,
queryClient: QueryClient,
activeSessionId: string | null
): UseMutationOptions<User, Error, TVariables, { previousUser?: User }> {
const {
mutationFn,
cancelQueryKeys = [],
optimisticUpdate,
errorMessage = 'Operation failed',
successMessage,
updateAuthStore = true,
invalidateUserQueries: shouldInvalidateUserQueries = true,
invalidateAccountQueries: shouldInvalidateAccountQueries = true,
onSuccess: customOnSuccess,
} = config;
return {
mutationFn,
onMutate: async (variables) => {
// Cancel queries that might conflict
await queryClient.cancelQueries({ queryKey: queryKeys.accounts.current() });
for (const key of cancelQueryKeys) {
await queryClient.cancelQueries({ queryKey: key });
}
// Snapshot previous user data
const previousUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
// Apply optimistic update if provided
if (previousUser && optimisticUpdate) {
const updates = optimisticUpdate(previousUser, variables);
const optimisticUser = { ...previousUser, ...updates };
queryClient.setQueryData<User>(queryKeys.accounts.current(), optimisticUser);
if (activeSessionId) {
queryClient.setQueryData<User>(queryKeys.users.profile(activeSessionId), optimisticUser);
}
}
return { previousUser };
},
onError: (error, _variables, context) => {
// Rollback optimistic update
if (context?.previousUser) {
queryClient.setQueryData(queryKeys.accounts.current(), context.previousUser);
if (activeSessionId) {
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), context.previousUser);
}
}
// Show error toast
const message = typeof errorMessage === 'function'
? errorMessage(error)
: (error instanceof Error ? error.message : errorMessage);
toast.error(message);
},
onSuccess: (data, variables) => {
// Update cache with server response
queryClient.setQueryData(queryKeys.accounts.current(), data);
if (activeSessionId) {
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
}
// Update authStore for immediate UI updates
if (updateAuthStore) {
useAuthStore.getState().setUser(data);
}
// Invalidate related queries
if (shouldInvalidateUserQueries) {
invalidateUserQueries(queryClient);
}
if (shouldInvalidateAccountQueries) {
invalidateAccountQueries(queryClient);
}
// Show success toast if configured
if (successMessage) {
toast.success(successMessage);
}
// Call custom onSuccess handler
if (customOnSuccess) {
customOnSuccess(data, variables, queryClient);
}
},
};
}
/**
* Configuration for creating a generic mutation (non-profile)
*/
export interface GenericMutationConfig<TData, TVariables, TContext> {
/** The mutation function */
mutationFn: (variables: TVariables) => Promise<TData>;
/** Query key for optimistic data */
queryKey: unknown[];
/** Function to create optimistic data */
optimisticData?: (previous: TData | undefined, variables: TVariables) => TData;
/** Error message */
errorMessage?: string;
/** Success message */
successMessage?: string;
/** Additional queries to invalidate on success */
invalidateQueries?: unknown[][];
}
/**
* Creates a generic mutation with optimistic updates
*/
export function createGenericMutation<TData, TVariables>(
config: GenericMutationConfig<TData, TVariables, { previous?: TData }>,
queryClient: QueryClient
): UseMutationOptions<TData, Error, TVariables, { previous?: TData }> {
const {
mutationFn,
queryKey,
optimisticData,
errorMessage = 'Operation failed',
successMessage,
invalidateQueries = [],
} = config;
return {
mutationFn,
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey });
const previous = queryClient.getQueryData<TData>(queryKey);
if (optimisticData) {
queryClient.setQueryData<TData>(queryKey, optimisticData(previous, variables));
}
return { previous };
},
onError: (error, _variables, context) => {
if (context?.previous !== undefined) {
queryClient.setQueryData(queryKey, context.previous);
}
toast.error(error instanceof Error ? error.message : errorMessage);
},
onSuccess: (data) => {
queryClient.setQueryData(queryKey, data);
for (const key of invalidateQueries) {
queryClient.invalidateQueries({ queryKey: key });
}
if (successMessage) {
toast.success(successMessage);
}
},
};
}