- Removed duplicate stores/chat.ts (old store) - Consolidated to features/chat/store/chatStore.ts (active store) - Updated ChatMessages.tsx to use feature store (currentConversationId + lookup) - Updated storeSelectors.ts to use feature store and export only existing methods - Updated stateHydration.ts to skip chat hydration (uses React Query) - Updated stateInvalidation.ts to not call fetchConversations (React Query handles it) - Updated stores/index.ts to export feature store - Updated documentation - Test files still reference old store (separate update needed) - Action 4.5.1.5 complete
272 lines
7.9 KiB
TypeScript
272 lines
7.9 KiB
TypeScript
/**
|
|
* State Hydration Utilities
|
|
* FE-STATE-003: Hydrate state from server on app load
|
|
*
|
|
* Provides utilities for hydrating Zustand stores with server data on application startup
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
|
import { useLibraryStore } from '@/stores/library';
|
|
// Action 4.5.1.5: Chat store moved to features/chat/store/chatStore.ts
|
|
// Chat hydration is disabled by default and not needed (ChatSidebar uses React Query)
|
|
import { TokenStorage } from '@/services/tokenStorage';
|
|
import { logger } from './logger';
|
|
|
|
/**
|
|
* Configuration for state hydration
|
|
*/
|
|
export interface HydrationConfig {
|
|
/** Whether to hydrate auth state */
|
|
hydrateAuth?: boolean;
|
|
/** Whether to hydrate library state */
|
|
hydrateLibrary?: boolean;
|
|
/** Whether to hydrate chat state */
|
|
hydrateChat?: boolean;
|
|
/** Whether to skip hydration if user is not authenticated */
|
|
requireAuth?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Result of state hydration
|
|
*/
|
|
export interface HydrationResult {
|
|
success: boolean;
|
|
hydrated: string[];
|
|
errors: Array<{ store: string; error: Error }>;
|
|
}
|
|
|
|
/**
|
|
* FE-STATE-003: Hydrate all stores from server
|
|
*
|
|
* This function loads initial state from the server for all configured stores.
|
|
* It should be called once on application startup.
|
|
*
|
|
* @param config Configuration for which stores to hydrate
|
|
* @returns Promise with hydration result
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // In App.tsx or main.tsx
|
|
* useEffect(() => {
|
|
* hydrateState({
|
|
* hydrateAuth: true,
|
|
* hydrateLibrary: true,
|
|
* hydrateChat: true,
|
|
* }).then((result) => {
|
|
* if (result.success) {
|
|
* console.log('State hydrated successfully');
|
|
* }
|
|
* });
|
|
* }, []);
|
|
* ```
|
|
*/
|
|
export async function hydrateState(
|
|
config: HydrationConfig = {},
|
|
): Promise<HydrationResult> {
|
|
const {
|
|
hydrateAuth = true,
|
|
hydrateLibrary = false,
|
|
hydrateChat = false,
|
|
requireAuth = true,
|
|
} = config;
|
|
|
|
const result: HydrationResult = {
|
|
success: true,
|
|
hydrated: [],
|
|
errors: [],
|
|
};
|
|
|
|
try {
|
|
// Check authentication first if required
|
|
if (requireAuth) {
|
|
const { isAuthenticated } = useAuthStore.getState();
|
|
if (!isAuthenticated) {
|
|
logger.debug(
|
|
'[StateHydration] User not authenticated, skipping hydration',
|
|
);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Hydrate auth state
|
|
if (hydrateAuth) {
|
|
try {
|
|
// Vérifier si on a déjà un user authentifié avec des tokens
|
|
// Si oui, ne pas appeler refreshUser() pour éviter de réinitialiser l'état après login
|
|
// Action 4.1.1.3: User data is now managed by React Query, only check isAuthenticated
|
|
const { isAuthenticated, isLoading } = useAuthStore.getState();
|
|
const hasTokens = TokenStorage.hasTokens();
|
|
|
|
// Si on a déjà un user authentifié avec des tokens, ne pas refresh
|
|
// Cela évite les problèmes de timing après le login
|
|
// React Query will handle user data caching automatically
|
|
if (isAuthenticated && hasTokens && !isLoading) {
|
|
logger.debug(
|
|
'[StateHydration] User already authenticated with tokens, skipping auth hydration',
|
|
);
|
|
result.hydrated.push('auth');
|
|
} else {
|
|
await hydrateAuthState();
|
|
result.hydrated.push('auth');
|
|
logger.debug('[StateHydration] Auth state hydrated');
|
|
}
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
result.errors.push({ store: 'auth', error: err });
|
|
result.success = false;
|
|
logger.error('[StateHydration] Failed to hydrate auth state', {
|
|
error: err.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Hydrate library state
|
|
if (hydrateLibrary) {
|
|
try {
|
|
await hydrateLibraryState();
|
|
result.hydrated.push('library');
|
|
logger.debug('[StateHydration] Library state hydrated');
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
result.errors.push({ store: 'library', error: err });
|
|
result.success = false;
|
|
logger.error('[StateHydration] Failed to hydrate library state', {
|
|
error: err.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Hydrate chat state
|
|
if (hydrateChat) {
|
|
try {
|
|
await hydrateChatState();
|
|
result.hydrated.push('chat');
|
|
logger.debug('[StateHydration] Chat state hydrated');
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
result.errors.push({ store: 'chat', error: err });
|
|
result.success = false;
|
|
logger.error('[StateHydration] Failed to hydrate chat state', {
|
|
error: err.message,
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
logger.error('[StateHydration] Fatal error during hydration', {
|
|
error: err.message,
|
|
});
|
|
result.success = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Hydrate auth state from server
|
|
* CRITIQUE: Ne pas appeler refreshUser si l'utilisateur est déjà authentifié
|
|
* pour éviter de réinitialiser l'état après navigation
|
|
*
|
|
* Action 4.1.1.3: User data is now managed by React Query, only check isAuthenticated
|
|
*/
|
|
async function hydrateAuthState(): Promise<void> {
|
|
const { refreshUser, isAuthenticated } = useAuthStore.getState();
|
|
const hasTokens = TokenStorage.hasTokens();
|
|
|
|
// Si l'utilisateur est déjà authentifié avec des tokens,
|
|
// ne pas appeler refreshUser pour éviter de réinitialiser l'état
|
|
// React Query will handle user data caching automatically
|
|
if (isAuthenticated && hasTokens) {
|
|
logger.debug(
|
|
'[StateHydration] User already authenticated, skipping refreshUser',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Sinon, appeler refreshUser pour vérifier l'état d'authentification
|
|
await refreshUser();
|
|
}
|
|
|
|
/**
|
|
* Hydrate library state from server
|
|
*/
|
|
async function hydrateLibraryState(): Promise<void> {
|
|
const { fetchFavorites } = useLibraryStore.getState();
|
|
|
|
// Fetch favorites to hydrate the library store
|
|
await fetchFavorites();
|
|
}
|
|
|
|
/**
|
|
* Hydrate chat state from server
|
|
* Action 4.5.1.5: Chat hydration removed - ChatSidebar uses React Query to fetch conversations
|
|
* The feature store doesn't have fetchConversations method, and it's not needed
|
|
* since conversations are fetched via React Query in ChatSidebar component
|
|
*/
|
|
async function hydrateChatState(): Promise<void> {
|
|
// Chat state is hydrated via React Query in ChatSidebar component
|
|
// No need to fetch conversations here
|
|
logger.debug('[StateHydration] Chat state hydration skipped - using React Query');
|
|
}
|
|
|
|
/**
|
|
* FE-STATE-003: React hook for state hydration
|
|
*
|
|
* Use this hook in your root component to automatically hydrate state on mount
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* function App() {
|
|
* useStateHydration({
|
|
* hydrateAuth: true,
|
|
* hydrateLibrary: true,
|
|
* });
|
|
*
|
|
* return <YourApp />;
|
|
* }
|
|
* ```
|
|
*/
|
|
export function useStateHydration(config: HydrationConfig = {}) {
|
|
const [isHydrating, setIsHydrating] = useState(true);
|
|
const [hydrationResult, setHydrationResult] =
|
|
useState<HydrationResult | null>(null);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
|
|
hydrateState(config)
|
|
.then((result) => {
|
|
if (mounted) {
|
|
setHydrationResult(result);
|
|
setIsHydrating(false);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (mounted) {
|
|
logger.error('[StateHydration] Hook error:', error);
|
|
setHydrationResult({
|
|
success: false,
|
|
hydrated: [],
|
|
errors: [
|
|
{
|
|
store: 'unknown',
|
|
error:
|
|
error instanceof Error ? error : new Error(String(error)),
|
|
},
|
|
],
|
|
});
|
|
setIsHydrating(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, []); // Only run once on mount
|
|
|
|
return {
|
|
isHydrating,
|
|
hydrationResult,
|
|
};
|
|
}
|