Skip to content

prevent app refetch on token silent renew#3898

Open
ghazwarhili wants to merge 18 commits into
mainfrom
prevent-refetch-on-silent-renew
Open

prevent app refetch on token silent renew#3898
ghazwarhili wants to merge 18 commits into
mainfrom
prevent-refetch-on-silent-renew

Conversation

@ghazwarhili
Copy link
Copy Markdown
Contributor

PR Summary

Each oidc silent renew dispatched a user action carrying a fresh User instance (new access_token, id_token, expires_at) while the identity (profile.sub) was unchanged.

Two issues cascaded from this:

  1. The reducer replaced state.user unconditionally, changing its reference on every renew. All components selecting state.user re-rendered, and effects depending on user re-ran, triggering redundant fetches (network visualization parameters, spreadsheet config, optional services, etc.).

  2. The WebSocket URLs generated by useNotificationsUrlGenerator embedded the token as a query parameter, so token rotation changed the URL objects. NotificationsProvider then tore down and recreated all WebSockets, and each new onopen broadcast caused listeners (useNodeAliases, network elements, etc.) to resync their data.

Changes:

  1. Split state.user: state.user keeps a stable reference across silent renews (only changes when profile.sub changes), and state.userToken holds the rotating tokens used by getUserToken and the WS layer.

  2. useNotificationsUrlGenerator now emits token-free URLs based on an isAuthenticated boolean, so the urls object stays stable when the token rotates.

  3. The fresh token is appended at WebSocket connection/reconnection time via getUserToken(), preserving reconnection behavior for real disconnects.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cedeb1f9-ccec-4ab7-a9c2-67e39454a9f7

📥 Commits

Reviewing files that changed from the base of the PR and between a6c3734 and 0812528.

📒 Files selected for processing (1)
  • src/services/utils.ts
💤 Files with no reviewable changes (1)
  • src/services/utils.ts

📝 Walkthrough

Walkthrough

Removes the ad-hoc user-store singleton, shifts token usage to explicit parameters, updates websocket URL generation to stop depending on a global token, and changes components to consume userProfile (from state.user?.profile) instead of the full user object. Minor import path tweak for App.

Changes

Single cohesive refactor

Layer / File(s) Summary
Data shape / API
src/redux/user-store.ts
Deleted the user-store module and its exports (store setter and getUserToken) — removes the global token getter and singleton closure.
Store initialization
src/redux/store.ts
Removed import/use of setUserStore(store) so store is no longer published via the removed singleton.
Core utility
src/services/utils.ts
getUrlWithToken signature changed to getUrlWithToken(baseUrl, tokenId) and no longer reads token from global state; uses provided tokenId to build URLs.
Hook / URL generation
src/hooks/use-notifications-url-generator.ts
Notification websocket URLs for CONFIG, GLOBAL_CONFIG, and DIRECTORY are built directly from wsBase + PREFIX_* constants (token no longer gates availability). STUDY and DIRECTORY_DELETE_STUDY remain conditional on studyUuid only. useMemo deps updated to drop tokenId.
Component state & props
src/components/app.jsx, src/components/app-top-bar.jsx
Selectors changed to read state.user?.profile (aliased userProfile) via shallowEqual. Props renamed from user to userProfile, effects and render guards now depend on userProfile instead of full user.
Module resolution
src/components/app-wrapper.jsx
Import for App changed from ./app to ./app.jsx (explicit file extension).

Suggested reviewers

  • sBouzols
  • SlimaneAmar
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main problem being solved: preventing unnecessary app refetches during OIDC silent token renewals, which is the core issue addressed across all file changes.
Description check ✅ Passed The description comprehensively explains the root causes (silent renew changing User reference, token-embedded URLs causing WebSocket recreation) and the three-part solution implemented across the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/redux/user-store.ts (1)

28-35: Reuse the exported UserToken type instead of redeclaring it inline.

The userToken shape here duplicates the UserToken type now exported from src/redux/reducer.type.ts. Duplicated structural types drift over time (e.g., if refresh_token or token_type is added later). Importing the canonical type keeps both in lockstep.

♻️ Proposed refactor
-import { User } from 'oidc-client';
+import { User } from 'oidc-client';
+import type { UserToken } from './reducer.type';

 type UserStoreState = {
     user: User | null;
-    userToken: {
-        access_token: string;
-        id_token: string;
-        expires_at: number;
-    } | null;
+    userToken: UserToken | null;
 };

No circular dependency risk: reducer.type.ts has no imports from redux files (store, reducer, user-store), so using it here is safe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/redux/user-store.ts` around lines 28 - 35, Replace the inline userToken
shape in the UserStoreState type with the canonical exported UserToken type:
import UserToken from the reducer.type file and change the userToken property
type from the inline object to UserToken | null (keeping user: User | null
unchanged); update any references to ensure they use UserStoreState and the
imported UserToken so the single source of truth is used instead of the
duplicated inline declaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hooks/use-notifications-url-generator.ts`:
- Around line 27-58: The current useMemo in use-notifications-url-generator
embeds a token via getUrlWithToken() but does not depend on the token, leading
to stale tokens on silent refresh; either stop baking the token into the
memorized URLs or include the live token in the memo deps. Fix by changing the
memo to generate token-less WS URLs (use wsBase + PREFIX_* paths and query
params but omit access_token) so the external
NotificationsProvider/useNotificationsListener can call getUserToken() at
connect time, or alternatively add the current access token (or getUserToken()
result / accessToken selector) to the dependency array of the useMemo so
getUrlWithToken(...) is re-run on token refresh; update references in
useNotificationsUrlGenerator (useMemo, getUrlWithToken, NotificationsUrlKeys,
studyUuid) and confirm provider behavior in useNotificationsListener before
choosing the token-less approach.

In `@src/redux/user-store.ts`:
- Around line 48-49: getUserToken currently returns userToken?.id_token but
callers (e.g., getUrlWithToken) expect the OAuth access token; update
getUserToken() to return userStore?.getState().userToken?.access_token ??
undefined so it returns the access token from the UserToken shape (preserving
the undefined fallback) and ensure any references to getUserToken() continue to
treat the value as an access token.

---

Nitpick comments:
In `@src/redux/user-store.ts`:
- Around line 28-35: Replace the inline userToken shape in the UserStoreState
type with the canonical exported UserToken type: import UserToken from the
reducer.type file and change the userToken property type from the inline object
to UserToken | null (keeping user: User | null unchanged); update any references
to ensure they use UserStoreState and the imported UserToken so the single
source of truth is used instead of the duplicated inline declaration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 596b1866-e2d9-4593-9f97-a9ae4ca82d6e

📥 Commits

Reviewing files that changed from the base of the PR and between 2e25058 and a4c7c62.

📒 Files selected for processing (4)
  • src/hooks/use-notifications-url-generator.ts
  • src/redux/reducer.ts
  • src/redux/reducer.type.ts
  • src/redux/user-store.ts

Comment thread src/hooks/use-notifications-url-generator.ts
Comment thread src/redux/user-store.ts Outdated
Copy link
Copy Markdown
Contributor

@flomillot flomillot left a comment

Choose a reason for hiding this comment

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

I would rather keep the previous code of useNotificationsUrlGenerator and improving it a bit, by injecting tokenId as prop to getUrlWithToken instead of fetching it again as it's alredy present in the component

Comment thread src/services/utils.ts
Comment thread src/redux/reducer.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/redux/reducer.ts (1)

1022-1028: Logic is correct; consider reordering the guard for readability.

All paths check out:

  • Initial login (state.user === null): falls through → state.user assigned.
  • Silent renew (same profile.sub): early return, but state.tokenId was already refreshed on line 1023 so the WebSocket layer still picks up the new token via getUserToken/selector.
  • Logout (action.user == null, state.user set): sub comparison is '...' === undefined → false → falls through → state.user = null, tokenId = undefined.
  • User switch (different sub): falls through → state.user replaced.

Minor readability nit — putting the null check first reads more naturally and short-circuits the optional chaining:

♻️ Optional readability tweak
     builder.addCase(USER, (state, action: UserAction) => {
         state.tokenId = action.user?.id_token ?? undefined;
-        if (state.user?.profile?.sub === action.user?.profile?.sub && state.user !== null) {
+        if (state.user !== null && state.user.profile?.sub === action.user?.profile?.sub) {
             return;
         }
         state.user = action.user;
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/redux/reducer.ts` around lines 1022 - 1028, Reorder the early-return
guard in the USER reducer for readability: first check that state.user is not
null, then compare profile.sub to the incoming action.user profile.sub before
returning; keep the token update (state.tokenId = action.user?.id_token ??
undefined) as-is and then set state.user = action.user when the guard doesn't
short-circuit. Reference: the USER case that takes (state, action: UserAction),
state.user, state.tokenId, and action.user.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/redux/reducer.ts`:
- Around line 1022-1028: Reorder the early-return guard in the USER reducer for
readability: first check that state.user is not null, then compare profile.sub
to the incoming action.user profile.sub before returning; keep the token update
(state.tokenId = action.user?.id_token ?? undefined) as-is and then set
state.user = action.user when the guard doesn't short-circuit. Reference: the
USER case that takes (state, action: UserAction), state.user, state.tokenId, and
action.user.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07fc4c29-26eb-4bd8-b14d-acaff775b89f

📥 Commits

Reviewing files that changed from the base of the PR and between a4c7c62 and 3c72796.

📒 Files selected for processing (7)
  • src/components/study-container.jsx
  • src/hooks/use-notifications-url-generator.ts
  • src/redux/reducer.ts
  • src/redux/reducer.type.ts
  • src/redux/store.ts
  • src/redux/user-store.ts
  • src/services/utils.ts
💤 Files with no reviewable changes (2)
  • src/redux/store.ts
  • src/redux/user-store.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/redux/reducer.type.ts
  • src/hooks/use-notifications-url-generator.ts

@ghazwarhili ghazwarhili requested a review from flomillot April 22, 2026 14:23
Comment thread src/services/utils.ts Outdated
@ghazwarhili ghazwarhili requested a review from flomillot May 6, 2026 11:40
Comment thread src/components/app.jsx
}, []);

const user = useSelector((state) => state.user);
const userProfile = useSelector((state) => state.user?.profile ?? null, shallowEqual);
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.

@coderabbitai is shallowEqual the appropriate equal method here ?

@sonarqubecloud
Copy link
Copy Markdown

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.

2 participants