Repository navigation
feat(console): entering an app loads its metadata bundle once, and a record open reads no metadata - #28321
Conversation
…e app bundle
loadAppBundle(appId) reads GET /meta/app/{appId}/bundle once per app entry
and revalidates with If-None-Match; an object, view, page, dashboard or app
the active bundle holds is answered from it and never asked by name.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzFu8iUogiZhxL9tAo
…en reads no metadata The bundle's members are read as the by-name envelopes the spec's AppMetadataBundleResponse declares; acceptance pins a warm record open at one page request and zero metadata requests, a cold one at one bundle request. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzFu8iUogiZhxL9tAo
…pp entry load Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzFu8iUogiZhxL9tAo
…ct transports answer the bundle Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzFu8iUogiZhxL9tAo
…tor goes back as served Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzFu8iUogiZhxL9tAo
…-xoio4p # Conflicts: # ui/packages/app-shell/src/views/RecordDetailView.recordPagePayload-27627.test.tsx
…nal getAppBundle, typed by the spec's bundle response Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzFu8iUogiZhxL9tAo
…urface Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzFu8iUogiZhxL9tAo
|
domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #27627 (the Seam card; the console-half claim 6538126238 split beside the producer claim 6538108232; the amendments 6541997127 (producer #28287 merged, closing keyword ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Verdict reasoning. The console enters an app with one request and from then on answers every by-name read of a bundled item from the caller's own bundle through the one door those reads share, with absence left to the route, currency bounded by the server's validator through every write and invalidation door the adapter has, Implemented-by: VERDICT: PASS |
|
domain:ui#2 · os-tesla · session_01L9qibxgfM8p8t3fo6y9eFK 维护者速读
席位核对ACCEPT · head
|
domain:ui#2 · os-tesla · session_01L9qibzFu8iUogiZhxL9tAo
Requested by Tesla · project thread
Fixes #27627
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
GET /api/v1/meta/app/{appId}/bundle)。之后包里有的对象、视图、页面、仪表盘和应用本身,一律从包里读,不再逐个请求。再次进入应用时带If-None-Match,服务端回 304 就继续用手上这份。/meta/object/…读取。本 PR 把这一波去掉了:热会话打开记录只发 1 个页面请求、0 个元数据请求;冷会话最多再加 1 个元数据包请求,而且和页面请求同一波发出。改前 main 上是 1 个页面请求加 5 个/meta/object,分两波。data-objectstack和app-shell;对外只新增适配器的一个公开方法loadAppBundle(条款② 放宽)。包里没有的条目照旧走单条路由,包括 Studio、应用以外的对象,以及调用者无权读、因此不在包里的条目。任何经客户端的元数据写入都会让包先重新校验再答下一次读取;切换组织或语言会清空缓存,包随之重新下载。服务端没有这条路由或拒绝时,行为和现在一样。回滚即 revert 本 PR。ui/packages/data-objectstack/src/app-bundle.ts(包的存放、校验与失效);index.ts里的installBundledItemReads和fetchObjectSchemaFresh(两个读取入口);app-shell/src/console/AppContent.tsx里的loadAppBundle调用(只对 URL 指名的应用)。pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.recordPagePayload-27627.test.tsx packages/data-objectstack/src/appBundle-27627.test.ts packages/app-shell/src/console/__tests__/AppContent.loadAppBundle-27627.test.tsx,预期 3 个文件、21 个测试全绿。What changes
The console's metadata reads on app entry and on a record open, measured on
main69ce01d7through the record page's own acceptance transport (RecordDetailView.recordPagePayload-27627, StrictMode, the real adapter):main)POST …/page+ 5 ×GET /meta/object/NAME, in two waves (the contact's own definition in wave 2)POST …/page+ 1 ×GET /meta/app/APP/bundle, both in wave 1POST …/pagealone, 0 metadata requestsLoad once, revalidate after (
ui/packages/data-objectstack/src/app-bundle.ts,ObjectStackAdapter.loadAppBundle):loadAppBundle(appId)asksGET /api/v1/meta/app/{appId}/bundlethrough the client's conditionalmeta.getAppBundle(appId, { ifNoneMatch })(feat(rest): one request loads an app's metadata — GET /meta/app/{appId}/bundle, per caller, with an ETag #28287). Calling it again for an app already held sendsIf-None-Matchwith the held validator as the server spelled it (the producer's weakW/"…"included, rebuilt from the client'setag: { value, weak }), and a304(notModified) keeps the bundle. An author-setIf-None-Matchmakes the browser pass the server's304through rather than answer from its own cache.AppMetadataBundleResponse(z.inputofAppMetadataBundleResponseSchema,@objectstack/spec/api): bare, with every member theGetMetaItemResponseenvelope its ownGET /meta/{type}/{name}returns. Each envelope is kept whole and handed back as that read's answer.Serve the by-name reads of bundled items (
index.ts):installBundledItemReadswraps the client'smeta.getItem(type, name), the one door thatgetView,getPage,getApp,getItems,probeMetaItemAccessand app-shell'sMetadataProvider(throughgetClient()) all go through. An item the active bundle holds is answered there, as a copy of its envelope. A call with options, and any item the bundle does not hold, goes to the route.fetchObjectSchemaFresh, the adapter's rawGET /meta/object/NAME, now asks the bundle first. The unwrap,disabledActionsandsortabilitystamping that follow are unchanged, so a bundled object carries the same symbols its by-name read gave it.Staying current:
saveItem,deleteItem,publishItem,rollbackItem) settles, the bundles are marked stale. So is anyinvalidateCache(key)whose key names a held item, in the cache-key spellings the adapter and its hosts use (a bare object name,view:OBJECT:NAME,TYPE:NAMEfrom app-shellMetadataService, andlist:TYPE, which covers every item of that type). The same applies toinvalidateViewKeysfor a held view.If-None-Matchbefore it answers its next read. That read never goes by name, so there is no dual path for a bundled item.clearCache()(organization, session or locale switch) drops every bundle's items and validator, and the next read downloads the bundle again. The response varies onAccept-Language, and this is what makes a locale switch refetch.App entry (
AppContent.tsx): an effect callsloadAppBundle(matchedApp.name)when the URL names an app. That holds on the package-id route the sidebar builds and on the name alias. A pseudo-route's fallback app is not entered. A host adapter withoutloadAppBundleis left alone (protocol-agnostic, AGENTS #1).Unchanged: list reads (
GET /meta/object,/meta/view,/meta/app, the package-scoped object list) keep their routes, and so do the preview (?preview=draft) client and Studio'sMetadataClient.Tests
ui/packages/data-objectstack/src/appBundle-27627.test.ts(new, 11), through the real adapter and client over a held transport:If-None-Match, and a304keeps what is held;sortability;W/included;no-storeanswers the reads that waited on it and is not kept: the next read of a member goes by name, and re-entry downloads it with no validator;403) leaves reads on their routes.RecordDetailView.recordPagePayload-27627.test.tsx, two cases added (the card's acceptance):GETof the bundle, data = exactly the page request, all in wave 1;AppContent.loadAppBundle-27627.test.tsx(new, 2), on a realAppContentmount: the package-id route and the name alias each load the bundle by the app's name, once.deniedVsUnpublished,settingsMarketplaceVerdict-27507,setupItemRoutesVerdict-12706) count by-name app probes with a regex that also matched the new bundle URL. Their transports now answer the bundle as refused before the probe regex runs. The probe assertions themselves are unchanged.Acceptance notes
appBundle-27627.test.ts(an unheld item goes to its route). The seat and spec seat 1 agreed this reading.main,requestAppBundlecalls the client'smeta.getAppBundle(appId, { ifNoneMatch })andindexAppBundletakesAppMetadataBundleResponse. The client reports the validator as{ value, weak }with the quotes andW/taken off, so the adapter rebuildsW/"value"(or"value") to send back; for a well-formed entity tag that is the header byte-for-byte (pinned with aW/"sha256-…"tag). The client does not exposeCache-Control, so a200with noETagis read as the producer'sno-storeanswer, the only one it serves without a validator.yes (widening). The dispatch expectedno. Builtui/packages/data-objectstack/dist/index.d.tsgains one public member on the exportedObjectStackAdapter,loadAppBundle(appId: string): Promise<void>(plus the privateappBundles/requestAppBundle, which consumers cannot reach).AppBundleStoreandindexAppBundleare not exported.@object-ui/app-shell's built declarations name nothing new.Metadata-protocol: nonestands: nothing a metadata author writes changes, so the changeset stayspatch(AGENTS.md Post-Task Checklist step 3).AppContentloadsmatchedApp, notactiveApp, so a pseudo-route (Setup, marketplace) that falls back to a default app does not load that app's bundle. This also keeps the three verdict suites' probe counts honest.sys_comment,sys_activityand the mention list (sys_user) beside the page request. Those are data reads, pinned by main's own cases; the acceptance cases here count them separately and assert 0 metadata requests and exactly one page request.cache.ttl(default 5 min), like the by-name answers it replaces. Past that, the next bundled read revalidates withIf-None-Matchfirst, so a warm open after the lifetime costs one304round trip, not five by-name reads.no-storebundle is not kept. When a member's field visibility is undetermined the producer servesprivate, no-storeand noETag; the adapter reads the missingETag. The bundle answers the reads that were waiting on it (the open in progress), and then the store holds nothing for that app. Later reads go by name until the app is entered again, which downloads it with no validator. The adapter's ownMetadataCachestill keeps an object definition it read during that open for its lifetime, exactly as it does for a by-name answer today; this PR does not change that cache.mainsince feat(rest): one request loads an app's metadata — GET /meta/app/{appId}/bundle, per caller, with an ETag #28287, but no live server was booted for this PR. The acceptance counts are measured through the record page's real adapter over its acceptance transport, which now serves the bundle with a weakETagandprivate, no-cache, as the route does. A server without the route answers404, and the console behaves as it does today (pinned: a refused bundle leaves every read to its route).8441a012). This PR is the card's last part, so it saysFixes #27627. The seat lands it.Verification
All on head
07e67218. Producer #28287 (8441a012) and the rest ofmainwere merged in, then the switch to the client'sgetAppBundleand the spec type, then the changeset'sClause-②: yes (widening).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 commands (the same set as on every earlier head). All 67 were run with</dev/null, and every one exited 0. Reconcile--ranexit 0: 67 derived, 67 run, 0 NOT-MEASURED. Alsopnpm --dir ui run check:new-line-citationsexit 0 (0 new citations) andpnpm check:nul-bytesexit 0.node scripts/check-sdui-manifest.mjsis in the derived list.pnpm install --frozen-lockfile, with a declarations build of the@object-ui/app-shellclosure and@objectstack/client-react....ui/packages/data-objectstacktsc --noEmitexit 0.ui/packages/app-shelltsc --noEmitexit 0 andtsc -p tsconfig.test.jsonexit 0.ui/packages/data-objectstack/dist/index.d.tsnamesloadAppBundle(appId: string): Promise<void>as the one new public member.ui/packages/app-shell/distdeclarations name no bundle symbol.pnpm --dir ui exec vitest run packages/data-objectstack packages/app-shell/src/console packages/app-shell/src/views/RecordDetailViewran 419 files and 3336 tests, all green. That includes the 3 card files (21 tests) and the three AppContent verdict files.node scripts/ablation-replace.mjs. Each was restored with the blob equal toHEADand an empty diff.ETagbranch off (the bundle kept), theno-storecase goes red.fetchObjectSchemaFresh, 7 tests go red.loadAppBundlecall off, both AppContent pins go red.origin/main: clean.Generated by Claude Code