Reduce userinfo and OpenID discovery API calls#2397
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
Preview build of published Zudoku package for commit bbda00f. See the deployment at: https://25957cf1.cosmocargo-public-package.pages.dev Note This is a preview of the Cosmo Cargo example using the Zudoku package published to a local registry to ensure it'll be working when published to the public NPM registry. Last updated: 2026-05-05T13:09:19.124Z |
There was a problem hiding this comment.
Pull request overview
Optimizes authentication-related data fetching by tuning React Query caching for user profile refreshes and adjusting related auth refresh behavior.
Changes:
- Added
staleTime(5 min) andgcTime(10 min) to theuseRefreshUserProfilequery to reduce unnecessary refetches and control cache lifetime. - Updated
useVerifiedEmailto userefetchOnWindowFocus: trueinstead of"always"to respect staleness. - Memoized OpenID discovery (
getAuthServer) by caching the in-flight Promise.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/zudoku/src/lib/authentication/providers/openid.tsx | Memoizes OIDC discovery/auth server resolution via a cached Promise. |
| packages/zudoku/src/lib/authentication/hook.ts | Introduces explicit React Query caching timings and aligns window-focus refetch behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
useVerifiedEmail forced refetchOnWindowFocus: "always" on the refresh-user-profile query, which bypasses staleTime and refetched userinfo on every focus event from any component using useAuth. Switch to refetchOnWindowFocus: true so it respects staleTime, and set explicit staleTime / gcTime on the query so refetches don't fire on every component mount or in environments with their own QueryClient. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT
getAuthServer cached the resolved AuthorizationServer, but the check- then-await-then-assign pattern raced: any caller arriving between the discoveryRequest await and the assignment also saw the field as undefined and fired its own request. On a cold load, onPageLoad and the first userinfo refresh run concurrently and both hit the discovery endpoint. Cache the in-flight promise instead so concurrent callers share a single discovery request. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT
The previous dedup change cached the in-flight promise on authorizationServer, but a rejection (e.g. transient network error) would stick around forever — every subsequent caller would await the same rejected promise and never retry. Clear the field on failure so the next caller starts a fresh request. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT
Cover the two cache invariants: - A failed discovery request must not poison the cache; the next caller should retry. - Concurrent callers should share a single in-flight discovery request. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT
6901681 to
831e1d5
Compare
The previous staleTime/gcTime change only deduped fetches within a single React Query cache lifetime. Every fresh page load instantiates a new QueryClient with no cache, so userinfo was still fetched on every reload — even though the persisted Zustand profile already held the latest data. Add profileFetchedAt to the persisted auth state and pass it as initialDataUpdatedAt to the user profile query. React Query then evaluates staleTime against that timestamp on mount, so a reload within staleTime skips the fetch entirely. The auth providers that populate profile (openid, clerk, firebase, plus setLoggedIn for firebase/supabase/azureb2c) all stamp the timestamp; logout clears it alongside the profile. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT
9ca641d to
87dd321
Compare
Reading profile and profileFetchedAt via useAuthState selectors meant the query observed whatever state Zustand had hydrated by the time the hook ran. If anything delayed hydration (SSR boundary, concurrent renders, etc.) the first observer saw profile=null on mount and React Query started fetching userinfo before the store caught up. Read the persisted state directly from localStorage in the initialData/initialDataUpdatedAt callbacks. localStorage is sync, so this returns the latest persisted values regardless of Zustand's hydration timing. Zustand remains the runtime source of truth — the queryFn still writes profileFetchedAt through setState, which the persist middleware syncs back to localStorage for the next reload. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT
Once the staleTime/initialDataUpdatedAt caching kicked in, a user waiting on email verification could reload or refocus the tab and keep seeing the stale "unverified" state for up to 5 minutes — the exact scenario where they need fresh data. When useVerifiedEmail observes profile.emailVerified === false, override refetchOnMount and refetchOnWindowFocus to "always", which ignores staleTime. Once verification succeeds the flag flips and the hook reverts to the normal cached behavior, so it doesn't add any ongoing fetch traffic for verified users. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT
Zustand 5 with createJSONStorage(() => localStorage) hydrates synchronously at module load, so useAuthState selectors return the persisted profile and profileFetchedAt on the very first render. Reading the JSON directly from localStorage was defensive and not actually load-bearing — go back to selectors and remove the helper plus the storage-key export. If reload-time fetches reappear, that points to a real Zustand hydration issue worth investigating directly rather than papering over. https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT
Summary
We were hitting rate limits on the userinfo endpoint and the OpenID configuration discovery endpoint because both were being fetched far more often than necessary. This PR addresses two distinct sources of over-fetching.
Userinfo: caching tweaks for
useRefreshUserProfilestaleTime: 1000 * 60 * 5andgcTime: 1000 * 60 * 10so component remounts and unrelated re-renders don't refetch userinfo.useVerifiedEmailfromrefetchOnWindowFocus: "always"torefetchOnWindowFocus: true. The"always"value bypasses staleTime entirely; withtrue, focus events only refetch when the query is actually stale. The explicitrefreshaction on the hook still forces an immediate refetch when the caller wants one.OpenID discovery: dedup concurrent calls in
getAuthServerOn a cold load,
onPageLoadand the firstuseRefreshUserProfilequery run in parallel. The previousgetAuthServerimplementation had a check-then-await-then-assign race: any caller arriving between thediscoveryRequestawait and the assignment also sawthis.authorizationServeras undefined and fired its own request.getAuthServernow caches the in-flightPromise<AuthorizationServer>, so concurrent callers share a single discovery request.https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT