perf(dashboard): load core data in parallel with /me
Build, Test & Push Frontend / quality-check (push) Successful in 36s
Build, Test & Push Frontend / docker (push) Successful in 22s

The dashboard no longer waits for /me before it calls the library.
History loads when games-count says canSync or fails. Refs #40.
This commit is contained in:
lobo committed 2026-10-11 11:05:18 +02:00
1 parent 84b16a2ca8
commit 4b25e7f5fe
3 files changed
+40 -15

No files matched your search

+10 -11
View File
@@ -60,17 +60,16 @@ export default function useDashboardData() {
useEffect(() => { useEffect(() => {
const fetchUserData = async () => { const fetchUserData = async () => {
const {ok, status, data} = await reloadUser(); // Core calls do not wait for /me. canSync is true with a Steam ID or a PlayStation link; when games-count
if (ok) { // fails there is no answer, so the history is asked anyway.
// Games added by hand need no Steam ID. The history and the achievements (Steam achievements, const withHistory = fetchSteamCount().then(async (counts) => {
// PlayStation trophies) also come from a PlayStation link, which games-count reports as canSync. if (counts == null || counts.canSync) await Promise.all([fetchActivity(), fetchAchievements()]);
const [counts] = await Promise.all([fetchSteamCount(), fetchTopGames(), fetchPlaying(), fetchUpNext(), });
fetchSummary(), ...(data.steamId ? [fetchActivity(), fetchAchievements()] : [])]); const [{ok, status}] = await Promise.all([reloadUser(), withHistory, fetchTopGames(), fetchPlaying(),
if (!data.steamId && counts?.canSync) await Promise.all([fetchActivity(), fetchAchievements()]); fetchUpNext(), fetchSummary()]);
} else if (status === 0) { if (!ok) {
setError('INFO.ERROR.NETWORK_FAILURE'); if (status === 0) setError('INFO.ERROR.NETWORK_FAILURE');
} else if (status !== 401) { else if (status !== 401) setError('INFO.ERROR.LOAD_PROFILE');
setError('INFO.ERROR.LOAD_PROFILE');
} }
setIsLoading(false); setIsLoading(false);
}; };
+27 -3
View File
@@ -32,7 +32,7 @@ const dashboardRoutes = (settings) => ({
'GET /api/library/steam-profile': json(200, {personaName: 'Lobo', avatarUrl: avatar}), '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: [ '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'}]}), {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/backlog': json(200, {games: [{appid: 7, name: 'Hades'}]}),
'GET /api/library/achievements/summary': json(200, {unlocked: 24, startedGames: 4, perfectGames: 2, 'GET /api/library/achievements/summary': json(200, {unlocked: 24, startedGames: 4, perfectGames: 2,
averageCompletion: 71.3, pendingGames: 0, hiddenGames: 0, closest: []}), 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 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 () => { it('shows today\'s tiles without a saved layout', async () => {
const container = mockDashboard(); const container = mockDashboard();
@@ -227,20 +243,28 @@ describe('Dashboard', () => {
it('watches the sync the server started for this visit', async () => { it('watches the sync the server started for this visit', async () => {
fetchRoutes({ fetchRoutes({
...dashboardRoutes(), ...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(); renderDashboard();
await waitFor(() => expect(watchSync).toHaveBeenCalledTimes(1)); 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', () => { describe('of a user with a PlayStation link and no Steam ID', () => {
const psnOnlyRoutes = (canSync) => ({ const psnOnlyRoutes = (canSync) => ({
...dashboardRoutes(), ...dashboardRoutes(),
'GET /api/users/me': json(200, {email: 'a@b.de', displayName: 'Lobo', settings: {}}), '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}), '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 () => { it('loads the activity, the achievements and the sync result, but no Steam profile', async () => {
fetchRoutes(psnOnlyRoutes(true)); fetchRoutes(psnOnlyRoutes(true));
+3 -1
View File
@@ -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. // The last column: what the page loads besides GET /api/users/me of the provider.
const pages = [ 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']], ['GameDetail', '/games/:appid', '/games/220', GameDetail, 'GAME_DETAIL.ERROR_LOAD', ['/api/library/games/220']],
['GameLibrary', '/library', '/library', GameLibrary, 'LIBRARY.ERROR_LOAD', ['GameLibrary', '/library', '/library', GameLibrary, 'LIBRARY.ERROR_LOAD',
['/api/library/games', '/api/library/games-count', '/api/library/sync-status']], ['/api/library/games', '/api/library/games-count', '/api/library/sync-status']],