From a7f7b03503f69df433c9b48cd5e5cab86ed96f1a Mon Sep 17 00:00:00 2001 From: Tboy123-emm Date: Fri, 29 May 2026 22:35:17 +0000 Subject: [PATCH] feat: warm critical caches during splash screen startup - Add warmCriticalCaches() that parallel-fetches courses + user profile - Replace fake 500ms delay in prepareApp with real cache warming - Failures are swallowed so warming never blocks startup - 6 unit tests covering all branches --- App.tsx | 5 +- src/__tests__/services/cacheWarming.test.ts | 63 +++++++++++++++++++++ src/services/cacheWarming.ts | 31 ++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/services/cacheWarming.test.ts create mode 100644 src/services/cacheWarming.ts diff --git a/App.tsx b/App.tsx index 70ae326..8ede2cc 100644 --- a/App.tsx +++ b/App.tsx @@ -28,6 +28,7 @@ import { handleCacheVersionUpdate } from './src/utils/cacheVersioning'; import { requireEnvVariables } from './src/utils/env'; import { appLogger } from './src/utils/logger'; import { handleNotificationReceived } from './src/utils/notificationHandlers'; +import { warmCriticalCaches } from './src/services/cacheWarming'; // Keep the splash screen visible while we fetch resources SplashScreen.preventAutoHideAsync(); @@ -78,8 +79,8 @@ const App = () => { // Zustand persist automatically hydrates, we can assume it's done or add a small delay // to ensure initial data fetching completes. - // 3. Initial data fetch (simulate or add real fetch) - await new Promise(resolve => setTimeout(resolve, 500)); + // 3. Warm critical caches (user profile + home feed) in parallel + await warmCriticalCaches(); } catch (e) { console.warn('Error during app initialization:', e); } finally { diff --git a/src/__tests__/services/cacheWarming.test.ts b/src/__tests__/services/cacheWarming.test.ts new file mode 100644 index 0000000..d354547 --- /dev/null +++ b/src/__tests__/services/cacheWarming.test.ts @@ -0,0 +1,63 @@ +import { warmCriticalCaches } from '../../services/cacheWarming'; +import { courseApi } from '../../services/api/courseApi'; +import { userApi } from '../../services/api/userApi'; +import { useAppStore } from '../../store'; + +jest.mock('../../services/api/courseApi', () => ({ + courseApi: { getCourses: jest.fn() }, +})); +jest.mock('../../services/api/userApi', () => ({ + userApi: { getUser: jest.fn() }, +})); +jest.mock('../../store', () => ({ + useAppStore: { getState: jest.fn() }, +})); + +const getCourses = courseApi.getCourses as jest.Mock; +const getUser = userApi.getUser as jest.Mock; +const getState = useAppStore.getState as jest.Mock; + +beforeEach(() => { + jest.clearAllMocks(); + getCourses.mockResolvedValue([]); + getUser.mockResolvedValue({ id: 'u1' }); +}); + +describe('warmCriticalCaches', () => { + it('always fetches the course list', async () => { + getState.mockReturnValue({ user: null }); + await warmCriticalCaches(); + expect(getCourses).toHaveBeenCalledTimes(1); + }); + + it('fetches user profile when userId is available', async () => { + getState.mockReturnValue({ user: { id: 'u1' } }); + await warmCriticalCaches(); + expect(getUser).toHaveBeenCalledWith('u1'); + }); + + it('skips user profile fetch when not authenticated', async () => { + getState.mockReturnValue({ user: null }); + await warmCriticalCaches(); + expect(getUser).not.toHaveBeenCalled(); + }); + + it('resolves even if course fetch fails', async () => { + getState.mockReturnValue({ user: null }); + getCourses.mockRejectedValue(new Error('network')); + await expect(warmCriticalCaches()).resolves.toBeUndefined(); + }); + + it('resolves even if user fetch fails', async () => { + getState.mockReturnValue({ user: { id: 'u1' } }); + getUser.mockRejectedValue(new Error('network')); + await expect(warmCriticalCaches()).resolves.toBeUndefined(); + }); + + it('fetches courses and user profile in parallel', async () => { + getState.mockReturnValue({ user: { id: 'u1' } }); + await warmCriticalCaches(); + expect(getCourses).toHaveBeenCalledTimes(1); + expect(getUser).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/services/cacheWarming.ts b/src/services/cacheWarming.ts new file mode 100644 index 0000000..77e1b9f --- /dev/null +++ b/src/services/cacheWarming.ts @@ -0,0 +1,31 @@ +import { useAppStore } from '../store'; +import { appLogger } from '../utils/logger'; +import { courseApi } from './api/courseApi'; +import { userApi } from './api/userApi'; + +/** + * Warm critical caches in parallel during the splash screen so home screen + * data is ready before the user sees it. + * + * - User profile (requires authenticated userId) + * - Home feed / course list (always fetched) + * + * Failures are swallowed — warming is best-effort and must never block startup. + */ +export async function warmCriticalCaches(): Promise { + const start = Date.now(); + + const userId = useAppStore.getState().user?.id; + + const tasks: Promise[] = [ + courseApi.getCourses().catch(() => null), + ]; + + if (userId) { + tasks.push(userApi.getUser(userId).catch(() => null)); + } + + await Promise.all(tasks); + + appLogger.infoSync(`[CacheWarming] Completed in ${Date.now() - start}ms`); +}