@samitouri / QOS-React / commits / 218efce013

[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 +});