Skip to content

perf(app-shell): a console-created app's object and page routes read what they name by name - #29542

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-w8lwl4
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-w8lwl4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29416

Clause-②: no
Metadata-protocol: none

Claim: 6581096637 · amendments: 6581167613, 6581536193

维护者速读

  • 改了什么:在控制台里创建的应用(没有包)里,三处读取不再下载整张元数据列表。侧边栏的能力门按名读对象,不再带视图。对象路由按名读本对象、requiresObject 和查找字段目标,不再读整张对象注册表。应用包(bundle)里没有的页面按名读单条,不再读整张 /meta/page 列表。
  • 为什么改:在 app-crm 上打开这种应用的一个对象页,每次都要下载 551 KB 的全部对象定义和 7 KB 的视图列表。打开包外页面时,还会额外下载 15 KB 的页面列表和视图列表。
  • 风险与回滚:读法从「整表一次」变成「按名多次」。对象页的请求数从 24 增加到 33,但字节数从 821 KB 降到 366 KB。界面文字逐字不变,视图页签和面包屑的标签已在测试中逐字节钉住。在有包的应用里,如果包外页面的按名回答属于另一个包或者不存在,会先多一次按名请求,再回退到整表(ADR-0048 preferLocal)。回滚方法是 revert 本 PR。
  • 先看:ui/packages/app-shell/src/console/appObjectScope.ts(readObjectRouteNames)、ui/packages/app-shell/src/hooks/useObjectPresence.ts(readItemPreferLocal)、ui/packages/app-shell/src/providers/MetadataProvider.tsx:1744(按名读取只组合提问者所问名字的视图)。
  • 两分钟验证:新建一个不带包的应用,打开它的对象页,在开发者工具 Network 里过滤 meta/object:应该没有不带名字的 GET /api/v1/meta/object。再打开该应用里一个包外页面:应该是 GET /api/v1/meta/page/<名字>,没有 /meta/page 整表,也没有 /meta/view。
  • 需要你做的:无,席位落地。

What changes

An app created in the console has no _packageId. On such an app, three readers still downloaded a whole metadata list. A page outside the app bundle had the same problem in any app.

Leg Before Now
1. useNavMemberGates inside a package-less app readObjectScope({ names: unionNames }) with views. The union names other apps' objects outside this bundle, so composing their views read the whole /meta/view list. { names: unionNames, views: false }, the same call it already made outside an app and on the package leg. Nothing in the app reads views off this presence answer.
1'. MetadataProvider names-only compose (probe-proved, amendment 6581536193) The names-only entry is shared by every names-only reader, and a with-views read composed the views of every name the entry held, so another reader's union leaked in. It composes the views of the names its own reader asked (MetadataProvider.tsx:1744).
2. readAppObjectScope for a package-less app's object route No scope: the whole object registry. readObjectRouteNames (appObjectScope.ts:547) reads the route's object, the app's requiresObject names and the palette's nav objects by name. It reads the object's lookup targets by name with views: false. Until the app list names the app, nothing is read (a candidate without a known package must not ask early). The app's root still reads the whole registry, as before.
2'. useServedViewItems names-only branch (amendment 6581536193) The served names were the app's requiresObject only, so a route object named only by a navigation entry lost its served view labels once the registry was no longer loaded. packagelessAppObjectNames(app): navigation type: 'object' names plus requiresObject, across navigation and every areas[].navigation.
3. A page the app bundle does not hold: AppContent's page scope, PageView, AppHeader's phone title preferLocal(metadata.pages, name, _packageId): the whole /meta/page list. readItemPreferLocal (useObjectPresence.ts:190), built on useReadItemsByName from #29413. While the type is idle, it asks the page by name. It takes the answer when the app has no package, or when the answer is the app package's own. Otherwise (another package's page, or no page) it falls back to the list, so the item is always the one preferLocal would pick. Once the list is loading or loaded, it reads the list and asks nothing.

Tests

  • console/__tests__/AppContent.packagelessReads-29416.test.tsx (7). These use the real AppContent, PageView and MetadataProvider, with a recording adapter and a ready app bundle.
    • (a) A package-less object route reads no object, view or page list.
    • (b) An out-of-bundle page is read by name, and a bundled page asks nothing.
    • (c) CONTROL: with the lists loaded, nothing more is asked.
    • (d) preferLocal: in a packaged app, another package's answer falls back to the list and draws the app's own page. The app's own answer reads no list. A package-less app takes the first match.
  • hooks/__tests__/packagelessServedViews-29416.test.tsx (2).
    • The route object's views stay served. The viewTabLabel labels (tab and crumb) are byte-identical before and after the whole registry loads, and no list is read.
    • The provider composes views only for the names its reader asked.
  • layout/__tests__/AppHeader.pageTitleByName-29416.test.tsx (2). The phone title asks the page by name with no list read, plus a CONTROL with the list loaded.
  • Updated pins whose CONTROL encoded the old whole-list read:
    • appObjectScope-12811: an app without a package now expects object/crm_account, object/sys_user.
    • AppContent.objectScope-12811: the route object and its lookup target by name.
    • AppContent.pageRouteObjects-12980: the double serves pages by name.
  • The census openPathObjectReaders-12811 is unchanged and green. No row moved.

Acceptance notes

  • Requests vs bytes on the object route: 24 → 33 requests, 821,430 → 365,762 B. The nine extra requests are by-name reads that replace one 551,834 B /meta/object list: sys_user and sys_organization (the lookup targets), and the Setup app's requiresObject sys_* names from the nav gate's union, which the whole list used to answer.
  • A page in a packaged app that resolves to another package's page, or to none, now costs one serial by-name read before the list.
  • Not changed, found while measuring (outside the claim's surface):
    • Every object route, packaged apps included, still reads /meta/view through ObjectView's saved-views overlay (ui/packages/app-shell/src/views/ObjectView.tsx:416, dataSource.listViews(objectName) → the adapter's readViewList). On this route it is now the only /meta/view reader.
    • A packaged app's object route whose lookup target is outside the bundle still reads /meta/view through the package scope's with-views lookup read.
    • A package-less app's record route still reads the /meta/page list (readPages). Its root still reads the whole registry.
  • Merging main (perf(app-shell): a dashboard opens from the app bundle and asks only its widgets' objects by name #29472, dashboard route reads) needed two resolution fixes in the merge commit: a duplicated ReadNamesWithoutViews import, and the preferLocal import that main's new dashboard read uses.

Verification

Production Console, app-crm (objectstack dev --seed-admin --fresh), before (app-shell sources at base 5f7d554c) and after (head bd111303; later commits only merge main)

Setup:

  • A runtime app ops_app with no package, saved through PUT /api/v1/meta/app/ops_app with two object entries (crm_account, crm_contact). crm_welcome is not in its navigation, so it is outside the bundle.
  • Playwright signs in and opens each route. It then reloads with the cache disabled and logs every /api/ request with its CDP initiator stack (Debugger.setAsyncCallStackDepth 64).

Object route /apps/ops_app/crm_account, en:

  • Before: 24 requests, 821,430 B. It reads GET /api/v1/meta/object (551,834 B) and /meta/view (7,014 B), both from get objects.
  • After: 33 requests, 365,762 B. There is no GET /api/v1/meta/object without a name. /meta/view (7,014 B) is still read, and its only initiator is ObjectView's listViews (see Acceptance notes).
  • For comparison, the packaged crm_app object route after: /meta/object?package=com.example.crm and /meta/view from appObjectScope's lookup read (unchanged).

Out-of-bundle page route /apps/ops_app/page/crm_welcome, en:

  • Before: 29 requests, 335,061 B. It reads GET /api/v1/meta/page (15,463 B, from get pages) and /meta/view (7,014 B, the nav gate's names-only scope).
  • After: 28 requests, 313,334 B. It reads GET /api/v1/meta/page/crm_welcome (1,044 B). There is no /meta/page list and no /meta/view.

The visible text of both routes is identical before and after, including the view tab All Records and the page body.

Ablations (each through scripts/ablation-replace.mjs, restored with git diff HEAD empty, at bd111303)
  • A1: the in-app gate asks with views. 3 red.
  • A2: a package-less app gets no scope. 2 red.
  • A3: readObjectRouteNames always reads the list. 5 red.
  • A4: the provider composes every name the entry holds. 4 red.
  • A5: the served names are requiresObject only. 1 red.
  • A6: readItemPreferLocal drops the ownership check. 1 red ((d), the other package's answer).
  • A7: package-less lookups asked with views. 1 red.
  • A8: AppHeader reads the page list. 1 red.
  • A9: PageView reads the page list. 3 red.
Local runs
  • Consumer sweep at bd111303, under os-verify-lock with --maxWorkers=2: 210 test files naming the touched modules, 1,920 tests, all passed.
  • After merging main, at 3a894d8b:
    • console/__tests__ and hooks/__tests__: 157 files, 1,273 tests. Main's AppContent.dashboardRouteReads-28784 was red on the dropped preferLocal import, then green after the fix.
    • The PageView, AppHeader, AppContent and appObjectScope files plus the new tests: 95 files, 754 tests, all passed.
  • turbo run type-check --filter=@object-ui/app-shell: 65/65, at both heads.
  • eslint --no-inline-config on the changed files: 0 errors.
  • Derived gates (dispatch-gates --commands): 34 commands, all exit 0 at bd111303, reconciled with --ran (34 derived, 34 run, 0 NOT-MEASURED). At 3a894d8b, check-sdui-manifest and check-changeset-no-major pass.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jTxHVLQcafzV1G73bX


Generated by Claude Code

…e read what they name by name

The nav gate asks its cross-app union without views, a package-less app's
object route reads its object and lookup targets through the names-only scope,
a page the app bundle does not hold is read by name under the prefer-local
rule, and a names-only scope composes the views of the names its reader asked.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTxHVLQcafzV1G73bX
…iew labels and the phone title's page read

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTxHVLQcafzV1G73bX
…pp list names its app

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTxHVLQcafzV1G73bX
…page read by name in the existing route pins

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTxHVLQcafzV1G73bX
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m

席位核对 · ACCEPT · 2026-11-02T18:39Z

Head-sha: 3a894d8bb296ee13f022bc53d110fec2c28f1edd

  • Shape: draft → main, Closes #29416. 15 files, +912 / −62 (974 lines ≤ 3000; source +180 / −48 over nine files, the rest tests and one changeset). No governed path: check-governed-merges --pr 29542 reports 0 of 15 paths, NOT governed. Clause-②: no and Metadata-protocol: none are line-initial in the body and the patch changeset for @object-ui/app-shell, as claimed (6581096637, amended 6581167613 and 6581536193).
  • Contract review: none owed (Clause-②: no). Every new name (readItemPreferLocal, readObjectRouteNames, packagelessAppObjectNames, the optional pending on ReadItemsByName) is internal to the private app-shell, and no metadata key or served shape changes.
  • File surface: inside the claim as amended: AppContent.tsx, appObjectScope.ts, useNavMemberGates.ts, useObjectPresence.ts, useServedViewItems.ts, plus AppHeader.tsx, PageView.tsx and the MetadataProvider.tsx compose line under the amendments, with their tests and the changeset.
  • Read in the diff:
    • Leg 1: useNavMemberGates asks its union with views: false in every app; the in-app branch is gone. MetadataProvider composes a names-only scope's views only for the names that reader asked (itemNames(entry.items).filter((name) => asked.includes(name))). This closes the shared-entry leak the jsdom probe found.
    • Leg 2: a package-less app's object route goes through readObjectRouteNames, the names-only scope (route object, requiresObject, navigation objects). Its lookup targets are read by name with views: false. useServedViewItems' names-only branch serves packagelessAppObjectNames(app), so view labels stay served.
    • Leg 3: readItemPreferLocal takes the by-name answer only when the app has no package or answer._packageId is the app's own package; otherwise it reads the list as before. AppContent's page scope, PageView and AppHeader's phone title read through it, and a bundled page asks nothing.
  • Tests (dev):
    • New suites: AppContent.packagelessReads-29416 (7, with a list-reading CONTROL and both preferLocal arms), packagelessServedViews-29416 (2, labels byte-identical to the whole-registry read, plus the compose pin) and AppHeader.pageTitleByName-29416 (2).
    • Three pins updated; census openPathObjectReaders-12811 unchanged.
    • Ablations A1–A9 each red, restored to HEAD. Sweeps: 210 files / 1,920 tests; after the main merge, 157 + 95 files green. type-check 65/65. Derived gates 34 of 34, --ran reconciled. Body gates exit 0.
  • Measured (dev): prod Console, app-crm, package-less ops_app, CDP initiator stacks:
    • Object route: 24 requests / 821,430 B before, with the 551,834 B /meta/object list. After: 33 / 365,762 B, with no unscoped /meta/object.
    • Out-of-bundle page route: 335,061 B before, 313,334 B after; /meta/page/crm_welcome (1,044 B) replaces the /meta/page and /meta/view lists.
    • Visible text is identical. Data: /mnt/project-files/issue-29416/measure/.
  • Gates (check-runs on this head): 44 success, 11 skipped, 0 failed or pending. mergeable_state is clean.
  • Out of scope: the three carrier-none observations in the dev report (the ObjectView saved-views /meta/view read, a packaged app's out-of-bundle lookup read with views, and the package-less record route's /meta/page list) are filed by the seat as Console: every object route reads the whole /meta/view list through ObjectView's saved-views overlay #29550, Console: a packaged app's object route reads the whole /meta/view list when a lookup target is outside its bundle #29551 and Console: a console-created (package-less) app's record route reads the whole /meta/page list, and its root the whole object registry #29552.
  • Landing: ordinary queue landing, pr_ready then automerge_enable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console: a console-created (package-less) app's routes and an out-of-bundle page route still read whole /meta/object, /meta/view and /meta/page lists

1 participant