Skip to content

perf(app-shell): Home's navigation roster gate asks its required objects in one ?names= request - #30864

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-0xx2dj
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-0xx2dj

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #29444

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Console 首页(应用外)导航名册的「对象是否存在」门禁,现在把所有 requiresObject 对象名合成一次 GET /meta/object?names=a,b,c 读取,不再每个对象名发一次请求。
  • 为什么改:app-crm 首页每次打开都要为这道门禁发 9 个对象元数据请求;服务端 ?names= 批量读(Metadata API: no batch by-names read, so the Console roster gate costs one /meta/object request per name #29236 裁决 A,PR feat(rest)!: serve ?names= on the /meta/:type list and close its query set #30709)已上线,实测改后只剩 1 个。
  • 风险与回滚:只影响应用外(首页等没有当前应用的页面);应用内仍逐个按名读,记录表单继续与数据适配器共用那次读取。批量答案里缺的名字按 404 处理(入口隐藏),请求失败则所有名字都算未答(入口照常显示),与原来逐个读的行为一致。回滚即还原本 PR。
  • 先看:ui/packages/app-shell/src/providers/MetadataProvider.tsx 的 fetchScopeNames(缺名、失败、刷新三种情形);ui/packages/app-shell/src/hooks/useNavMemberGates.ts 里 outsideApp 那一处分支;ui/packages/app-shell/src/console/home/__tests__/homeObjectReads-29121.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/console/home/__tests__/homeObjectReads-29121.test.tsx,预期 7 个用例全绿,其中门禁用例只记到一条 object?names=sys_cloud_only,sys_queue 请求。
  • 需要你做的:无,席位落地。
  • 席位意见:ui 席 2 对照 head 382ce259 的 diff 复核:5 个文件、+221 / −26,只在 app 外(首页)把路由门要的对象并成一次 GET /meta/object?names= 读;缺名照旧算未注册(同按名 404),整次读失败则每个名都算未答(同按名读失败),刷新再并读一次,预览与 app 内仍按名读,路由读者不会拿列表回答当自己的。MetadataContextValue 类型未改(钩子内本地类型转交 batch),无新导出,Clause-② no 成立,不欠契约复审;382ce259 只改测试以过 Lint UI。CI 全绿,同意落地。

What changes

  • The roster gate outside an app asks once. useNavMemberGates reads the app list's requiresObject union through the names-only scope. Outside an app (a frame whose app has no name: Home, or a frame with no expression scope), it now passes a package-local batch: true hand-off beside views: false. Inside an app nothing changes: the union is still read by name, because the route's own objects are among those names and their by-name read is shared with the adapter's getObjectSchema cache (objectui#13044).
  • The provider answers the hand-off from its own scope entry. MetadataProvider.readObjectScope keys a batched read under NAMES_BATCH_SCOPE, a symbol, so no package id can collide with it. It never shares the names-only entry: a list answer carries no by-name envelope and cannot tell an absent object from a refused one (OBJECT_PACKAGE_DISABLED), so a route reader never takes it for its own. The names not asked yet go out in one meta.getItems('object', { names }) (the client door PR feat(rest)!: serve ?names= on the /meta/:type list and close its query set #30709 added; no new URL builder).
    • A name in the answer is answered with its definition, through the same list ingestion as every object list (ingestObjectList).
    • A name the answer leaves out is answered missing, exactly as a by-name 404 was (ruling A on Metadata API: no batch by-names read, so the Console roster gate costs one /meta/object request per name #29236, 6580261740: an unknown name is simply absent).
    • A read that gets no answer fails every name it asked, as a failed by-name read failed its one (objectui#13009, objectui#13052). The gate shows those entries and does not ask again until invalidate, refresh or an organization switch.
    • Only the asked names are read out of the answer, so an item the request did not name never lands in the scope.
    • refresh asks the batched names again in one request.
    • Under the draft preview, whose MetadataClient.list has no names filter, the hand-off falls back to the names-only scope (by name).
  • Same filtering as the by-name read (measured on main b618224). The list door runs ?names= after the per-caller list gate (whose object arm is the by-name read's filterObjectListViewsForUser) and before the ADR-0106 mask and translation. The store read drops a disabled package's objects, where the by-name read refuses them 404 OBJECT_PACKAGE_DISABLED. The gate reads both answers as "not registered". The answer is the { type, items: [...] } array envelope, which extractItems already reads.
  • No new export from @object-ui/app-shell. The hand-off type ReadNamesInOneRequest is local to useNavMemberGates.ts.

Tests

  • homeObjectReads-29121.test.tsx (real MetadataProvider + real gate, fake adapter):
    • Outside an app, the gate sends one object?names=sys_cloud_only,sys_queue request. The absent sys_cloud_only is hidden and sys_queue is shown. This replaces the two by-name reads the case pinned before.
    • A batch that rejects leaves both entries shown and is not asked again.
    • refresh('object') re-asks the names in one request.
  • navGateUnionScope-12980.test.tsx: the outside-app case asserts the single ?names= request for the five-name union. Its fake adapter now honours names the way the door does: it keeps those items, and an unknown name is absent. The package-less-app cases (inside an app, by name) are unchanged and green.
  • Ablations, each through scripts/ablation-replace.mjs, which confirmed the mutation landed and the restore matched the HEAD blob:
    • gate hand-off off (outsideApp = false): 2 cases red;
    • absent name not recorded missing: 1 red;
    • refresh asks by name: 1 red.
Related suite (347 files that import the provider or the gate)

pnpm --dir ui exec vitest run --maxWorkers=2 over every app-shell / console test that names readObjectScope, useNavMemberGates or MetadataProvider, before the union-scope pin below was updated. 345 files passed and 2 failed:

  • navGateUnionScope-12980 (outside-app case): its fake adapter ignored names. Updated above, and the file is now 12/12 with the Home file.
  • spec-symbol-parity.test.ts: 16 cases failed because the probe reads @objectstack/spec's .d.ts files, and that run's build used OS_SKIP_DTS=1 (SPEC_NAMES.size 0). After a declarations build, the file is green together with the two gate files: 3 files, 53 tests.

Verification

Live, on app-crm. Backend objectstack dev --seed-admin --fresh -p 4417 in examples/app-crm. Console vite --port 5197 --strictPort with DEV_PROXY_TARGET pointed at it. Headless Chromium signed in as the seeded admin, then loaded /home fresh, counting every request whose path starts with /api/v1/meta/object.

Tree Roster gate requests Other object reads on Home
origin/main (the two source files restored from b618224) 9: GET /meta/object/NAME for sys_assignment_rule, sys_business_unit, sys_notification, sys_notification_preference, sys_notification_template, sys_queue, sys_queue_member, sys_record_share, sys_sharing_rule 1 (sys_activity, the activity feed's presence check)
this branch 1: GET /meta/object?names= with the same nine names, 200, 9 items 1 (sys_activity, unchanged)

The card counted 8 names on app-crm. The app list now carries 9.

Gates

At head c2ce30e3. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 36 families from this diff; each ran with stdin closed, and the exit code was captured before any pipe. --ran reconciliation: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN.

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/ci/snapshot-next.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --dir ui run check:new-line-citations :: exit 0
pnpm --filter @object-ui/app-shell type-check :: exit 0

pnpm --filter @object-ui/app-shell type-check (both tsc --noEmit and tsconfig.test.json) ran against a forced declarations build of @object-ui/app-shell^.... node scripts/check-sdui-manifest.mjs is in the derived list above.

Acceptance notes

  • sys_activity's by-name read on Home belongs to useObjectPresence (the activity feeds), not to the roster gate. It is left as it is: one name, and its by-name read is the one the adapter cache shares.
  • Inside an app with no package, the gate still reads the union by name (objectstack#29416's path). Folding it too would cost the record form's shared by-name read (objectui#13044) for the route's own objects. That is not this card's acceptance, so it is noted here and not changed.
  • Under the draft preview the gate keeps its by-name reads, because MetadataClient.list in @object-ui/data-objectstack has no names option. Adding one is a data-adapter change this card does not need.

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
The Home roster refresh case reassigned an outer variable during render,
which the React compiler lint rule refuses (Lint UI). The handle is now
filled from an effect on a holder object passed as a prop.

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

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

席位核对 · ACCEPT · 2026-11-06T07:45Z · head 382ce2596cd68c067693aad5bde3ec8aa968cd39

Seat domain:ui#2 reviewed the diff of its own dispatch (claim 6678209725 on #29444, card thread session_01L9qibvowqEy7Lkj2dKGw2i, report 6679442712; the same seat dispatched and reviews, the code is the thread's).

  1. Shape: 5 files, +221 / -26: MetadataProvider.tsx gains a batched names scope (symbol key NAMES_BATCH_SCOPE, its own entry, never the names-only scope's) read by one meta.getItems('object', { names }); useNavMemberGates.ts asks it only outside an app (no app name, no _packageId). Absent name → missing (as a by-name 404); failed read → every asked name failed (as a failed by-name read); refresh re-asks the same names in one request; draft preview and in-app reads stay by name. Only asked names are read out of the answer. Fixes #29444.
  2. Premise measured: live app-crm Home: roster gate 9 by-name requests → 1 ?names= (200, 9 items), per the report.
  3. Gates / check-runs: 55 check runs on this head, 44 success, 11 skipped, 0 red. 36 derived gates exit 0 (--ran 0 NOT-MEASURED); app-shell type-check 0 on a dts build; 3 ablations red. 382ce259 is test-only (a render-time reassignment in the new refresh case moved to an effect, for the React compiler lint rule).
  4. Accepted deviation: the branch is a few commits behind main; mergeable_state reads clean and the merge queue tests the merged ref.
  5. Out of scope / Skills-docs: none filed. Metadata-protocol: none, Skills-docs: not owed.
  6. Landing: check-governed-merges --branch: 0 of 5 paths governed, 247 changed lines → Tier S path, no human approval owed. Clause-②: no (MetadataContextValue.readObjectScope keeps its exported type; the batch hand-off is a hook-local type, no new export): no contract review owed. Landing on the maintainer's standing word 「按 pm-dispatch 审过的pr你可以自主落地」 (2026-10-30T14:20Z) and 「ui 车道跟进一下 3小时内没更新的pr和已派发的issue,接手处理。」 (2026-11-06T05:20Z): relay pr_ready, then automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 07:47
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit 3e04799 Nov 6, 2026
60 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-0xx2dj branch November 6, 2026 08:11
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/m 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(home): fold the roster gate's by-name object reads into one ?names= request (#29236 ruling A follow-up)

1 participant