Skip to content

Reduce userinfo and OpenID discovery API calls#2397

Open
mosch wants to merge 8 commits into
mainfrom
claude/reduce-api-calls-TxWsR
Open

Reduce userinfo and OpenID discovery API calls#2397
mosch wants to merge 8 commits into
mainfrom
claude/reduce-api-calls-TxWsR

Conversation

@mosch

@mosch mosch commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

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 useRefreshUserProfile

  • Added staleTime: 1000 * 60 * 5 and gcTime: 1000 * 60 * 10 so component remounts and unrelated re-renders don't refetch userinfo.
  • Changed useVerifiedEmail from refetchOnWindowFocus: "always" to refetchOnWindowFocus: true. The "always" value bypasses staleTime entirely; with true, focus events only refetch when the query is actually stale. The explicit refresh action on the hook still forces an immediate refetch when the caller wants one.

OpenID discovery: dedup concurrent calls in getAuthServer

On a cold load, onPageLoad and the first useRefreshUserProfile query run in parallel. The previous getAuthServer implementation had a check-then-await-then-assign race: any caller arriving between the discoveryRequest await and the assignment also saw this.authorizationServer as undefined and fired its own request.

  • getAuthServer now caches the in-flight Promise<AuthorizationServer>, so concurrent callers share a single discovery request.
  • On rejection, the cached promise is cleared so the next caller retries (rather than awaiting a permanently-rejected promise).
  • Added regression tests for both invariants.

https://claude.ai/code/session_01TEE3iuxgJLvoHXgrmrP4jT

@vercel

vercel Bot commented Apr 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zudoku-cosmo-cargo Ready Ready Preview, Comment May 5, 2026 1:06pm
zudoku-dev Ready Ready Preview, Comment May 5, 2026 1:06pm

Request Review

@github-actions

github-actions Bot commented Apr 27, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 63.22%
⬆️ +0.05%
3290 / 5204
🔵 Statements 62.12%
⬆️ +0.05%
3477 / 5597
🔵 Functions 52.97%
⬆️ +0.13%
784 / 1480
🔵 Branches 50.82%
⬇️ -0.02%
2249 / 4425
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/zudoku/src/lib/authentication/hook.ts 33.33%
⬆️ +2.57%
18.18%
⬆️ +6.18%
33.33%
⬆️ +13.33%
33.33%
⬆️ +2.57%
33-35, 46-81, 99-150
packages/zudoku/src/lib/authentication/state.ts 73.91%
⬆️ +7.25%
66.66%
⬆️ +16.66%
60%
⬆️ +4.45%
83.33%
⬆️ +5.56%
49-55, 65-71, 92, 99
packages/zudoku/src/lib/authentication/providers/openid.tsx 67.77%
⬆️ +0.54%
57.83%
⬇️ -0.99%
57.89%
⬆️ +2.34%
68.15%
⬆️ +0.54%
383-481, 127, 188-192, 263, 315, 332, 346-347, 354-355, 371-372, 384-480, 490, 498, 561-568, 591
Generated in workflow #5259 for commit fc3a8fa by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Apr 27, 2026

Copy link
Copy Markdown

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and gcTime (10 min) to the useRefreshUserProfile query to reduce unnecessary refetches and control cache lifetime.
  • Updated useVerifiedEmail to use refetchOnWindowFocus: true instead 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.

Comment thread packages/zudoku/src/lib/authentication/providers/openid.tsx
Comment thread packages/zudoku/src/lib/authentication/providers/openid.tsx
claude added 4 commits May 4, 2026 12:25
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
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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants