Files
thrilltrack-explorer/src-old/hooks/moderation/useEntityCache.ts

294 lines
8.9 KiB
TypeScript

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<Ride> & { id: string; name: string };
parks: Partial<Park> & { id: string; name: string };
companies: Partial<Company> & { id: string; name: string };
}
/**
* Cache structure for entities with flexible typing
*/
interface EntityCacheStructure {
rides: Map<string, Partial<Ride> & { id: string; name: string }>;
parks: Map<string, Partial<Park> & { id: string; name: string }>;
companies: Map<string, Partial<Company> & { 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<EntityCacheStructure>({
rides: new Map(),
parks: new Map(),
companies: new Map(),
});
/**
* Get a cached entity by ID with type safety
*/
const getCached = useCallback(<T extends EntityType>(
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(<T extends EntityType>(
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 <T extends EntityType>(
type: T,
ids: string[]
): Promise<EntityTypeMap[T][]> => {
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<Record<string, unknown>>).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<string, string | number>; submission_type?: string }>): Promise<void> => {
const rideIds = new Set<string>();
const parkIds = new Set<string>();
const companyIds = new Set<string>();
// 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<any[]>[] = [];
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,
};
}