Repository navigation
perf(app-shell,react): a console open reads the current app's objects, not the whole object registry (objectui#12811) - #12871
Conversation
…, not the whole registry (objectui#12811) The open path's three readers of useMetadata().objects (AppContent, useActionModal, useNavMemberGates) loaded GET /api/v1/meta/object for every registered object on every console open. They now read the app's object scope: the new optional MetadataContextValue.readObjectScope asks ?package= with the package id the app's served item carries, and by name each object outside the package the open reads. objects keeps its meaning, the whole registry, for every other reader. Claude-Session: https://claude.ai/code/session_01AxUw9eBGehV2FAP7Smwn3N Co-authored-by: Claude <noreply@anthropic.com>
… feed read the app scope (objectui#12811)
Measured live once the three objects readers had moved: AppHeader still
loaded the whole registry through getItemsByType('object') in
useServedViewItems and useObjectPresence (the bell's sys_activity
presence). Both read the app's object scope now, until something loads
the whole registry; readObjectScope also returns the definitions as
served. The capability gate asks nothing itself: AppContent names the
objects the app's own entries require, so the bell's cross-app roster
no longer asks another app's objects one by one. The census pin reads
the getItemsByType('object') spelling too.
Claude-Session: https://claude.ai/code/session_01AxUw9eBGehV2FAP7Smwn3N
Co-authored-by: Claude <noreply@anthropic.com>
…eaders pin's probe inside its mount (objectui#12811) Claude-Session: https://claude.ai/code/session_01AxUw9eBGehV2FAP7Smwn3N Co-authored-by: Claude <noreply@anthropic.com>
…e the permission read (objectui#12811) Measured on HotCRM behind a 50 ms round trip, production builds: the whole object list used to start at the console root (useActionModal), beside me/permissions; the scope AppContent asked for started only after it, so the splash on the branch held about 140 ms longer than on main. The scope question moves to console/appObjectScope.ts, which AppContent and a root hook in ConsoleShell's action runtime both ask, so the package list starts as soon as the app list names the app. Claude-Session: https://claude.ai/code/session_01AxUw9eBGehV2FAP7Smwn3N Co-authored-by: Claude <noreply@anthropic.com>
…ect list at boot (objectui#12811) The pin's third request was the whole registry, which useActionModal loaded on every open. Its purpose stands: no metadata request before auth, and each eager request once after it. Claude-Session: https://claude.ai/code/session_01AxUw9eBGehV2FAP7Smwn3N Co-authored-by: Claude <noreply@anthropic.com>
…e registry (objectui#12811) Measured on HotCRM: Setup's package holds no objects, and its navigation requires 15 objects of other packages. Its scope asked 11 of them by name, and its landing loaded the whole registry anyway: 47 API calls against 35 on main. The package list is read first; an app it shows to own no objects reads the whole registry, and no name is asked. Claude-Session: https://claude.ai/code/session_01AxUw9eBGehV2FAP7Smwn3N Co-authored-by: Claude <noreply@anthropic.com>
…ect, not its lookup targets (objectui#12811) Measured on HotCRM behind a 50 ms round trip: the lookup targets (asked by name once the package list lands) held the splash one more round trip. They decide only whether a lookup cell draws a link, and the requiresObject names only whether an entry stays shown; both are asked beside the route and land after it, as the whole list's did. Claude-Session: https://claude.ai/code/session_01AxUw9eBGehV2FAP7Smwn3N Co-authored-by: Claude <noreply@anthropic.com>
席位核对 — ACCEPT · objectui#12811 · head
|
main e43a162 |
this branch | |
|---|---|---|
whole meta/object |
96 objects, 858,893 B | not requested |
?package=app.objectstack.hotcrm |
— | 15 objects, 316,198 B |
| object bytes | 908,679 B in 2 calls | 430,281 B in 7 calls |
| API calls | 32 | 37 |
| splash | 877 / 754 / 802 ms | 740 / 780 / 781 ms |
Controls: a record page whose owner lookup points at sys_user (outside the package) renders it, with 16 related lists, on both sides. The metadata admin still reads the whole list. An object outside the app's package (crm_case) is asked by name and renders its list.
Screenshots, read by the seat (light; never-merged claude/issue-12811-evidence at ded02e9):
- the Accounts list on the branch, 9 records, with the view tabs and the header intact;
- Globex Industries' record page, with Account Owner "Dev Admin" (
sys_user, outside the package) and Related 16.
Sent to the maintainer at 2026-10-17T09:08Z.
Tests:
- Five new pins:
MetadataProvider.objectScope-12811(7);AppContent.objectScope-12811(7);appObjectScope-12811(2);scopedRegistryReaders-12811(3);- the census (3).
- One updated pin,
ConnectedShell.sessionGate, whose third boot request was the whole list. - Seven ablations, each red and each restored.
- app-shell in 8 shards: 14516 tests passed.
apps/consolein 2 shards: 2304 passed.packages/react: 1426 passed. type-checkandbuildfor@object-ui/reactand@object-ui/app-shell, the console build, andcheck:eager-closure(3138.2 of 3744.1 KB),check:spec-symbols,check:i18n-keys,check:i18n-driftand changeset presence: all exit 0.
Changesets: @object-ui/react minor (one optional context member) and @object-ui/app-shell patch; user-facing prose.
CI: running at this record. Landing waits for green and for the contract review below.
Rulings
-
The surface supplement: A, accepted. The claim named the open path's readers of
useMetadata().objectsas its class, and these sit inside it:AppShellContext.tsxdeclares the one member the claim'sClause-②reason anticipated;- the new
appObjectScope.tsis the one place that answers "which route reads which scope"; ConsoleShell.tsxmounts the scope's prefetch, and the claim named it asuseActionModal's mount;useServedViewItemsanduseObjectPresenceare two more open-path readers, found live once the named ones stopped loading the whole list first;ConnectedShell.sessionGateasserted the very request this card removes.
No other claim holds those files.
-
Record routes and record overlays keep the whole registry: A,
Part of #12811.- A record's related lists come from every object that points at its object, in any package. On HotCRM,
crm_case(the service package) points atcrm_account. A by-name guess would drop such a list silently. - A reverse-relation door on the list route is not filed now. No producer pulls for it, and the card's measured route, the object-list open behind objectstack-ai/cloud#2914's splash, is met here.
- After this lands, the card goes back to triage with that half named, to close or re-scope.
- A record's related lists come from every object that points at its object, in any package. On HotCRM,
-
An app whose package holds no objects of its own reads the whole registry: A. Measured on Setup: by-name reads cost 11 calls, the landing loaded the whole list anyway, and parity is now 36 vs 35 calls.
-
Five more calls for 479 KB fewer object bytes: A. The calls are small (8–24 KB), parallel, and none holds the splash. The staging cost the card measured (5.7 s for the whole list) is gone from the object-list open.
-
The published-face notes: accepted as declared in the changesets.
- The first object form modal in a session waits for the object list (one request).
- Inside an app with a package, while the whole registry is idle, the nav capability gate reads an unanswered object as registered. That is
main's own rule while its list is empty, narrowed: an object the server answered as absent is pruned. - The bell's cross-app roster no longer asks other apps'
requiresObjectobjects by name.
-
The three pre-existing observations (the adapter's own by-name read after paint, three more
meta/viewreads, and a hand-typed?recordId=on a page-surface object) stay notes. Each is on both sides and outside this card.
Implemented-by: cloud session session_01AxUw9eBGehV2FAP7Smwn3N, branch claude/issue-12811-scoped-object-metadata
Generated by Claude Code
✅ 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
|
Contract reviewServed-tier: Face: the Inputs read:
Nothing was built or run locally. ① Derived judgments
② Semver level
Both are in ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Landing · head
|
Part of #12811
Clause-②: yes
The object list open path (the card's measured route, /apps/crm_enterprise/crm_account) no longer requests the whole object registry. One half of the card stays open: record routes and record overlays still read the whole registry, because a record's related lists come from every object that points at its object, in any package. On HotCRM, crm_case (app.objectstack.hotcrm.service) points at crm_account. That half needs a decision (a reverse-relation door, or keep as is), recorded on the card.
What changed
The scoped read (
@object-ui/react,MetadataProvider). A new optional member,MetadataContextValue.readObjectScope({ packageId, names }):GET /api/v1/meta/object?package=IDwith the package id the app's served item carries (ADR-0048;@objectstack/client'smeta.getItems(type, { packageId })sends it);getItem;normalizeSchemaReferenceKeys, thencomposeObjects), so a scoped definition equals the same object fromobjects;objects,served(the definitions as served, before views are merged),missingandsettled;Scope entries are dropped and refetched only with the
objecttype (invalidate,refresh, an organization or preview-mode switch); there is no timestamp. When the whole registry is already loaded (after Studio, say), a scope is cut from it and nothing is requested.useMetadata().objectsis unchanged: it is the whole registry wherever it is read.Which route reads what (
console/appObjectScope.ts). These routes read the app's scope: the app root, an object's list, and a list view, with no record overlay in the URL and an app item that carries a package id. They read the package's objects, plus by name:requiresObject;The splash and the route skeleton wait for the package list and the route's own object. The other names are asked in parallel and are not waited on: a lookup cell draws its value without a link until its target lands.
Everything else reads the whole registry, as before:
The open path's readers, and what each reads now:
AppContent: the scope for the route element,ConsoleLayout,CommandPaletteanduseTrackRouteAsRecent. It no longer destructuresobjects.useActionModal(mounted fromConsoleShell): readsobjectsonly while an object's form modal is open, and the open waits forensureType('object'). The form's subforms are derived from every object.useNavMemberGates: inside an app with a package, while nothing has loaded the whole registry, answers from the scope. It asks for nothing itself.useServedViewItems(AppHeader and ObjectView view-tab labels) anduseObjectPresence(the bell's activity feed): read the scope until something loads the whole registry. These two were found live; main hid them because the whole list was already in flight.ConsoleShell: starts the app's scope as soon as the app list names the app.AppContentmounts only after the session's permission read, while the whole list used to start at the console root beside it.openPathObjectReaders-12811.test.tsfinds every reader ofobjectsorgetItemsByType('object')in the packages and the console, and fails on one it does not classify (scope, action time, or off the open path).Requests, measured
HotCRM
objectstack dev --fresh, behind a proxy adding 25 ms on each leg (a 50 ms round trip), production builds served byvite preview, Chromium via Playwright: one warm open, then three measured opens per side of/apps/crm_enterprise/crm_account. Times are milliseconds from navigation start.On the Setup app (
/apps/setup, a package with no objects of its own): main 35 calls, splash 855 ms; branch 36 calls (one empty?package=answer, then the whole list), splash 858 ms.On this local backend the whole list costs about 200 ms, so the splash does not move. The card measured it at 5.7 s on staging, which this read no longer makes on the object-list open.
Controls, live
/apps/crm_enterprise/crm_account/record/01T4qqNXvOOOINtK: the Account Owner lookup (sys_user, outside the package) renders as Dev Admin, with 16 related lists. Same on main and on the branch./apps/crm_enterprise/metadata/object: still requests the wholemeta/objectwhen it mounts and lists the same rows on both sides./apps/crm_enterprise/crm_case(an object outside the app's package): asked by name, renders its list, no not-found state.Deviations
packages/react/src/context/AppShellContext.tsx(the context type), the newconsole/appObjectScope.ts,ConsoleShell.tsx,useServedViewItems.ts,useObjectPresence.ts. Existing pins:ConnectedShell.sessionGate.test.tsx(its third boot request was the whole list) and a comment inMetadataProvider.requestBudget.test.tsx.MetadataContextValue.useObjectPresenceand the capability gate answer from the app scope inside an app with a package; the bell's cross-app roster no longer asks other apps'requiresObjectobjects by name.Screenshots (light, evidence branch, not merged here)
Tests, pinned to e7a2e8a
MetadataProvider.objectScope-12811:?package=, never the bare list;missing;objectsis the whole registry, and so is a scope with no package;invalidateandrefreshdrop and refetch it.AppContent.objectScope-12811(real provider, recorded client):appObjectScope-12811: the console root asks the package list, then the lookup targets; nothing outside an app, on a record overlay, or for an app without a package id.scopedRegistryReaders-12811:useServedViewItemsanduseObjectPresenceread the scope inside an app; CONTROLs outside an app and once the whole registry loads.openPathObjectReaders-12811: the census, in both directions.git diff HEAD: whole-registry forced (3 red), scope readiness dropped (2 red), a newobjectsreader in AppHeader (census red),objectsmade partial (2 red),useServedViewItemsreverted (1 red), no-own-objects rule deleted (1 red), gate waiting for lookups (1 red).pnpm exec vitest run packages/app-shell/ --shard=K/8; the whole suite exceeds the container's foreground cap): every shard exit 0, 1539 files (1538 passed, 1 skipped), 14516 tests passed, 9 skipped.pnpm exec vitest run apps/console/in 2 shards (it also exceeds the cap): both exit 0, 223 files, 2304 tests.packages/react: 113 files, 1426 tests.type-checkandbuildfor@object-ui/reactand@object-ui/app-shell: 0. Console build: 0.check:eager-closure0,check:spec-symbols0,check:i18n-keys0,check:i18n-drift0,check:new-line-citations0,check:test-path-roots0,node scripts/check-changeset-presence.mjs0.no-explicit-any, in the files' existing idiom.Acceptance notes
meta/viewis asked three more times after the first paint.inlineEditfield, so a scope'sattachInlineSubformssees the same children as the whole list there. The record form, which shows subforms, reads the whole registry everywhere.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01AxUw9eBGehV2FAP7Smwn3N