perf(frontend): parallelize auth bootstrap fetches (#826)

* perf(frontend): parallelize auth bootstrap fetches

AuthContext.checkAuth previously chained three sequential fetches:
/api/auth/status, then /api/auth/check, then /api/permissions/me.
The status check has to come first because its response decides
whether to early-return on the setup or mfa-pending path. The next
two are independent for an authenticated session and were costing
an extra round-trip on every cold load.

Run /api/auth/check and /api/permissions/me in parallel via
Promise.all. The permissions request is wasted on the rare
not-authenticated path (cookie expired or logged out) but that
trade-off is worth saving the round-trip on the common success
path. The .catch(() => null) on the permissions fetch and the
inner try/catch around .json() preserve the original fault
tolerance: a network failure or malformed body falls back to no
permissions data, with the global role still authoritative.

* fix(frontend): commit auth state without waiting on /permissions/me

The previous Promise.all awaited both fetches before calling
setAppStatus('authenticated'), so a slow /api/permissions/me would
delay the dashboard commit relative to the old serial code. The E2E
deploy-feedback tests at e2e/deploy-log-panel.spec.ts:168 and :236
race the dashboard render after page.reload() and were timing out
waiting for GET /api/stacks/<name> because the click target was not
yet wired when the slower permissions response held up state.

Keep both requests in parallel on the wire, but await only the auth
check before committing state. The permissions promise resolves in
the background and updates state via void permsPromise.then() so the
non-critical request never gates the bootstrap.
This commit is contained in:
Anso
2026-04-28 09:02:08 -04:00
committed by GitHub
parent e74b4db44d
commit 405f9cd921
+17 -12
View File
@@ -71,25 +71,30 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return;
}
// Then check if already authenticated
const authResponse = await fetch('/api/auth/check', {
credentials: 'include',
});
// Auth check and permissions fetch are independent for an authenticated
// session, so fire both on the wire at the same time. Await only the
// auth check before committing app state — otherwise a slow
// /permissions/me delays setAppStatus('authenticated') and races
// post-reload UI that expects the dashboard to commit promptly. The
// permissions promise updates state in the background when it resolves.
const authPromise = fetch('/api/auth/check', { credentials: 'include' });
const permsPromise = fetch('/api/permissions/me', { credentials: 'include' }).catch(() => null);
const authResponse = await authPromise;
if (authResponse.ok) {
const data = await authResponse.json();
setUser(data.user ?? null);
setAppStatus('authenticated');
// Fetch effective permissions
try {
const permsRes = await fetch('/api/permissions/me', { credentials: 'include' });
if (permsRes.ok) {
setPermissions(await permsRes.json());
void permsPromise.then(async (res) => {
if (res?.ok) {
try {
setPermissions(await res.json());
} catch {
// Permissions fetch is non-critical — fallback to global role only
}
}
} catch {
// Permissions fetch is non-critical — fallback to global role only
}
});
} else {
setUser(null);
setPermissions(null);