fix: random logouts on unstable network (only clear session on server rejection)#43
Merged
Conversation
🦋 Changeset detectedLatest commit: 565a708 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #34 — random logouts on unstable/no network.
Root cause
getAccessToken's catch-all treated every refresh failure as a dead session: any error (including plain network failures fromperformAsync) triggeredstorage.clear()+set(emptySession)— destroying a perfectly valid refresh token. Because the auto-restore path callsgetAccessToken({ forceRefresh: true })on every cold launch, opening the app offline guaranteed a logout; mid-session refreshes near token expiry had the same hole. Thestorage.clear()made it irreversible — tokens were gone even once the network recovered.Fix
getAccessToken: only clears the session when the server actually rejects the refresh (TokenError, e.g.invalid_grant). Transient failures (network, server hiccups) keep tokens and state so the next call retries naturally — and return the stored access token when it's still fresh (TokenResponse.isTokenFresh), so an offline cold start still yields a usable token. Corrupt storage / invalid response shapes still clear, as before.signOut(inverse bug found during review): revocation is now best-effort — previously an offline revoke threw beforestorage.clear(), so users couldn't sign out without a connection. Local session now always clears.Tests
Replaced the old always-clear expectation with:
TokenError) → session cleared (unchanged intent)nullwhen stalesignOut→ resolves and clears locally29/29 passing; coverage 96% stmts / 94% branches (thresholds 80/80). Full CI mirror run locally: build, type-check, format, publint, attw — all green.
Includes a patch changeset (→ 0.1.3) and updates the CLAUDE.md design notes.
Also: single-flight refresh (second commit)
Concurrent
getAccessToken()calls each fired their own refresh. WorkOS refresh tokens are single-use, so parallel refreshes race and the loser getsinvalid_grant— a genuineTokenError, indistinguishable from remote revocation → spurious logout. Now all concurrent callers share one in-flight refresh promise (reset infinally, so a failed refresh doesn't wedge future calls). This closes the last forced-logout path that isn't a real revocation.Additional tests: concurrent calls → exactly one refresh request; failed refresh → next call starts a fresh one. 31/31 passing, coverage 96.6% stmts / 94.2% branches.