Skip to content

Fix duplicate /users/me requests during console sign-in - #40

Open
PasinduYeshan wants to merge 1 commit into
thunder-id:mainfrom
PasinduYeshan:fix/4115-duplicate-users-me
Open

Fix duplicate /users/me requests during console sign-in#40
PasinduYeshan wants to merge 1 commit into
thunder-id:mainfrom
PasinduYeshan:fix/4115-duplicate-users-me

Conversation

@PasinduYeshan

@PasinduYeshan PasinduYeshan commented Jul 27, 2026

Copy link
Copy Markdown

Fixes thunder-id/thunderid#4115 — a single console sign-in issued ~10 GET /users/me (measured: 9 and 12) instead of 1.

Two independent defects, measured separately at runtime.

1. ProtectedRoute retried its own render, re-issuing sign-in each time

signIn() was called from the render body, which updates ThunderIDProvider state during render:

Cannot update a component (ThunderIDProvider) while rendering a different component (ProtectedRoute)

Worse, the throw new ThunderIDRuntimeError('ProtectedRoute misconfiguration.', ...) sat after the if (!isSignedIn) block and no branch inside returned. Since isSignedIn is already known false at that point, it fired on every unauthenticated render, including ones the component had just handled correctly (signInUrl, onSignIn, and the default sign-in all fell through to it).

React discarded each throw and re-rendered the root synchronously (11× There was an error during concurrent rendering but React was able to recover), and every retry re-ran the render-phase signIn(). That loop is the request burst. It self-terminated once auth state settled, which is why the bug was invisible except in the network tab.

Decisions:

  • Sign-in moved into useEffect, guarded by a ref so it fires once per unauthenticated state. The ref resets when the state leaves "unauthenticated", so a later sign-out re-initiates rather than rendering the loader forever.
  • The unconditional throw is removed, not returned-around. Every state now returns explicitly (isLoading→loader, isSignedIn→children, fallback, redirectTo<Navigate>, otherwise initiate sign-in and render the loader), so ProtectedRoute-Misconfiguration-001 is unreachable by construction. It never detected a misconfiguration; it was a missing return.
  • ProtectedRoute-SignInError-001 is kept but reported, not thrown. It previously lived inside an async IIFE, so it could never reach React and became an unhandled rejection. It is now constructed with the same code and origin, carries the original error as details, and goes to the package logger — matching how CallbackRoute already handles async auth failures.

2. ThunderIDProvider resumed the session twice per mount

The mount effect took isAlreadySignedIn = await client.isSignedIn(), ran resumeSession() if true, then unconditionally scheduled the refresh, re-checked isSignedIn(), and ran resumeSession() again. The pre-check can only be true when the token is valid — a state in which the re-check is also trivially true — so an already-signed-in load always paid 2 /users/me and 3 startAutoRefreshToken().

Decisions:

  • Auto-refresh is not removed; it now runs once instead of three times. startAutoRefreshToken() is still called unconditionally on every mount, before the sign-in check. Keeping it there preserves the case it exists for: when the access token expired but the refresh token is still valid, startAutoRefreshToken() refreshes immediately (timeUntilRefresh <= 0), so the isSignedIn() check that follows observes the refreshed state. A regression test covers exactly this ordering.
  • Only the redundant isSignedIn() pre-check was dropped, along with the duplicate resumeSession() it guarded. The second check is retained because it is the one that can observe a silent refresh. This also matches @thunderid/vue, which has always done this as a single if.
  • updateSession() is now single-flight. Concurrent callers share the in-flight promise. Deliberately not a cache: the ref clears on settle, so a later call (e.g. revalidation after a profile update) fetches again.

Scope decisions

  • Fixed in the SDK, not in the product. Console has 33 prop-less <ProtectedRoute> elements and Gate mounts its own provider; making the prop-less usage correct fixes both, instead of editing 33 call sites with loader/fallback and relying on convention for new routes.
  • One change was implemented, measured, and then dropped. Making signIn() await the session sync before clearing the loading state closed the window where isLoading === false while isSignedIn === false. Once ProtectedRoute no longer loops on that window, it made no difference to the request count, so it is not included. The window itself still exists and is worth a separate look.

Verification

Measured against a real console sign-in (Playwright, counting requests), building each variant separately:

Variant Fresh login Reload
Before 9, then 12 2
ProtectedRoute fix only 2 2
+ provider mount-path collapse 2 1
+ updateSession() single-flight 1 1

Final state re-measured 3× at 1/1. POST /oauth2/token unchanged (1 on login, 0 on reload). All render-phase errors gone.

Tests: 11 new for ProtectedRoute (7 fail without this change) and 5 new for ThunderIDProvider. pnpm build and pnpm format:check pass; lint findings on the touched files are unchanged or one fewer; the 2 TokenCallback failures and the nuxt vue-tsc tooling gap are pre-existing on main.

Supporting changes: re-export createPackageComponentLogger from @thunderid/react (react-router depends only on react and it was not re-exported); add @testing-library/react devDep, the missing *.tsx entries in tsconfig.spec.json, and a .vitest-attachments/ ignore to react-router — it had no component-test setup before.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented repeated sign-in attempts when protected routes re-render.
    • Improved sign-in failure handling without interrupting page rendering.
    • Ensured concurrent session refreshes are consolidated into a single request.
    • Improved session restoration and token refresh behavior during startup.
  • New Features

    • Added support for immediate sign-in URL navigation and custom sign-in callbacks.
    • Exposed component logging support for React integrations.
  • Tests

    • Added coverage for protected-route authentication flows and session management scenarios.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

ProtectedRoute sign-in flow

Layer / File(s) Summary
Effect-driven sign-in and validation
packages/react-router/src/components/ProtectedRoute.tsx, packages/react-router/src/components/__tests__/ProtectedRoute.test.tsx, packages/react/src/index.ts, packages/react-router/package.json, packages/react-router/tsconfig.spec.json, packages/react-router/.gitignore
Sign-in is initiated once from an effect, supports redirects and custom handlers, logs failures, renders the loader during pending sign-in, and adds coverage and test tooling. createPackageComponentLogger is re-exported.

Session refresh coordination

Layer / File(s) Summary
Single-flight session updates
packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx, packages/react/src/contexts/ThunderID/__tests__/ThunderIDProvider.test.tsx
Concurrent session updates share one in-flight promise, while mount-time auto-refresh and profile restoration use the updated session path.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: brionmario

Sequence Diagram(s)

sequenceDiagram
  participant ProtectedRoute
  participant ThunderIDRuntime
  participant Navigation
  participant Logger
  ProtectedRoute->>ThunderIDRuntime: read authentication state
  ProtectedRoute->>Navigation: navigate to signInUrl
  ProtectedRoute->>ThunderIDRuntime: initiate signIn
  ThunderIDRuntime-->>ProtectedRoute: return sign-in failure
  ProtectedRoute->>Logger: log ThunderIDRuntimeError
Loading
sequenceDiagram
  participant ThunderIDProvider
  participant ThunderIDReactClient
  participant SessionUpdate
  participant UsersMeAPI
  ThunderIDProvider->>ThunderIDReactClient: startAutoRefreshToken
  ThunderIDProvider->>ThunderIDReactClient: check isSignedIn
  ThunderIDProvider->>SessionUpdate: request updateSession
  SessionUpdate->>UsersMeAPI: fetch user profile
  ThunderIDProvider->>SessionUpdate: issue concurrent update
  SessionUpdate-->>ThunderIDProvider: reuse pending promise
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 summarizes the main fix: preventing duplicate /users/me requests during console sign-in.
Description check ✅ Passed The description is substantively complete with purpose, approach, verification, and testing details, though it doesn't use the template headings exactly.
Linked Issues check ✅ Passed The changes address #4115 by eliminating repeated sign-in/session-update paths that caused multiple GET /users/me requests.
Out of Scope Changes check ✅ Passed The supporting test, config, dependency, and logger-export changes all directly support the sign-in and session-update fix.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: private package registry requires authentication. Disable ESLint in CodeRabbit settings or use public packages.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react-router/src/components/ProtectedRoute.tsx`:
- Around line 169-228: Update the guard reset logic in the ProtectedRoute
sign-in useEffect so isLoading changes do not clear hasInitiatedSignInRef while
sign-in remains needed; reset it only when isSignedIn, fallback, or redirectTo
indicates sign-in is no longer required. In the signIn rejection handler, clear
hasInitiatedSignInRef before logging the error so a later recovery can retry.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fff3a076-f714-4b99-8920-6df21515c9ad

📥 Commits

Reviewing files that changed from the base of the PR and between 0d057c0 and 016c3cf.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • packages/react-router/.gitignore
  • packages/react-router/package.json
  • packages/react-router/src/components/ProtectedRoute.tsx
  • packages/react-router/src/components/__tests__/ProtectedRoute.test.tsx
  • packages/react-router/tsconfig.spec.json
  • packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx
  • packages/react/src/contexts/ThunderID/__tests__/ThunderIDProvider.test.tsx
  • packages/react/src/index.ts

Comment on lines +169 to +228
const hasInitiatedSignInRef: RefObject<boolean> = useRef<boolean>(false);

const shouldInitiateSignIn: boolean = !isLoading && !isSignedIn && !fallback && !redirectTo;

// Sign-in must be initiated from an effect, never from render: it updates the provider's state,
// and a render-phase update makes React discard the render and retry it, re-running the sign-in
// on every retry.
useEffect(() => {
if (!shouldInitiateSignIn) {
// Reset so a later sign-out initiates sign-in again instead of rendering the loader forever.
hasInitiatedSignInRef.current = false;
return;
}

if (hasInitiatedSignInRef.current) {
return;
}

hasInitiatedSignInRef.current = true;

if (signInUrl) {
navigate(signInUrl);
return;
}
if (onSignIn) {
onSignIn(signIn, overriddenSignInOptions);
return;
}

const mergedParams: Record<string, unknown> | undefined = (overriddenTokenRequest ?? tokenRequest)?.params;

signIn(
overriddenSignInOptions ?? signInOptions,
undefined,
undefined,
undefined,
mergedParams && Object.keys(mergedParams).length > 0 ? {params: mergedParams} : undefined,
).catch((error: unknown) => {
// Logged rather than thrown: a rejection here cannot reach React, and CallbackRoute treats
// async auth failures the same way.
const signInError: ThunderIDRuntimeError = new ThunderIDRuntimeError(
'Sign-in failed in ProtectedRoute.',
'ProtectedRoute-SignInError-001',
'react-router',
error,
);

logger.error(signInError.message, signInError);
});
}, [
shouldInitiateSignIn,
signInUrl,
onSignIn,
signIn,
signInOptions,
overriddenSignInOptions,
tokenRequest,
overriddenTokenRequest,
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect signIn()/isLoading() timing in the underlying client
ast-grep outline packages/react/src/ThunderIDReactClient.ts --items all --match 'signIn|isLoading'

Repository: thunder-id/javascript-sdks

Length of output: 221


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "files:"
git ls-files | rg '(^|/)ProtectedRoute\.tsx$|ThunderIDReactClient|ThunderIDProvider|SignIn' || true

echo
echo "ProtectedRoute outline if present:"
for f in $(git ls-files | rg '(^|/)ProtectedRoute\.tsx$'); do
  echo "--- $f"
  ast-grep outline "$f" --items all 2>/dev/null || true
done

echo
echo "Relevant line ranges:"
for f in $(git ls-files | rg '(^|/)ProtectedRoute\.tsx$'); do
  echo "--- $f lines 130-250"
  sed -n '130,250p' "$f" | nl -ba -v130
done

echo
echo "Search for isLoadingSync/isLoading/loading refs:"
rg -n "isLoadingSync|function isLoading|\bisLoading\b|hasInitiatedSignInRef|shouldInitiateSignIn|onSignIn\(" packages -S --glob '*.ts' --glob '*.tsx' || true

Repository: thunder-id/javascript-sdks

Length of output: 2956


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- packages/react-router/src/components/ProtectedRoute.tsx lines 1-280"
python3 - <<'PY'
from pathlib import Path
p=Path('packages/react-router/src/components/ProtectedRoute.tsx')
print(p, p.exists(), p.stat().st_size)
for i,line in enumerate(p.read_text().splitlines(), 1):
    if i <= 280:
        print(f"{i:4d}: {line}")
PY

echo
echo "--- packages/react/src/ThunderIDReactClient.ts outline"
python3 - <<'PY'
from pathlib import Path
import re
p=Path('packages/react/src/ThunderIDReactClient.ts')
print('exists', p.exists(), p.stat().st_size if p.exists() else None)
text=p.read_text().splitlines()
pat=re.compile(r'(?:function\s+(\w+)|class\s+(\w+)|\s*(?:public\s+)?(?:async\s+)?(?:invoke|handleError)\b|\s*(?:async\s+)?(\w+)\s*(?::[\w\s<>,\.]+)?\s*\([^{]*\)\s*\{)')
seen=set()
for i,line in enumerate(text,1):
    if any(s in line for s in ['signIn', 'isLoading', 'handleError', 'invoke', 'signOut', 'reloadState']):
        print(f"{i:5d}: {line}")
PY

echo
echo "--- ThunderIDProvider relevant symbols"
p=Path('packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx')
print(p, p.exists(), p.stat().st_size if p.exists() else None)
text=p.read_text().splitlines()
for i,line in enumerate(text, 1):
    if any(s in line for s in ['useIsLoading', 'setIsLoading', 'isLoadingSync', 'isSignedIn', 'signIn', 'isLoading', 'ThunderIDClient']):
        print(f"{i:5d}: {line}")

echo
echo "--- packages/react/src/ThunderIDReactClient.ts relevant sections"
for start in $(python3 -c 'print("\n".join([str(i) for i,l in enumerate(Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines(),1) if "signIn" in l]))' 2>/dev/null || true); do
  start=$(( ${start%.} - 10 )); if [ "$start" -lt 1 ]; then start=1; fi;
  end=$(( start + 220 )); echo "--- around line $start"
  python3 - <<PY
from pathlib import Path
lines=Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines()
start=max(1, int("$start")-10)
end=min(len(lines), int("$start")+220)
for i,line in enumerate(lines[start-1:end], start):
    print(f"{i:5d}: {line}")
PY
done

Repository: thunder-id/javascript-sdks

Length of output: 9995


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- packages/react/src/ThunderIDReactClient.ts lines 110-235"
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines(), 1):
    if 110 <= i <= 235:
        print(f"{i:5d}: {line}")
PY

echo
echo "--- packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx relevant sections"
python3 - <<'PY'
from pathlib import Path
lines=Path("packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx").read_text().splitlines()
for i, line in enumerate(lines, 1):
    term="isLoadingSync" in line or "useIsLoading" in line or "setIsLoading" in line or "signIn" in line or "isSignedIn" in line or "isLoading" in line
    if term:
        print(f"{i:5d}: {line}")
PY

echo
echo "--- packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx section around hook/provider "
python3 - <<'PY'
from pathlib import Path
lines=Path("packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx").read_text().splitlines()
# crude around 100-240
for i,line in enumerate(lines,1):
    if 90 <= i <= 250:
        print(f"{i:5d}: {line}")
PY

echo
echo "--- Read-only behavioral probe: guard state transitions and failure path"
python3 - <<'PY'
class State:
    def __init__(self, is_signed_in=False, is_loading=False, fallback=None, redirect_to=None, initiated=False):
        self.is_signed_in=is_signed_in
        self.is_loading=is_loading
        self.fallback=fallback
        self.redirect_to=redirect_to
        self.has_initiated_signed_in_ref_current=initiated
        self.should_initiate_sign_in=None

    `@property`
    def should_initiate_sign_in(self):
        self.should_initiate_sign_in = not self.is_loading and not self.is_signed_in and not self.fallback and not self.redirect_to
        return self.should_initiate_sign_in

def effect(action):
    if not state.should_initiate_sign_in:
        if state.has_initiated_signed_in_ref_current and action != "SIGN_IN":
            print("RESET at", action)
        state.has_initiated_signed_in_ref_current=False
        return "RESET_RETIRE"
    if state.has_initiated_signed_in_ref_current:
        return "GUARD_BLOCKED"
    state.has_initiated_signed_in_ref_current=True
    return "ACTION"

for title,(init,is_loading,is_signed_in,rejected) in [
    ("normal flow starts", (False, False, False, None)),
    ("signin calls isLoadingSync(true) synchronously", (False, True, False, None)),
    ("after sign-in success", (True, False, True, None)),
    ("rejected promise after isLoading transient reset", (False, True, False, "rejected")),
    ("rejected promise later still loading", (False, True, False, None)),
    ("rejected promise later not loading (fallback needed)", (False, False, False, None)),
]:
    state=State(*init)
    print("---", title, "init", init, "initiated", state.has_initiated_signed_in_ref_current)
    res=effect("PROVIDER_SET_LOADING_TRUE")
    print("  effect", action or "PROVIDER_SET_LOADING_TRUE", "=>", res, "initiated", state.has_initiated_signed_in_ref_current)
    state.is_loading=False
    res=effect("PROVIDER_IS_LOADING_RESOLVED")
    print("  effect", action or "PROVIDER_IS_LOADING_RESOLVED", "=>", res, "initiated", state.has_initiated_signed_in_ref_current)
    state.is_signed_in=True
    res=effect("PROVIDER_SET_SIGNED_IN")
    print("  effect", action or "PROVIDE_SET_SIGNED_IN", "=>", res, "initiated", state.has_initiated_signed_in_ref_current)
    res=effect("NEED_RETRY")
    print("  effect", "NEED_RETRY", "=>", res, "initiated", state.has_initiated_signed_in_ref_current)
PY

Repository: thunder-id/javascript-sdks

Length of output: 15271


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- Read-only behavioral probe: guard state transitions and failure path"
python3 - <<'PY'
class State:
    def __init__(self, is_signed_in=False, is_loading=False, fallback=None, redirect_to=None, initiated=False):
        self.is_signed_in=is_signed_in
        self.is_loading=is_loading
        self.fallback=fallback
        self.redirect_to=redirect_to
        self.has_initiated_signed_in_ref_current=initiated
        self.should_initiate_sign_in=None

    def reset_should_initiate(self):
        self.should_initiate_sign_in = not self.is_loading and not self.is_signed_in and not self.fallback and not self.redirect_to
        return self.should_initiate_sign_in

    def effect(self, label):
        self.reset_should_initiate()
        if not self.should_initiate_sign_in:
            if self.has_initiated_signed_in_ref_current and label != "SIGN_IN":
                print("  RESET at", label)
            self.has_initiated_signed_in_ref_current=False
            return "RESET_RETIRE"
        if self.has_initiated_signed_in_ref_current:
            return "GUARD_BLOCKED"
        self.has_initiated_signed_in_ref_current=True
        return "ACTION"

cases = [
    ("normal flow starts", (False, False, False, None)),
    ("signin caller sets provider loading", (False, True, False, None)),
    ("after sign-in success", (True, False, True, None)),
    ("rejected promise later still loading", (False, True, False, None)),
    ("rejected promise becomes non-loading while still not signed in", (False, False, False, None)),
]
for title, (is_loading, is_loading_initially, is_signed_in, loading_finally) in cases:
    print("---", title)
    state=State(is_loading=is_loading_initially, is_signed_in=is_signed_in)
    print("initial", "initiated", state.has_initiated_signed_in_ref_current)

    # Initial effect before sign-in runs.
    res=state.effect("INITIAL_RENDER")
    print("initial effect", "=>", res, "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())

    # Provider starts loading/sessions sync because signIn() called.
    state.is_loading=True
    res=state.effect("ENTER_LOADING_DURING_CALL")
    print("enter loading effect", "=>", res, "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())

    # Reject occurs.
    print("rejected", "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())

    # Loading resolves but user still not signed in, same sign-in remains necessary.
    if loading_finally is False:
        state.is_loading=False
        state.is_signed_in=False
        res=state.effect("LOADING_RESOLVED_POST_FAILURE")
        print("loading_resolved effect", "=>", res, "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())

    # User remounts or otherwise needs sign-in again.
    state = State(is_loading=is_loading,  is_signed_in=is_signed_in, initiated=state.has_initiated_signed_in_ref_current)
    res=state.effect("NEED_RETRY")
    print("retry effect", "=>", res, "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())
PY

echo
echo "--- ThunderIDReactClient withLoading implementation"
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines(), 1):
    if any(s in line for s in ['withLoading', 'loadingState', 'async withLoading', 'function withLoading', 'isLoading()']):
        print(f"{i:5d}: {line}")
PY

Repository: thunder-id/javascript-sdks

Length of output: 2559


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- packages/react/src/ThunderIDReactClient.ts lines 1-75"
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines(), 1):
    if 1 <= i <= 75:
        print(f"{i:5d}: {line}")
PY

echo
echo "--- packages/javascript/src/client/ThunderIDClient.ts withLoading implementation"
P=$(git ls-files | rg 'packages/javascript/src/client/\w+Client\.ts$|packages/javascript/src/client/\w+\w+Client\.ts$' | grep -i thunderid | head -5)
printf '%s\n' "$P"
python3 - <<'PY'
from pathlib import Path
roots = [Path(r) for r in open('/tmp/coderabbit-shell-logs/shell-output-YNNT-probe-placeholder.txt', 'r') if 'packages/javascript/src/client/' in r]
PY
for p in $P; do
  if grep -q 'function withLoading\|async withLoading\|withLoading' "$p"; then
    echo "--- $p"
    python3 - <<PY
from pathlib import Path
for i, line in enumerate(Path("$p").read_text().splitlines(), 1):
    if 'withLoading' in line or 'loadingState' in line or 'isLoading()' in line:
        print(f"{i:5d}: {line}")
PY
  fi
done

Repository: thunder-id/javascript-sdks

Length of output: 3019


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- all withLoading definitions"
rg -n "withLoading|loadingState|isLoading:" packages -g '*.ts' -g '*.tsx' || true

echo
echo "--- packages/javascript/src imports and ThunderIDBrowserClient definition candidates"
git ls-files packages/javascript/src | rg 'ThunderID.*Client|BrowserClient|client' | head -80

echo
echo "--- packages/browser/src exports/ThunderIDBrowserClient"
git ls-files packages/browser/src | rg 'ThunderID|Client' | head -80
for f in $(git ls-files packages/browser/src | rg 'ThunderID|Client'); do
  if rg -q 'class .*Client|async withLoading|function withLoading|loadingState|isLoading' "$f"; then
    echo "--- $f"
    rg -n -C 4 'class .*Client|async withLoading|function withLoading|loadingState|isLoading' "$f"
  fi
done

Repository: thunder-id/javascript-sdks

Length of output: 18972


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- packages/browser/src/ThunderIDBrowserClient.ts lines 130-280"
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("packages/browser/src/ThunderIDBrowserClient.ts").read_text().splitlines(), 1):
    if 130 <= i <= 280:
        print(f"{i:5d}: {line}")
PY

echo
echo "--- packages/javascript/src/ThunderIDJavaScriptClient.ts isLoading implementation and sign-in helpers"
python3 - <<'PY'
from pathlib import Path
text=Path("packages/javascript/src/ThunderIDJavaScriptClient.ts").read_text().splitlines()
for i,line in enumerate(text,1):
    if 'isLoading()' in line or 'signIn' in line or 'updateSession' in line or 'navigate' in line:
        print(f"{i:5d}: {line}")
PY

Repository: thunder-id/javascript-sdks

Length of output: 6361


Do not reset the sign-in guard while sign-in is still needed.

Only retire hasInitiatedSignInRef when the route no longer needs sign-in (isSignedIn, fallback, or redirectTo). If isLoading resets the guard, the in-flight signIn() can set isLoadingSync(true) and then clear it before the browser unloads for the redirect, causing ProtectedRoute to call signIn() again. Also reset the guard when the failed signIn() rejects so later recovery can retry instead of remaining permanently blocked.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-router/src/components/ProtectedRoute.tsx` around lines 169 -
228, Update the guard reset logic in the ProtectedRoute sign-in useEffect so
isLoading changes do not clear hasInitiatedSignInRef while sign-in remains
needed; reset it only when isSignedIn, fallback, or redirectTo indicates sign-in
is no longer required. In the signIn rejection handler, clear
hasInitiatedSignInRef before logging the error so a later recovery can retry.

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.

Multiple 'GET /users/me' requests are sent when logging into the console

1 participant