implement bounded concurrency for catalog loading and add loading screen timeout

This commit is contained in:
tapframe 2026-02-20 21:59:25 +05:30
parent 29e5dee001
commit a05a16f67b

View file

@ -74,6 +74,8 @@ import { useScrollToTop } from '../contexts/ScrollToTopContext';
// Constants // Constants
const CATALOG_SETTINGS_KEY = 'catalog_settings'; const CATALOG_SETTINGS_KEY = 'catalog_settings';
const MAX_CONCURRENT_CATALOG_REQUESTS = 4;
const HOME_LOADING_SCREEN_TIMEOUT_MS = 5000;
// In-memory cache for catalog settings to avoid repeated MMKV reads // In-memory cache for catalog settings to avoid repeated MMKV reads
let cachedCatalogSettings: Record<string, boolean> | null = null; let cachedCatalogSettings: Record<string, boolean> | null = null;
@ -134,6 +136,7 @@ const HomeScreen = () => {
const [loadedCatalogCount, setLoadedCatalogCount] = useState(0); const [loadedCatalogCount, setLoadedCatalogCount] = useState(0);
const [hasAddons, setHasAddons] = useState<boolean | null>(null); const [hasAddons, setHasAddons] = useState<boolean | null>(null);
const [hintVisible, setHintVisible] = useState(false); const [hintVisible, setHintVisible] = useState(false);
const [loadingScreenTimedOut, setLoadingScreenTimedOut] = useState(false);
const totalCatalogsRef = useRef(0); const totalCatalogsRef = useRef(0);
const [visibleCatalogCount, setVisibleCatalogCount] = useState(5); // Reduced for memory const [visibleCatalogCount, setVisibleCatalogCount] = useState(5); // Reduced for memory
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@ -185,6 +188,7 @@ const HomeScreen = () => {
if (isFetchingRef.current) return; if (isFetchingRef.current) return;
isFetchingRef.current = true; isFetchingRef.current = true;
setLoadingScreenTimedOut(false);
setCatalogsLoading(true); setCatalogsLoading(true);
setCatalogs([]); setCatalogs([]);
setLoadedCatalogCount(0); setLoadedCatalogCount(0);
@ -210,6 +214,7 @@ const HomeScreen = () => {
catalogService.getAllAddons(), catalogService.getAllAddons(),
stremioService.getInstalledAddonsAsync() stremioService.getInstalledAddonsAsync()
]); ]);
const manifestByAddonId = new Map(addonManifests.map((manifest: any) => [manifest.id, manifest]));
// Set hasAddons state based on whether we have any addons - ensure on main thread // Set hasAddons state based on whether we have any addons - ensure on main thread
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
@ -220,14 +225,20 @@ const HomeScreen = () => {
let catalogIndex = 0; let catalogIndex = 0;
const catalogQueue: (() => Promise<void>)[] = []; const catalogQueue: (() => Promise<void>)[] = [];
// Launch all catalog loaders in parallel // Launch loaders with bounded concurrency to reduce startup pressure
const launchAllCatalogs = () => { const launchCatalogLoaders = () => {
while (catalogQueue.length > 0) { const workerCount = Math.min(MAX_CONCURRENT_CATALOG_REQUESTS, catalogQueue.length);
const catalogLoader = catalogQueue.shift(); const workers = Array.from({ length: workerCount }, async () => {
if (catalogLoader) { while (catalogQueue.length > 0) {
catalogLoader(); const catalogLoader = catalogQueue.shift();
if (!catalogLoader) return;
await catalogLoader();
} }
} });
void Promise.all(workers).catch((error) => {
if (__DEV__) console.warn('[HomeScreen] Catalog loader worker failed:', error);
});
}; };
for (const addon of addons) { for (const addon of addons) {
@ -243,7 +254,7 @@ const HomeScreen = () => {
const catalogLoader = async () => { const catalogLoader = async () => {
try { try {
const manifest = addonManifests.find((a: any) => a.id === addon.id); const manifest = manifestByAddonId.get(addon.id);
if (!manifest) return; if (!manifest) return;
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1); const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
@ -345,8 +356,8 @@ const HomeScreen = () => {
setCatalogs(new Array(catalogIndex).fill(null)); setCatalogs(new Array(catalogIndex).fill(null));
}); });
// Start all catalog requests in parallel // Start catalog requests with bounded concurrency
launchAllCatalogs(); launchCatalogLoaders();
} catch (error) { } catch (error) {
if (__DEV__) console.error('[HomeScreen] Error in progressive catalog loading:', error); if (__DEV__) console.error('[HomeScreen] Error in progressive catalog loading:', error);
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
@ -356,14 +367,29 @@ const HomeScreen = () => {
} }
}, []); }, []);
// Hard cap for initial home loading spinner.
// Keeps Home responsive even if one or more catalog addons are slow.
useEffect(() => {
if (!(catalogsLoading && loadedCatalogCount === 0)) {
return;
}
const timer = setTimeout(() => {
setLoadingScreenTimedOut(true);
}, HOME_LOADING_SCREEN_TIMEOUT_MS);
return () => clearTimeout(timer);
}, [catalogsLoading, loadedCatalogCount]);
// Only count feature section as loading if it's enabled in settings // Only count feature section as loading if it's enabled in settings
// For catalogs, we show them progressively, so loading should be false as soon as we have any content // For catalogs, we show them progressively, so loading should be false as soon as we have any content
const isLoading = useMemo(() => { const isLoading = useMemo(() => {
if (loadingScreenTimedOut) return false;
// Exit loading as soon as at least one catalog is ready, regardless of featured // Exit loading as soon as at least one catalog is ready, regardless of featured
if (loadedCatalogCount > 0) return false; if (loadedCatalogCount > 0) return false;
const heroLoading = showHeroSection ? featuredLoading : false; const heroLoading = showHeroSection ? featuredLoading : false;
return heroLoading && (catalogsLoading && loadedCatalogCount === 0); return heroLoading && (catalogsLoading && loadedCatalogCount === 0);
}, [showHeroSection, featuredLoading, catalogsLoading, loadedCatalogCount]); }, [loadingScreenTimedOut, showHeroSection, featuredLoading, catalogsLoading, loadedCatalogCount]);
// Update global loading state // Update global loading state
useEffect(() => { useEffect(() => {
@ -1482,4 +1508,3 @@ const HomeScreenWithFocusSync = (props: any) => {
}; };
export default React.memo(HomeScreenWithFocusSync); export default React.memo(HomeScreenWithFocusSync);