fix(plugin-tree): key ObjectTree's schema-settled gate to the bound object - #6696
Merged
Merged
Conversation
…bject
Adopt the shared `useSettledSchema` hook from `@object-ui/react` in
`ObjectTree`, replacing BOTH the unkeyed `schemaSettled` boolean and the
separate `objectSchema` state with the hook's single `{ key, def } | null`.
Two independent pieces of state cannot express "settled, but for a DIFFERENT
object". `schemaSettled` was a one-way latch set `true` in a `finally` and
never reset, so on an object switch the gate read `true` from the previous
object's settle while `objectSchema` still held the previous object's fields,
and the record query went out as
find(newObject, { $filter: …, $expand: [ …previous object's fields… ] })
Readiness is now derived during render by comparing the settled key against
the currently bound object, so the gate closes in the same commit that changes
the object rather than one commit later.
Deliberately unchanged: gate PLACEMENT stays inside the object-provider branch
of the record effect, and every exit still settles (no `getObjectSchema`, no
object name, or a rejected read each settle with no definition) so a tree whose
adapter serves no schema still queries instead of waiting forever.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c
Contributor
✅ 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
|
os-sales
marked this pull request as ready for review
August 28, 2026 16:09
os-sales
pushed a commit
that referenced
this pull request
Aug 28, 2026
…#6696 PM coordinator flagged (after this branch was already cut, and after ObjectTree.tsx had already been edited on this branch) that PR #6696 (objectui#6481, "key ObjectTree's schema-settled gate to the bound object") is open, ready, and rewrites this exact file: it replaces the objectSchema/schemaSettled state pair this branch also touched with a shared `useSettledSchema` hook, re-keyed onto a derived `schemaKey` primitive as an incidental improvement -- already satisfying this card's acceptance direction for that one effect. ObjectTree's SECOND effect (the record fetch, ObjectTree.tsx:468 on current main) still closes over bare `dataConfig` and is untouched by #6696 -- a real, confirmed remaining census member -- but #6696 is "ready and heading for the queue" per the coordinator, so this file is contested until it lands. Reverts ObjectTree.tsx to its origin/main content (verified byte-identical to origin/main @ e0d83da, which does not yet contain #6696) and drops the new ObjectTree.discardedConfigMemo.test.tsx pin along with it, so this branch edits a file #6696 is mid-flight on nowhere. The changeset drops the @object-ui/plugin-tree entry to match. plugin-map / plugin-calendar / plugin-gantt are unaffected and ship as originally verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c
This was referenced Aug 28, 2026
os-sales
pushed a commit
that referenced
this pull request
Aug 28, 2026
…nfig primitives (#6700) The record-fetch effect closed over `dataConfig`, a `useMemo` over the `schema` prop, whose identity `useMemo` carries no semantic guarantee for -- React may discard the cache and recompute even when `schema` is unchanged, and `getDataConfig(schema)` builds a fresh `{ provider, object }` / `{ provider, items }` wrapper object on every call. A discard alone was therefore enough to re-run the effect and issue an extra `dataSource.find` call, with nothing about the bound object actually different. Re-keys the effect onto the three primitive fields it actually reads off `dataConfig` (`provider`, and conditionally `object` / `items`) instead of the container object, mirroring the pattern already shipped for `ObjectMap`/`ObjectCalendar`/`ObjectGantt` (objectui#6592). This is the last `dataConfig`-identity dependence in the component: the schema-resolution effect was already re-keyed onto `useSettledSchema`'s primitive `schemaKey` by #6696. New pin (`ObjectTree.discardedConfigMemo.test.tsx`) simulates a discarded memo via a schema reference swap with identical primitive content (verified against this component's own `dataConfig = useMemo(..., [schema])`, which reliably recomputes on that swap) and asserts no extra `dataSource.find` call, alongside a counter-probe that a genuinely different `objectName` still refetches. Reverse-verified RED against the pre-fix source (2 calls observed vs. 1 expected) and GREEN after restoring the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6481
ObjectTreegated its record query on a schema-settled latch that was a bareboolean — never keyed by, nor reset for, the bound object. This adopts the
shared
useSettledSchemahook that landed for the #6482 ruling, replacingboth that boolean and the separate
objectSchemastate, and keepsObjectTree's own gate placement.The defect, reproduced on this component
Triage set the evidence class here to read-from-source, so the object switch was
measured on
ObjectTreeitself before a shape was adopted. #6453's and #6419'snumbers do not transfer: this component's record effect has its own dependency
set, and its gate sits inside the object-provider branch rather than at the
top of the effect.
The reproduction binds two objects whose expandable field sets are disjoint
apart from the parent pointer —
headonly onbusiness_unit,regiononly onterritory— withgetObjectSchemaheld open by a hand-resolved promise, so"not settled yet" is a real, observable state. Bind
business_unit, let itsettle, then swap the bound object to
territory. Measured onorigin/mainatdba7d8443:That array is the
$expandof a real query:find('territory', { $expand: ['parent_id', 'head'] }).headis not a fieldterritorydeclares. One suchquery per switch — rejected or silently ignored depending on the adapter, plus
the transient it paints — before the correct second query follows.
Before / after state shape
Before — two independent pieces of state:
setSchemaSettledwas only ever called withtrue, in afinally; nothing setit back. Two independent values cannot express "settled, but for a
different object" — so after a switch the gate read
trueleft over from theprevious object's settle while the definition still held the previous object's
fields.
After — one piece of state, readiness derived during render:
(Explicit type arguments elided in both snippets.) The hook holds a single
{ key, def } | nulland compares the settled key against the current key atrender time, so the gate closes in the same commit that changes the object
rather than one commit later. "Ready for the wrong object" is not merely
repaired but unwritable — there is no second piece of state left to disagree
with the first.
Deliberately unchanged: the gate did not move. It is still
if (!schemaSettled) return;inside the object-provider branch atObjectTree.tsx:422, and the record effect's dependency list is still at:468.The hook's own effect re-keys on the derived
schemaKeystring rather than ondataConfig, so a host that rebuilds its schema object each render no longerre-reads metadata for an object that never changed.
The settle-on-every-exit guarantee, preserved and pinned
The
finallyexisted so that the two earlyreturns (nodataSourceor nogetObjectSchema; no object name) and a rejected read all settled too —otherwise the gated record query waits forever and a tree whose adapter serves no
schema never renders a row. An adoption that lost this would trade one hang for
another.
The hook makes the guarantee structural rather than incidental: each of those
exits settles explicitly with
def: null, which the hook documents as an outcomedistinct from "not ready yet". Worth naming for review: after this change
ObjectTreecontains no settle code of its own, so its pins are consumer-sidepins of a provider-side guarantee. Both halves are pinned here, and ablation
leg B below shows they actually discriminate:
still queries when the adapter exposes no getObjectSchema (every exit settles)still queries when the schema read rejects (every exit settles)Ablation — prediction stated before each leg ran
Both legs mutate, prove the mutation landed on disk by anchored counts plus a
moved blob hash (never an editor's exit code), restore via
git checkout HEAD --against an absolute path from a
trap, and prove restoration by an emptygit diff HEADplus a matching non-emptygit hash-object.ObjectTree.tsxto the bare boolean (dba7d8443)$expandpin reds naming the switch; bothfinallypins stay green -> 1 failed / 19 passedexpected [ [ 'parent_id', 'head' ] ] to deeply equal []getObjectSchemapin reds; the rejected-read pin (a different exit) and the 4 pre-existing files stay green -> 1 failed / 19 passedstill queries when the adapter exposes no getObjectSchemaLeg A anchors: bare-boolean anchor 0 -> 1, hook-call anchor 1 -> 0, blob
941e7ae6->9b1d1e3f. Leg B anchors: settle-call count 2 -> 1, injectedmarker 1, blob
1d89c21a->67e278a2.Two notes rather than a tidy match:
finallypins green, which is the useful reading: theypin a property the bare boolean also satisfied, so they are not a restatement
of leg A and would have caught a regression leg A cannot.
ObjectTreefiles green. That is positiveevidence for gate placement: those files render inline/static data, and an
unsettled resolution does not hold branches that issue no metadata read.
Leg B necessarily mutated
packages/react/src/hooks/useSettledSchema.ts, which isoutside this PR's surface — after the adoption there is no settle code left in
plugin-treeto ablate. It was transient, inside this worktree only, and restoredbyte-identical (
1d89c21aboth sides, ablation marker count back to 0). This PRtouches no file under
packages/react.Gate table
Verdicts quoted from each gate's own printed line; exit captured by
redirect-then-capture, never after a pipe. The union below was re-run after
the final commit, at
91311f0c9.pnpm exec vitest run packages/plugin-tree/src/from repo root (objectui#3378)Test Files 5 passed (5)/Tests 20 passed (20)pnpm --filter @object-ui/plugin-tree type-checktsc --noEmit && tsc -p tsconfig.test.json,command-exit 0node scripts/check-control-bytes.mjs(new filegit add-ed first)check-control-bytes: OK (scanned 5547 tracked text file(s); skipped 85 binary)pnpm run changeset:checkAll workspace packages are in the changeset fixed group./No changeset declares a major bump.pnpm run check:phantom-depsEvery in-scope import is declared by the package that publishes it.pnpm run check:self-importNo package names itself inside its own src/.pnpm run check:vi-mock-specifierscheck-vi-mock-specifiers: OK (3907 tracked source file(s) …)pnpm run check:esm-specifiersno un-ledgered package emits an extensionless relative specifier(this script states its load leg was not run)eslint .inpackages/plugin-tree,--format json@typescript-eslint/no-explicit-any, the same rule and class as every sibling file here;lint.ymlsets no--max-warningsby designTypecheck coverage, not assumed.
tsconfig.jsonexcludes**/*.test.tsx, so"type-check is clean" would say nothing about the new test on its own. Counted via
--listFiles: the build program containsObjectTree.tsx(1) and not the test(0, by design); the
tsconfig.test.jsonprogram contains the new test (1). Thebuild program also reads
packages/react/dist/hooks/useSettledSchema.d.ts(1),so the adoption is checked against freshly rebuilt dependency typings — the
closure was built first (
pnpm --filter '@object-ui/plugin-tree^...' build,command-exit 0) becausepackages/react/distwas absent and both type-checklegs resolve
@object-ui/reactthrough it.Lint narrowing, declared. CI runs the whole farm via
turbo run lint; this runwas narrowed to
packages/plugin-tree, the only package the diff touches(
git diff --cached --name-only: two files there plus a.changesetmarkdown,which eslint does not lint). Population read from eslint's own
--format jsonoutput — 8 file entries — not from a guess about which files count. Invariance:
the flat config extends
tseslint.configs.recommendedwith noparserOptions.projector
projectServiceanywhere, so type-aware linting is off and no untouched file'sverdict can depend on this diff.
Heavy runs were serialized through the shared verify lock; every wrapper verdict
quoted above read
command-exit 0.Generated by Claude Code
Generated by Claude Code