Repository navigation
fix(plugin-view,plugin-dashboard): object-view's non-grid views and a dashboard filter's optionsFrom options re-read on the data-invalidation bus (objectui#10887) - #10898
Conversation
… dashboard filter's optionsFrom options re-read on the data-invalidation bus (objectui#10887) ObjectView's non-grid fetch effect now names the useDataInvalidation nonce for schema.objectName, subscribed exactly when that effect queries (no host renderListView, not the grid, an object and an adapter). SelectFilter's optionsFrom read names the nonce for optionsFrom.object; the options stay on screen until the re-read answers, and the selected value (the dashboard variable) is never written by the read. Pins through the real SchemaRenderer: one read on mount, one more after '*', one more after the view's own object, none after an unrelated object, the same inner instance, the selected filter value kept, and a control with no object that never reads. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…re drawn; changesets (objectui#10887) The host-only tree and chart renderers query for themselves and read the bus themselves (ObjectTree runs its own query ahead of the rows handed to it and re-queries when that array changes; ObjectChart never reads them), so the non-grid fetch no longer subscribes for them: a re-read there would only add reads. Gantt joins the pinned view types, and two controls pin the host-composed tree and chart. One patch changeset per touched package. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…s call reads type-check (objectui#10887) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…d pin instead of casting to any (objectui#10887) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…ach pin as its lit control (objectui#10887) A grid object-view (ObjectGrid reads the bus itself) and an object-metric block move on the same event the members are pinned on, green on the base and on the head. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
|
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
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…nvalidation (objectui#10887) (objectstack-ai#10973) Fixes objectstack-ai#10887 Clause-②: yes — one identifier, `useDataInvalidation`, joins the published author scope of `kind: 'react'` pages, and `content/docs/guide/react-pages.md` teaches it. **Why a closing keyword.** The card has three members. Members 1 and 2 landed in PR objectstack-ai#10898 (squash `9f0c84a44`). This PR lands member 3, ruled A (ruling `5864668610`), so all three are in once it merges. The four review notes from the members 1 and 2 round ride this round: (a) and (d) are text and are corrected here; (b) and (c) need a behaviour change, so they are reported as findings and not changed here (see Acceptance notes). The objectstack half of the ruling (the docs page, the skill and the showcase pages) is carried by objectstack-ai/objectstack#20395. The `Blocked-by` line on objectui#10519 is the seat's to update; this PR does not touch that card. ## What changed - **`@object-ui/components`, the react-page renderer.** `ReactKindPage` injects `useDataInvalidation` from `@object-ui/react` into the author scope, beside `useAdapter`. It is the same hook `ListView` reads to refresh its rows: `useDataInvalidation('object')` returns a nonce that moves when the bus reports a write to that object, or `'*'`. A page names it in the dependency list of the effect that reads through `useAdapter`, and that read re-runs in place. The hook is a module-level function, and the scope memo still depends on the schema and the adapter only, so the scope keeps its identity and no page recompiles or loses state. The file header's scope list gains the entry. - **The react-pages guide.** The scope table gains a `useDataInvalidation` row. The Live data example reads `const changed = useDataInvalidation('showcase_project')` and names `changed` in the effect's dependency array, followed by one sentence on why: the nonce moves each time the bus reports a write to that object, so the effect reads again in place without remounting the page. - **Changeset** `10887-react-page-data-invalidation`: `minor` for `@object-ui/components`, `Clause-②: yes`. - **No new dependency edge.** `useDataInvalidation` is exported by `@object-ui/react`, which `@object-ui/components` already depends on and already imports `useAdapter` from. ## Review notes from the members 1 and 2 round (`5864212493`) | note | what it said | here | | --- | --- | --- | | (a) | the plugin-view changeset's "switches off that view's own bus reader" is imprecise for a gantt handed zero rows | **Corrected (text).** The pending changeset `10887-object-view-non-grid-bus-reader` now names the exception: a gantt handed zero rows still queries for itself and keeps its own bus reader (objectui#7333). Frontmatter byte-identical (the first three lines hash the same before and after); the file has not shipped (no CHANGELOG carries its text), so it is corrected in place. The same imprecision in `ObjectView`'s member 1 comment is corrected too. | | (b) | `fetchDrawsView` is a deny-list, so a host `views` entry the renderer does not draw adds one unused read | **Not changed: behaviour.** An allow-list changes which views subscribe. Reported as a finding. | | (c) | a calendar re-read flashes its loading placeholder | **Not changed: behaviour.** `ObjectView`'s non-grid fetch sets `loading` on every run and passes it to the view, and `ObjectCalendar` shows it; a silent re-read changes behaviour. Reported as a finding. | | (d) | the `identityKey` comment is incomplete | **Corrected (comment).** It now says the non-grid fetch re-reads when `refreshKey` moves or when the bus reports a change to the object (objectui#10887), that a gantt handed rows draws them while a gantt handed zero rows queries for itself (objectui#7333), and that `tree` reads the bus itself (objectui#10778). | Both comment edits sit inside `ObjectView`'s component body, so neither reaches a published `.d.ts`; no plugin-view surface moves and the plugin-view changeset keeps its `patch`. ## Pin, through a real react page `packages/components/src/__tests__/react-page-invalidation.test.tsx`. The page is compiled from source by `@object-ui/react-runtime` inside the real `ReactKindPage`, dispatched by the real `PageRenderer` for `type: 'home'`; the events go through the real bus (`notifyDataChanged`). Only the adapter is a stand-in, so reads can be counted. The page source is the guide's Live data pattern with the nonce. - One record-scoped bus event on `showcase_project` re-runs the page's `find` exactly once (1 to 2), the re-read rows reach the page, and a counter the user clicked before the event still reads 1, so the page was not remounted. - Lit control: an event on `showcase_invoice` does not re-run the read (`find` stays at 1, the rows stay the first answer), while a bare `useDataInvalidation('showcase_invoice')` reader mounted beside the page moves from 0 to 1, so the event demonstrably reached the bus. | run | result | | --- | --- | | base `react-page.tsx` (blob `f70b8f425`, restored from `HEAD` afterwards: blob equal, `git diff HEAD` empty) | `Tests 2 failed (2)`: both tests, each at the page-error assertion, `ReferenceError: useDataInvalidation is not defined` in the page's error panel | | head | `Tests 2 passed (2)` on the implementation's first commit `f64ba6e66`, and again inside the package run below at `af76d224c` | | ablation: the injected hook replaced by a stub that never moves (`ablation-replace`, anchor `useDataInvalidation,` hit once, blob `ab712dfac` to `9625f569b`, restored to the `HEAD` blob, `git diff HEAD` empty) | `Tests 1 failed \| 1 passed (2)`: only the re-read test, `expected 'read 1' to be 'read 2'`; the lit control stays green | The pin imports its subject by relative path, and `@object-ui/react` and `@object-ui/react-runtime` through the root vitest alias to their sources, so no `dist/` sits between a mutation and the run. ## Tests and gates (head `af76d224c`) - `pnpm exec vitest run packages/components/ packages/plugin-view/ --maxWorkers=2` under the verify lock at `af76d224c` (the run printed that sha): `Test Files 379 passed | 1 skipped (380)`, `Tests 3689 passed | 24 skipped (3713)`. - `pnpm exec vitest run scripts/ --maxWorkers=2` under the verify lock at `af76d224c`: the whole `scripts/` test project, so every `scripts/__tests__` gate whose corpus reads a touched file (the changesets, the guide, the package sources) is in it: `Test Files 177 passed | 2 skipped (179)`, `Tests 5332 passed | 2 skipped (5334)`. - `type-check` for `@object-ui/components` and `@object-ui/plugin-view` (echoed `tsc --noEmit && tsc -p tsconfig.test.json`): Done, Done, at `2ca2478ee`, after `turbo run build` over both packages' dependency closures (15 tasks). The one later commit reflows comment and changeset prose only. `tsc -p tsconfig.test.json --listFiles` in `packages/components` names the new pin once, so the test program compiles it. - At `af76d224c`, each exit 0: `check:control-bytes`, `check:new-line-citations` (`0 new citation(s)`), `changeset:check`, `check-changeset-presence` (`3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)`), `check-changeset-overwrite` (reports the plugin-view changeset edit as a correction, its case 2), `check:changeset-claims`, `check:pending-changeset-literals`, `check:test-path-roots`, `check:phantom-deps`, `check:unused-deps`, `check:shell-escape-residue`, `check:doc-fences`, `check:doc-types`, `check:doc-example-ids`, `check:doc-example-readers`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:comment-mask-corpus`. - ESLint over the three changed source files, `--no-inline-config --format json`: 3 files, 0 errors. The new pin carries 0 warnings. `react-page.tsx` (6) and `ObjectView.tsx` (60) carry the same warnings per rule as their base blobs, read through `--stdin-filename`. This is a narrowed run, not the repo lint: the root ESLint config enables no type-aware linting (no `parserOptions.project`, no `projectService`), so this diff cannot move a verdict on an untouched file. `pnpm lint` is CI's. - `check:doc-snippets` and `check:doc-examples`: NOT MEASURED locally, prerequisite not met (they need the whole covered package set built). The guide's changed block is fenced `jsx`, which `check:doc-snippets` does not compile (its compiled fence set is `ts`, `tsx`, `typescript`). CI runs both. - `check-governed-queue-guard --test` over the 6 changed paths: NOT GOVERNED. ## One case the widened scope can break The scope's names become the parameters of the function the page source is evaluated in. A page that declares its own top-level `const` or `let` named `useDataInvalidation` therefore now fails with a `SyntaxError` (`Identifier 'useDataInvalidation' has already been declared`), shown in the page's error panel; a `function` or `var` declaration of that name only shadows the injected one. Measured once through the real renderer with a throwaway test (panel shown, message as quoted), not kept. Census: no page source in objectui (apps, examples, content, skills) or in objectstack `origin/main` (examples, content, skills, packages) declares the name. The changeset states this case. ## Serial Open PRs, read again just before this PR (12 open, every file list complete under 100 entries): none touches `react-page.tsx`, the `react-page-*` tests, `react-pages.md` or the `10887-*` changesets. One touches `ObjectView.tsx`: PR objectstack-ai#10967 (opened after this claim, route 2's named-view reads). `git merge-tree --write-tree` of this head against its head `600cf8901`: clean, exit 0. Its hunks and this PR's two comment hunks do not overlap. PR objectstack-ai#10777 touches `PageView.tsx`, its tests and its own changesets only. ## Acceptance notes - **(b), not filed.** `ObjectView`'s `fetchDrawsView` names the types it excludes (`grid`, `tree`, `chart`) rather than the types `generateViewSchema` draws, so a view type `generateViewSchema` has no case for (it returns `null`, and `renderContent` falls through to `ObjectGrid`, which queries for itself) still subscribes here and re-reads once per matching event, for rows nothing renders. Pre-existing shape of member 1; carrier: none. - **(c), not filed.** The non-grid fetch sets `loading` at the start of every run, including a bus re-read, and `ObjectCalendar` swaps to its loading placeholder while it is set, the same flash as the calendar's own objectui#10572 reader. A silent re-read would change behaviour; carrier: none. - **objectstack half, beyond objectstack-ai/objectstack#20395's named files.** The spec's generated react-blocks reference (`skills/objectstack-ui/references/react-blocks.md`, written by `packages/spec/scripts/build-react-blocks-contract.ts`) lists the injected scope as `React`, `useAdapter`, `data`, `variables`, `page`. After this lands that list is incomplete. objectstack-ai/objectstack#20395 names `react-pages.mdx`, `rules/pages.md` and the showcase pages, not this generator; it is the natural carrier. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #10887
Clause-②: no — members 1 and 2 add a re-read on the canonical bus to two existing readers and change no declared contract. Member 3 stops at
needs_decisionbefore any scope widening: this PR touches neither the react-page scope nor its guide.Part of, not a closing keyword: member 3 (thekind: 'react'page scope) measured shipped readers and stops atneeds_decision, as the dispatch order rules for that branch. Members 1 and 2 land here, and the card stays open for member 3.What changed
@object-ui/plugin-view.ObjectView's non-grid fetch effect (the one commented "Fetch data for non-grid view types") names theuseDataInvalidationnonce forschema.objectName. The hook sits directly above that effect, and the nonce is the last entry of the effect's dependency list. No second channel, no remount, no key change.@object-ui/plugin-dashboard.SelectFilterinDashboardFilterBarnames the nonce foroptionsFrom.objectin the options effect, on both reads (the dataset GROUP BY and the client-sidefindfallback). The effect never resetsdynamicOptionsand never writes the dashboard variable, so the options stay on screen until the re-read answers and the selected value is kept.patchchangeset per package:10887-object-view-non-grid-bus-readerand10887-dashboard-filter-options-bus-reader.renderContent(PR fix(plugin-view): the host delegation reads a named view's remaining protocol members off the named view (objectui#10758) #10884's region),PageView(PR fix(app-shell): PageView refreshes a page action’s data in place on the invalidation bus instead of remounting the page (objectui#10519) #10777), the react-page scope andreact-pagesguide.H1 — reproduced on base
35d68c4ce5, through the realSchemaRendererEach block is rendered in the page-region shape (
{ type, properties }forobject-view, adashboardnode withglobalFilters) underSchemaRendererProvider, over a data source that counts reads and implements noonMutation(a page action over raw HTTP announces none).'*'object-viewkanban, calendar, gallery, timeline, map, gantt — basefinddashboardoptionsFromfilter, dataset read — basequeryDatasetfindfallback — basefindBase failure text, quoted:
the rows never re-read after the bus reported a change: expected "vi.fn()" to be called 2 times, but got 1 times(everyobject-viewtype) andthe options never re-read after the bus reported a change: expected "vi.fn()" to be called 2 times, but got 1 times(both option reads).Lit controls, green on base and head in the same tree: a bare
useDataInvalidationreader moves to 1 on every event (asserted before the count), a gridobject-viewre-reads once throughObjectGrid's own reader, and anobject-metricblock beside the dashboard re-reads once through its own.H2 — seam
useDataInvalidation(fetchDrawsView && dataSource ? schema.objectName || undefined : undefined), wherefetchDrawsViewis "no hostrenderListView, and the view type is none ofgrid,tree,chart". Effect: the non-grid fetch, dependency list extended byinvalidationNonce.dataChangeMatches(change, schema.objectName)with no record id: true for'*', and for any change whoseobjectNameequals the view's, record-scoped or not. False for another object.renderListViewandgridare excluded becauseListViewandObjectGridread the bus themselves, and the effect body returns early for both anyway.treeandchart(host-only, reachable only through aviewsprop) are excluded because their renderers query for themselves and read the bus themselves:ObjectTreeruns its own query ahead of the rows handed to it (objectui#10778) and re-queries whenever that array changes, andObjectChartnever reads the rows. Subscribing there would add a read per event beside theirs (ablation 3 showsfind1 → 2 for each). This narrows the card's parenthetical list, which namestreeamong the fetch-driven views: the fetch does not drive the tree's rows once a data source is present.useDataInvalidation(from && dataSource && (queryDataset or find is a function) ? from.object || undefined : undefined). Effect: theoptionsFromoptions effect, dependency list extended byinvalidationNonce.dataChangeMatches(change, optionsFrom.object): true for'*'and for any change to that object. False for another object.optionsand nooptionsFromdoes not subscribe and reads nothing (pinned).H3 — member 3 census (the
kind: 'react'page scope)Enumerated from the committed trees, not a working tree: objectui
origin/main35d68c4ce5and objectstackorigin/main862b6ce869, over examples, docs, skills and templates.kind: 'react'pages that ship: 3, all in objectstack'sexamples/app-showcase(showcase_crm_workbench,showcase_renewals_pipeline,showcase_task_desk). objectui ships none inexamples/; its react pages live in thereact-pagesguide and the console's SDUI preview harnesses.useAdapter: 2 of 3.showcase_crm_workbench: the KPI strip readsadapter.find('showcase_project', { $top: 200 })in an effect keyed on the page's ownreloadKey, bumped only by its ownObjectFormonSuccess.showcase_renewals_pipeline: twoadapter.findreads (showcase_project,showcase_invoice) in an effect keyed on the selected account.showcase_task_desk: nouseAdapterread.react-pagesguide ("Live data" teachesadapter.find(...)in an effect keyed on[adapter], and the record-family table teachesuseAdapter().findOne); objectstackcontent/docs/ui/react-pages.mdx; objectstackskills/objectstack-ui/rules/pages.md.useAdapterpattern hits the scope's injection site in@object-ui/components' react-page renderer 7 times.Verdict: a shipped and taught react page reads data through the injected
useAdapter, so injectinguseDataInvalidationinto the author scope widens a public surface. Member 3 stops atneeds_decision, with the four-axis analysis in the dev report on the card. Nothing is injected and the guide is not edited here.Pins, red on base and green on head
ObjectView.busReread-10887.test.tsx(plugin-view) andDashboardFilterBar.busReread-10887.test.tsx(plugin-dashboard), both through the realSchemaRendererand each package's own registration. The inner non-grid views ofobject-vieware stand-ins registered in the real registry (plugin-view does not depend on the plugins that register them, and the rows areObjectView's read); each carries an instance id, so a remount would show.'*', a second after its own object, none after an unrelated object; forobject-view, the re-read rows reach the view and the view keeps its instance.object-viewwith no object and a filter with nooptionsFromread nothing on mount or on'*'; host-composedtreeandchartviews are not re-read by this fetch; the two lit controls above.Final head
56354a1c38:Tests 23 passed (23). The same committed pins over base's two source files (restored fromHEADafterwards, blob equal andgit diff HEADempty):Tests 17 failed | 6 passed (23), the 6 being the controls.Ablations (each through
ablation-replace: anchor hit once, blob changed, restored to theHEADblob,git diff HEADempty)ObjectView's fetch dependency listinvalidationNonce,Tests 12 failed | 11 passed (23): only member 1's rows (6 types x 2)SelectFilter's dependency list, invalidationNonce]);Tests 5 failed | 18 passed (23): only member 2's rowstree/chartexclusioncurrentViewTypecomparisonsTests 2 failed | 21 passed (23): only the two host-only controls,find1 → 2 eachThe pins import their subjects by relative path and
@object-ui/reactthrough the root vitest alias, so nodist/sits between the mutation and the run.Consumer sweep
plugin-viewsuite (under the verify lock):Test Files 53 passed (53),Tests 527 passed (527).plugin-dashboardsuite (under the verify lock):Test Files 147 passed (147),Tests 1339 passed | 6 skipped (1345).object-viewor adashboardwithglobalFilters, 9 files in one run:Test Files 9 passed (9),Tests 657 passed (657):timelineAxisRefusalReach-7070(console),catalog-gallery-renderandplugin-dashboard-gallery-render(schema-catalog),rowClickModifierPayloadForward-9462,ObjectView.densityModeThroughFold-7924,ViewPreview.optionsFold-7928,StudioDesignSurface.gridRefresh,DashboardWidgetInspector(app-shell),DashboardDesignPage.presetAlias(plugin-designer).main: none (0 test files underapps/orpackages/app-shellname 10519, at base and atorigin/main6a7f24e92c; the same grep overpackages/test files at base finds the 11 earlier reader pins, so the pattern is live).a70c7607c4. Every later commit touches only the two pin files, which were re-run at56354a1c38.Gates (head
56354a1c38)type-checkfor both packages (echoed astsc --noEmit && tsc -p tsconfig.test.json, the second program includes the pins): Done, Done.check:control-bytes,check:new-line-citations(0 new citation(s)),check:phantom-deps,check:unused-deps,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:shell-escape-residue,changeset:check,check-changeset-presence(4 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)): all exit 0.check-governed-queue-guard --testover the 6 changed paths:NOT GOVERNED.--no-inline-config --format json: 4 files, 0 errors; the pins carry 0 warnings. The two source files carry the same warnings per rule on base and head (ObjectView.tsx60,DashboardFilterBar.tsx18, read by linting each file's base blob through--stdin-filename). A narrowed run, not the repo lint: the root config enables no type-aware linting, so this diff cannot move a verdict on an untouched file.pnpm lintis CI's.turbo run buildover the^...closures of both packages, 16 tasks), becausetype-checkreads the built.d.ts.Serial
35d68c4ce5, which is this branch's base).git merge-tree --write-treeof this head against that PR's head5dc80e3998and againstorigin/main6a7f24e92c: both clean.ObjectViewhunks are the@object-ui/reactimport, the hook above the non-grid fetch effect, and that effect's dependency list, all aboverenderContent.DashboardFilterBarat claim time (claim record).Acceptance notes
object-view(kanban, plus calendar, gallery, timeline, map, gantt, all measured here) and for a dashboard with anoptionsFromfilter can now be green without the remount. The react-page row waits on the member 3 decision.renderContentcomment onidentityKeystill says the data-fed views are re-read "whenrefreshKeymoves". That is now incomplete (they also re-read on the bus) but not false, and it sits in the region this PR was told not to touch. Carrier: none.treeandchart,ObjectViewstill runs its non-grid fetch on mount and onrefreshKeyalthough neither renderer draws those rows. That is a read nobody uses; it predates this change and this change adds no read to it. Carrier: none.Generated by Claude Code