import { useRef, useCallback } from 'react'; import { supabase } from '@/lib/supabaseClient'; import { createTableQuery } from '@/lib/supabaseHelpers'; import { logger } from '@/lib/logger'; import { getErrorMessage } from '@/lib/errorHandler'; import { MODERATION_CONSTANTS } from '@/lib/moderation/constants'; import type { Database } from '@/integrations/supabase/types'; /** * Entity types supported by the cache */ type EntityType = 'rides' | 'parks' | 'companies'; /** * Type definitions for cached entities (can be partial) */ type Ride = Database['public']['Tables']['rides']['Row']; type Park = Database['public']['Tables']['parks']['Row']; type Company = Database['public']['Tables']['companies']['Row']; /** * Discriminated union for all cached entity types */ type CachedEntity = Ride | Park | Company; /** * Map entity type strings to their corresponding types * Cache stores partial entities with at least id and name */ interface EntityTypeMap { rides: Partial & { id: string; name: string }; parks: Partial & { id: string; name: string }; companies: Partial & { id: string; name: string }; } /** * Cache structure for entities with flexible typing */ interface EntityCacheStructure { rides: Map & { id: string; name: string }>; parks: Map & { id: string; name: string }>; companies: Map & { id: string; name: string }>; } /** * Hook for managing entity name caching (rides, parks, companies) * * Uses ref-based storage to avoid triggering re-renders while providing * efficient caching for entity lookups during moderation. * * @example * ```tsx * const entityCache = useEntityCache(); * * // Get cached entity * const ride = entityCache.getCached('rides', rideId); * * // Bulk fetch and cache entities * await entityCache.bulkFetch('rides', [id1, id2, id3]); * * // Clear specific cache * entityCache.clear('rides'); * * // Clear all caches * entityCache.clearAll(); * ``` */ export function useEntityCache() { // Use ref to prevent re-renders on cache updates const cacheRef = useRef({ rides: new Map(), parks: new Map(), companies: new Map(), }); /** * Get a cached entity by ID with type safety */ const getCached = useCallback(( type: T, id: string ): EntityTypeMap[T] | undefined => { return cacheRef.current[type].get(id) as EntityTypeMap[T] | undefined; }, []); /** * Check if an entity is cached */ const has = useCallback((type: EntityType, id: string): boolean => { return cacheRef.current[type].has(id); }, []); /** * Set a cached entity with LRU eviction and type safety */ const setCached = useCallback(( type: T, id: string, data: EntityTypeMap[T] ): void => { const cache = cacheRef.current[type]; // LRU eviction: remove oldest entry if cache is full if (cache.size >= MODERATION_CONSTANTS.MAX_ENTITY_CACHE_SIZE) { const firstKey = cache.keys().next().value; if (firstKey) { cache.delete(firstKey); logger.log(`♻️ [EntityCache] Evicted ${type}/${firstKey} (LRU)`); } } cache.set(id, data); }, []); /** * Get uncached IDs from a list */ const getUncachedIds = useCallback((type: EntityType, ids: string[]): string[] => { return ids.filter(id => !cacheRef.current[type].has(id)); }, []); /** * Bulk fetch entities from the database and cache them * Only fetches entities that aren't already cached */ const bulkFetch = useCallback(async ( type: T, ids: string[] ): Promise => { if (ids.length === 0) return []; // Filter to only uncached IDs const uncachedIds = getUncachedIds(type, ids); if (uncachedIds.length === 0) { // All entities are cached, return them return ids.map(id => getCached(type, id)).filter((item): item is EntityTypeMap[T] => item !== undefined); } try { let data: unknown[] | null = null; let error: unknown = null; // Use type-safe table queries switch (type) { case 'rides': const ridesResult = await createTableQuery('rides') .select('id, name, slug, park_id') .in('id', uncachedIds); data = ridesResult.data; error = ridesResult.error; break; case 'parks': const parksResult = await createTableQuery('parks') .select('id, name, slug') .in('id', uncachedIds); data = parksResult.data; error = parksResult.error; break; case 'companies': const companiesResult = await createTableQuery('companies') .select('id, name, slug, company_type') .in('id', uncachedIds); data = companiesResult.data; error = companiesResult.error; break; default: // Unknown entity type - skip return []; } if (error) { // Silent - cache miss is acceptable return []; } // Cache the fetched entities if (data) { (data as Array>).forEach((entity) => { if (entity && typeof entity === 'object' && 'id' in entity && 'name' in entity) { setCached(type, entity.id as string, entity as EntityTypeMap[T]); } }); } return (data as EntityTypeMap[T][]) || []; } catch (error: unknown) { // Silent - cache operations are non-critical return []; } }, [getCached, setCached, getUncachedIds]); /** * Fetch and cache related entities based on submission content * Automatically determines which entities to fetch from submission data */ const fetchRelatedEntities = useCallback(async (submissions: Array<{ content?: Record; submission_type?: string }>): Promise => { const rideIds = new Set(); const parkIds = new Set(); const companyIds = new Set(); // Collect all entity IDs from submissions submissions.forEach(submission => { const content = submission.content; if (content && typeof content === 'object') { if (typeof content.ride_id === 'string') rideIds.add(content.ride_id); if (typeof content.park_id === 'string') parkIds.add(content.park_id); if (typeof content.company_id === 'string') companyIds.add(content.company_id); if (typeof content.entity_id === 'string') { if (submission.submission_type === 'ride') rideIds.add(content.entity_id); if (submission.submission_type === 'park') parkIds.add(content.entity_id); if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type || '')) { companyIds.add(content.entity_id); } } if (typeof content.manufacturer_id === 'string') companyIds.add(content.manufacturer_id); if (typeof content.designer_id === 'string') companyIds.add(content.designer_id); if (typeof content.operator_id === 'string') companyIds.add(content.operator_id); if (typeof content.property_owner_id === 'string') companyIds.add(content.property_owner_id); } }); // Fetch all entities in parallel const fetchPromises: Promise[] = []; if (rideIds.size > 0) { fetchPromises.push(bulkFetch('rides', Array.from(rideIds))); } if (parkIds.size > 0) { fetchPromises.push(bulkFetch('parks', Array.from(parkIds))); } if (companyIds.size > 0) { fetchPromises.push(bulkFetch('companies', Array.from(companyIds))); } await Promise.all(fetchPromises); }, [bulkFetch]); /** * Clear a specific entity type cache */ const clear = useCallback((type: EntityType): void => { cacheRef.current[type].clear(); }, []); /** * Clear all entity caches */ const clearAll = useCallback((): void => { cacheRef.current.rides.clear(); cacheRef.current.parks.clear(); cacheRef.current.companies.clear(); }, []); /** * Get cache size for a specific type */ const getSize = useCallback((type: EntityType): number => { return cacheRef.current[type].size; }, []); /** * Get total cache size across all entity types */ const getTotalSize = useCallback((): number => { return cacheRef.current.rides.size + cacheRef.current.parks.size + cacheRef.current.companies.size; }, []); /** * 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, fetchRelatedEntities, clear, clearAll, getSize, getTotalSize, getCacheRef, }; }