Repository navigation
fix(console,app-shell,auth): one request per resource on console open — entitlements, get-session, runtime/config (objectui#12808) - #12840
Conversation
- Entitlements: every reader of useEnvironmentEntitlements shares one organisation-keyed resource (in-flight resolution + settled answer, held while a reader is mounted). CloudOnboardingNext moves onto it. A refreshKey change re-resolves the shared answer for every reader. - Session: the pre-React preflight leaves its valid get-session answer in a page-load slot; AuthProvider takes it once for its first state instead of asking again. No exported member is added. - Runtime config: index.html keeps its early runtime/config answer for the page's lifetime; the first initRuntimeConfig() call reads it, and a failed early fetch still fetches as before. Claude-Session: https://claude.ai/code/session_01H8duocHdwfqNvZ3vLSn47m Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#12808) The preflight reads the get-session body with res.json() as before and hands it on as JSON text, so callers with a json-only response double keep working. The entitlements hook updates its resolve inputs in an effect, and the session pin reads useAuth through a layout-effect sink. Claude-Session: https://claude.ai/code/session_01H8duocHdwfqNvZ3vLSn47m Co-authored-by: Claude <noreply@anthropic.com>
…ctui#12808) Claude-Session: https://claude.ai/code/session_01H8duocHdwfqNvZ3vLSn47m 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#12808 · head
|
Contract reviewServed-tier: Face: the ① Derived judgments
② Semver levelThree patch changesets ( ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
CI red on
Generated by Claude Code |
…s name (objectui#12808) `ResolveOptions` is a symbol `@objectstack/spec/system` owns; the module-internal type is a different shape, so it takes its own name. Claude-Session: https://claude.ai/code/session_01H8duocHdwfqNvZ3vLSn47m 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
|
Landing · head
|
Fixes #12808
Clause-②: yes
Each of the three resources the card measured is now fetched once per console open.
What changed, per resource
Environment entitlements (
packages/app-shell/src/environment/useEnvironmentEntitlements.ts). Every reader ofuseEnvironmentEntitlementsnow shares one resource per control-plane origin and organisation. The resource holds the in-flight resolution and the settled answer (the summary, with the row-derived fallback when the summary is unavailable). The readers are the threecloud:plan-statuscards,cloud:onboarding-next,cloud:workspace-timezone-noticeand the environment list.CloudOnboardingNextdrops its own fetch and reads the shared resource; only asummaryanswer resolves it, so a failed or row-derived answer still reads asunknown, as before. The resource is refetched when the organisation changes, and when a reader'srefreshKeymoves: the environment list after a create explicitly re-resolves the shared answer, and every reader gets the new one. Nothing is time-based. The answer lives only while at least one reader is mounted. The drop waits for the end of the current task, so an unmount and a mount committed together hand the answer over. A later mount asks the server again, as every mount did before.Session (
apps/console/src/lib/auth-preflight.ts,packages/auth/src/createAuthClient.ts,packages/auth/src/AuthProvider.tsx). When the preflight finds the stored Bearer valid, it leaves itsget-sessionanswer in a page-load slot onglobalThis(Symbol.for('objectui.authPreflightSession')) along with the auth URL and the token it asked with.AuthProvidertakes the answer once, in auseStateinitializer, as its first state, and its mount effect does not ask again. That includes the StrictMode effect replay. The handoff only applies when the URL matches, the stored token is still the one the preflight validated, and the provider builds its own client. The body is parsed with better-auth'sparseJSON, the same parsergetSession()uses, so dates are revived the same way. A stale token leaves nothing in the slot. Sign-in, sign-out, organisation switch, rotation andrefreshSessionall read from the server as before.Runtime config (
apps/console/index.html,packages/app-shell/src/runtime-config.ts). The early-branding script used to publish its request to the in-flight registry and drop it on settle. It now keeps its parsed-body promise for the page's lifetime in a slot (Symbol.for('objectui.earlyRuntimeConfig'), with the URL it asked). The firstinitRuntimeConfig()call takes it when the URL matches: it joins the request if it is still in flight and reads the body if it has settled. If the early fetch failed,initRuntimeConfig()fetches as before. Every later call re-fetches, so the repeat-call contract is unchanged.Before and after: one console open, live
hotcrm
objectstack dev --freshbackend, console via Vite dev, Chromium through Playwright. The browser signs in once (cookie plus Bearer in localStorage), opens the console once to warm Vite, then the measured open is recorded. Two runs on each side gave identical counts.AuthProvider's mount effect. A production build shows the card's 2. With this branch it is 1 in dev too, because the replay reuses the handed-off answer.packages/auth.The CONTROL that the console still renders: console home after the change, light, https://github.com/objectstack-ai/objectui/blob/73c020afbaf65fed269ff19dc5b52e169450db4f/docs/design/findings/12808/console-home-after-12808.png?raw=true (on the evidence branch
claude/issue-12808-evidence, which is not merged here).Tests: one per resource, counting the requests a mount issues
useEnvironmentEntitlements.oneRequest-12808.test.tsx(app-shell): five readers mounted together issue one GET. CONTROLs: an organisation change issues exactly one more; arefreshKeychange re-resolves once for every reader; a later mount after all readers unmounted asks again.sessionBootDedup.test.tsx(console): the real preflight plus the realAuthProviderin StrictMode issue one get-session, and dates are revived. CONTROLs: sign-out then sign-in thenrefreshSessionreads the session again; a second provider asks the server, so the answer is used once; a stale token hands nothing on.runtimeConfigBootDedup.test.ts(console, runs the shipped script text): new pin that the early answer is read even after it settled. CONTROLs: a failed early fetch still lets app-shell fetch once; a failure on both keeps the defaults. The old pin, which expected an early failure to suppress app-shell's fetch, is replaced as the card's ruling asks.Ablation, one per new pin. Each mutation landed (
ablation-replace.mjsanchor 1 to 0), turned the pin red, and was restored with blob equal to HEAD andgit diff HEADempty:acquiregives 5 GETs instead of 1; all 4 tests red.AuthProvider's mount effect gives 3 get-session instead of 1; 2 tests red.Evidence, pinned to cef7052
pnpm --filter @object-ui/app-shell test: exit 0. 1510 files passed and 1 skipped (1511); 14117 tests passed and 9 skipped. Run on 745e4b2; cef7052 does not touch app-shell.pnpm --filter @object-ui/auth test: exit 0, 33 files, 311 tests (on cef7052).faviconAfterNavigation.test.tsxandanonSeedScope-5746.enumeration.test.tsx, 5 files, 40 tests, exit 0.type-checkfor auth, app-shell and console: exit 0.buildof the console's dependency closure (includes app-shell and auth): exit 0.pnpm --filter @object-ui/console build: exit 0.pnpm check:i18n-keys0,pnpm check:i18n-drift0,node scripts/check-changeset-presence.mjs0,pnpm check:eager-closure0 (within budget),pnpm check:new-line-citations0 (no new citations).Deviations
AuthProviderPropsmember.AuthProvideris mounted inapps/console/src/App.tsx, which is outside the claimed file surface. The slot mirrors theindex.htmltoruntime-config.tshandoff, so both handoffs work the same way.takePreflightSessionis exported from thecreateAuthClient.tsmodule but not from the package entry: the package's only export is., whoseindex.tsnamescreateAuthClientandTokenStorage. So@object-ui/auth's published surface gains no member.hasProductionEnvafter an environment is deleted outside the list, for example from a record page, and the list then remounts atrefreshKey0. The card's no-stale rule forbids that.CloudOnboardingNext: on a failed summary it now waits for the shared resource's row fallback before showingunknown. This only happens on the failure path. The Welcome page's timezone notice already issued that rows read.apps/console/src/main.tsxis in the claimed surface but needed no change.Acceptance notes
Session:
https://claude.ai/code/session_01H8duocHdwfqNvZ3vLSn47mGenerated by Claude Code