Skip to content

Commit 66253d3

Browse files
committed
feat: enhance Reactotron server connection handling and UI updates
- Added .eslintignore to exclude bundled files from linting. - Removed Reactotron server start command from metro.config.js. - Updated package.json to change the output format of the bundled server. - Improved connection status handling in SidebarMenu with manual reconnect functionality. - Enhanced logging in standalone-server.js for better connection tracking and error handling. - Implemented reconnect logic in connectToServer.ts to manage connection attempts and status updates. - Updated AppDelegate.mm to use the user's default shell for running commands, improving compatibility with nvm/asdf setups. - Added new patch for reactotron-core-server to address compatibility issues.
1 parent c8d0246 commit 66253d3

File tree

11 files changed

+237
-61
lines changed

11 files changed

+237
-61
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.bundle.js

app/components/Sidebar/SidebarMenu.tsx

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Animated, View, ViewStyle, Pressable, TextStyle, Text } from "react-native"
22
import { themed, useTheme, useThemeName } from "../../theme/theme"
33
import { useGlobal } from "../../state/useGlobal"
4+
import { manualReconnect } from "../../state/connectToServer"
45
import { Icon } from "../Icon"
56

67
const MENU_ITEMS = [
@@ -24,6 +25,8 @@ export const SidebarMenu = ({ progress, mounted, collapsedWidth }: SidebarMenuPr
2425
const theme = useTheme()
2526
const [themeName, setTheme] = useThemeName()
2627
const [isConnected] = useGlobal("isConnected", false)
28+
const [connectionStatus] = useGlobal<string>("connectionStatus", "Disconnected")
29+
const [clientIds] = useGlobal<string[]>("clientIds", [])
2730
const [error] = useGlobal("error", null)
2831
const arch = (global as any)?.nativeFabricUIManager ? "Fabric" : "Paper"
2932

@@ -92,7 +95,12 @@ export const SidebarMenu = ({ progress, mounted, collapsedWidth }: SidebarMenuPr
9295
})}
9396
</View>
9497
<View>
95-
<View style={[$menuItem(), $statusItemContainer]}>
98+
<Pressable
99+
style={({ pressed }) => [$menuItem(), $statusItemContainer, pressed && $menuItemPressed]}
100+
onPress={manualReconnect}
101+
accessibilityRole="button"
102+
accessibilityLabel={`Connection status: ${connectionStatus}. Tap to retry.`}
103+
>
96104
<View style={[{ width: iconColumnWidth }, $iconColumn()]}>
97105
<View
98106
style={[
@@ -104,17 +112,22 @@ export const SidebarMenu = ({ progress, mounted, collapsedWidth }: SidebarMenuPr
104112
</View>
105113

106114
{mounted && (
107-
<Animated.Text
108-
style={[$menuItemText(), { opacity: labelOpacity }]}
109-
numberOfLines={1}
110-
ellipsizeMode="clip"
111-
accessibilityElementsHidden={!mounted}
112-
importantForAccessibility={mounted ? "auto" : "no-hide-descendants"}
113-
>
114-
Connection
115-
</Animated.Text>
115+
<Animated.View style={[$connectionContainer, { opacity: labelOpacity }]}>
116+
<Animated.Text
117+
style={[$menuItemText(), $connectionStatusText()]}
118+
numberOfLines={2}
119+
ellipsizeMode="tail"
120+
accessibilityElementsHidden={!mounted}
121+
importantForAccessibility={mounted ? "auto" : "no-hide-descendants"}
122+
>
123+
{connectionStatus}
124+
</Animated.Text>
125+
{isConnected && clientIds.length === 0 && (
126+
<Text style={[$menuItemText(), $helpText()]}>Port 9292</Text>
127+
)}
128+
</Animated.View>
116129
)}
117-
</View>
130+
</Pressable>
118131
<View style={[$menuItem(), $statusItemContainer]}>
119132
<View style={[{ width: iconColumnWidth }, $iconColumn()]}>
120133
<View
@@ -218,4 +231,15 @@ const $statusText = themed<TextStyle>(({ colors }) => ({
218231
fontWeight: "600",
219232
marginLeft: -4,
220233
}))
221-
const $statusItemContainer: ViewStyle = { cursor: "default", height: 32 }
234+
const $connectionStatusText = themed<TextStyle>(() => ({
235+
fontSize: 11,
236+
lineHeight: 14,
237+
}))
238+
const $helpText = themed<TextStyle>(({ colors }) => ({
239+
fontSize: 10,
240+
lineHeight: 12,
241+
color: colors.neutral,
242+
marginTop: 2,
243+
}))
244+
const $connectionContainer: ViewStyle = { flex: 1 }
245+
const $statusItemContainer: ViewStyle = { cursor: "pointer", minHeight: 32 }

app/state/connectToServer.ts

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ export const getReactotronAppId = () => {
1616
return reactotronAppId
1717
}
1818

19+
let reconnectAttempts = 0
20+
let reconnectTimeout: NodeJS.Timeout | null = null
21+
const MAX_RECONNECT_ATTEMPTS = 10
22+
const INITIAL_RECONNECT_DELAY = 1000 // 1 second
23+
1924
/**
2025
* Connects to the reactotron-core-server via websocket.
2126
*
2227
* Populates the following global state:
2328
* - isConnected: boolean
29+
* - connectionStatus: string (Connecting, Connected, Disconnected, Retrying...)
2430
* - error: Error | null
2531
* - clientIds: string[]
2632
* - timelineItems: TimelineItem[]
@@ -30,6 +36,7 @@ export const getReactotronAppId = () => {
3036
export function connectToServer(props: { port: number } = { port: 9292 }): UnsubscribeFn {
3137
const reactotronAppId = getReactotronAppId()
3238
const [_c, setIsConnected] = withGlobal("isConnected", false)
39+
const [_cs, setConnectionStatus] = withGlobal<string>("connectionStatus", "Connecting...")
3340
const [_e, setError] = withGlobal<Error | null>("error", null)
3441
const [clientIds, setClientIds] = withGlobal<string[]>("clientIds", [])
3542
const [, setActiveClientId] = withGlobal("activeClientId", "")
@@ -38,11 +45,36 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
3845
[clientId: string]: StateSubscription[]
3946
}>("stateSubscriptionsByClientId", {})
4047

41-
ws.socket = new WebSocket(`ws://localhost:${props.port}`)
42-
if (!ws.socket) throw new Error("Failed to connect to Reactotron server")
48+
// Clear any existing reconnect timeout
49+
if (reconnectTimeout) {
50+
clearTimeout(reconnectTimeout)
51+
reconnectTimeout = null
52+
}
53+
54+
setConnectionStatus("Connecting...")
55+
setError(null)
56+
57+
try {
58+
ws.socket = new WebSocket(`ws://localhost:${props.port}`)
59+
} catch (error) {
60+
setError(error as Error)
61+
setConnectionStatus("Failed to connect")
62+
scheduleReconnect(props)
63+
return () => {}
64+
}
65+
66+
if (!ws.socket) {
67+
setError(new Error("Failed to create WebSocket"))
68+
setConnectionStatus("Failed to connect")
69+
scheduleReconnect(props)
70+
return () => {}
71+
}
4372

4473
// Tell the server we are a Reactotron app, not a React client.
4574
ws.socket.onopen = () => {
75+
reconnectAttempts = 0 // Reset on successful connection
76+
setConnectionStatus("Connected")
77+
setError(null)
4678
ws.socket?.send(
4779
JSON.stringify({
4880
type: "reactotron.subscribe",
@@ -54,16 +86,25 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
5486
}
5587

5688
// Handle errors
57-
ws.socket.onerror = (event) => setError(new Error(`WebSocket error: ${event.message}`))
89+
ws.socket.onerror = (event) => {
90+
const errorMsg = event.message || "Connection failed"
91+
setError(new Error(`WebSocket error: ${errorMsg}`))
92+
setConnectionStatus(`Error: ${errorMsg}`)
93+
}
5894

5995
// Handle messages coming from the server, intended to be sent to the client or Reactotron app.
6096
ws.socket.onmessage = (event) => {
6197
const data = JSON.parse(event.data)
98+
console.tron.log("Received message from server:", data.type)
6299

63-
if (data.type === "reactotron.connected") setIsConnected(true)
100+
if (data.type === "reactotron.connected") {
101+
console.tron.log("Reactotron app marked as connected")
102+
setIsConnected(true)
103+
}
64104

65105
if (data.type === "connectionEstablished") {
66106
const clientId = data?.conn?.clientId
107+
console.tron.log("connectionEstablished event for client:", clientId)
67108
if (!clientIds.includes(clientId)) {
68109
setClientIds((prev) => [...prev, clientId])
69110
setActiveClientId(clientId)
@@ -75,6 +116,12 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
75116
}
76117

77118
if (data.type === "connectedClients") {
119+
console.tron.log(
120+
"connectedClients event. Count:",
121+
data.clients?.length,
122+
"Clients:",
123+
data.clients,
124+
)
78125
let newestClientId = ""
79126
data.clients.forEach((client: any) => {
80127
// Store the client data in global state
@@ -89,6 +136,7 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
89136

90137
if (newestClientId) {
91138
// Set the active client to the newest client
139+
console.tron.log("Setting active client to:", newestClientId)
92140
setActiveClientId(newestClientId)
93141
}
94142
}
@@ -159,8 +207,9 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
159207
}
160208

161209
// Clean up after disconnect
162-
ws.socket.onclose = () => {
163-
console.tron.log("Reactotron server disconnected")
210+
ws.socket.onclose = (event) => {
211+
console.tron.log("Reactotron server disconnected", event.code, event.reason)
212+
164213
// Clear individual client data
165214
clientIds.forEach((clientId) => {
166215
const [_, setClientData] = withGlobal(`client-${clientId}`, {})
@@ -172,6 +221,14 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
172221
setActiveClientId("")
173222
setTimelineItems([])
174223
setStateSubscriptionsByClientId({})
224+
225+
// Only attempt reconnect if it wasn't a normal close
226+
if (event.code !== 1000) {
227+
setConnectionStatus("Disconnected")
228+
scheduleReconnect(props)
229+
} else {
230+
setConnectionStatus("Disconnected")
231+
}
175232
}
176233

177234
// Send a message to the server (which will be forwarded to the client)
@@ -181,9 +238,47 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
181238
}
182239

183240
return () => {
184-
ws.socket?.close()
241+
if (reconnectTimeout) {
242+
clearTimeout(reconnectTimeout)
243+
reconnectTimeout = null
244+
}
245+
reconnectAttempts = 0
246+
ws.socket?.close(1000) // Normal closure
247+
ws.socket = null
248+
}
249+
}
250+
251+
function scheduleReconnect(props: { port: number }) {
252+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
253+
const [_, setConnectionStatus] = withGlobal<string>("connectionStatus", "")
254+
setConnectionStatus(`Failed after ${MAX_RECONNECT_ATTEMPTS} attempts`)
255+
return
256+
}
257+
258+
reconnectAttempts++
259+
const delay = Math.min(INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1), 30000)
260+
const [_, setConnectionStatus] = withGlobal<string>("connectionStatus", "")
261+
setConnectionStatus(
262+
`Retrying in ${Math.round(delay / 1000)}s... (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`,
263+
)
264+
265+
reconnectTimeout = setTimeout(() => {
266+
console.tron.log(`Reconnecting... attempt ${reconnectAttempts}`)
267+
connectToServer(props)
268+
}, delay)
269+
}
270+
271+
export function manualReconnect() {
272+
reconnectAttempts = 0
273+
if (reconnectTimeout) {
274+
clearTimeout(reconnectTimeout)
275+
reconnectTimeout = null
276+
}
277+
if (ws.socket) {
278+
ws.socket.close(1000)
185279
ws.socket = null
186280
}
281+
connectToServer()
187282
}
188283

189284
export function sendToClient(message: string | object, payload?: object, clientId?: string) {

app/state/useGlobal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ function buildSetValue<T>(id: string, persist: boolean) {
113113
}
114114

115115
export function deleteGlobal(id: string): void {
116-
delete globals[id]
116+
delete _globals[id]
117117
}
118118

119119
/**

0 commit comments

Comments
 (0)