Skip to content

fix: fail tree observation without an attached app and replace re-flushed roots - #59

Open
thymikee wants to merge 1 commit into
mainfrom
fix/no-app-attached-and-reflushed-roots
Open

fix: fail tree observation without an attached app and replace re-flushed roots#59
thymikee wants to merge 1 commit into
mainfrom
fix/no-app-attached-and-reflushed-roots

Conversation

@thymikee

Copy link
Copy Markdown
Member

What is this?

Two daemon-side fixes reported through callstack/agent-device#2430, where agent-react-devtools runs behind agent-device react-devtools as an agent-facing verification step.

  1. Tree observation without an attached app now fails. get tree, get component, find, count, errors and profile start returned an empty result with exit 0 when zero apps were connected. The response now carries reason: 'no-app-attached' and the CLI exits 1 with a message that says whether an app ever connected, and when it disconnected.
  2. A tree re-flushed by another DevTools backend replaces the frozen copy instead of being counted twice.

Why?

1. With 149 components attached and with nothing attached at all, errors printed the byte-identical No components with errors or warnings and exited 0. Read commands also call ensureDaemon(), so with the daemon down they silently started one and still reported a clean pass. An agent cannot tell "no problems" from "nothing was observed", and wait --connected was the only command that behaved.

2. When React Native DevTools opens (or any other client, such as a CDP session, attaches) while the daemon is connected, react-devtools-core runs initBackend again: attach() creates a new renderer interface with a fresh fiber-ID space, hook.rendererInterfaces.set(id, …) replaces the old one, and flushInitialOperations emits on the hook-wide operations channel that every agent subscribes to (sub('operations', agent.onHookOperations)). The daemon receives exactly one message: a full tree under a new root id, with no rendererAttached or backendInitialized. Measured on a real React Native 0.87.1 app with #58 applied: count went 149 → 298 after one attach, 1075 → 6450 after five on an Expo app, and find App returned one hit per attach. Because commits dispatch through rendererInterfaces.get(rendererID), the daemon's original root never updates again, so the fix has to replace it.

How does it work?

  • daemon.ts: requireAttachedApp() guards the observation commands and supersedes the old get-tree hint, which only fired when an app had previously connected.
  • component-tree.ts: reconcileReflushedRoot(rootId) drops an older root of the same renderer whose structure (type, display name, key, child order) matches. A structurally different second root is kept, so genuine multi-root apps are unaffected.
  • devtools-bridge.ts: runs the reconciliation only when a connection holds more than one root, so single-root apps pay nothing per operations batch.
  • types.ts: IpcFailureReason so callers key on a typed reason, not error text.

Verification

  • Unit: four new ComponentTree.reconcileReflushedRoot cases. e2e: five new no-app CLI cases, two reflush cases, and the disconnect test now expects the typed failure. daemon-auto-restart probed liveness with get tree and no app; it now uses profile slow, which still goes through ensureDaemon(). 129 unit and 36 e2e protocol tests pass.
  • Live, on a bare [email protected] app with feat: restore React Native DevTools connections #58 merged on top: attach at 149 components, two foreign attaches via Metro's inspector proxy leave count at 149 and find App --exact at one result; killing the app makes errors exit 1 with app disconnected 3s ago, waiting for reconnect.

Relation to #58

Independent of the transport. #58 restores attachment on React Native 0.87+; this PR makes what the daemon reports trustworthy once attached. Merging both leaves one trivial docs conflict in SKILL.md; I will rebase whichever lands second.

…shed roots

Observation commands (get tree, get component, find, count, errors,
profile start) returned an empty result with exit 0 when no React app was
attached, so "No components with errors or warnings" was indistinguishable
from a check that never ran. They now fail with a typed `no-app-attached`
reason and a message that says whether an app ever connected.

When another React DevTools backend attaches to the same app (React Native
DevTools opening, another agent), react-devtools-core creates a fresh
renderer interface with a new fiber-ID space and flushes the whole tree
through the hook-wide operations channel every agent subscribes to. Later
commits reach only that new interface, so the daemon's existing root is
frozen, not merely duplicated. The bridge now asks the tree to reconcile a
second root on the same connection: a root that structurally duplicates an
older root of the same renderer replaces it; a genuinely different root is
kept.

Reported through callstack/agent-device#2430.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant