[DevTools] Shut down standalone Bridge on socket close (#37076)
Ensures the standalone DevTools Bridge fully shuts down when its WebSocket closes, with re-entrancy protection. Adds tests confirming event-only shutdown leaves the Bridge active while socket closure shuts it down.
Ruslan Lesiutin committed
Jul 23, 2026 at 10:39 UTC
218efce013b05907e05ee80ab5d848f990fcf6b9
2 files changed
+70
-8
packages/react-devtools-core/src/backend.js
+12
-8
@@ -132,6 +132,16 @@ export function connectToDevTools(options: ?ConnectOptions) {
132
133
let bridge: BackendBridge | null = null;
134
135
+ function shutdownBridge(): void {
136
+ const bridgeToShutdown = bridge;
137
+ if (bridgeToShutdown !== null) {
138
+ // Clear the active reference before shutdown flushes its final message
139
+ // through a potentially closed socket.
140
+ bridge = null;
141
+ bridgeToShutdown.shutdown();
142
+ }
143
+ }
144
+
145
const messageListeners = [];
146
const uri = protocol + '://' + host + ':' + port + prefixedPath;
147
@@ -170,10 +180,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
180
);
181
}
182
173
- if (bridge !== null) {
174
- bridge.shutdown();
175
- }
176
-
183
+ shutdownBridge();
184
scheduleRetry();
185
}
186
},
@@ -273,10 +280,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
280
debug('WebSocket.onclose');
281
}
282
276
- if (bridge !== null) {
277
- bridge.emit('shutdown');
278
- }
279
-
283
+ shutdownBridge();
284
scheduleRetry();
285
}
286
packages/react-devtools-shared/src/__tests__/backend-test.js
new
+58
@@ -0,0 +1,58 @@
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
+ * @flow
8
+ */
9
+
10
+describe('connectToDevTools', () => {
11
+ let connectToDevTools;
12
+ let hook;
13
+
14
+ beforeEach(() => {
15
+ jest.resetModules();
16
+ delete window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
17
+
18
+ const backend = require('react-devtools-core/src/backend');
19
+ backend.initialize();
20
+ connectToDevTools = backend.connectToDevTools;
21
+ hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
22
+ });
23
+
24
+ afterEach(() => {
25
+ jest.clearAllTimers();
26
+ delete window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
27
+ });
28
+
29
+ function createWebSocket(): WebSocket {
30
+ return {
31
+ CLOSED: 3,
32
+ OPEN: 1,
33
+ readyState: 1,
34
+ send: jest.fn(),
35
+ } as any as WebSocket;
36
+ }
37
+
38
+ it('shuts down cleanly when the WebSocket closes', () => {
39
+ const websocket = createWebSocket();
40
+ const onShutdown = jest.fn();
41
+ const unsubscribe = hook.sub('shutdown', onShutdown);
42
+
43
+ try {
44
+ connectToDevTools({websocket});
45
+ websocket.onopen();
46
+ websocket.readyState = websocket.CLOSED;
47
+ websocket.onclose();
48
+ jest.runAllTimers();
49
+
50
+ expect(onShutdown).toHaveBeenCalledTimes(1);
51
+ expect(global.consoleWarnMock).not.toHaveBeenCalledWith(
52
+ 'Bridge was already shutdown.',
53
+ );
54
+ } finally {
55
+ unsubscribe();
56
+ }
57
+ });
58
+});