223 lines
5.8 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
|