fix(middleware): re-bind the React outbound channel when a session reconnects - #477
Merged
Merged
Conversation
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.
Description
Re-binds the React agent domain to the device when an Agent session reconnects, so React DevTools tools that have to send a message to the app keep working after the app restarts.
createReactDomainServicecalledstore.registerDevice(...)exactly once, at construction — that call is what stores the outboundsendMessage. On socket close,session.tscallsservice.onDisconnected(), which ranstore.unregisterDevice(...)and deleted the per-device state,sendMessageincluded. The session then recovers on its own, butLocalAgentToolServicehas noonConnectedhook and nothing re-ranregisterDevice. The state was recreated lazily by the next inbound message, withsendMessage === undefined.onDisconnectednow clears the stale tree, inspections and profiling state as before, then immediately re-binds the outbound channel.Related Issue
Closes #476
Context
This is what produced the exact inbound/outbound split in the issue. Inbound survived because
ingestReactDevToolsMessagerecreates the state on its own, sogetTree/getChildren/searchNodeskept returning a correct, freshly ingested tree. Outbound was dead because the bridge is created withsendMessage: state.sendMessage, sosend()became a silent no-op:getProps/getComponentcheck!state.sendMessageexplicitly and failed loudly with the reported unavailable-channel error.startProfilingflipped the local store to started and dropped the event, reporting{ok: true};stopProfilingset the phase toprocessingand dropped the event, so noprofilingDataever came back andisProcessingDatastayedtrueindefinitely.waitForProfilingDatabounds its own wait, but nothing ever cleared the phase.session createreportedconnectedbecause the session genuinely had reconnected — only the React domain's binding was silently dropped.Re-binding in
onDisconnectedis the smallest fix that holds:deps.sendReactDevToolsMessagecloses over the session'ssendDomainMessage, not over one socket generation, so it stays valid across recovery. Adding anonConnectedhook to the service contract and threading it through the session's recovery path would work too, but touches every local domain service for no additional benefit here.Two hardening items from the issue are deliberately left out to keep this focused on the root cause, and are worth separate changes: giving
startProfiling/stopProfilingthe same!state.sendMessageguardgetPropsalready has, and having the profiling store leave theprocessingphase on a bounded timeout rather than parking on it.Testing
Automated:
react domain service outbound channel > keeps sending to the device after the app reconnectstopackages/middleware/src/__tests__/agent-local-domains.test.ts. It ingests realoperationspayloads, callsonDisconnected(), ingests a fresh tree, and assertsgetPropsreaches the device andstartProfilingis actually sent. Confirmed it fails against the unfixed code withexpected [Function] to throw error matching /No props snapshot available/ but got 'React DevTools outbound channel is un…'.pnpm checks:affected— 107 tasks successful.pnpm test:affected— 69 tasks successful.Manual, on the running iPhone 17 Pro simulator with the playground:
simctl terminate+simctl launch,session createreturnedstatus: "connected",getTreereturned the new tree (roots renumbered244→1), andgetPropson a node from that new tree failed with the outbound-channel error.getTreereturns the fresh tree andgetPropson node 3 now returns real props.agent-devicebetween start and stop:stopProfilingreturned{"session":{"roots":[1],"totalCommits":4,"totalRenderDurationMs":42.075},...}— real commits, nopartialflag, noisProcessingDatahang.