mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 14:31:12 -05:00
294 lines
8.9 KiB
TypeScript
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,
|
|
};
|
|
}
|