Repository navigation
fix(auth,app-shell): take the organization chain and the metadata preload off the console splash's critical path (objectui#12809) - #12855
Conversation
…load off the splash's serial path (objectui#12809) The active organization is named from session.activeOrganizationId and the organization/list answer, and the member row is asked for beside the list. AppContent waits only for the lazy metadata the current route reads. Claude-Session: https://claude.ai/code/session_019B3t48EDiMzo9xVuXhracK Co-authored-by: Claude <noreply@anthropic.com>
…a reads (objectui#12809) Re-asks a route bucket that drops back to idle, and adds the changeset. Claude-Session: https://claude.ai/code/session_019B3t48EDiMzo9xVuXhracK Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
席位核对 — ACCEPT · objectui#12809 · head
|
main 5d69b0e |
this branch | |
|---|---|---|
| splash on screen | 756–904 ms | 628–796 ms |
get-full-organization |
740–1057 | not requested |
get-active-member |
1068–1171, after the full org | 629–716, beside the list |
me/permissions |
1068–1192 | 737–1037 |
| dashboard, report, page metadata | 1198–1338, gating | 1043–1151, not gating |
The waits after get-session went from four serial steps to two. Isolation on the branch, adding 600 ms to one request at a time:
sys_user_preference,me/localizationandmeta/pageleave the splash unchanged;me/permissionsmoves it.
Screenshots, read by the seat (never-merged claude/issue-12809-evidence at 42c8870):
- the console home after the change (CONTROL): it renders, with the apps, My work and Recently Accessed;
- Sales Performance opened after the first paint, its metadata slowed to 4 s: the dashboard-shaped skeleton inside the chrome, with the sidebar and the top bar live;
- the same route once the metadata landed.
Sent to the maintainer at 2026-10-17T04:32Z.
Tests:
orgChainParallel-12809(auth), 6 tests: the member row is asked while the list is pending; the full organisation is never asked; the active org is the list row the session names. CONTROLs:- a StrictMode replay asks once;
- a switch re-reads the member row;
- a session without the key takes the old path;
- a stale id clears the stored id;
- one membership is repaired once.
AppContent.splashCriticalPath-12809(app-shell), 3 tests: the realAuthProvideranduseWorkspaceAdminStatusunderAppContent. The splash leaves after the route'sobjectbucket, not after a never-answeringget-full-organizationor the other buckets. CONTROLs: a later route draws its skeleton in the chrome, then its content; a refetching bucket keeps the route mounted.appRouteMetadataReads-12809(app-shell), 18 tests: reads everyRoutepath out ofAppContent.tsxand checks it against the table, in both directions.- Three ablations, one per pin. Each went red and was restored to an empty
git diff HEAD. - Package suites:
@object-ui/auth: 317 tests;- app-shell in 8 shards: 14395 tests passed;
apps/console: 2300 tests, all exit 0.
- Type-check and build for both packages and the console,
check:eager-closure,check:spec-symbols(objectui#12808's red), the i18n checks, changeset presence: all exit 0.
Changeset: @object-ui/auth and @object-ui/app-shell, a minor each, for the new optional AuthClientSession.activeOrganizationId; user-facing prose.
CI: running at this record. Landing waits for green and for the contract review below.
Rulings
-
The contract conflict: as dispatched. The relay is refused from the dev's container, so the seat opened the PR and relayed the report.
-
One gate in
AppContent, not a wait in each reader: A, accepted as the in-surface reading of the ruling. The ruling's outcome holds:- a route waits for the buckets it reads;
- it shows a skeleton, never an empty or "not found" state;
- the enumeration is pinned against the route source, so a new route cannot skip its row.
Waits in each reader would have landed in
views/,layout/,plugin-designerandapps/console, all outside the claimed surface, and a missed reader would fail silently. -
A session answer without the key takes the old path: A, as built. Reading the absence as "no active organisation" would be a wrong answer on a sign-in response or a server without the plugin.
-
packages/auth/src/types.ts: accepted. It is outside the listed files, but the claim's Clause-② text anticipated it: one optional member on the type ofuseAuth().session, which the ruling's "fewest optional members" allows. It is reported under deviations. -
The new
appRouteMetadataReads.ts: accepted. It sits besideAppContentunderconsole/, and the package entry does not export it. -
The observations stay notes, not filed. All three are pre-existing and unchanged here:
- StrictMode double requests in dev;
MetadataProviderloadsappandviewserially;- the recent-items tracker misses the boot landing.
The serial
app→viewload is the next lever on the splash. If it is measured to gate it, it belongs on its own card.
Implemented-by: cloud session session_019B3t48EDiMzo9xVuXhracK, branch claude/issue-12809-splash-critical-path
Generated by Claude Code
Contract reviewServed-tier: Face: the Inputs read:
Nothing was built or run locally. ① Derived judgments
② Semver levelOne changeset:
Nothing is removed. Consistent. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Landing · head
|
Fixes #12809
Clause-②: yes
The console splash now waits for the session, the membership answer,
me/permissionsand the metadata the current route reads. It no longer waits forget-full-organizationor for metadata the route does not read.What changed
The organization chain (
packages/auth/src/AuthProvider.tsx,types.ts). When theget-sessionanswer carries better-auth'sactiveOrganizationId,AuthProvidernames the active organization from that id and theorganization/listanswer. It asks for the member row (get-active-member) at the same time as the list, not after the active organization lands.get-full-organizationis no longer asked on boot. Every first-paint reader ofactiveOrganizationreads onlyid,nameorslug, and the list row carries all three:actionContextOrg,WorkspaceSwitcher,CurrentOrganizationIndicator, the metadata cache scope and the filter scope. The early member answer is taken once, by the first member read for the same organization, and dropped on a switch, a sign-out or an id the list does not confirm. It is not a cache: nothing time-based was added. A session answer without the key (a sign-in response, or a server without the organization plugin) takes the old path. So do mutation callers ofrefreshOrganizations. An organization switch, the ADR-0081 single-membership repair, the stale-id clear (objectui#12698), the session-user-change invariant (objectui#5664), sign-in and sign-out are unchanged, and their existing pins pass unchanged.The splash gate (
packages/app-shell/src/console/AppContent.tsx, newappRouteMetadataReads.ts). ThescopeMetaReadypreload ofobject,dashboard,reportandpageis gone.APP_ROUTE_METADATA_READSdeclares, per route, the buckets its element reads at render. Examples: an object list readsobject; a record page readsobjectandpage; a dashboard readsdashboardandobject; a redirect reads nothing. The boot splash waits for the current route's buckets only. After the first paint, a route whose buckets have not landed shows a skeleton in the route slot, inside the console chrome, instead of an empty list or a not-found state. A bucket that is refetching keeps the route mounted. A route the host adds throughextraRouteswaits for all four buckets, as before.The route readers. One gate in
AppContentreplaces a wait in each reader. Without a wait, these readers would draw an empty or not-found state on an empty bucket: ObjectView, ObjectDataPage, RecordFormPage, RecordDetailView (objects), ReportView, DashboardDesignPage, the CreateAppPage and EditAppPage pickers, SearchResultsPage, and AppRootLanding's nav prune.appRouteMetadataReads-12809.test.tsreads the route table out ofAppContent.tsxand fails on a route without a row, so a new route cannot skip its wait.What the splash waited for, measured
Setup: hotcrm
objectstack dev --fresh, behind a local proxy that adds 50 ms to every request on both sides. The console was a production build served byvite preview, opened in Chromium via Playwright: sign in, one warm open, then the measured open of/apps/crm_enterprise/crm_account. Times are milliseconds from navigation start; three opens per side.The waits after
get-sessionwent from four serial steps to two:get-full-organization, then the member row andme/permissions, then the dashboard, report and page preload;me/permissions.me/permissionsused to wait forget-full-organizationbecause the console mounts it belowRequireOrganization, which waits for the organization load to finish. On the branch, slowing one request by 600 ms leaves the splash unchanged forsys_user_preference,me/localizationandmeta/page; slowingme/permissionsmoves it, as ruled ('me' is on the critical path).A Vite dev-server open measured 1744 ms on main and 1726 ms on the branch. That interval is dominated by module loading, so the production builds above are the reading.
Screenshots (light, evidence branch, not merged here)
Tests
orgChainParallel-12809.test.tsx(auth): the member row is asked whileorganization/listis still pending;get-full-organizationis never asked; the active organization is the list row the session names. CONTROLs: a StrictMode replay asks once; a switch re-reads the member row for the new organization; a session without the key takes the old path; a stale id names no organization and clears the stored id; one membership is repaired once.AppContent.splashCriticalPath-12809.test.tsx(app-shell): realAuthProvideranduseWorkspaceAdminStatusunderAppContent. The splash leaves after the session, the member row and the route'sobjectbucket. It does not wait forget-full-organization(whose double never answers) or for the dashboard, report and page buckets. CONTROLs: a later route draws its skeleton inside the chrome, then its content; a refetching bucket keeps the route mounted.appRouteMetadataReads-12809.test.ts(app-shell): the enumeration, in both directions, plus URL-to-row matching with host routes.Ablation, one per new pin, each mutation confirmed on disk and each restore confirmed against HEAD with an empty
git diff HEAD:searchrow: 1 test red.Evidence, pinned to 82150d0
pnpm --filter @object-ui/auth test: exit 0, 34 files, 317 tests.pnpm exec vitest run packages/app-shell/ --shard=K/8; the whole suite exceeds the container's foreground cap): every shard exit 0. 1527 files (1526 passed, 1 skipped); 14395 tests passed, 9 skipped.pnpm exec vitest run apps/console/: exit 0, 222 files, 2300 tests.type-checkfor auth and app-shell: 0.build: auth, app-shell, the console's dependency closure, and the console: all 0.pnpm check:eager-closure0,check:i18n-keys0,check:i18n-drift0,check:spec-symbols0,check:new-line-citations0,check:test-path-roots0,node scripts/check-changeset-presence.mjs0.react-hooks/refswarnings (render-time ref reads in the first-paint latch and the settled-type set) and removes oneset-state-in-effectwarning; lint sets no--max-warnings.Deviations
packages/auth/src/types.ts: one new optional member,AuthClientSession.activeOrganizationId, the type ofuseAuth().session.packages/app-shell/src/console/appRouteMetadataReads.ts. It is not exported from the package entry. No route component was edited; the wait lives inAppContent, for the reasons under 'The route readers'.useWorkspaceAdminStatus.tsis unchanged.me/permissionsis the console host'sConsolePermissionsScope, not this hook, and it stays on the critical path.activeOrganization,activeMember,isMembershipResolved, andisOrganizationsLoadingturning false.activeOrganizationis now the list row.Acceptance notes
get-full-organization, and the cloud control plane's own calls. objectui#12808 already landed as 5d69b0e.MetadataProviderloads its eagerappandviewtypes one after the other; the recent-items tracker does not record the boot landing when objects are not yet loaded. All three are pre-existing and unchanged here.Session:
https://claude.ai/code/session_019B3t48EDiMzo9xVuXhracK🤖 Generated with Claude Code
https://claude.ai/code/session_019B3t48EDiMzo9xVuXhracK