veza/apps/web/src/utils/stateHydration.ts

223 lines
5.8 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 { apiClient } from '@/services/api/client';
import { useAuthStore } from '@/stores/auth';
import { useLibraryStore } from '@/stores/library';
import { useChatStore } from '@/stores/chat';
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 {
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:', err);
}
}
// 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:', err);
}
}
// 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:', err);
}
}
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
logger.error('[StateHydration] Fatal error during hydration:', err);
result.success = false;
}
return result;
}
/**
* Hydrate auth state from server
*/
async function hydrateAuthState(): Promise<void> {
const { refreshUser } = useAuthStore.getState();
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
*/
async function hydrateChatState(): Promise<void> {
const { fetchConversations } = useChatStore.getState();
// Fetch conversations to hydrate the chat store
await fetchConversations();
}
/**
* 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 = {}) {
// Dynamic import to avoid circular dependencies
const React = require('react');
const { useEffect, useState } = React;
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,
};
}