Repository navigation
fix(app-shell,plugin-detail): a record open sends one approvals read and one record read (objectui#12701) - #12748
Conversation
…and one record read (objectui#12701) useRecordApprovals shares one read per object + record across every consumer mounted on the page, and joins a read already in flight, so StrictMode's replayed mount no longer sends a second one. The record page's invalidation effect re-reads approvals only when the nonce moves. record:details hands DetailView the new recordFromHost prop: the body waits for the page's record instead of reading the same row again. Claude-Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B Co-authored-by: Claude <noreply@anthropic.com>
…(objectui#12701) Mounts the real record page over the real adapter and hook under StrictMode, on an authored page that also places record:approvals: approvals 1, record 1, the activity read unchanged; a CONTROL that every approvals consumer still shows its state; and a wider expansion that triggers exactly one refetch with the union. The objectui#11699 pin now expects the body to send no record read of its own. Adds a DetailView pin for recordFromHost, and the changeset. Claude-Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B Co-authored-by: Claude <noreply@anthropic.com>
… request count (objectui#12701) With equal populate lists the adapter's in-flight sharing folded the body's own record read into the page's, so the count could not see it. A relation the page expands and no section draws reproduces the measured two distinct URLs. Claude-Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B Co-authored-by: Claude <noreply@anthropic.com>
…ut of render-time ref access (objectui#12701) The per-instance "approvals route absent" flag is state adjusted during render and mirrored into the ref the mount effect and refresh read, so the hook reads no ref while rendering. The DetailView pin drops its any. Claude-Session: https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4B Co-authored-by: Claude <noreply@anthropic.com>
…cord-page-one-fetch
席位核对 — ACCEPT · objectui#12701 · head
|
✅ 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 ① Derived judgments
② Semver levelThe changeset bumps ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Landing · head
|
Fixes #12701
Clause-②: yes
What changed
Target 1, approvals: one read per record open, shared by every consumer.
useRecordApprovalsnow keeps the read and its answer in a module-scoped store keyed by object + record. This is the same structural choice ashooks/sharedUserFeeds.ts: no provider is needed above every call site, so no consumer can opt out. It has no polling. Every instance mounted for a record shares oneGET /approvals/requests?object=…&recordId=…. A consumer that mounts later (an authoredrecord:approvalsblock, a tab opened after load) is served the stored answer, and a read already on the wire is joined. The entry lives while any consumer is mounted, so opening the record again reads it again.refresh()always reads (after a decision or a saved edit), and every consumer of the record sees the result. InRecordDetailView, the approvals invalidation effect now fires only when the invalidation nonce actually moves; it used to skip only its first run.Target 2, the record: one fetch.
record:detailspassesDetailViewa new prop,recordFromHost. With it, the body never reads the record itself: whileschema.datais absent for the record the host names, it shows its loading state, and it renders the page's record when that arrives. A host that names no record (a binding with no row) renders exactly what it rendered before. Without the prop,DetailViewreads its record as before. The page's own read already expands every relation the viewer may read, which covers any section's list. When that list grows (the definition gains a relation), the page reads once more with the union, and the body still sends nothing.Target 3:
RecordDetailView.recordOpenRequests-12701.test.tsxcounts requests by URL on the real page, the real adapter and the real hook, underReact.StrictModeas the console renders it, on an authored page that also placesrecord:approvals.What the measurement found
The three identical approvals reads were not three consumers. Caller stacks put all three on the page's one
useRecordApprovals: two from its mount effect, which StrictMode replays in development, and the third from the page's skip-the-first-run invalidation effect, which the replay walked through. The fix also covers the genuine multi-consumer case (an authoredrecord:approvalsmounts a second hook instance), and the test pins that case. The second record read came fromrecord:details'DetailView, reading the row while the page's read was still on the wire. That one is not StrictMode-related.Deviations
packages/plugin-detail/src/renderers/record-details.tsxis touched: it is the call site that passes the new prop.RecordDetailView.recordOpenRequests-11699.test.tsxis updated: its pin counted the body's ownGET /data/OBJ/IDas 1, and that is the read this change removes. It now expects 0.DetailViewprop,recordFromHost. The app-shell entry is unchanged; the store's test reset is exported fromuseRecordApprovals.tsonly.Network log, HotCRM, objectstack dev against the vite console
All requests naming the record, from one full page load each. Before =
e6175b57f, after = this branch. The rest of each log is console boot reads (session, localization, runtime config, the bell's feeds), whose counts vary from run to run and do not reach this diff.Lead
/apps/app.objectstack.hotcrm/crm_lead/record/2ZErCkJGyOdJVC1A, before:After:
The lead has no approval request, so I also measured an opportunity that has a pending, record-locking request (created through the API: amount 200,000, which opens HotCRM's Large Deal Approval). Record-scoped approvals and record requests only,
/apps/app.objectstack.hotcrm/crm_opportunity/record/USK6xSbdaYCtPBCP, before:After:
The opportunity page draws the same thing before and after: the decision bar (Approve, Reject, Reassign, Send back, Request info, Recall, Reply), the Locked for approval band with Recall approval, the disabled Edit, and the details body. The lead page draws the same thing too. The record's
sys_activityread is once before and once after; it is not duplicated.Tests, at the head this body names
Head
13a95b0(origin/main merged in; no conflict):pnpm exec vitest run packages/app-shell/ --shard=K/6, K = 1 to 6: every shard exit 0. 1471 of 1471 files (1 skipped), 13841 tests passed, 9 skipped.pnpm --filter @object-ui/plugin-detail test: exit 0. Test Files 276 passed, 1 skipped; Tests 2666 passed, 14 skipped.type-checkfor both packages, including their test tsconfigs: exit 0. Build of@object-ui/app-shelland its dependency closure, including plugin-detail: 29 of 29 tasks.eslint --no-inline-configon the touched files: 0 errors.check:i18n-keys,check:i18n-drift,check:new-line-citations,check-changeset-presence: all exit 0.Each new pin was ablated once. Each mutation was confirmed on disk, the targeted test went red, and the file was restored with
git diff HEADempty:recordFromHostremoved fromrecord:details: record reads 2, and the objectui#11699 pin also goes red;Acceptance notes
git mergeunder a running vite dev server, the HMR graph kept serving the oldrecord-detailsmodule and the second record read reappeared. A fresh dev server reads 1. This is a dev-server artifact, not a code defect.top=3and once withtop=1(as filtered count reads): quotes, line items and tasks. These are different queries, outside this change.Session:
https://claude.ai/code/session_01LbpjhSB18hFom4adrPRd4BGenerated by Claude Code