@samitouri / QOS-React / commits / 63b86e1995

Disable unsupported Bridge protocol version dialog and add workaround for old protocol operations format (#24093)

Rationale: The only case where the unsupported dialog really matters is React Naive. That's the case where the frontend and backend versions are most likely to mismatch. In React Native, the backend is likely to send the bridge protocol version before sending operations– since the agent does this proactively during initialization. I've tested the React Native starter app– after forcefully downgrading the backend version to 4.19.1 (see #23307 (comment)) and verified that this change "fixes" things. Not only does DevTools no longer throw an error that causes the UI to be hidden– it works (meaning that the Components tree can be inspected and interacted with).

Brian Vaughn committed Mar 15, 2022 at 10:48 UTC 63b86e19955a3f68c3e6e4928f4e5b24fd8a8342
5 files changed +43 -25
packages/react-devtools-core/src/standalone.js
+2 -2
@@ -11,7 +11,7 @@ import {createElement} from 'react';
11 import {
12 // $FlowFixMe Flow does not yet know about flushSync()
13 flushSync,
14 -} from 'react-dom/client';
14 +} from 'react-dom';
15 import {createRoot} from 'react-dom/client';
16 import Bridge from 'react-devtools-shared/src/bridge';
17 import Store from 'react-devtools-shared/src/devtools/store';
@@ -106,9 +106,9 @@ function safeUnmount() {
106 flushSync(() => {
107 if (root !== null) {
108 root.unmount();
109 + root = null;
110 }
111 });
111 - root = null;
112 }
113
114 function reload() {
packages/react-devtools-shared/src/bridge.js
+3
@@ -54,6 +54,9 @@ export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
54 minNpmVersion: '"<4.11.0"',
55 maxNpmVersion: '"<4.11.0"',
56 },
57 + // Versions 4.11.x – 4.12.x contained the backwards breaking change,
58 + // but we didn't add the "fix" of checking the protocol version until 4.13,
59 + // so we don't recommend downgrading to 4.11 or 4.12.
60 {
61 version: 1,
62 minNpmVersion: '4.13.0',
packages/react-devtools-shared/src/devtools/store.js
+28 -14
@@ -180,7 +180,8 @@ export default class Store extends EventEmitter<{|
180 _rootSupportsBasicProfiling: boolean = false;
181 _rootSupportsTimelineProfiling: boolean = false;
182
183 - _unsupportedBridgeProtocol: BridgeProtocol | null = null;
183 + _bridgeProtocol: BridgeProtocol | null = null;
184 + _unsupportedBridgeProtocolDetected: boolean = false;
185 _unsupportedRendererVersionDetected: boolean = false;
186
187 // Total number of visible elements (within all roots).
@@ -375,6 +376,10 @@ export default class Store extends EventEmitter<{|
376 this.emit('componentFilters');
377 }
378
379 + get bridgeProtocol(): BridgeProtocol | null {
380 + return this._bridgeProtocol;
381 + }
382 +
383 get errorCount(): number {
384 return this._cachedErrorCount;
385 }
@@ -466,8 +471,8 @@ export default class Store extends EventEmitter<{|
471 return this._supportsTraceUpdates;
472 }
473
469 - get unsupportedBridgeProtocol(): BridgeProtocol | null {
470 - return this._unsupportedBridgeProtocol;
474 + get unsupportedBridgeProtocolDetected(): boolean {
475 + return this._unsupportedBridgeProtocolDetected;
476 }
477
478 get unsupportedRendererVersionDetected(): boolean {
@@ -942,11 +947,21 @@ export default class Store extends EventEmitter<{|
947 (operations[i] & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0;
948 i++;
949
945 - const supportsStrictMode = operations[i] > 0;
946 - i++;
950 + let supportsStrictMode = false;
951 + let hasOwnerMetadata = false;
952
948 - const hasOwnerMetadata = operations[i] > 0;
949 - i++;
953 + // If we don't know the bridge protocol, guess that we're dealing with the latest.
954 + // If we do know it, we can take it into consideration when parsing operations.
955 + if (
956 + this._bridgeProtocol === null ||
957 + this._bridgeProtocol.version >= 2
958 + ) {
959 + supportsStrictMode = operations[i] > 0;
960 + i++;
961 +
962 + hasOwnerMetadata = operations[i] > 0;
963 + i++;
964 + }
965
966 this._roots = this._roots.concat(id);
967 this._rootIDToRendererID.set(id, rendererID);
@@ -1383,14 +1398,13 @@ export default class Store extends EventEmitter<{|
1398 this._onBridgeProtocolTimeoutID = null;
1399 }
1400
1401 + this._bridgeProtocol = bridgeProtocol;
1402 +
1403 if (bridgeProtocol.version !== currentBridgeProtocol.version) {
1387 - this._unsupportedBridgeProtocol = bridgeProtocol;
1388 - } else {
1389 - // If we should happen to get a response after timing out...
1390 - this._unsupportedBridgeProtocol = null;
1404 + // Technically newer versions of the frontend can, at least for now,
1405 + // gracefully handle older versions of the backend protocol.
1406 + // So for now we don't need to display the unsupported dialog.
1407 }
1392 -
1393 - this.emit('unsupportedBridgeProtocolDetected');
1408 };
1409
1410 onBridgeProtocolTimeout = () => {
@@ -1398,7 +1412,7 @@ export default class Store extends EventEmitter<{|
1412
1413 // If we timed out, that indicates the backend predates the bridge protocol,
1414 // so we can set a fake version (0) to trigger the downgrade message.
1401 - this._unsupportedBridgeProtocol = BRIDGE_PROTOCOL[0];
1415 + this._bridgeProtocol = BRIDGE_PROTOCOL[0];
1416
1417 this.emit('unsupportedBridgeProtocolDetected');
1418 };
packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js
+3 -5
@@ -33,20 +33,18 @@ export default function UnsupportedBridgeProtocolDialog(_: {||}) {
33 useEffect(() => {
34 const updateDialog = () => {
35 if (!isVisible) {
36 - if (store.unsupportedBridgeProtocol !== null) {
36 + if (store.unsupportedBridgeProtocolDetected) {
37 dispatch({
38 canBeDismissed: false,
39 id: MODAL_DIALOG_ID,
40 type: 'SHOW',
41 content: (
42 - <DialogContent
43 - unsupportedBridgeProtocol={store.unsupportedBridgeProtocol}
44 - />
42 + <DialogContent unsupportedBridgeProtocol={store.bridgeProtocol} />
43 ),
44 });
45 }
46 } else {
49 - if (store.unsupportedBridgeProtocol === null) {
47 + if (!store.unsupportedBridgeProtocolDetected) {
48 dispatch({
49 type: 'HIDE',
50 id: MODAL_DIALOG_ID,
packages/react-devtools-shared/src/devtools/views/hooks.js
+7 -4
@@ -239,10 +239,13 @@ export function useModalDismissSignal(
239 // It's important to listen to the ownerDocument to support the browser extension.
240 // Here we use portals to render individual tabs (e.g. Profiler),
241 // and the root document might belong to a different window.
242 - ownerDocument = ((modalRef.current: any): HTMLDivElement).ownerDocument;
243 - ownerDocument.addEventListener('keydown', handleDocumentKeyDown);
244 - if (dismissOnClickOutside) {
245 - ownerDocument.addEventListener('click', handleDocumentClick, true);
242 + const div = modalRef.current;
243 + if (div != null) {
244 + ownerDocument = div.ownerDocument;
245 + ownerDocument.addEventListener('keydown', handleDocumentKeyDown);
246 + if (dismissOnClickOutside) {
247 + ownerDocument.addEventListener('click', handleDocumentClick, true);
248 + }
249 }
250 }, 0);
251