import { useRef, useCallback } from 'react'; import { supabase } from '@/lib/supabaseClient'; import { logger } from '@/lib/logger'; import { getErrorMessage } from '@/lib/errorHandler'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; import type { ModerationItem } from '@/types/moderation'; /** * Profile data structure returned from the database */ export interface CachedProfile { user_id: string; username: string; display_name?: string; avatar_url?: string; } /** * Hook for managing user profile caching * * Uses ref-based storage to avoid triggering re-renders while providing * efficient caching for user profile lookups during moderation. * * @example * ```tsx * const profileCache = useProfileCache(); * * // Get cached profile * const profile = profileCache.getCached(userId); * * // Bulk fetch and cache profiles * const profiles = await profileCache.bulkFetch([id1, id2, id3]); * * // Check if profile exists in cache * if (profileCache.has(userId)) { * const profile = profileCache.getCached(userId); * } * * // Clear cache * profileCache.clear(); * ``` */ export function useProfileCache() { // Use ref to prevent re-renders on cache updates const cacheRef = useRef>(new Map()); /** * Get a cached profile by user ID */ const getCached = useCallback((userId: string): CachedProfile | undefined => { return cacheRef.current.get(userId); }, []); /** * Check if a profile is cached */ const has = useCallback((userId: string): boolean => { return cacheRef.current.has(userId); }, []); /** * Set a cached profile with LRU eviction */ const setCached = useCallback((userId: string, profile: CachedProfile): void => { const cache = cacheRef.current; // LRU eviction if (cache.size >= MODERATION_CONSTANTS.MAX_PROFILE_CACHE_SIZE) { const firstKey = cache.keys().next().value; if (firstKey) { cache.delete(firstKey); logger.log(`♻️ [ProfileCache] Evicted ${firstKey} (LRU)`); } } cache.set(userId, profile); }, []); /** * Get uncached user IDs from a list */ const getUncachedIds = useCallback((userIds: string[]): string[] => { return userIds.filter(id => !cacheRef.current.has(id)); }, []); /** * Bulk fetch user profiles from the database and cache them * Only fetches profiles that aren't already cached * * @param userIds - Array of user IDs to fetch * @returns Array of fetched profiles */ const bulkFetch = useCallback(async (userIds: string[]): Promise => { if (userIds.length === 0) return []; // Filter to only uncached IDs const uncachedIds = getUncachedIds(userIds); if (uncachedIds.length === 0) { // All profiles are cached, return them return userIds.map(id => getCached(id)).filter((p): p is CachedProfile => !!p); } try { const { data, error } = await supabase .from('profiles') .select('user_id, username, display_name, avatar_url') .in('user_id', uncachedIds); if (error) { // Silent - cache miss is acceptable return []; } // Cache the fetched profiles if (data) { data.forEach((profile) => { const cachedProfile: CachedProfile = { ...profile, display_name: profile.display_name || undefined, avatar_url: profile.avatar_url || undefined }; setCached(profile.user_id, cachedProfile); }); } return (data || []).map(profile => ({ ...profile, display_name: profile.display_name || undefined, avatar_url: profile.avatar_url || undefined })); } catch (error: unknown) { // Silent - cache operations are non-critical return []; } }, [getCached, setCached, getUncachedIds]); /** * Fetch and return profiles for a list of user IDs * Returns a Map for easy lookup * * @param userIds - Array of user IDs to fetch * @returns Map of userId -> profile */ const fetchAsMap = useCallback(async (userIds: string[]): Promise> => { const profiles = await bulkFetch(userIds); return new Map(profiles.map(p => [p.user_id, p])); }, [bulkFetch]); /** * Fetch profiles for submitters and reviewers from submissions * Automatically extracts user IDs and reviewer IDs from submission data * * @param submissions - Array of submissions with user_id and reviewer_id * @returns Map of userId -> profile for all users involved */ const fetchForSubmissions = useCallback(async (submissions: ModerationItem[]): Promise> => { const userIds = submissions.map(s => s.user_id).filter(Boolean); const reviewerIds = submissions.map(s => s.reviewer_id).filter((id): id is string => !!id); const allUserIds = [...new Set([...userIds, ...reviewerIds])]; return await fetchAsMap(allUserIds); }, [fetchAsMap]); /** * Get a display name for a user (display_name or username) * Returns 'Unknown User' if not found in cache */ const getDisplayName = useCallback((userId: string): string => { const profile = getCached(userId); if (!profile) return 'Unknown User'; return profile.display_name || profile.username || 'Unknown User'; }, [getCached]); /** * Invalidate (remove) a specific profile from cache */ const invalidate = useCallback((userId: string): void => { cacheRef.current.delete(userId); }, []); /** * Clear all cached profiles */ const clear = useCallback((): void => { cacheRef.current.clear(); }, []); /** * Get cache size */ const getSize = useCallback((): number => { return cacheRef.current.size; }, []); /** * Get all cached profile user IDs */ const getAllCachedIds = useCallback((): string[] => { return Array.from(cacheRef.current.keys()); }, []); /** * Get direct access to cache ref (for advanced use cases) * Use with caution - prefer using the provided methods */ const getCacheRef = useCallback(() => cacheRef.current, []); // Return without useMemo wrapper (OPTIMIZED) return { getCached, has, setCached, getUncachedIds, bulkFetch, fetchAsMap, fetchForSubmissions, getDisplayName, invalidate, clear, getSize, getAllCachedIds, getCacheRef, }; }