Skip to content

Commit be6b15c

Browse files
perf(app-shell): a console-created app's record route reads the objects pointing at the record from the bundle, not the registry (#29697)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbCuqLbVvnJHftXrbkzXKBiC)_ Part of #29552 Clause-②: no Metadata-protocol: none Claim: 6591721332 · amendment 6592122772 (leg 1 re-scoped to the record route's registry read). The record route's `/meta/page` list (15.8 KB) stays, by the seat's ruling in that amendment. ## 维护者速读 - 改了什么:在控制台里创建的应用(没有包)中,打开一条记录、而该对象没有声明记录页时,不再下载整张对象注册表;改为从应用包(app bundle)里取出所有指向该对象的对象,连同记录对象及其查找目标一起按名读取,相关列表照旧。 - 为什么改:在 app-crm 上,这类记录页一次打开共 888,599 B,其中整张对象注册表就占 553,397 B。 - 风险与回滚:相关列表的集合与顺序都不变。#29680(服务端按对象列表顺序返回 bundle 的 objects)已合入,合入 main 后实测顺序仍是 Activities·Contacts·Leads·Opportunities,与有包应用同一条记录的页面正文逐字一致。有包的应用、导航外的对象、草稿预览、无 bundle、记录抽屉仍读整表,不受影响。回滚方法是 revert 本 PR。 - 先看:`ui/packages/app-shell/src/console/appObjectScope.ts` 记录分支里 `!page` 的处理与 `inRelatedOrder`;`ui/packages/app-shell/src/providers/MetadataProvider.tsx` 的 `readBundledReferencing` 及其等待 bundle 的 effect。 - 两分钟验证:新建一个不带包的应用,导航里放 Account 和 Contact,打开一条 Account 记录,在开发者工具 Network 里过滤 `meta/object`:应只看到按名读取(如 `meta/object/sys_user`),没有不带名字的 `meta/object`;页面上的相关列表与有包应用里同一条记录一致。 - 需要你做的:无,席位落地。 ## What changes A record route whose object has no declared record page draws a synthesized page, and that page derived its related lists from the whole object registry: `readAppObjectScope`'s record arm answered `scope: null` and `AppContent` read `metadata.objects`. In an app with no package, for an object the app's navigation reaches, the route now reads a names scope instead: the record's object, its lookup targets, the app's `requiresObject` names and every object that points at the record's object, which the app bundle already holds (the server adds them as `inboundRelationObjects`, over the same object list the registry is read from). Bundled objects are answered from the bundle with no request. - `providers/bundledReads.ts`, `providers/MetadataProvider.tsx`: one new `BundledReads` member, `objectsReferencing(appName, objectName)`. It is a hit with the names of the bundled objects whose `lookup` / `master_detail` field `reference`s the object, only when the active bundle is that app's and its navigation (top level, areas, children at any depth) reaches the object. It is pending while that app's bundle is not in: the provider's effect looks again after the app is entered. It misses with no bundle, in draft preview, or when the bundle answered without the app. `@object-ui/app-shell` is private, so no published export moves. - `console/appObjectScope.ts`: the record arm's `!page` branch reads `readObjectRouteNames` with the related names in an app with no package. It waits while the bundle is pending, and reads the registry as before on a miss. `readObjectRouteNames` gains `relatedNames`: they are asked with the route's names, the route waits for them, and they are handed out in the bundle's order (`inRelatedOrder`). A names-only scope holds names in landing order across readers, and an app's `requiresObject` names land first. - `console/AppContent.tsx`: hands the reader in, beside the page and dashboard readers. Unchanged: a packaged app's record route (the registry, byte-identical), a declared record page, a record drawer (reads the registry beside the scope), a record form overlay, and the `/meta/page` list. ## Tests New `console/__tests__/AppContent.packagelessRecordRelated-29552.test.tsx` (11), over the real `MetadataProvider` and `AppContent` with a recording client that answers bundled items from the bundle, as the adapter does: - (a) a package-less record route reads no object list, only `object/sys_user` by name; - (b) CONTROL: the related lists drawn are the set the whole registry draws today, also for an object reached inside a navigation group and pointed at from another package; - (c) while the bundle is on the wire nothing is read, then reads by name; - (e) with the bundle in object-list order, the lists are drawn in the registry's order, including when `requiresObject` names a related object; - CONTROLs, registry as before: object outside the navigation, no bundle, bundle without the app, record drawer, and a packaged app (exactly `['object']`, the same lists). Ablations through `scripts/ablation-replace.mjs`, each restored with `git diff HEAD` empty: A1 (answer `scope: null` again) 3 red; A2 (drop the package gate) 1 red, the packaged pin; A3 (drop the navigation-reach check) 1 red, the outside-navigation pin; A4 (drop `inRelatedOrder`) 1 red, the `requiresObject` order pin. Sweep: 135 app-shell test files naming `AppContent`, `appObjectScope`, `bundledReads` plus every provider test, 1,104 tests passed before the order commit, 1,106 at f3895a4, and 1,107 at the final head 3b62862 (after merging main with #29752). `@object-ui/app-shell` type-check (both programs) exit 0, and ESLint 0 errors on the changed files. ## Acceptance notes - **Related-list order: the registry's, measured live now that #29680 has landed.** Before #29680 the server listed the bundle's objects as navigation objects first, then inbound ones, so at 98b5aa1 this branch drew Contacts, Activities, Leads, Opportunities on the Initech record. #29752 (#29680) now serves them in object-list order. At head 3b62862, which merges it, the package-less route draws Activities, Contacts, Leads, Opportunities. That is the pre-change order (b391feb), and the record body text is identical to the packaged `crm_app` route on the same record, which still reads the registry. The (e) pins hold that order with a list-order bundle, and `inRelatedOrder` keeps it when `requiresObject` names land first. - **Packaged apps** keep the registry read. Measured on `crm_app` (Initech, no declared page): 24 requests, 1,569,536 B, of which `/meta/object` is 553,397 B. That is #29681, not built here. - **No reachable drop found.** For objects the item chain never refuses a served document (`meta-item-read-gate.ts`, the `object` arm only prunes list views), and stored `reference_to` rows are rewritten to `reference` on rehydration for both the list and the bundle, so the bundle's inbound set and the registry's agree. Measured equal on app-crm: activity, contact, lead and opportunity. - **The registry list gates per caller exactly as the bundle does, so no related list drops for a caller.** Neither door refuses an object document whole: the object item gate only prunes `listViews` the caller does not hold (`meta-item-read-gate.ts`, `createMetaItemReadGate`'s `object` arm), and the list gate does the same per item (`createMetaListReadGate`'s `object` arm). Both apply the same ADR-0106 field mask (`projectMetaObjectSchema`): on the list in step 7 of `createMetaListAnswer`, on each bundle member through `createMetaItemAnswer`. A schema the mask empties fails the whole list and the whole bundle alike. So an inbound object is in the bundle exactly when it is in the list. If the mask hides an inbound object's lookup field from this caller, the registry draws no related list for it today, and neither does this branch: the server picks inbound candidates from the unmasked list, but the client reads `reference` from the masked members it was served. Re-checked at 3b62862: #29752 changed only the order of the bundle's members, not the gate code. Live, as admin, the objects pointing at `crm_account` and at `crm_contact` are the same set in the same order in `/meta/object` and in the bundle (`inbound-check.cjs`). A non-member account was refused 403 by both doors alike. - An object reached only as a lookup target or an inbound relation (not a navigation entry) still reads the registry: the server computes inbound only for the navigation's objects. ## Verification Production Console (`pnpm build` and `pnpm console:build`), app-crm (`objectstack dev --fresh --seed-admin`), a package-less `ops_app` (Account, Contact, Welcome) saved with `PUT /api/v1/meta/app/ops_app`, and a package-less record page of `crm_contact` as the control. Measured with `measure2.cjs`: fresh navigation through about:blank, cache disabled, CDP initiator stacks. <details><summary>Before (origin/main b391feb), after (98b5aa1) and final (3b62862, with #29752), bytes and requests</summary> | route | before | after | final | |---|---|---|---| | ops_app `crm_account` record, no declared page | 23 requests, 888,599 B; `/meta/page` 15,785, `/meta/object` 553,397, `/meta/view` 7,014 | 31 requests, 400,662 B; `/meta/page` 15,785 only, no `/meta/object` list, no `/meta/view` | 31 requests, 401,878 B; `/meta/page` 15,785 only; tabs Activities, Contacts, Leads, Opportunities | | ops_app `crm_contact` record, declared page (control) | 32 requests, 396,322 B | 32 requests, 396,325 B, text identical | 32 requests, 397,910 B | | crm_app `crm_account` record, packaged (unchanged arm) | n/a | 24 requests, 1,569,536 B, `/meta/object` 553,397 | 24 requests, 1,580,091 B, `/meta/object` 558,633; body text identical to the ops_app route | The after route reads 11 objects by name (sys_user, sys_organization, sys_business_unit and eight sys_* names the declared-page route also reads), 100,553 B together. The final run is on a fresh `--fresh` seed (Console now at `/_console/`), so it compares record text within the run, not across runs. Data, screenshots and scripts: the project's `issue-29552/leg1/measure/` (`final-3b62862c/`, `inbound-check.cjs`). </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jQxTKQHZstB1KT2GXV)_ <!-- signed: domain:ui#2 · zhuangjianguo · session_01QsR2jQxTKQHZstB1KT2GXV --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent eeb3117 commit be6b15c

6 files changed

Lines changed: 555 additions & 8 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
perf(app-shell): a record of an app built in the console reads the objects that point at it from the app bundle
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
An app created in the console has no package. Opening a record whose object has no declared record page read the whole object registry, so the synthesized page could find the objects that point at the record's object for its related lists: 553 KB of an 888 KB record open on the CRM example. When the app's navigation reaches the record's object, the app bundle already holds every object that points at it, so the route now reads those by name, from the bundle, together with the record's object and its lookup targets. The related lists are the same. The route still reads the registry for an object outside the app's navigation, with no bundle (draft preview, or a host without one), under a record drawer, and in an app that has a package.

‎ui/packages/app-shell/src/console/AppContent.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { useObjectTranslation, useObjectLabel, recordSaveMessage as buildRecordS
2020
import type { AppAccessVerdict, ConnectionState } from '@object-ui/data-objectstack';
2121
import { useAuth, useWorkspaceAdminStatus } from '@object-ui/auth';
2222
import { useMetadata } from '../providers/MetadataProvider.js';
23-
import { useBundledReads } from '../providers/bundledReads.js';
23+
import { BUNDLE_MISS, useBundledReads } from '../providers/bundledReads.js';
2424
import { useAdapter } from '../providers/AdapterProvider.js';
2525
import { usePreviewDrafts } from '../preview/PreviewModeContext.js';
2626
import { PreviewDraftEmptyState } from '../preview/PreviewDraftEmptyState.js';
@@ -521,6 +521,13 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
521521
settled: status === undefined || status === 'ready' || status === 'error',
522522
};
523523
},
524+
// objectstack#29552 — a record route with no declared record page, in an
525+
// app with no package: the objects that point at the record's object,
526+
// from the app bundle, when it can say.
527+
(objectName) => {
528+
const appName = typeof activeApp?.name === 'string' ? activeApp.name : undefined;
529+
return bundledReads && appName ? bundledReads.objectsReferencing(appName, objectName) : BUNDLE_MISS;
530+
},
524531
);
525532
// Until the app list has loaded, a scope candidate does not know its package
526533
// yet, and reads nothing (the splash is up).

0 commit comments

Comments
 (0)