Refactor image handling and caching strategies in multiple components for enhanced performance

This update modifies the image handling in ContentItem, ContinueWatchingSection, and FeaturedContent components to utilize a more efficient memory caching strategy and adjusted transition durations. Additionally, the HomeScreen component has been optimized for image prefetching, limiting concurrent requests to reduce memory pressure. The ThisWeekSection has been simplified to always refresh episodes when library items change, improving data handling. These changes aim to create a smoother user experience while navigating through content.
This commit is contained in:
tapframe 2025-06-21 23:48:38 +05:30
parent 15767886b3
commit 14dd507d50
8 changed files with 179 additions and 356 deletions

View file

@ -102,12 +102,11 @@ const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster}
contentFit="cover"
cachePolicy="memory-disk"
transition={150}
cachePolicy="memory"
transition={200}
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
priority="high"
onLoadStart={() => {
setImageLoaded(false);
setImageError(false);

View file

@ -309,12 +309,11 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.continueWatchingPoster}
contentFit="cover"
cachePolicy="memory-disk"
transition={150}
cachePolicy="memory"
transition={200}
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
priority="high"
/>
</View>

View file

@ -426,111 +426,106 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
style={styles.featuredContainer as ViewStyle}
>
<Animated.View style={[styles.imageContainer, posterAnimatedStyle]}>
<ExpoImage
<ImageBackground
source={{ uri: bannerUrl || featuredContent.poster }}
style={styles.featuredImage}
contentFit="cover"
cachePolicy="memory-disk"
transition={300}
priority="high"
placeholder={{ uri: 'https://via.placeholder.com/1080x1920' }}
placeholderContentFit="cover"
recyclingKey={featuredContent.id}
/>
{/* Subtle content overlay for better readability */}
<Animated.View style={[styles.contentOverlay, overlayAnimatedStyle]} />
<LinearGradient
colors={[
'rgba(0,0,0,0.1)',
'rgba(0,0,0,0.2)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
currentTheme.colors.darkBackground,
]}
locations={[0, 0.2, 0.5, 0.8, 1]}
style={styles.featuredGradient as ViewStyle}
style={styles.featuredImage as ViewStyle}
resizeMode="cover"
>
<Animated.View
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
{/* Subtle content overlay for better readability */}
<Animated.View style={[styles.contentOverlay, overlayAnimatedStyle]} />
<LinearGradient
colors={[
'rgba(0,0,0,0.1)',
'rgba(0,0,0,0.2)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
currentTheme.colors.darkBackground,
]}
locations={[0, 0.2, 0.5, 0.8, 1]}
style={styles.featuredGradient as ViewStyle}
>
{logoUrl && !logoLoadError ? (
<Animated.View style={logoAnimatedStyle}>
<ExpoImage
source={{ uri: logoUrl }}
style={styles.featuredLogo as ImageStyle}
contentFit="contain"
cachePolicy="memory"
transition={300}
recyclingKey={`logo-${featuredContent.id}`}
onError={onLogoLoadError}
<Animated.View
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
>
{logoUrl && !logoLoadError ? (
<Animated.View style={logoAnimatedStyle}>
<ExpoImage
source={{ uri: logoUrl }}
style={styles.featuredLogo as ImageStyle}
contentFit="contain"
cachePolicy="memory"
transition={300}
recyclingKey={`logo-${featuredContent.id}`}
onError={onLogoLoadError}
/>
</Animated.View>
) : (
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}>
{featuredContent.name}
</Text>
)}
<View style={styles.genreContainer as ViewStyle}>
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
<React.Fragment key={index}>
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}>
{genre}
</Text>
{index < array.length - 1 && (
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}></Text>
)}
</React.Fragment>
))}
</View>
</Animated.View>
<Animated.View style={[styles.featuredButtons as ViewStyle, buttonsAnimatedStyle]}>
<TouchableOpacity
style={styles.myListButton as ViewStyle}
onPress={handleSaveToLibrary}
activeOpacity={0.7}
>
<MaterialIcons
name={isSaved ? "bookmark" : "bookmark-border"}
size={24}
color={currentTheme.colors.white}
/>
</Animated.View>
) : (
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}>
{featuredContent.name}
</Text>
)}
<View style={styles.genreContainer as ViewStyle}>
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
<React.Fragment key={index}>
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}>
{genre}
</Text>
{index < array.length - 1 && (
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}></Text>
)}
</React.Fragment>
))}
</View>
</Animated.View>
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
{isSaved ? "Saved" : "Save"}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
onPress={() => {
if (featuredContent) {
navigation.navigate('Streams', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
activeOpacity={0.8}
>
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play
</Text>
</TouchableOpacity>
<Animated.View style={[styles.featuredButtons as ViewStyle, buttonsAnimatedStyle]}>
<TouchableOpacity
style={styles.myListButton as ViewStyle}
onPress={handleSaveToLibrary}
activeOpacity={0.7}
>
<MaterialIcons
name={isSaved ? "bookmark" : "bookmark-border"}
size={24}
color={currentTheme.colors.white}
/>
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
{isSaved ? "Saved" : "Save"}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
onPress={() => {
if (featuredContent) {
navigation.navigate('Streams', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
activeOpacity={0.8}
>
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.infoButton as ViewStyle}
onPress={handleInfoPress}
activeOpacity={0.7}
>
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
Info
</Text>
</TouchableOpacity>
</Animated.View>
</LinearGradient>
<TouchableOpacity
style={styles.infoButton as ViewStyle}
onPress={handleInfoPress}
activeOpacity={0.7}
>
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
Info
</Text>
</TouchableOpacity>
</Animated.View>
</LinearGradient>
</ImageBackground>
</Animated.View>
</TouchableOpacity>
</Animated.View>

View file

@ -130,15 +130,10 @@ export const ThisWeekSection = () => {
// Load episodes when library items change
useEffect(() => {
if (!libraryLoading) {
// Only fetch if we have library items or if this is the first load
if (libraryItems.length > 0 || episodes.length === 0) {
console.log('[ThisWeekSection] Library items changed, refreshing episodes. Items count:', libraryItems.length);
fetchThisWeekEpisodes();
} else {
console.log('[ThisWeekSection] Skipping refresh - no library items and episodes already loaded');
}
console.log('[ThisWeekSection] Library items changed, refreshing episodes. Items count:', libraryItems.length);
fetchThisWeekEpisodes();
}
}, [libraryLoading, libraryItems, fetchThisWeekEpisodes, episodes.length]);
}, [libraryLoading, libraryItems, fetchThisWeekEpisodes]);
const handleEpisodePress = (episode: ThisWeekEpisode) => {
// For upcoming episodes, go to the metadata screen

View file

@ -242,37 +242,16 @@ export function useFeaturedContent() {
// Load featured content initially and when content source changes
useEffect(() => {
if (!settings.showHeroSection) {
setFeaturedContent(null);
// Force refresh when switching to catalogs or when catalog selection changes
if (contentSource === 'catalogs') {
// Clear cache when switching to catalogs mode
setAllFeaturedContent([]);
setLoading(false);
return;
}
// Only load if we don't have cached data or if settings actually changed
const now = Date.now();
const cacheAge = now - persistentStore.lastFetchTime;
const hasValidCache = persistentStore.featuredContent &&
persistentStore.allFeaturedContent.length > 0 &&
cacheAge < CACHE_TIMEOUT;
// Check if this is truly a settings change or just a re-render
const sourceChanged = persistentStore.lastSettings.featuredContentSource !== contentSource;
const catalogsChanged = JSON.stringify(persistentStore.lastSettings.selectedHeroCatalogs) !== JSON.stringify(selectedCatalogs);
if (hasValidCache && !sourceChanged && !catalogsChanged) {
// Use existing cache without reloading
console.log('Using existing cached featured content, no reload needed');
setFeaturedContent(persistentStore.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false);
return;
}
// Force refresh when switching modes or when selection changes
if (sourceChanged || catalogsChanged) {
console.log('Settings changed, refreshing featured content');
// Clear cache when switching modes
setFeaturedContent(null);
persistentStore.allFeaturedContent = [];
persistentStore.featuredContent = null;
loadFeaturedContent(true);
} else if (contentSource === 'tmdb' && contentSource !== persistentStore.featuredContent?.type) {
// Clear cache when switching to TMDB mode from catalogs
setAllFeaturedContent([]);
setFeaturedContent(null);
persistentStore.allFeaturedContent = [];
@ -282,7 +261,7 @@ export function useFeaturedContent() {
// Normal load (might use cache if available)
loadFeaturedContent(false);
}
}, [loadFeaturedContent, contentSource, selectedCatalogs, settings.showHeroSection]);
}, [loadFeaturedContent, contentSource, selectedCatalogs]);
useEffect(() => {
if (featuredContent) {

View file

@ -325,34 +325,35 @@ const HomeScreen = () => {
if (!content.length) return;
try {
// More conservative prefetching to prevent memory issues
const BATCH_SIZE = 2;
// Limit concurrent prefetching to prevent memory pressure
const MAX_CONCURRENT_PREFETCH = 5;
const BATCH_SIZE = 3;
// Only prefetch poster images (most important) and limit to 6 items
const posterImages = content.slice(0, 6)
.map(item => item.poster)
const allImages = content.slice(0, 10) // Limit total images to prefetch
.map(item => [item.poster, item.banner, item.logo])
.flat()
.filter(Boolean) as string[];
// Process in small batches with longer delays
for (let i = 0; i < posterImages.length; i += BATCH_SIZE) {
const batch = posterImages.slice(i, i + BATCH_SIZE);
// Process in small batches to prevent memory pressure
for (let i = 0; i < allImages.length; i += BATCH_SIZE) {
const batch = allImages.slice(i, i + BATCH_SIZE);
try {
await Promise.all(
batch.map(async (imageUrl) => {
try {
await ExpoImage.prefetch(imageUrl);
// Longer delay between prefetches
await new Promise(resolve => setTimeout(resolve, 50));
// Small delay between prefetches to reduce memory pressure
await new Promise(resolve => setTimeout(resolve, 10));
} catch (error) {
// Silently handle individual prefetch errors
}
})
);
// Longer delay between batches to allow GC
if (i + BATCH_SIZE < posterImages.length) {
await new Promise(resolve => setTimeout(resolve, 200));
// Delay between batches to allow GC
if (i + BATCH_SIZE < allImages.length) {
await new Promise(resolve => setTimeout(resolve, 50));
}
} catch (error) {
// Continue with next batch if current batch fails
@ -371,18 +372,26 @@ const HomeScreen = () => {
if (!featuredContent) return;
try {
// Clear image cache to reduce memory pressure before orientation change
if (typeof (global as any)?.ExpoImage?.clearMemoryCache === 'function') {
try {
(global as any).ExpoImage.clearMemoryCache();
} catch (e) {
// Ignore cache clear errors
}
}
// Lock orientation to landscape before navigation to prevent glitches
// Don't clear image cache - let ExpoImage manage memory efficiently
try {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
// Longer delay to ensure orientation is fully set before navigation
await new Promise(resolve => setTimeout(resolve, 200));
} catch (orientationError) {
// If orientation lock fails, continue anyway but log it
logger.warn('[HomeScreen] Orientation lock failed:', orientationError);
// Still add a small delay
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise(resolve => setTimeout(resolve, 100));
}
navigation.navigate('Player', {
@ -425,23 +434,17 @@ const HomeScreen = () => {
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// Only refresh continue watching section if it's empty (first load or error state)
if (continueWatchingRef.current) {
// Check if continue watching is empty before refreshing
// This prevents unnecessary reloads when just switching tabs
console.log('[HomeScreen] Screen focused - checking if continue watching needs refresh');
}
// Only refresh continue watching section on focus
refreshContinueWatching();
// Don't reload catalogs unless they haven't been loaded yet
// Catalogs will be refreshed through context updates when addons change
if (catalogs.length === 0 && !catalogsLoading) {
console.log('[HomeScreen] Loading catalogs for first time');
loadCatalogsProgressively();
}
});
return unsubscribe;
}, [navigation, loadCatalogsProgressively, catalogs.length, catalogsLoading]);
}, [navigation, refreshContinueWatching, loadCatalogsProgressively, catalogs.length, catalogsLoading]);
// Memoize the loading screen to prevent unnecessary re-renders
const renderLoadingScreen = useMemo(() => {

View file

@ -1,5 +1,4 @@
import { logger } from '../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface CachedImage {
url: string;
@ -8,55 +7,10 @@ interface CachedImage {
expiresAt: number;
}
interface PersistentCacheEntry {
url: string;
timestamp: number;
expiresAt: number;
accessCount: number;
lastAccessed: number;
}
class ImageCacheService {
private cache = new Map<string, CachedImage>();
private persistentCache = new Map<string, PersistentCacheEntry>();
private readonly CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days (longer cache)
private readonly MAX_CACHE_SIZE = 200; // Increased cache size for better performance
private readonly PERSISTENT_CACHE_KEY = 'image_cache_persistent';
private isInitialized = false;
/**
* Initialize the persistent cache from storage
*/
public async initialize(): Promise<void> {
if (this.isInitialized) return;
try {
const stored = await AsyncStorage.getItem(this.PERSISTENT_CACHE_KEY);
if (stored) {
const entries = JSON.parse(stored) as PersistentCacheEntry[];
entries.forEach(entry => {
this.persistentCache.set(entry.url, entry);
});
logger.log(`[ImageCache] Loaded ${entries.length} persistent cache entries`);
}
this.isInitialized = true;
} catch (error) {
logger.error('[ImageCache] Failed to load persistent cache:', error);
this.isInitialized = true;
}
}
/**
* Save persistent cache to storage
*/
private async savePersistentCache(): Promise<void> {
try {
const entries = Array.from(this.persistentCache.values());
await AsyncStorage.setItem(this.PERSISTENT_CACHE_KEY, JSON.stringify(entries));
} catch (error) {
logger.error('[ImageCache] Failed to save persistent cache:', error);
}
}
private readonly CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
private readonly MAX_CACHE_SIZE = 100; // Maximum number of cached images
/**
* Get a cached image URL or cache the original if not present
@ -66,66 +20,27 @@ class ImageCacheService {
return originalUrl; // Don't cache placeholder images
}
await this.initialize();
// Check memory cache first (fastest)
// Check if we have a valid cached version
const cached = this.cache.get(originalUrl);
if (cached && cached.expiresAt > Date.now()) {
logger.log(`[ImageCache] Retrieved from memory cache: ${originalUrl}`);
logger.log(`[ImageCache] Retrieved from cache: ${originalUrl}`);
return cached.localPath;
}
// Check persistent cache
const persistent = this.persistentCache.get(originalUrl);
if (persistent && persistent.expiresAt > Date.now()) {
// Update access stats
persistent.accessCount++;
persistent.lastAccessed = Date.now();
// Add to memory cache for faster access
const cachedImage: CachedImage = {
url: originalUrl,
localPath: originalUrl,
timestamp: persistent.timestamp,
expiresAt: persistent.expiresAt,
};
this.cache.set(originalUrl, cachedImage);
logger.log(`[ImageCache] Retrieved from persistent cache: ${originalUrl}`);
return originalUrl;
}
try {
// Create new cache entry
const now = Date.now();
const expiresAt = now + this.CACHE_DURATION;
// For now, return the original URL but mark it as cached
// In a production app, you would implement actual local caching here
const cachedImage: CachedImage = {
url: originalUrl,
localPath: originalUrl,
timestamp: now,
expiresAt,
};
const persistentEntry: PersistentCacheEntry = {
url: originalUrl,
timestamp: now,
expiresAt,
accessCount: 1,
lastAccessed: now,
localPath: originalUrl, // In production, this would be a local file path
timestamp: Date.now(),
expiresAt: Date.now() + this.CACHE_DURATION,
};
this.cache.set(originalUrl, cachedImage);
this.persistentCache.set(originalUrl, persistentEntry);
this.enforceMaxCacheSize();
// Save persistent cache periodically (every 10 new entries)
if (this.persistentCache.size % 10 === 0) {
this.savePersistentCache();
}
logger.log(`[ImageCache] ✅ NEW CACHE ENTRY: ${originalUrl} (Memory: ${this.cache.size}, Persistent: ${this.persistentCache.size})`);
logger.log(`[ImageCache] ✅ NEW CACHE ENTRY: ${originalUrl} (Cache size: ${this.cache.size})`);
return cachedImage.localPath;
} catch (error) {
logger.error('[ImageCache] Failed to cache image:', error);
@ -136,82 +51,44 @@ class ImageCacheService {
/**
* Check if an image is cached
*/
public async isCached(url: string): Promise<boolean> {
await this.initialize();
public isCached(url: string): boolean {
const cached = this.cache.get(url);
if (cached && cached.expiresAt > Date.now()) {
return true;
}
const persistent = this.persistentCache.get(url);
return persistent !== undefined && persistent.expiresAt > Date.now();
return cached !== undefined && cached.expiresAt > Date.now();
}
/**
* Log cache status (for debugging)
*/
public async logCacheStatus(): Promise<void> {
await this.initialize();
public logCacheStatus(): void {
const stats = this.getCacheStats();
logger.log(`[ImageCache] 📊 Cache Status: ${stats.size} total, ${stats.expired} expired`);
const memoryStats = this.getCacheStats();
const persistentCount = this.persistentCache.size;
const persistentExpired = Array.from(this.persistentCache.values())
.filter(entry => entry.expiresAt <= Date.now()).length;
logger.log(`[ImageCache] 📊 Memory Cache: ${memoryStats.size} total, ${memoryStats.expired} expired`);
logger.log(`[ImageCache] 📊 Persistent Cache: ${persistentCount} total, ${persistentExpired} expired`);
// Log most accessed images
const topImages = Array.from(this.persistentCache.entries())
.sort(([, a], [, b]) => b.accessCount - a.accessCount)
.slice(0, 5);
topImages.forEach(([url, entry]) => {
logger.log(`[ImageCache] 🔥 Popular: ${url.substring(0, 60)}... (${entry.accessCount} accesses)`);
// Log first 5 cached URLs for debugging
const entries = Array.from(this.cache.entries()).slice(0, 5);
entries.forEach(([url, cached]) => {
const isExpired = cached.expiresAt <= Date.now();
const timeLeft = Math.max(0, cached.expiresAt - Date.now()) / 1000 / 60; // minutes
logger.log(`[ImageCache] - ${url.substring(0, 60)}... (${isExpired ? 'EXPIRED' : `${timeLeft.toFixed(1)}m left`})`);
});
}
/**
* Clear expired cache entries
*/
public async clearExpiredCache(): Promise<void> {
await this.initialize();
public clearExpiredCache(): void {
const now = Date.now();
let removedMemory = 0;
let removedPersistent = 0;
// Clear memory cache
for (const [url, cached] of this.cache.entries()) {
if (cached.expiresAt <= now) {
this.cache.delete(url);
removedMemory++;
}
}
// Clear persistent cache
for (const [url, entry] of this.persistentCache.entries()) {
if (entry.expiresAt <= now) {
this.persistentCache.delete(url);
removedPersistent++;
}
}
if (removedPersistent > 0) {
await this.savePersistentCache();
}
logger.log(`[ImageCache] Cleared ${removedMemory} memory entries, ${removedPersistent} persistent entries`);
}
/**
* Clear all cached images
*/
public async clearAllCache(): Promise<void> {
public clearAllCache(): void {
this.cache.clear();
this.persistentCache.clear();
await AsyncStorage.removeItem(this.PERSISTENT_CACHE_KEY);
logger.log('[ImageCache] Cleared all cached images');
}
@ -235,40 +112,26 @@ class ImageCacheService {
}
/**
* Enforce maximum cache size by removing oldest/least accessed entries
* Enforce maximum cache size by removing oldest entries
*/
private enforceMaxCacheSize(): void {
// Enforce memory cache limit
if (this.cache.size > this.MAX_CACHE_SIZE) {
const entries = Array.from(this.cache.entries()).sort(
(a, b) => a[1].timestamp - b[1].timestamp
);
const toRemove = this.cache.size - this.MAX_CACHE_SIZE;
for (let i = 0; i < toRemove; i++) {
this.cache.delete(entries[i][0]);
}
logger.log(`[ImageCache] Removed ${toRemove} old memory entries to enforce cache size limit`);
if (this.cache.size <= this.MAX_CACHE_SIZE) {
return;
}
// Enforce persistent cache limit (larger limit)
const persistentLimit = this.MAX_CACHE_SIZE * 3;
if (this.persistentCache.size > persistentLimit) {
// Remove least recently accessed entries
const entries = Array.from(this.persistentCache.entries()).sort(
(a, b) => a[1].lastAccessed - b[1].lastAccessed
);
const toRemove = this.persistentCache.size - persistentLimit;
for (let i = 0; i < toRemove; i++) {
this.persistentCache.delete(entries[i][0]);
}
// Convert to array and sort by timestamp (oldest first)
const entries = Array.from(this.cache.entries()).sort(
(a, b) => a[1].timestamp - b[1].timestamp
);
this.savePersistentCache();
logger.log(`[ImageCache] Removed ${toRemove} old persistent entries to enforce cache size limit`);
// Remove oldest entries
const toRemove = this.cache.size - this.MAX_CACHE_SIZE;
for (let i = 0; i < toRemove; i++) {
this.cache.delete(entries[i][0]);
}
logger.log(`[ImageCache] Removed ${toRemove} old entries to enforce cache size limit`);
}
}
export const imageCacheService = new ImageCacheService();
export const imageCacheService = new ImageCacheService();

View file

@ -6,8 +6,7 @@ const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
// Initialize cache as an empty object
let customNamesCache: { [key: string]: string } = {};
let cacheTimestamp: number = 0; // 0 indicates cache is invalid/empty initially
let isLoading: boolean = false; // Prevent multiple concurrent loads
const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes (longer cache for less frequent loads)
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
async function loadCustomNamesIfNeeded(): Promise<{ [key: string]: string }> {
const now = Date.now();
@ -16,13 +15,6 @@ async function loadCustomNamesIfNeeded(): Promise<{ [key: string]: string }> {
return customNamesCache; // Cache is valid and guaranteed to be an object
}
// Prevent multiple concurrent loads
if (isLoading) {
// Wait for current load to complete by returning existing cache or empty object
return customNamesCache || {};
}
isLoading = true;
try {
logger.info('Loading custom catalog names from storage...');
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
@ -36,8 +28,6 @@ async function loadCustomNamesIfNeeded(): Promise<{ [key: string]: string }> {
cacheTimestamp = 0;
// Return the last known cache (which might be empty {}), or a fresh empty object
return customNamesCache || {}; // Return cache (could be outdated but non-null) or empty {}
} finally {
isLoading = false;
}
}