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. import EventEmitter from '../events';
    
  11. import throttle from 'lodash.throttle';
    
  12. import {
    
  13.   SESSION_STORAGE_LAST_SELECTION_KEY,
    
  14.   SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
    
  15.   SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
    
  16.   __DEBUG__,
    
  17. } from '../constants';
    
  18. import {
    
  19.   sessionStorageGetItem,
    
  20.   sessionStorageRemoveItem,
    
  21.   sessionStorageSetItem,
    
  22. } from 'react-devtools-shared/src/storage';
    
  23. import setupHighlighter from './views/Highlighter';
    
  24. import {
    
  25.   initialize as setupTraceUpdates,
    
  26.   toggleEnabled as setTraceUpdatesEnabled,
    
  27. } from './views/TraceUpdates';
    
  28. import {patch as patchConsole} from './console';
    
  29. import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
    
  30. 
    
  31. import type {BackendBridge} from 'react-devtools-shared/src/bridge';
    
  32. import type {
    
  33.   InstanceAndStyle,
    
  34.   NativeType,
    
  35.   OwnersList,
    
  36.   PathFrame,
    
  37.   PathMatch,
    
  38.   RendererID,
    
  39.   RendererInterface,
    
  40.   ConsolePatchSettings,
    
  41. } from './types';
    
  42. import type {
    
  43.   ComponentFilter,
    
  44.   BrowserTheme,
    
  45. } from 'react-devtools-shared/src/frontend/types';
    
  46. import {isSynchronousXHRSupported} from './utils';
    
  47. 
    
  48. const debug = (methodName: string, ...args: Array<string>) => {
    
  49.   if (__DEBUG__) {
    
  50.     console.log(
    
  51.       `%cAgent %c${methodName}`,
    
  52.       'color: purple; font-weight: bold;',
    
  53.       'font-weight: bold;',
    
  54.       ...args,
    
  55.     );
    
  56.   }
    
  57. };
    
  58. 
    
  59. type ElementAndRendererID = {
    
  60.   id: number,
    
  61.   rendererID: number,
    
  62. };
    
  63. 
    
  64. type StoreAsGlobalParams = {
    
  65.   count: number,
    
  66.   id: number,
    
  67.   path: Array<string | number>,
    
  68.   rendererID: number,
    
  69. };
    
  70. 
    
  71. type CopyElementParams = {
    
  72.   id: number,
    
  73.   path: Array<string | number>,
    
  74.   rendererID: number,
    
  75. };
    
  76. 
    
  77. type InspectElementParams = {
    
  78.   forceFullData: boolean,
    
  79.   id: number,
    
  80.   path: Array<string | number> | null,
    
  81.   rendererID: number,
    
  82.   requestID: number,
    
  83. };
    
  84. 
    
  85. type OverrideHookParams = {
    
  86.   id: number,
    
  87.   hookID: number,
    
  88.   path: Array<string | number>,
    
  89.   rendererID: number,
    
  90.   wasForwarded?: boolean,
    
  91.   value: any,
    
  92. };
    
  93. 
    
  94. type SetInParams = {
    
  95.   id: number,
    
  96.   path: Array<string | number>,
    
  97.   rendererID: number,
    
  98.   wasForwarded?: boolean,
    
  99.   value: any,
    
  100. };
    
  101. 
    
  102. type PathType = 'props' | 'hooks' | 'state' | 'context';
    
  103. 
    
  104. type DeletePathParams = {
    
  105.   type: PathType,
    
  106.   hookID?: ?number,
    
  107.   id: number,
    
  108.   path: Array<string | number>,
    
  109.   rendererID: number,
    
  110. };
    
  111. 
    
  112. type RenamePathParams = {
    
  113.   type: PathType,
    
  114.   hookID?: ?number,
    
  115.   id: number,
    
  116.   oldPath: Array<string | number>,
    
  117.   newPath: Array<string | number>,
    
  118.   rendererID: number,
    
  119. };
    
  120. 
    
  121. type OverrideValueAtPathParams = {
    
  122.   type: PathType,
    
  123.   hookID?: ?number,
    
  124.   id: number,
    
  125.   path: Array<string | number>,
    
  126.   rendererID: number,
    
  127.   value: any,
    
  128. };
    
  129. 
    
  130. type OverrideErrorParams = {
    
  131.   id: number,
    
  132.   rendererID: number,
    
  133.   forceError: boolean,
    
  134. };
    
  135. 
    
  136. type OverrideSuspenseParams = {
    
  137.   id: number,
    
  138.   rendererID: number,
    
  139.   forceFallback: boolean,
    
  140. };
    
  141. 
    
  142. type PersistedSelection = {
    
  143.   rendererID: number,
    
  144.   path: Array<PathFrame>,
    
  145. };
    
  146. 
    
  147. export default class Agent extends EventEmitter<{
    
  148.   hideNativeHighlight: [],
    
  149.   showNativeHighlight: [NativeType],
    
  150.   startInspectingNative: [],
    
  151.   stopInspectingNative: [],
    
  152.   shutdown: [],
    
  153.   traceUpdates: [Set<NativeType>],
    
  154.   drawTraceUpdates: [Array<NativeType>],
    
  155.   disableTraceUpdates: [],
    
  156. }> {
    
  157.   _bridge: BackendBridge;
    
  158.   _isProfiling: boolean = false;
    
  159.   _recordChangeDescriptions: boolean = false;
    
  160.   _rendererInterfaces: {[key: RendererID]: RendererInterface, ...} = {};
    
  161.   _persistedSelection: PersistedSelection | null = null;
    
  162.   _persistedSelectionMatch: PathMatch | null = null;
    
  163.   _traceUpdatesEnabled: boolean = false;
    
  164. 
    
  165.   constructor(bridge: BackendBridge) {
    
  166.     super();
    
  167. 
    
  168.     if (
    
  169.       sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
    
  170.     ) {
    
  171.       this._recordChangeDescriptions =
    
  172.         sessionStorageGetItem(
    
  173.           SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
    
  174.         ) === 'true';
    
  175.       this._isProfiling = true;
    
  176. 
    
  177.       sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
    
  178.       sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY);
    
  179.     }
    
  180. 
    
  181.     const persistedSelectionString = sessionStorageGetItem(
    
  182.       SESSION_STORAGE_LAST_SELECTION_KEY,
    
  183.     );
    
  184.     if (persistedSelectionString != null) {
    
  185.       this._persistedSelection = JSON.parse(persistedSelectionString);
    
  186.     }
    
  187. 
    
  188.     this._bridge = bridge;
    
  189. 
    
  190.     bridge.addListener('clearErrorsAndWarnings', this.clearErrorsAndWarnings);
    
  191.     bridge.addListener('clearErrorsForFiberID', this.clearErrorsForFiberID);
    
  192.     bridge.addListener('clearWarningsForFiberID', this.clearWarningsForFiberID);
    
  193.     bridge.addListener('copyElementPath', this.copyElementPath);
    
  194.     bridge.addListener('deletePath', this.deletePath);
    
  195.     bridge.addListener('getBackendVersion', this.getBackendVersion);
    
  196.     bridge.addListener('getBridgeProtocol', this.getBridgeProtocol);
    
  197.     bridge.addListener('getProfilingData', this.getProfilingData);
    
  198.     bridge.addListener('getProfilingStatus', this.getProfilingStatus);
    
  199.     bridge.addListener('getOwnersList', this.getOwnersList);
    
  200.     bridge.addListener('inspectElement', this.inspectElement);
    
  201.     bridge.addListener('logElementToConsole', this.logElementToConsole);
    
  202.     bridge.addListener('overrideError', this.overrideError);
    
  203.     bridge.addListener('overrideSuspense', this.overrideSuspense);
    
  204.     bridge.addListener('overrideValueAtPath', this.overrideValueAtPath);
    
  205.     bridge.addListener('reloadAndProfile', this.reloadAndProfile);
    
  206.     bridge.addListener('renamePath', this.renamePath);
    
  207.     bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
    
  208.     bridge.addListener('startProfiling', this.startProfiling);
    
  209.     bridge.addListener('stopProfiling', this.stopProfiling);
    
  210.     bridge.addListener('storeAsGlobal', this.storeAsGlobal);
    
  211.     bridge.addListener(
    
  212.       'syncSelectionFromNativeElementsPanel',
    
  213.       this.syncSelectionFromNativeElementsPanel,
    
  214.     );
    
  215.     bridge.addListener('shutdown', this.shutdown);
    
  216.     bridge.addListener(
    
  217.       'updateConsolePatchSettings',
    
  218.       this.updateConsolePatchSettings,
    
  219.     );
    
  220.     bridge.addListener('updateComponentFilters', this.updateComponentFilters);
    
  221.     bridge.addListener('viewAttributeSource', this.viewAttributeSource);
    
  222.     bridge.addListener('viewElementSource', this.viewElementSource);
    
  223. 
    
  224.     // Temporarily support older standalone front-ends sending commands to newer embedded backends.
    
  225.     // We do this because React Native embeds the React DevTools backend,
    
  226.     // but cannot control which version of the frontend users use.
    
  227.     bridge.addListener('overrideContext', this.overrideContext);
    
  228.     bridge.addListener('overrideHookState', this.overrideHookState);
    
  229.     bridge.addListener('overrideProps', this.overrideProps);
    
  230.     bridge.addListener('overrideState', this.overrideState);
    
  231. 
    
  232.     if (this._isProfiling) {
    
  233.       bridge.send('profilingStatus', true);
    
  234.     }
    
  235. 
    
  236.     // Send the Bridge protocol and backend versions, after initialization, in case the frontend has already requested it.
    
  237.     // The Store may be instantiated beore the agent.
    
  238.     const version = process.env.DEVTOOLS_VERSION;
    
  239.     if (version) {
    
  240.       this._bridge.send('backendVersion', version);
    
  241.     }
    
  242.     this._bridge.send('bridgeProtocol', currentBridgeProtocol);
    
  243. 
    
  244.     // Notify the frontend if the backend supports the Storage API (e.g. localStorage).
    
  245.     // If not, features like reload-and-profile will not work correctly and must be disabled.
    
  246.     let isBackendStorageAPISupported = false;
    
  247.     try {
    
  248.       localStorage.getItem('test');
    
  249.       isBackendStorageAPISupported = true;
    
  250.     } catch (error) {}
    
  251.     bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported);
    
  252.     bridge.send('isSynchronousXHRSupported', isSynchronousXHRSupported());
    
  253. 
    
  254.     setupHighlighter(bridge, this);
    
  255.     setupTraceUpdates(this);
    
  256.   }
    
  257. 
    
  258.   get rendererInterfaces(): {[key: RendererID]: RendererInterface, ...} {
    
  259.     return this._rendererInterfaces;
    
  260.   }
    
  261. 
    
  262.   clearErrorsAndWarnings: ({rendererID: RendererID}) => void = ({
    
  263.     rendererID,
    
  264.   }) => {
    
  265.     const renderer = this._rendererInterfaces[rendererID];
    
  266.     if (renderer == null) {
    
  267.       console.warn(`Invalid renderer id "${rendererID}"`);
    
  268.     } else {
    
  269.       renderer.clearErrorsAndWarnings();
    
  270.     }
    
  271.   };
    
  272. 
    
  273.   clearErrorsForFiberID: ElementAndRendererID => void = ({id, rendererID}) => {
    
  274.     const renderer = this._rendererInterfaces[rendererID];
    
  275.     if (renderer == null) {
    
  276.       console.warn(`Invalid renderer id "${rendererID}"`);
    
  277.     } else {
    
  278.       renderer.clearErrorsForFiberID(id);
    
  279.     }
    
  280.   };
    
  281. 
    
  282.   clearWarningsForFiberID: ElementAndRendererID => void = ({
    
  283.     id,
    
  284.     rendererID,
    
  285.   }) => {
    
  286.     const renderer = this._rendererInterfaces[rendererID];
    
  287.     if (renderer == null) {
    
  288.       console.warn(`Invalid renderer id "${rendererID}"`);
    
  289.     } else {
    
  290.       renderer.clearWarningsForFiberID(id);
    
  291.     }
    
  292.   };
    
  293. 
    
  294.   copyElementPath: CopyElementParams => void = ({
    
  295.     id,
    
  296.     path,
    
  297.     rendererID,
    
  298.   }: CopyElementParams) => {
    
  299.     const renderer = this._rendererInterfaces[rendererID];
    
  300.     if (renderer == null) {
    
  301.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  302.     } else {
    
  303.       const value = renderer.getSerializedElementValueByPath(id, path);
    
  304. 
    
  305.       if (value != null) {
    
  306.         this._bridge.send('saveToClipboard', value);
    
  307.       } else {
    
  308.         console.warn(`Unable to obtain serialized value for element "${id}"`);
    
  309.       }
    
  310.     }
    
  311.   };
    
  312. 
    
  313.   deletePath: DeletePathParams => void = ({
    
  314.     hookID,
    
  315.     id,
    
  316.     path,
    
  317.     rendererID,
    
  318.     type,
    
  319.   }: DeletePathParams) => {
    
  320.     const renderer = this._rendererInterfaces[rendererID];
    
  321.     if (renderer == null) {
    
  322.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  323.     } else {
    
  324.       renderer.deletePath(type, id, hookID, path);
    
  325.     }
    
  326.   };
    
  327. 
    
  328.   getInstanceAndStyle({
    
  329.     id,
    
  330.     rendererID,
    
  331.   }: ElementAndRendererID): InstanceAndStyle | null {
    
  332.     const renderer = this._rendererInterfaces[rendererID];
    
  333.     if (renderer == null) {
    
  334.       console.warn(`Invalid renderer id "${rendererID}"`);
    
  335.       return null;
    
  336.     }
    
  337.     return renderer.getInstanceAndStyle(id);
    
  338.   }
    
  339. 
    
  340.   getBestMatchingRendererInterface(node: Object): RendererInterface | null {
    
  341.     let bestMatch = null;
    
  342.     for (const rendererID in this._rendererInterfaces) {
    
  343.       const renderer = ((this._rendererInterfaces[
    
  344.         (rendererID: any)
    
  345.       ]: any): RendererInterface);
    
  346.       const fiber = renderer.getFiberForNative(node);
    
  347.       if (fiber !== null) {
    
  348.         // check if fiber.stateNode is matching the original hostInstance
    
  349.         if (fiber.stateNode === node) {
    
  350.           return renderer;
    
  351.         } else if (bestMatch === null) {
    
  352.           bestMatch = renderer;
    
  353.         }
    
  354.       }
    
  355.     }
    
  356.     // if an exact match is not found, return the first valid renderer as fallback
    
  357.     return bestMatch;
    
  358.   }
    
  359. 
    
  360.   getIDForNode(node: Object): number | null {
    
  361.     const rendererInterface = this.getBestMatchingRendererInterface(node);
    
  362.     if (rendererInterface != null) {
    
  363.       try {
    
  364.         return rendererInterface.getFiberIDForNative(node, true);
    
  365.       } catch (error) {
    
  366.         // Some old React versions might throw if they can't find a match.
    
  367.         // If so we should ignore it...
    
  368.       }
    
  369.     }
    
  370.     return null;
    
  371.   }
    
  372. 
    
  373.   getBackendVersion: () => void = () => {
    
  374.     const version = process.env.DEVTOOLS_VERSION;
    
  375.     if (version) {
    
  376.       this._bridge.send('backendVersion', version);
    
  377.     }
    
  378.   };
    
  379. 
    
  380.   getBridgeProtocol: () => void = () => {
    
  381.     this._bridge.send('bridgeProtocol', currentBridgeProtocol);
    
  382.   };
    
  383. 
    
  384.   getProfilingData: ({rendererID: RendererID}) => void = ({rendererID}) => {
    
  385.     const renderer = this._rendererInterfaces[rendererID];
    
  386.     if (renderer == null) {
    
  387.       console.warn(`Invalid renderer id "${rendererID}"`);
    
  388.     }
    
  389. 
    
  390.     this._bridge.send('profilingData', renderer.getProfilingData());
    
  391.   };
    
  392. 
    
  393.   getProfilingStatus: () => void = () => {
    
  394.     this._bridge.send('profilingStatus', this._isProfiling);
    
  395.   };
    
  396. 
    
  397.   getOwnersList: ElementAndRendererID => void = ({id, rendererID}) => {
    
  398.     const renderer = this._rendererInterfaces[rendererID];
    
  399.     if (renderer == null) {
    
  400.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  401.     } else {
    
  402.       const owners = renderer.getOwnersList(id);
    
  403.       this._bridge.send('ownersList', ({id, owners}: OwnersList));
    
  404.     }
    
  405.   };
    
  406. 
    
  407.   inspectElement: InspectElementParams => void = ({
    
  408.     forceFullData,
    
  409.     id,
    
  410.     path,
    
  411.     rendererID,
    
  412.     requestID,
    
  413.   }) => {
    
  414.     const renderer = this._rendererInterfaces[rendererID];
    
  415.     if (renderer == null) {
    
  416.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  417.     } else {
    
  418.       this._bridge.send(
    
  419.         'inspectedElement',
    
  420.         renderer.inspectElement(requestID, id, path, forceFullData),
    
  421.       );
    
  422. 
    
  423.       // When user selects an element, stop trying to restore the selection,
    
  424.       // and instead remember the current selection for the next reload.
    
  425.       if (
    
  426.         this._persistedSelectionMatch === null ||
    
  427.         this._persistedSelectionMatch.id !== id
    
  428.       ) {
    
  429.         this._persistedSelection = null;
    
  430.         this._persistedSelectionMatch = null;
    
  431.         renderer.setTrackedPath(null);
    
  432.         this._throttledPersistSelection(rendererID, id);
    
  433.       }
    
  434. 
    
  435.       // TODO: If there was a way to change the selected DOM element
    
  436.       // in native Elements tab without forcing a switch to it, we'd do it here.
    
  437.       // For now, it doesn't seem like there is a way to do that:
    
  438.       // https://github.com/bvaughn/react-devtools-experimental/issues/102
    
  439.       // (Setting $0 doesn't work, and calling inspect() switches the tab.)
    
  440.     }
    
  441.   };
    
  442. 
    
  443.   logElementToConsole: ElementAndRendererID => void = ({id, rendererID}) => {
    
  444.     const renderer = this._rendererInterfaces[rendererID];
    
  445.     if (renderer == null) {
    
  446.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  447.     } else {
    
  448.       renderer.logElementToConsole(id);
    
  449.     }
    
  450.   };
    
  451. 
    
  452.   overrideError: OverrideErrorParams => void = ({
    
  453.     id,
    
  454.     rendererID,
    
  455.     forceError,
    
  456.   }) => {
    
  457.     const renderer = this._rendererInterfaces[rendererID];
    
  458.     if (renderer == null) {
    
  459.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  460.     } else {
    
  461.       renderer.overrideError(id, forceError);
    
  462.     }
    
  463.   };
    
  464. 
    
  465.   overrideSuspense: OverrideSuspenseParams => void = ({
    
  466.     id,
    
  467.     rendererID,
    
  468.     forceFallback,
    
  469.   }) => {
    
  470.     const renderer = this._rendererInterfaces[rendererID];
    
  471.     if (renderer == null) {
    
  472.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  473.     } else {
    
  474.       renderer.overrideSuspense(id, forceFallback);
    
  475.     }
    
  476.   };
    
  477. 
    
  478.   overrideValueAtPath: OverrideValueAtPathParams => void = ({
    
  479.     hookID,
    
  480.     id,
    
  481.     path,
    
  482.     rendererID,
    
  483.     type,
    
  484.     value,
    
  485.   }) => {
    
  486.     const renderer = this._rendererInterfaces[rendererID];
    
  487.     if (renderer == null) {
    
  488.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  489.     } else {
    
  490.       renderer.overrideValueAtPath(type, id, hookID, path, value);
    
  491.     }
    
  492.   };
    
  493. 
    
  494.   // Temporarily support older standalone front-ends by forwarding the older message types
    
  495.   // to the new "overrideValueAtPath" command the backend is now listening to.
    
  496.   overrideContext: SetInParams => void = ({
    
  497.     id,
    
  498.     path,
    
  499.     rendererID,
    
  500.     wasForwarded,
    
  501.     value,
    
  502.   }) => {
    
  503.     // Don't forward a message that's already been forwarded by the front-end Bridge.
    
  504.     // We only need to process the override command once!
    
  505.     if (!wasForwarded) {
    
  506.       this.overrideValueAtPath({
    
  507.         id,
    
  508.         path,
    
  509.         rendererID,
    
  510.         type: 'context',
    
  511.         value,
    
  512.       });
    
  513.     }
    
  514.   };
    
  515. 
    
  516.   // Temporarily support older standalone front-ends by forwarding the older message types
    
  517.   // to the new "overrideValueAtPath" command the backend is now listening to.
    
  518.   overrideHookState: OverrideHookParams => void = ({
    
  519.     id,
    
  520.     hookID,
    
  521.     path,
    
  522.     rendererID,
    
  523.     wasForwarded,
    
  524.     value,
    
  525.   }) => {
    
  526.     // Don't forward a message that's already been forwarded by the front-end Bridge.
    
  527.     // We only need to process the override command once!
    
  528.     if (!wasForwarded) {
    
  529.       this.overrideValueAtPath({
    
  530.         id,
    
  531.         path,
    
  532.         rendererID,
    
  533.         type: 'hooks',
    
  534.         value,
    
  535.       });
    
  536.     }
    
  537.   };
    
  538. 
    
  539.   // Temporarily support older standalone front-ends by forwarding the older message types
    
  540.   // to the new "overrideValueAtPath" command the backend is now listening to.
    
  541.   overrideProps: SetInParams => void = ({
    
  542.     id,
    
  543.     path,
    
  544.     rendererID,
    
  545.     wasForwarded,
    
  546.     value,
    
  547.   }) => {
    
  548.     // Don't forward a message that's already been forwarded by the front-end Bridge.
    
  549.     // We only need to process the override command once!
    
  550.     if (!wasForwarded) {
    
  551.       this.overrideValueAtPath({
    
  552.         id,
    
  553.         path,
    
  554.         rendererID,
    
  555.         type: 'props',
    
  556.         value,
    
  557.       });
    
  558.     }
    
  559.   };
    
  560. 
    
  561.   // Temporarily support older standalone front-ends by forwarding the older message types
    
  562.   // to the new "overrideValueAtPath" command the backend is now listening to.
    
  563.   overrideState: SetInParams => void = ({
    
  564.     id,
    
  565.     path,
    
  566.     rendererID,
    
  567.     wasForwarded,
    
  568.     value,
    
  569.   }) => {
    
  570.     // Don't forward a message that's already been forwarded by the front-end Bridge.
    
  571.     // We only need to process the override command once!
    
  572.     if (!wasForwarded) {
    
  573.       this.overrideValueAtPath({
    
  574.         id,
    
  575.         path,
    
  576.         rendererID,
    
  577.         type: 'state',
    
  578.         value,
    
  579.       });
    
  580.     }
    
  581.   };
    
  582. 
    
  583.   reloadAndProfile: (recordChangeDescriptions: boolean) => void =
    
  584.     recordChangeDescriptions => {
    
  585.       sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
    
  586.       sessionStorageSetItem(
    
  587.         SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
    
  588.         recordChangeDescriptions ? 'true' : 'false',
    
  589.       );
    
  590. 
    
  591.       // This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
    
  592.       // In that case, the shell must also listen for this specific message to know when it needs to reload the app.
    
  593.       // The agent can't do this in a way that is renderer agnostic.
    
  594.       this._bridge.send('reloadAppForProfiling');
    
  595.     };
    
  596. 
    
  597.   renamePath: RenamePathParams => void = ({
    
  598.     hookID,
    
  599.     id,
    
  600.     newPath,
    
  601.     oldPath,
    
  602.     rendererID,
    
  603.     type,
    
  604.   }) => {
    
  605.     const renderer = this._rendererInterfaces[rendererID];
    
  606.     if (renderer == null) {
    
  607.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  608.     } else {
    
  609.       renderer.renamePath(type, id, hookID, oldPath, newPath);
    
  610.     }
    
  611.   };
    
  612. 
    
  613.   selectNode(target: Object): void {
    
  614.     const id = this.getIDForNode(target);
    
  615.     if (id !== null) {
    
  616.       this._bridge.send('selectFiber', id);
    
  617.     }
    
  618.   }
    
  619. 
    
  620.   setRendererInterface(
    
  621.     rendererID: RendererID,
    
  622.     rendererInterface: RendererInterface,
    
  623.   ) {
    
  624.     this._rendererInterfaces[rendererID] = rendererInterface;
    
  625. 
    
  626.     if (this._isProfiling) {
    
  627.       rendererInterface.startProfiling(this._recordChangeDescriptions);
    
  628.     }
    
  629. 
    
  630.     rendererInterface.setTraceUpdatesEnabled(this._traceUpdatesEnabled);
    
  631. 
    
  632.     // When the renderer is attached, we need to tell it whether
    
  633.     // we remember the previous selection that we'd like to restore.
    
  634.     // It'll start tracking mounts for matches to the last selection path.
    
  635.     const selection = this._persistedSelection;
    
  636.     if (selection !== null && selection.rendererID === rendererID) {
    
  637.       rendererInterface.setTrackedPath(selection.path);
    
  638.     }
    
  639.   }
    
  640. 
    
  641.   setTraceUpdatesEnabled: (traceUpdatesEnabled: boolean) => void =
    
  642.     traceUpdatesEnabled => {
    
  643.       this._traceUpdatesEnabled = traceUpdatesEnabled;
    
  644. 
    
  645.       setTraceUpdatesEnabled(traceUpdatesEnabled);
    
  646. 
    
  647.       for (const rendererID in this._rendererInterfaces) {
    
  648.         const renderer = ((this._rendererInterfaces[
    
  649.           (rendererID: any)
    
  650.         ]: any): RendererInterface);
    
  651.         renderer.setTraceUpdatesEnabled(traceUpdatesEnabled);
    
  652.       }
    
  653.     };
    
  654. 
    
  655.   syncSelectionFromNativeElementsPanel: () => void = () => {
    
  656.     const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
    
  657.     if (target == null) {
    
  658.       return;
    
  659.     }
    
  660.     this.selectNode(target);
    
  661.   };
    
  662. 
    
  663.   shutdown: () => void = () => {
    
  664.     // Clean up the overlay if visible, and associated events.
    
  665.     this.emit('shutdown');
    
  666.   };
    
  667. 
    
  668.   startProfiling: (recordChangeDescriptions: boolean) => void =
    
  669.     recordChangeDescriptions => {
    
  670.       this._recordChangeDescriptions = recordChangeDescriptions;
    
  671.       this._isProfiling = true;
    
  672.       for (const rendererID in this._rendererInterfaces) {
    
  673.         const renderer = ((this._rendererInterfaces[
    
  674.           (rendererID: any)
    
  675.         ]: any): RendererInterface);
    
  676.         renderer.startProfiling(recordChangeDescriptions);
    
  677.       }
    
  678.       this._bridge.send('profilingStatus', this._isProfiling);
    
  679.     };
    
  680. 
    
  681.   stopProfiling: () => void = () => {
    
  682.     this._isProfiling = false;
    
  683.     this._recordChangeDescriptions = false;
    
  684.     for (const rendererID in this._rendererInterfaces) {
    
  685.       const renderer = ((this._rendererInterfaces[
    
  686.         (rendererID: any)
    
  687.       ]: any): RendererInterface);
    
  688.       renderer.stopProfiling();
    
  689.     }
    
  690.     this._bridge.send('profilingStatus', this._isProfiling);
    
  691.   };
    
  692. 
    
  693.   stopInspectingNative: (selected: boolean) => void = selected => {
    
  694.     this._bridge.send('stopInspectingNative', selected);
    
  695.   };
    
  696. 
    
  697.   storeAsGlobal: StoreAsGlobalParams => void = ({
    
  698.     count,
    
  699.     id,
    
  700.     path,
    
  701.     rendererID,
    
  702.   }) => {
    
  703.     const renderer = this._rendererInterfaces[rendererID];
    
  704.     if (renderer == null) {
    
  705.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  706.     } else {
    
  707.       renderer.storeAsGlobal(id, path, count);
    
  708.     }
    
  709.   };
    
  710. 
    
  711.   updateConsolePatchSettings: ({
    
  712.     appendComponentStack: boolean,
    
  713.     breakOnConsoleErrors: boolean,
    
  714.     browserTheme: BrowserTheme,
    
  715.     hideConsoleLogsInStrictMode: boolean,
    
  716.     showInlineWarningsAndErrors: boolean,
    
  717.   }) => void = ({
    
  718.     appendComponentStack,
    
  719.     breakOnConsoleErrors,
    
  720.     showInlineWarningsAndErrors,
    
  721.     hideConsoleLogsInStrictMode,
    
  722.     browserTheme,
    
  723.   }: ConsolePatchSettings) => {
    
  724.     // If the frontend preferences have changed,
    
  725.     // or in the case of React Native- if the backend is just finding out the preferences-
    
  726.     // then reinstall the console overrides.
    
  727.     // It's safe to call `patchConsole` multiple times.
    
  728.     patchConsole({
    
  729.       appendComponentStack,
    
  730.       breakOnConsoleErrors,
    
  731.       showInlineWarningsAndErrors,
    
  732.       hideConsoleLogsInStrictMode,
    
  733.       browserTheme,
    
  734.     });
    
  735.   };
    
  736. 
    
  737.   updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void =
    
  738.     componentFilters => {
    
  739.       for (const rendererID in this._rendererInterfaces) {
    
  740.         const renderer = ((this._rendererInterfaces[
    
  741.           (rendererID: any)
    
  742.         ]: any): RendererInterface);
    
  743.         renderer.updateComponentFilters(componentFilters);
    
  744.       }
    
  745.     };
    
  746. 
    
  747.   viewAttributeSource: CopyElementParams => void = ({id, path, rendererID}) => {
    
  748.     const renderer = this._rendererInterfaces[rendererID];
    
  749.     if (renderer == null) {
    
  750.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  751.     } else {
    
  752.       renderer.prepareViewAttributeSource(id, path);
    
  753.     }
    
  754.   };
    
  755. 
    
  756.   viewElementSource: ElementAndRendererID => void = ({id, rendererID}) => {
    
  757.     const renderer = this._rendererInterfaces[rendererID];
    
  758.     if (renderer == null) {
    
  759.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  760.     } else {
    
  761.       renderer.prepareViewElementSource(id);
    
  762.     }
    
  763.   };
    
  764. 
    
  765.   onTraceUpdates: (nodes: Set<NativeType>) => void = nodes => {
    
  766.     this.emit('traceUpdates', nodes);
    
  767.   };
    
  768. 
    
  769.   onFastRefreshScheduled: () => void = () => {
    
  770.     if (__DEBUG__) {
    
  771.       debug('onFastRefreshScheduled');
    
  772.     }
    
  773. 
    
  774.     this._bridge.send('fastRefreshScheduled');
    
  775.   };
    
  776. 
    
  777.   onHookOperations: (operations: Array<number>) => void = operations => {
    
  778.     if (__DEBUG__) {
    
  779.       debug(
    
  780.         'onHookOperations',
    
  781.         `(${operations.length}) [${operations.join(', ')}]`,
    
  782.       );
    
  783.     }
    
  784. 
    
  785.     // TODO:
    
  786.     // The chrome.runtime does not currently support transferables; it forces JSON serialization.
    
  787.     // See bug https://bugs.chromium.org/p/chromium/issues/detail?id=927134
    
  788.     //
    
  789.     // Regarding transferables, the postMessage doc states:
    
  790.     // If the ownership of an object is transferred, it becomes unusable (neutered)
    
  791.     // in the context it was sent from and becomes available only to the worker it was sent to.
    
  792.     //
    
  793.     // Even though Chrome is eventually JSON serializing the array buffer,
    
  794.     // using the transferable approach also sometimes causes it to throw:
    
  795.     //   DOMException: Failed to execute 'postMessage' on 'Window': ArrayBuffer at index 0 is already neutered.
    
  796.     //
    
  797.     // See bug https://github.com/bvaughn/react-devtools-experimental/issues/25
    
  798.     //
    
  799.     // The Store has a fallback in place that parses the message as JSON if the type isn't an array.
    
  800.     // For now the simplest fix seems to be to not transfer the array.
    
  801.     // This will negatively impact performance on Firefox so it's unfortunate,
    
  802.     // but until we're able to fix the Chrome error mentioned above, it seems necessary.
    
  803.     //
    
  804.     // this._bridge.send('operations', operations, [operations.buffer]);
    
  805.     this._bridge.send('operations', operations);
    
  806. 
    
  807.     if (this._persistedSelection !== null) {
    
  808.       const rendererID = operations[0];
    
  809.       if (this._persistedSelection.rendererID === rendererID) {
    
  810.         // Check if we can select a deeper match for the persisted selection.
    
  811.         const renderer = this._rendererInterfaces[rendererID];
    
  812.         if (renderer == null) {
    
  813.           console.warn(`Invalid renderer id "${rendererID}"`);
    
  814.         } else {
    
  815.           const prevMatch = this._persistedSelectionMatch;
    
  816.           const nextMatch = renderer.getBestMatchForTrackedPath();
    
  817.           this._persistedSelectionMatch = nextMatch;
    
  818.           const prevMatchID = prevMatch !== null ? prevMatch.id : null;
    
  819.           const nextMatchID = nextMatch !== null ? nextMatch.id : null;
    
  820.           if (prevMatchID !== nextMatchID) {
    
  821.             if (nextMatchID !== null) {
    
  822.               // We moved forward, unlocking a deeper node.
    
  823.               this._bridge.send('selectFiber', nextMatchID);
    
  824.             }
    
  825.           }
    
  826.           if (nextMatch !== null && nextMatch.isFullMatch) {
    
  827.             // We've just unlocked the innermost selected node.
    
  828.             // There's no point tracking it further.
    
  829.             this._persistedSelection = null;
    
  830.             this._persistedSelectionMatch = null;
    
  831.             renderer.setTrackedPath(null);
    
  832.           }
    
  833.         }
    
  834.       }
    
  835.     }
    
  836.   };
    
  837. 
    
  838.   onUnsupportedRenderer(rendererID: number) {
    
  839.     this._bridge.send('unsupportedRendererVersion', rendererID);
    
  840.   }
    
  841. 
    
  842.   _throttledPersistSelection: any = throttle(
    
  843.     (rendererID: number, id: number) => {
    
  844.       // This is throttled, so both renderer and selected ID
    
  845.       // might not be available by the time we read them.
    
  846.       // This is why we need the defensive checks here.
    
  847.       const renderer = this._rendererInterfaces[rendererID];
    
  848.       const path = renderer != null ? renderer.getPathForElement(id) : null;
    
  849.       if (path !== null) {
    
  850.         sessionStorageSetItem(
    
  851.           SESSION_STORAGE_LAST_SELECTION_KEY,
    
  852.           JSON.stringify(({rendererID, path}: PersistedSelection)),
    
  853.         );
    
  854.       } else {
    
  855.         sessionStorageRemoveItem(SESSION_STORAGE_LAST_SELECTION_KEY);
    
  856.       }
    
  857.     },
    
  858.     1000,
    
  859.   );
    
  860. }