Pull Request Overview
- Opened on September 4, 2026
- Status Merged
- Commit count 1 with first commit September 4, 2026
Total Delta
Open Days
Test Delta
How long has this pull request spent in each phase of its lifecycle?
Data pending calculation for pull request
fix(auth): stop the login screen firing AccountSelect on every render
Closes JS-9871
Problem
Logging in with a recovery phrase fires 4 AccountSelect RPCs on the same session token, and each one re-runs the entire post-login boot.
A gRPC capture of one login (800 request rows, one WalletCreateSession, one AccountRecover, one token, no extra tabs or windows) shows:
- 4Γ
AccountSelect - 4Γ each of
NotificationList/FileNodeUsage/MembershipV2GetProducts/ChatSubscribeToMessagePreviewsβ i.e.U.Data.onAuthOnce()ran four times - 16Γ
ObjectCrossSpaceSearchSubscribe, 6ΓWorkspaceOpen, 12ΓObjectOpen, 696ΓObjectSearchSubscribe
| t (ms) | event |
|---|---|
| 14086 | AccountSelect #1 (431 ms) |
| 14527 | onAuthOnce batch #1 |
| ~14586 | Animation.from callback re-opens the guard |
| 14652 | AccountSelect #2 (+66 ms) |
| 14805 | AccountSelect #3 |
| 16181 | AccountSelect #4 |
| 16982 | WorkspaceOpen β routing finally lands, page unmounts, loop ends |
Root cause
Accounts arrive as AccountShow events, not in AccountRecover's response β there is no Response.AccountRecover. So the login page watched the store instead: useEffect(() => { focus(); select(); }) with no dependency array, on a component auto-wrapped in observer by vite.auto-observer.ts. A render is being used as an event.
That left isSelecting as the only thing between a re-render and a fresh RPC β and on the success path it was cleared inside the Animation.from(...) callback, which fires after Animation.getDuration() (~50 ms), while routing only happens seconds later when onAuthWithoutSpace β U.Subscription.createGlobal β U.Space.openFirstSpaceOrVoid completes. Every render in that window fired another AccountSelect, and each success re-armed the guard, so the loop sustained itself until routing unmounted the page.
The error branch had the same hole: it re-opened the guard while S.Auth.accountList still held the account, and the two error codes that route away (FAILED_TO_FIND_ACCOUNT_INFO, ACCOUNT_STORE_NOT_MIGRATED) return from setErrorHandler before it reaches accountListClear().
Fix
-
Keep the guard closed on success β the page is on its way out; nothing legitimate re-selects. -
Clear the account list wherever the guard re-opens, so the two always move together. Closes the identical loop on the error branch. -
Key the effect on the account list length instead of runningselect()on every render. This removes the amplifier β the actual root cause β and demotes the guard to a backstop. It also givesconst length = accounts.lengtha real job; it previously existed only to register the MobX read.
History
Latent since 91993d1beb (2024-04-02, "fix possible double AccountSelect"), which added the guard and, in the same commit, the reset that re-arms it. 64a684cbdc (2025-04-17) added the if (spaceId) switchSpace(...) branch, which does not reset the guard β narrowing the bug to logins with no stored spaceId, i.e. first login on a device.
Follow-ups found while investigating (not in this PR)
-
U.Subscription.createGlobalruns twice per successful login round β tail ofU.Data.onAuthOnce()and again fromU.Data.onAuthWithoutSpace(); same atapp.tsx. It opens withS.Record.spaceMap.clear()thendestroyListβAction.dbClearRoot, so the second run wipes thesubId.spacerecords the first is still filling.U.Space.getList()reads those andopenFirstSpaceOrVoidroutes to/main/void/erroror/main/void/loadingon zero β a candidate cause of spurious void screens on login. -
src/ts/component/page/index.tsxhas a block that computes nothing but readsaccount.status.type, subscribingPageIndexto everyAccountUpdateand re-rendering every page child through its inlinestorageGet/storageSetclosures. One confirmed re-render trigger in the capture above. -
setup.tsxhas no re-entrancy guard at all, only an empty dep array β one careless edit from the same bug.
Testing
typecheck and eslint clean. Needs a manual phrase login: the gRPC log should show exactly one AccountSelect.
PR was closed without comments.