Skip to content

Commit eec9808

Browse files
committed
Improve Android packager reconnect messaging
1 parent 2ff3b81 commit eec9808

5 files changed

Lines changed: 177 additions & 2 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ public abstract class DevSupportManagerBase(
198198
private var isShakeDetectorStarted = false
199199
private var isDevSupportEnabled = false
200200
private var isPackagerConnected = false
201+
private val packagerConnectionStatusNotifier =
202+
PackagerConnectionStatusNotifier(devLoadingViewManager)
201203
private val errorCustomizers: MutableList<ErrorCustomizer> = mutableListOf()
202204
private var packagerLocationCustomizer: PackagerLocationCustomizer? = null
203205
private val jSExecutorDescription: String?
@@ -966,12 +968,14 @@ public abstract class DevSupportManagerBase(
966968
javaClass.simpleName,
967969
object : PackagerCommandListener {
968970
override fun onPackagerConnected() {
971+
packagerConnectionStatusNotifier.onPackagerConnected()
969972
isPackagerConnected = true
970973
perfMonitorOverlayManager?.enable()
971974
perfMonitorOverlayManager?.startBackgroundTrace()
972975
}
973976

974977
override fun onPackagerDisconnected() {
978+
packagerConnectionStatusNotifier.onPackagerDisconnected()
975979
isPackagerConnected = false
976980
perfMonitorOverlayManager?.disable()
977981
perfMonitorOverlayManager?.stopBackgroundTrace()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.devsupport
9+
10+
import com.facebook.react.devsupport.interfaces.DevLoadingViewManager
11+
12+
internal class PackagerConnectionStatusNotifier(
13+
private val devLoadingViewManager: DevLoadingViewManager?
14+
) {
15+
private var hasConnected = false
16+
private var connectionLost = false
17+
18+
fun onPackagerConnected() {
19+
if (connectionLost) {
20+
devLoadingViewManager?.showMessage(RECONNECTED_MESSAGE)
21+
}
22+
hasConnected = true
23+
connectionLost = false
24+
}
25+
26+
fun onPackagerDisconnected() {
27+
if (hasConnected && !connectionLost) {
28+
connectionLost = true
29+
devLoadingViewManager?.showMessage(CONNECTION_LOST_MESSAGE)
30+
}
31+
}
32+
33+
private companion object {
34+
const val CONNECTION_LOST_MESSAGE = "Connection to Metro lost. Retrying..."
35+
const val RECONNECTED_MESSAGE = "Reconnected to Metro."
36+
}
37+
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public class ReconnectingWebSocket(
4242
private val okHttpClient = DevSupportHttpClient.websocketClient
4343
private var closed = false
4444
private var suppressConnectionErrors = false
45+
private var connected = false
4546
private var webSocket: WebSocket? = null
4647

4748
public fun connect() {
@@ -95,6 +96,7 @@ public class ReconnectingWebSocket(
9596
@Synchronized
9697
override fun onOpen(webSocket: WebSocket, response: Response) {
9798
this.webSocket = webSocket
99+
connected = true
98100
suppressConnectionErrors = false
99101

100102
connectionCallback?.onConnected()
@@ -106,7 +108,7 @@ public class ReconnectingWebSocket(
106108
abort("Websocket exception", t)
107109
}
108110
if (!closed) {
109-
connectionCallback?.onDisconnected()
111+
notifyDisconnectedIfConnected()
110112
reconnect()
111113
}
112114
}
@@ -125,11 +127,18 @@ public class ReconnectingWebSocket(
125127
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
126128
this.webSocket = null
127129
if (!closed) {
128-
connectionCallback?.onDisconnected()
130+
notifyDisconnectedIfConnected()
129131
reconnect()
130132
}
131133
}
132134

135+
private fun notifyDisconnectedIfConnected() {
136+
if (connected) {
137+
connected = false
138+
connectionCallback?.onDisconnected()
139+
}
140+
}
141+
133142
@Synchronized
134143
@Throws(IOException::class)
135144
public fun sendMessage(message: String) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.devsupport
9+
10+
import com.facebook.react.devsupport.interfaces.DevLoadingViewManager
11+
import org.assertj.core.api.Assertions.assertThat
12+
import org.junit.Test
13+
14+
class PackagerConnectionStatusNotifierTest {
15+
16+
private val devLoadingViewManager = RecordingDevLoadingViewManager()
17+
private val notifier = PackagerConnectionStatusNotifier(devLoadingViewManager)
18+
19+
@Test
20+
fun testInitialConnectionDoesNotShowReconnectedMessage() {
21+
notifier.onPackagerConnected()
22+
23+
assertThat(devLoadingViewManager.messages).isEmpty()
24+
}
25+
26+
@Test
27+
fun testLostConnectionShowsRetryingOnceUntilReconnect() {
28+
notifier.onPackagerConnected()
29+
30+
notifier.onPackagerDisconnected()
31+
notifier.onPackagerDisconnected()
32+
33+
assertThat(devLoadingViewManager.messages)
34+
.containsExactly("Connection to Metro lost. Retrying...")
35+
}
36+
37+
@Test
38+
fun testReconnectAfterLossShowsReconnectedMessage() {
39+
notifier.onPackagerConnected()
40+
notifier.onPackagerDisconnected()
41+
42+
notifier.onPackagerConnected()
43+
44+
assertThat(devLoadingViewManager.messages)
45+
.containsExactly("Connection to Metro lost. Retrying...", "Reconnected to Metro.")
46+
}
47+
48+
private class RecordingDevLoadingViewManager : DevLoadingViewManager {
49+
val messages = mutableListOf<String>()
50+
51+
override fun showMessage(message: String) {
52+
messages.add(message)
53+
}
54+
55+
override fun showMessage(
56+
message: String,
57+
color: Double?,
58+
backgroundColor: Double?,
59+
dismissButton: Boolean?,
60+
) {
61+
messages.add(message)
62+
}
63+
64+
override fun updateProgress(status: String?, done: Int?, total: Int?, percent: Int?) = Unit
65+
66+
override fun hide() = Unit
67+
}
68+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.packagerconnection
9+
10+
import com.facebook.react.packagerconnection.ReconnectingWebSocket.ConnectionCallback
11+
import java.io.IOException
12+
import okhttp3.Response
13+
import okhttp3.WebSocket
14+
import org.junit.Test
15+
import org.junit.runner.RunWith
16+
import org.mockito.kotlin.mock
17+
import org.mockito.kotlin.never
18+
import org.mockito.kotlin.times
19+
import org.mockito.kotlin.verify
20+
import org.robolectric.RobolectricTestRunner
21+
22+
@RunWith(RobolectricTestRunner::class)
23+
class ReconnectingWebSocketTest {
24+
25+
@Test
26+
fun testConnectionFailureBeforeOpenDoesNotNotifyDisconnected() {
27+
val connectionCallback = mock<ConnectionCallback>()
28+
val reconnectingWebSocket = createWebSocket(connectionCallback)
29+
30+
reconnectingWebSocket.onFailure(mock<WebSocket>(), IOException("failed"), null)
31+
32+
verify(connectionCallback, never()).onConnected()
33+
verify(connectionCallback, never()).onDisconnected()
34+
}
35+
36+
@Test
37+
fun testConnectionFailureAfterOpenNotifiesDisconnectedOnceUntilReconnect() {
38+
val connectionCallback = mock<ConnectionCallback>()
39+
val reconnectingWebSocket = createWebSocket(connectionCallback)
40+
val webSocket = mock<WebSocket>()
41+
42+
reconnectingWebSocket.onOpen(webSocket, mock<Response>())
43+
reconnectingWebSocket.onFailure(webSocket, IOException("failed"), null)
44+
reconnectingWebSocket.onFailure(mock<WebSocket>(), IOException("retry failed"), null)
45+
reconnectingWebSocket.onOpen(mock<WebSocket>(), mock<Response>())
46+
47+
verify(connectionCallback, times(2)).onConnected()
48+
verify(connectionCallback, times(1)).onDisconnected()
49+
}
50+
51+
private fun createWebSocket(connectionCallback: ConnectionCallback): ReconnectingWebSocket =
52+
ReconnectingWebSocket(
53+
"ws://localhost:8081/message?role=android",
54+
messageCallback = null,
55+
connectionCallback = connectionCallback,
56+
)
57+
}

0 commit comments

Comments
 (0)