Repository navigation
fix(plugin-list,plugin-kanban,plugin-dashboard,plugin-calendar): four page blocks re-read on the data-invalidation bus (objectui#10572) - #10620
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…n bus Co-Authored-By: Claude <noreply@anthropic.com>
…on bus Co-Authored-By: Claude <noreply@anthropic.com>
…ocks-read-invalidation-bus
…tion bus, gated on pristine Co-Authored-By: Claude <noreply@anthropic.com>
…ocks-read-invalidation-bus # Conflicts: # packages/plugin-list/src/ListView.tsx
|
changeset-claim-re-read
|
✅ 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
|
…the same commit Co-Authored-By: Claude <noreply@anthropic.com>
…ocks-read-invalidation-bus
Co-Authored-By: Claude <noreply@anthropic.com>
✅ 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
|
Fixes #10572
Clause-②: no
What this does
All five page-embeddable data blocks on objectui#10572 now re-read when the data-invalidation bus (
notifyDataChangedfrom@object-ui/react) reports a change to the object they query. Each renderer uses the shape from PR objectui#10494: its fetch effect names theuseDataInvalidationnonce for its own object, and a'*'change matches too. There is no host counter prop, no relay and nokey=remount. There is one commit per package; the form commit came in a second round, after the seat ruled on the fork below. A third change, in the app-shell object page, makes one console write reach the list as one read (see below).@object-ui/plugin-listListView(list-view,view:list)data, no gantt that owns its endpoint)@object-ui/plugin-kanbanObjectKanban@object-ui/plugin-dashboardObjectMetricWidget(object-metric)objectName@object-ui/plugin-calendarObjectCalendarobjectprovider and no external events@object-ui/app-shellObjectView(host, not a reader)@object-ui/plugin-formObjectForm(object-form, edit and view)recordId, not create mode, no inline fields; keyed on the record, so another record's change reads nothingCensus: real
SchemaRenderer, real registrations, counting fake data sourceThese are reads on mount, then reads after
notifyDataChanged({ objectName: '*' }). It is a one-off probe (not committed), run before and after the change.f169a030a)1e6aad5a8)object-grid(control)object-chart(control)detail-view(control)list-viewview:listobject-kanbanobject-metricobject-calendarobject-form, edit modeFor list, kanban, metric and calendar, the committed pins are per package, beside each renderer (
*.invalidationRefetch-10572.test.tsx); the form's pins are in their own section below. Each renders throughSchemaRendererand that package's own registration, and asserts four things. A'*'change costs exactly one re-read. A change to the block's own object (record-scoped) costs one. A change to another object costs none. For list and kanban, inline rows query nothing. The cross-package census stays a probe because a committed version would importplugin-grid,plugin-chartsandplugin-detailfrom a package that doesn't declare them (check:phantom-deps).Ablation for those four (fix committed first; the trap restores from
HEAD). I removedinvalidationNoncefrom the four dependency lists and checked on disk that each anchor went from 1 to 0. The four pin files then gaveTests 8 failed | 2 passed (10); the 2 passes are the inline-rows cases, which assert no query either way. After the restore,git diff HEADis empty and all four blob hashes matchHEAD.object-form: re-read gated on pristineA bare re-read overwrote a dirty field. I measured this with a probe: I typed
User typedintoname, the server's value moved toServer v2, and I fired the bus. Both refetch variants broke the edit:Loading form...branch, which unmounts the form.defaultValues.Either way the input read
Server v2, and the re-read would also have advanced the OCC token the save sends. The seat ruled option A, gate on pristine (comment 5832287152 on the card):ifMatchtoken the edit started from both stay, so a real conflict still surfaces through the existing conflict dialog at save. One re-read is replayed once the form is pristine again, which happens either through the form renderer's existingonDirtyChange(false)(a reset or a revert) or when this form's save lands.Dirtiness is read from that existing channel into a private ref, and
ObjectFormpassesonDirtyChangeon both the flat and the sectioned form schemas. It is an existingFormSchemamember, so nothing new is declared. The components form renderer is untouched.Pins are in
ObjectForm.invalidationRefetch-10572.test.tsx, throughSchemaRendererand the real registration:Server v2.User typedsurvives the bus event with no read, and the save sendsifMatchequal to the originalupdated_at.Ablation (the fix was committed first, and a trap restores from
HEAD):Tests 4 failed | 2 passed (6): both (b) cases and both (c) cases fail.Tests 1 failed | 5 passed (6): case (a) fails.Each leg's anchor count went from 1 to 0 on disk, and after each restore
git diff HEADwas empty. My first ablation attempt counted a multi-line anchor withgrep -c, which cannot match across lines. It aborted before changing anything, and I reran it with exact-string counts.One console write, one list read: the app-shell object page
CI went red on
ObjectView.refreshInPlace-10035.test.tsx: the grid, kanban and gantt/chart-whitelist cases each read 2 where the pin expects 1. The cause was the host, notListView.externalRefreshKeyand callnotifyDataChangedin the same tick.externalRefreshKeyinto its ownrefreshKeythrough a passive effect, one commit later.ListViewreads the bus, it saw the bus nonce in one commit andrefreshTriggerin the next, and read twice.PR objectui#10494's contract is that a writer's two notices land in one render. Every other writer already does that:
refreshData, plugin-view'sannounceOwnWriteandonMutation, and the console bridge.The fix is the seat's option H, and there is no mirror left. The page's own counter becomes
ownRefreshKey.refreshKeyis nowownRefreshKey + externalRefreshKey, summed in the render that receives the prop. Every existing reader keeps its name and sees an external bump in that same commit: the list'srefreshTrigger, the saved-view and draft read, the list-view metadata read, the record-count probe, and the environment entitlements.ListViewkeeps its bus read, with no gate and no debounce.Pins:
ObjectView.refreshInPlace-10035.test.tsxis unchanged and 5/5 green, including the realtime case.ObjectView.externalRefreshSum-10572.test.tsxadds three cases on the same harness. An undo-shaped write (bump plus bus notice) costs one list read and one record-count read. A counter-only bump with nothing on the bus still re-reads the list once and the count once. A realtime change costs one list read.Reverse-verify: I restored the mirror-only
ObjectView.tsxfrom before this change. The pins gaveTests 4 failed | 4 passed (8): the three #10035 list cases and the undo-shaped case went red. After the restore,git diff HEADwas empty and the blob hash matched.Acceptance notes
ObjectCalendarshows itsLoading calendar…placeholder during a re-read, as its existingonMutationrefresh already does.currentDateand the view mode live inObjectCalendarstate and survive.ListViewusesRefreshIndicatorover the current rows. The board and the metric keep their surfaces mounted.ListView,ObjectKanbanandObjectCalendaralso keep theirdataSource.onMutationsubscriptions. A data-source write reaches both that subscription and the bus bridge in the same synchronous dispatch, so React batches the two state bumps into one effect run.object-timeline1 → 1,object-gallery1 → 1,object-map2 → 2,element:number1 → 1,element:repeater1 → 1.origin/mainwas merged once before opening, as the claim asked, so the diff can merge with draft PR objectui#10597. TheListViewchange is one declaration above the fetch effect plus one dependency-list name.Gates
Round 4 is at
190d86bfc, after one moreorigin/mainmerge (d9bef0395, no conflicts). The five package suites ran atd9bef0395; the only later commit removes two unused imports from the new app-shell pin, and those pins were re-run at190d86bfc.ObjectView,refreshKeyorexternalRefreshKeyplugin-listplugin-kanbanplugin-dashboardplugin-calendarplugin-formtype-checklintThe first app-shell lint run found 2 unused imports in the new pin, which I removed in
190d86bfc. These cheap gates all exit 0:check-changeset-presence(12 source files of 6 released packages, 6 changesets declared),check-changeset-no-major,check:changeset-claims,check:pending-changeset-literals,check:new-line-citations(0 new),check:control-bytes,check:phantom-deps,check:test-path-rootsandcheck:vi-mock-specifiers.Earlier rounds:
type-checkandlintfor the five plugin packages are green (round 1 at6e70f10a2; plugin-form and plugin-list in round 2 at6caed987d).I didn't run the repo-wide
pnpm lintandpnpm testlocally; CI runs them.Session:
https://claude.ai/code/session_01D486C1axmXnrkJMNUfz2eb(seatdomain:ui#3).Generated by Claude Code