Skip to content

fix(sidebar): clear provider metadata synchronously when connection changes - #848

Merged
cevheri merged 1 commit into
libredb:mainfrom
Asgabani:fix/tree-nodes-connection-switch
Sep 16, 2026
Merged

cevheri merged 1 commit into
libredb:mainfrom
Asgabani:fix/tree-nodes-connection-switch

Conversation

@Asgabani

Copy link
Copy Markdown
Contributor

Fixes #846.

Root cause

useProviderMetadata reset the previous connection's metadata (and thus its
capabilities) to null inside a useEffect. That effect only fires after
React commits the render, but the connection prop it's keyed on changes
during the render itself. So for exactly one committed render, this hook
still returns the previous connection's capabilities while connection
already points at the new one.

Sidebar passes both straight through to ObjectTree:

{metadata ? (
  <ObjectTree connection={activeConnection} capabilities={metadata.capabilities} ... />
) : ...}

and useTreeNodes derives the shape of its very first read (depth, via
containerDepth(capabilities)) from capabilities alone — it has no way to
know those capabilities don't belong to connection yet. When the two
engines have different container depths, that one render is enough to issue
a root read shaped for the wrong engine, which the route correctly refuses
with HTTP 400 before the next render corrects it.

Fix

Move the reset from the effect into a render-time state update — React's own
documented escape hatch for "reset some state when a prop changes": calling
setState conditionally during render discards that render's output and
re-runs the component immediately with the cleared state, so the mismatched
(new connection, old capabilities) pair is never committed to the DOM or
handed to a child. The effect still exists for the async fetch itself; the
render-time branch only pulls the "clear it now" half one render earlier.

Testing

Added a regression test in tests/hooks/use-provider-metadata.test.ts that
mounts a real child component alongside the hook (rather than only asserting
on result.current via renderHook, which only observes state once React
has already flushed effects — exactly the render this bug disappears from).
It fails on main — the child is rendered once with the new connection's id
paired with the previous connection's supportsInlineRowEdit — and passes
after the fix.

Ran the full local gate: format, lint, typecheck, knip, all four
drift-check scripts, the full test suite (537 files / 17469 passing), the
100% line-coverage gate, and build — all green.

I did not attempt the issue's "open question" about read-ordering (whether a
late-arriving refusal could overwrite an already-filled slot) — this fix
removes the mismatched read entirely, so that ordering question no longer
has anything to land on for this path.

…hanges (libredb#846)

useProviderMetadata reset the previous connection's capabilities to
null inside an effect, which commits one render after the connection
prop itself changes. In that one committed render, Sidebar hands
ObjectTree the NEW connection paired with the PREVIOUS connection's
capabilities, and useTreeNodes derives the shape of its first read
from capabilities alone - so a connection switch between engines of
different container depth sends one read built for the wrong engine
before self-healing on the next render.

Move the reset into a render-time state update (React's documented
pattern for this exact case) so the mismatched pair is never
committed, and add a regression test that renders a child component
alongside the hook to catch it.
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri

cevheri commented Sep 15, 2026

Copy link
Copy Markdown
Member

Thanks @Asgabani
before this, can you check this pr: #821 (asmall changes)

@cevheri cevheri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Measured it the way the issue was, with only your hook file swapped between the arms. On main the first object read is POST /api/db/objects/counts carrying container: [], refused with 400. On your head that call is never made: containers 200, then counts ["public"] 200.

Your test is real. Reverting only the source file fails exactly that test and leaves the other 17 green, with the precise pair #846 describes: conn-2 carrying conn-1's supportsInlineRowEdit.

The equal-depth control the issue asked for holds too. PostgreSQL to MySQL still issues containers and counts within about 120 ms of the metadata answer, so the correctness cost no slower switch.

Main has moved since you branched, so I test-merged and ran the whole suite on the result: 543 files, 543 pass.

One aside, no change wanted: readKey carries attempt, so the render branch fires on a retry as well and the two reset lines left in the effect are redundant. The new comment says the opposite. I will adjust that sentence myself.

Merging this. Thank you.

@cevheri cevheri added the enhancement New feature or request label Sep 16, 2026
@cevheri

cevheri commented Sep 16, 2026

Copy link
Copy Markdown
Member

small notes for your next PR: please write a comment on releated-issue(#846 ), before create PR

@cevheri
cevheri merged commit 0b03ce4 into libredb:main Sep 16, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Object tree issues the first catalog read with the previous connection's container depth

2 participants