From 4b25e7f5feae43f80a06a9d34d3cdce79fb21cf6 Mon Sep 17 00:00:00 2001 From: lobo Date: Sun, 11 Oct 2026 11:05:18 +0200 Subject: [PATCH] perf(dashboard): load core data in parallel with /me The dashboard no longer waits for /me before it calls the library. History loads when games-count says canSync or fails. Refs #40. --- src/hooks/useDashboardData.js | 21 ++++++++++----------- src/pages/Dashboard.test.jsx | 30 +++++++++++++++++++++++++++--- src/pages/languageSwitch.test.jsx | 4 +++- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/hooks/useDashboardData.js b/src/hooks/useDashboardData.js index aaf4da5..bd93584 100644 --- a/src/hooks/useDashboardData.js +++ b/src/hooks/useDashboardData.js @@ -60,17 +60,16 @@ export default function useDashboardData() { useEffect(() => { const fetchUserData = async () => { - const {ok, status, data} = await reloadUser(); - if (ok) { - // Games added by hand need no Steam ID. The history and the achievements (Steam achievements, - // PlayStation trophies) also come from a PlayStation link, which games-count reports as canSync. - const [counts] = await Promise.all([fetchSteamCount(), fetchTopGames(), fetchPlaying(), fetchUpNext(), - fetchSummary(), ...(data.steamId ? [fetchActivity(), fetchAchievements()] : [])]); - if (!data.steamId && counts?.canSync) await Promise.all([fetchActivity(), fetchAchievements()]); - } else if (status === 0) { - setError('INFO.ERROR.NETWORK_FAILURE'); - } else if (status !== 401) { - setError('INFO.ERROR.LOAD_PROFILE'); + // Core calls do not wait for /me. canSync is true with a Steam ID or a PlayStation link; when games-count + // fails there is no answer, so the history is asked anyway. + const withHistory = fetchSteamCount().then(async (counts) => { + if (counts == null || counts.canSync) await Promise.all([fetchActivity(), fetchAchievements()]); + }); + const [{ok, status}] = await Promise.all([reloadUser(), withHistory, fetchTopGames(), fetchPlaying(), + fetchUpNext(), fetchSummary()]); + if (!ok) { + if (status === 0) setError('INFO.ERROR.NETWORK_FAILURE'); + else if (status !== 401) setError('INFO.ERROR.LOAD_PROFILE'); } setIsLoading(false); }; diff --git a/src/pages/Dashboard.test.jsx b/src/pages/Dashboard.test.jsx index 4ab9aa4..a880696 100644 --- a/src/pages/Dashboard.test.jsx +++ b/src/pages/Dashboard.test.jsx @@ -32,7 +32,7 @@ const dashboardRoutes = (settings) => ({ 'GET /api/library/steam-profile': json(200, {personaName: 'Lobo', avatarUrl: avatar}), 'GET /api/library/activity': json(200, {totalMinutes: 90, days: [{day: '2026-09-23', minutes: 0}, {day: '2026-09-24', minutes: 90}], recentGames: [ {appid: 1086940, name: "Baldur's Gate 3", playtimeMinutes: 900, playtime2Weeks: 90, status: 'PLAYING'}]}), - 'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true}), + 'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true, canSync: true}), 'GET /api/library/backlog': json(200, {games: [{appid: 7, name: 'Hades'}]}), 'GET /api/library/achievements/summary': json(200, {unlocked: 24, startedGames: 4, perfectGames: 2, averageCompletion: 71.3, pendingGames: 0, hiddenGames: 0, closest: []}), @@ -81,6 +81,22 @@ describe('Dashboard', () => { }); const order = (container) => [...container.querySelectorAll('[data-tile]')].map((el) => el.dataset.tile); + const asked = (path) => fetch.mock.calls.some(([url]) => url.includes(path)); + + it('asks for the library before /me answers', async () => { + let answerMe; + const meAnswer = new Promise((resolve) => { answerMe = resolve; }); + const routes = dashboardRoutes(); + const me = routes['GET /api/users/me']; + fetchRoutes({...routes, 'GET /api/users/me': () => meAnswer}); + const container = renderDashboard().container; + + await waitFor(() => expect(asked('/api/library/games')).toBe(true)); + expect(asked('/api/library/summary')).toBe(true); + + answerMe(me); + await waitFor(() => expect(container.querySelector(`img[src="${avatar}"]`)).not.toBeNull()); + }); it('shows today\'s tiles without a saved layout', async () => { const container = mockDashboard(); @@ -227,20 +243,28 @@ describe('Dashboard', () => { it('watches the sync the server started for this visit', async () => { fetchRoutes({ ...dashboardRoutes(), - 'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true, syncStarted: true}), + 'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true, canSync: true, syncStarted: true}), }); renderDashboard(); await waitFor(() => expect(watchSync).toHaveBeenCalledTimes(1)); }); + it('loads the activity and the achievements of a Steam user when games-count fails', async () => { + fetchRoutes({...dashboardRoutes(), 'GET /api/library/games-count': json(500, {})}); + renderDashboard(); + + expect(await screen.findByText(/DASHBOARD.HELLO/)).toBeTruthy(); + await waitFor(() => expect(asked('/api/library/activity')).toBe(true)); + await waitFor(() => expect(asked('/api/library/achievements/summary')).toBe(true)); + }); + describe('of a user with a PlayStation link and no Steam ID', () => { const psnOnlyRoutes = (canSync) => ({ ...dashboardRoutes(), 'GET /api/users/me': json(200, {email: 'a@b.de', displayName: 'Lobo', settings: {}}), 'GET /api/library/games-count': json(200, {count: 1, hasSteamId: false, canSync}), }); - const asked = (path) => fetch.mock.calls.some(([url]) => url.includes(path)); it('loads the activity, the achievements and the sync result, but no Steam profile', async () => { fetchRoutes(psnOnlyRoutes(true)); diff --git a/src/pages/languageSwitch.test.jsx b/src/pages/languageSwitch.test.jsx index fd51627..23d23a9 100644 --- a/src/pages/languageSwitch.test.jsx +++ b/src/pages/languageSwitch.test.jsx @@ -17,7 +17,9 @@ vi.mock('react-i18next', () => import('../test/i18n-mock.js')); // The last column: what the page loads besides GET /api/users/me of the provider. const pages = [ - ['Dashboard', '/', '/', Dashboard, 'INFO.ERROR.LOAD_PROFILE', []], + ['Dashboard', '/', '/', Dashboard, 'INFO.ERROR.LOAD_PROFILE', + ['/api/library/games', '/api/library/games-count', '/api/library/backlog', '/api/library/summary', + '/api/library/activity', '/api/library/achievements/summary']], ['GameDetail', '/games/:appid', '/games/220', GameDetail, 'GAME_DETAIL.ERROR_LOAD', ['/api/library/games/220']], ['GameLibrary', '/library', '/library', GameLibrary, 'LIBRARY.ERROR_LOAD', ['/api/library/games', '/api/library/games-count', '/api/library/sync-status']],