veza/apps/web/src/utils/stateHydration.ts
senke c933bbaefa state-ownership: consolidate chat stores to feature store
- 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
2026-01-15 19:31:40 +01:00

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,
};
}