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 {copy} from 'clipboard-js';
    
  11. import EventEmitter from '../events';
    
  12. import {inspect} from 'util';
    
  13. import {
    
  14.   PROFILING_FLAG_BASIC_SUPPORT,
    
  15.   PROFILING_FLAG_TIMELINE_SUPPORT,
    
  16.   TREE_OPERATION_ADD,
    
  17.   TREE_OPERATION_REMOVE,
    
  18.   TREE_OPERATION_REMOVE_ROOT,
    
  19.   TREE_OPERATION_REORDER_CHILDREN,
    
  20.   TREE_OPERATION_SET_SUBTREE_MODE,
    
  21.   TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
    
  22.   TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
    
  23. } from '../constants';
    
  24. import {ElementTypeRoot} from '../frontend/types';
    
  25. import {
    
  26.   getSavedComponentFilters,
    
  27.   setSavedComponentFilters,
    
  28.   separateDisplayNameAndHOCs,
    
  29.   shallowDiffers,
    
  30.   utfDecodeString,
    
  31. } from '../utils';
    
  32. import {localStorageGetItem, localStorageSetItem} from '../storage';
    
  33. import {__DEBUG__} from '../constants';
    
  34. import {printStore} from './utils';
    
  35. import ProfilerStore from './ProfilerStore';
    
  36. import {
    
  37.   BRIDGE_PROTOCOL,
    
  38.   currentBridgeProtocol,
    
  39. } from 'react-devtools-shared/src/bridge';
    
  40. import {StrictMode} from 'react-devtools-shared/src/frontend/types';
    
  41. 
    
  42. import type {
    
  43.   Element,
    
  44.   ComponentFilter,
    
  45.   ElementType,
    
  46. } from 'react-devtools-shared/src/frontend/types';
    
  47. import type {
    
  48.   FrontendBridge,
    
  49.   BridgeProtocol,
    
  50. } from 'react-devtools-shared/src/bridge';
    
  51. import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';
    
  52. 
    
  53. const debug = (methodName: string, ...args: Array<string>) => {
    
  54.   if (__DEBUG__) {
    
  55.     console.log(
    
  56.       `%cStore %c${methodName}`,
    
  57.       'color: green; font-weight: bold;',
    
  58.       'font-weight: bold;',
    
  59.       ...args,
    
  60.     );
    
  61.   }
    
  62. };
    
  63. 
    
  64. const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
    
  65.   'React::DevTools::collapseNodesByDefault';
    
  66. const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
    
  67.   'React::DevTools::recordChangeDescriptions';
    
  68. 
    
  69. type ErrorAndWarningTuples = Array<{id: number, index: number}>;
    
  70. 
    
  71. type Config = {
    
  72.   checkBridgeProtocolCompatibility?: boolean,
    
  73.   isProfiling?: boolean,
    
  74.   supportsNativeInspection?: boolean,
    
  75.   supportsProfiling?: boolean,
    
  76.   supportsReloadAndProfile?: boolean,
    
  77.   supportsTimeline?: boolean,
    
  78.   supportsTraceUpdates?: boolean,
    
  79. };
    
  80. 
    
  81. export type Capabilities = {
    
  82.   supportsBasicProfiling: boolean,
    
  83.   hasOwnerMetadata: boolean,
    
  84.   supportsStrictMode: boolean,
    
  85.   supportsTimeline: boolean,
    
  86. };
    
  87. 
    
  88. /**
    
  89.  * The store is the single source of truth for updates from the backend.
    
  90.  * ContextProviders can subscribe to the Store for specific things they want to provide.
    
  91.  */
    
  92. export default class Store extends EventEmitter<{
    
  93.   backendVersion: [],
    
  94.   collapseNodesByDefault: [],
    
  95.   componentFilters: [],
    
  96.   error: [Error],
    
  97.   mutated: [[Array<number>, Map<number, number>]],
    
  98.   recordChangeDescriptions: [],
    
  99.   roots: [],
    
  100.   rootSupportsBasicProfiling: [],
    
  101.   rootSupportsTimelineProfiling: [],
    
  102.   supportsNativeStyleEditor: [],
    
  103.   supportsReloadAndProfile: [],
    
  104.   unsupportedBridgeProtocolDetected: [],
    
  105.   unsupportedRendererVersionDetected: [],
    
  106. }> {
    
  107.   // If the backend version is new enough to report its (NPM) version, this is it.
    
  108.   // This version may be displayed by the frontend for debugging purposes.
    
  109.   _backendVersion: string | null = null;
    
  110. 
    
  111.   _bridge: FrontendBridge;
    
  112. 
    
  113.   // Computed whenever _errorsAndWarnings Map changes.
    
  114.   _cachedErrorCount: number = 0;
    
  115.   _cachedWarningCount: number = 0;
    
  116.   _cachedErrorAndWarningTuples: ErrorAndWarningTuples | null = null;
    
  117. 
    
  118.   // Should new nodes be collapsed by default when added to the tree?
    
  119.   _collapseNodesByDefault: boolean = true;
    
  120. 
    
  121.   _componentFilters: Array<ComponentFilter>;
    
  122. 
    
  123.   // Map of ID to number of recorded error and warning message IDs.
    
  124.   _errorsAndWarnings: Map<number, {errorCount: number, warningCount: number}> =
    
  125.     new Map();
    
  126. 
    
  127.   // At least one of the injected renderers contains (DEV only) owner metadata.
    
  128.   _hasOwnerMetadata: boolean = false;
    
  129. 
    
  130.   // Map of ID to (mutable) Element.
    
  131.   // Elements are mutated to avoid excessive cloning during tree updates.
    
  132.   // The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.
    
  133.   _idToElement: Map<number, Element> = new Map();
    
  134. 
    
  135.   // Should the React Native style editor panel be shown?
    
  136.   _isNativeStyleEditorSupported: boolean = false;
    
  137. 
    
  138.   // Can the backend use the Storage API (e.g. localStorage)?
    
  139.   // If not, features like reload-and-profile will not work correctly and must be disabled.
    
  140.   _isBackendStorageAPISupported: boolean = false;
    
  141. 
    
  142.   // Can DevTools use sync XHR requests?
    
  143.   // If not, features like reload-and-profile will not work correctly and must be disabled.
    
  144.   // This current limitation applies only to web extension builds
    
  145.   // and will need to be reconsidered in the future if we add support for reload to React Native.
    
  146.   _isSynchronousXHRSupported: boolean = false;
    
  147. 
    
  148.   _nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;
    
  149. 
    
  150.   // Older backends don't support an explicit bridge protocol,
    
  151.   // so we should timeout eventually and show a downgrade message.
    
  152.   _onBridgeProtocolTimeoutID: TimeoutID | null = null;
    
  153. 
    
  154.   // Map of element (id) to the set of elements (ids) it owns.
    
  155.   // This map enables getOwnersListForElement() to avoid traversing the entire tree.
    
  156.   _ownersMap: Map<number, Set<number>> = new Map();
    
  157. 
    
  158.   _profilerStore: ProfilerStore;
    
  159. 
    
  160.   _recordChangeDescriptions: boolean = false;
    
  161. 
    
  162.   // Incremented each time the store is mutated.
    
  163.   // This enables a passive effect to detect a mutation between render and commit phase.
    
  164.   _revision: number = 0;
    
  165. 
    
  166.   // This Array must be treated as immutable!
    
  167.   // Passive effects will check it for changes between render and mount.
    
  168.   _roots: $ReadOnlyArray<number> = [];
    
  169. 
    
  170.   _rootIDToCapabilities: Map<number, Capabilities> = new Map();
    
  171. 
    
  172.   // Renderer ID is needed to support inspection fiber props, state, and hooks.
    
  173.   _rootIDToRendererID: Map<number, number> = new Map();
    
  174. 
    
  175.   // These options may be initially set by a configuration option when constructing the Store.
    
  176.   _supportsNativeInspection: boolean = true;
    
  177.   _supportsProfiling: boolean = false;
    
  178.   _supportsReloadAndProfile: boolean = false;
    
  179.   _supportsTimeline: boolean = false;
    
  180.   _supportsTraceUpdates: boolean = false;
    
  181. 
    
  182.   // These options default to false but may be updated as roots are added and removed.
    
  183.   _rootSupportsBasicProfiling: boolean = false;
    
  184.   _rootSupportsTimelineProfiling: boolean = false;
    
  185. 
    
  186.   _bridgeProtocol: BridgeProtocol | null = null;
    
  187.   _unsupportedBridgeProtocolDetected: boolean = false;
    
  188.   _unsupportedRendererVersionDetected: boolean = false;
    
  189. 
    
  190.   // Total number of visible elements (within all roots).
    
  191.   // Used for windowing purposes.
    
  192.   _weightAcrossRoots: number = 0;
    
  193. 
    
  194.   constructor(bridge: FrontendBridge, config?: Config) {
    
  195.     super();
    
  196. 
    
  197.     if (__DEBUG__) {
    
  198.       debug('constructor', 'subscribing to Bridge');
    
  199.     }
    
  200. 
    
  201.     this._collapseNodesByDefault =
    
  202.       localStorageGetItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) ===
    
  203.       'true';
    
  204. 
    
  205.     this._recordChangeDescriptions =
    
  206.       localStorageGetItem(LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
    
  207.       'true';
    
  208. 
    
  209.     this._componentFilters = getSavedComponentFilters();
    
  210. 
    
  211.     let isProfiling = false;
    
  212.     if (config != null) {
    
  213.       isProfiling = config.isProfiling === true;
    
  214. 
    
  215.       const {
    
  216.         supportsNativeInspection,
    
  217.         supportsProfiling,
    
  218.         supportsReloadAndProfile,
    
  219.         supportsTimeline,
    
  220.         supportsTraceUpdates,
    
  221.       } = config;
    
  222.       this._supportsNativeInspection = supportsNativeInspection !== false;
    
  223.       if (supportsProfiling) {
    
  224.         this._supportsProfiling = true;
    
  225.       }
    
  226.       if (supportsReloadAndProfile) {
    
  227.         this._supportsReloadAndProfile = true;
    
  228.       }
    
  229.       if (supportsTimeline) {
    
  230.         this._supportsTimeline = true;
    
  231.       }
    
  232.       if (supportsTraceUpdates) {
    
  233.         this._supportsTraceUpdates = true;
    
  234.       }
    
  235.     }
    
  236. 
    
  237.     this._bridge = bridge;
    
  238.     bridge.addListener('operations', this.onBridgeOperations);
    
  239.     bridge.addListener(
    
  240.       'overrideComponentFilters',
    
  241.       this.onBridgeOverrideComponentFilters,
    
  242.     );
    
  243.     bridge.addListener('shutdown', this.onBridgeShutdown);
    
  244.     bridge.addListener(
    
  245.       'isBackendStorageAPISupported',
    
  246.       this.onBackendStorageAPISupported,
    
  247.     );
    
  248.     bridge.addListener(
    
  249.       'isNativeStyleEditorSupported',
    
  250.       this.onBridgeNativeStyleEditorSupported,
    
  251.     );
    
  252.     bridge.addListener(
    
  253.       'isSynchronousXHRSupported',
    
  254.       this.onBridgeSynchronousXHRSupported,
    
  255.     );
    
  256.     bridge.addListener(
    
  257.       'unsupportedRendererVersion',
    
  258.       this.onBridgeUnsupportedRendererVersion,
    
  259.     );
    
  260. 
    
  261.     this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
    
  262. 
    
  263.     // Verify that the frontend version is compatible with the connected backend.
    
  264.     // See github.com/facebook/react/issues/21326
    
  265.     if (config != null && config.checkBridgeProtocolCompatibility) {
    
  266.       // Older backends don't support an explicit bridge protocol,
    
  267.       // so we should timeout eventually and show a downgrade message.
    
  268.       this._onBridgeProtocolTimeoutID = setTimeout(
    
  269.         this.onBridgeProtocolTimeout,
    
  270.         10000,
    
  271.       );
    
  272. 
    
  273.       bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
    
  274.       bridge.send('getBridgeProtocol');
    
  275.     }
    
  276. 
    
  277.     bridge.addListener('backendVersion', this.onBridgeBackendVersion);
    
  278.     bridge.send('getBackendVersion');
    
  279. 
    
  280.     bridge.addListener('saveToClipboard', this.onSaveToClipboard);
    
  281.   }
    
  282. 
    
  283.   // This is only used in tests to avoid memory leaks.
    
  284.   assertExpectedRootMapSizes() {
    
  285.     if (this.roots.length === 0) {
    
  286.       // The only safe time to assert these maps are empty is when the store is empty.
    
  287.       this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');
    
  288.       this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');
    
  289.     }
    
  290. 
    
  291.     // These maps should always be the same size as the number of roots
    
  292.     this.assertMapSizeMatchesRootCount(
    
  293.       this._rootIDToCapabilities,
    
  294.       '_rootIDToCapabilities',
    
  295.     );
    
  296.     this.assertMapSizeMatchesRootCount(
    
  297.       this._rootIDToRendererID,
    
  298.       '_rootIDToRendererID',
    
  299.     );
    
  300.   }
    
  301. 
    
  302.   // This is only used in tests to avoid memory leaks.
    
  303.   assertMapSizeMatchesRootCount(map: Map<any, any>, mapName: string) {
    
  304.     const expectedSize = this.roots.length;
    
  305.     if (map.size !== expectedSize) {
    
  306.       this._throwAndEmitError(
    
  307.         Error(
    
  308.           `Expected ${mapName} to contain ${expectedSize} items, but it contains ${
    
  309.             map.size
    
  310.           } items\n\n${inspect(map, {
    
  311.             depth: 20,
    
  312.           })}`,
    
  313.         ),
    
  314.       );
    
  315.     }
    
  316.   }
    
  317. 
    
  318.   get backendVersion(): string | null {
    
  319.     return this._backendVersion;
    
  320.   }
    
  321. 
    
  322.   get collapseNodesByDefault(): boolean {
    
  323.     return this._collapseNodesByDefault;
    
  324.   }
    
  325.   set collapseNodesByDefault(value: boolean): void {
    
  326.     this._collapseNodesByDefault = value;
    
  327. 
    
  328.     localStorageSetItem(
    
  329.       LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY,
    
  330.       value ? 'true' : 'false',
    
  331.     );
    
  332. 
    
  333.     this.emit('collapseNodesByDefault');
    
  334.   }
    
  335. 
    
  336.   get componentFilters(): Array<ComponentFilter> {
    
  337.     return this._componentFilters;
    
  338.   }
    
  339.   set componentFilters(value: Array<ComponentFilter>): void {
    
  340.     if (this._profilerStore.isProfiling) {
    
  341.       // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
    
  342.       // If necessary, we could support this- but it doesn't seem like a necessary use case.
    
  343.       this._throwAndEmitError(
    
  344.         Error('Cannot modify filter preferences while profiling'),
    
  345.       );
    
  346.     }
    
  347. 
    
  348.     // Filter updates are expensive to apply (since they impact the entire tree).
    
  349.     // Let's determine if they've changed and avoid doing this work if they haven't.
    
  350.     const prevEnabledComponentFilters = this._componentFilters.filter(
    
  351.       filter => filter.isEnabled,
    
  352.     );
    
  353.     const nextEnabledComponentFilters = value.filter(
    
  354.       filter => filter.isEnabled,
    
  355.     );
    
  356.     let haveEnabledFiltersChanged =
    
  357.       prevEnabledComponentFilters.length !== nextEnabledComponentFilters.length;
    
  358.     if (!haveEnabledFiltersChanged) {
    
  359.       for (let i = 0; i < nextEnabledComponentFilters.length; i++) {
    
  360.         const prevFilter = prevEnabledComponentFilters[i];
    
  361.         const nextFilter = nextEnabledComponentFilters[i];
    
  362.         if (shallowDiffers(prevFilter, nextFilter)) {
    
  363.           haveEnabledFiltersChanged = true;
    
  364.           break;
    
  365.         }
    
  366.       }
    
  367.     }
    
  368. 
    
  369.     this._componentFilters = value;
    
  370. 
    
  371.     // Update persisted filter preferences stored in localStorage.
    
  372.     setSavedComponentFilters(value);
    
  373. 
    
  374.     // Notify the renderer that filter preferences have changed.
    
  375.     // This is an expensive operation; it unmounts and remounts the entire tree,
    
  376.     // so only do it if the set of enabled component filters has changed.
    
  377.     if (haveEnabledFiltersChanged) {
    
  378.       this._bridge.send('updateComponentFilters', value);
    
  379.     }
    
  380. 
    
  381.     this.emit('componentFilters');
    
  382.   }
    
  383. 
    
  384.   get bridgeProtocol(): BridgeProtocol | null {
    
  385.     return this._bridgeProtocol;
    
  386.   }
    
  387. 
    
  388.   get errorCount(): number {
    
  389.     return this._cachedErrorCount;
    
  390.   }
    
  391. 
    
  392.   get hasOwnerMetadata(): boolean {
    
  393.     return this._hasOwnerMetadata;
    
  394.   }
    
  395. 
    
  396.   get nativeStyleEditorValidAttributes(): $ReadOnlyArray<string> | null {
    
  397.     return this._nativeStyleEditorValidAttributes;
    
  398.   }
    
  399. 
    
  400.   get numElements(): number {
    
  401.     return this._weightAcrossRoots;
    
  402.   }
    
  403. 
    
  404.   get profilerStore(): ProfilerStore {
    
  405.     return this._profilerStore;
    
  406.   }
    
  407. 
    
  408.   get recordChangeDescriptions(): boolean {
    
  409.     return this._recordChangeDescriptions;
    
  410.   }
    
  411.   set recordChangeDescriptions(value: boolean): void {
    
  412.     this._recordChangeDescriptions = value;
    
  413. 
    
  414.     localStorageSetItem(
    
  415.       LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
    
  416.       value ? 'true' : 'false',
    
  417.     );
    
  418. 
    
  419.     this.emit('recordChangeDescriptions');
    
  420.   }
    
  421. 
    
  422.   get revision(): number {
    
  423.     return this._revision;
    
  424.   }
    
  425. 
    
  426.   get rootIDToRendererID(): Map<number, number> {
    
  427.     return this._rootIDToRendererID;
    
  428.   }
    
  429. 
    
  430.   get roots(): $ReadOnlyArray<number> {
    
  431.     return this._roots;
    
  432.   }
    
  433. 
    
  434.   // At least one of the currently mounted roots support the Legacy profiler.
    
  435.   get rootSupportsBasicProfiling(): boolean {
    
  436.     return this._rootSupportsBasicProfiling;
    
  437.   }
    
  438. 
    
  439.   // At least one of the currently mounted roots support the Timeline profiler.
    
  440.   get rootSupportsTimelineProfiling(): boolean {
    
  441.     return this._rootSupportsTimelineProfiling;
    
  442.   }
    
  443. 
    
  444.   get supportsNativeInspection(): boolean {
    
  445.     return this._supportsNativeInspection;
    
  446.   }
    
  447. 
    
  448.   get supportsNativeStyleEditor(): boolean {
    
  449.     return this._isNativeStyleEditorSupported;
    
  450.   }
    
  451. 
    
  452.   // This build of DevTools supports the legacy profiler.
    
  453.   // This is a static flag, controled by the Store config.
    
  454.   get supportsProfiling(): boolean {
    
  455.     return this._supportsProfiling;
    
  456.   }
    
  457. 
    
  458.   get supportsReloadAndProfile(): boolean {
    
  459.     // Does the DevTools shell support reloading and eagerly injecting the renderer interface?
    
  460.     // And if so, can the backend use the localStorage API and sync XHR?
    
  461.     // All of these are currently required for the reload-and-profile feature to work.
    
  462.     return (
    
  463.       this._supportsReloadAndProfile &&
    
  464.       this._isBackendStorageAPISupported &&
    
  465.       this._isSynchronousXHRSupported
    
  466.     );
    
  467.   }
    
  468. 
    
  469.   // This build of DevTools supports the Timeline profiler.
    
  470.   // This is a static flag, controled by the Store config.
    
  471.   get supportsTimeline(): boolean {
    
  472.     return this._supportsTimeline;
    
  473.   }
    
  474. 
    
  475.   get supportsTraceUpdates(): boolean {
    
  476.     return this._supportsTraceUpdates;
    
  477.   }
    
  478. 
    
  479.   get unsupportedBridgeProtocolDetected(): boolean {
    
  480.     return this._unsupportedBridgeProtocolDetected;
    
  481.   }
    
  482. 
    
  483.   get unsupportedRendererVersionDetected(): boolean {
    
  484.     return this._unsupportedRendererVersionDetected;
    
  485.   }
    
  486. 
    
  487.   get warningCount(): number {
    
  488.     return this._cachedWarningCount;
    
  489.   }
    
  490. 
    
  491.   containsElement(id: number): boolean {
    
  492.     return this._idToElement.has(id);
    
  493.   }
    
  494. 
    
  495.   getElementAtIndex(index: number): Element | null {
    
  496.     if (index < 0 || index >= this.numElements) {
    
  497.       console.warn(
    
  498.         `Invalid index ${index} specified; store contains ${this.numElements} items.`,
    
  499.       );
    
  500. 
    
  501.       return null;
    
  502.     }
    
  503. 
    
  504.     // Find which root this element is in...
    
  505.     let rootID;
    
  506.     let root;
    
  507.     let rootWeight = 0;
    
  508.     for (let i = 0; i < this._roots.length; i++) {
    
  509.       rootID = this._roots[i];
    
  510.       root = ((this._idToElement.get(rootID): any): Element);
    
  511.       if (root.children.length === 0) {
    
  512.         continue;
    
  513.       } else if (rootWeight + root.weight > index) {
    
  514.         break;
    
  515.       } else {
    
  516.         rootWeight += root.weight;
    
  517.       }
    
  518.     }
    
  519. 
    
  520.     // Find the element in the tree using the weight of each node...
    
  521.     // Skip over the root itself, because roots aren't visible in the Elements tree.
    
  522.     let currentElement = ((root: any): Element);
    
  523.     let currentWeight = rootWeight - 1;
    
  524.     while (index !== currentWeight) {
    
  525.       const numChildren = currentElement.children.length;
    
  526.       for (let i = 0; i < numChildren; i++) {
    
  527.         const childID = currentElement.children[i];
    
  528.         const child = ((this._idToElement.get(childID): any): Element);
    
  529.         const childWeight = child.isCollapsed ? 1 : child.weight;
    
  530. 
    
  531.         if (index <= currentWeight + childWeight) {
    
  532.           currentWeight++;
    
  533.           currentElement = child;
    
  534.           break;
    
  535.         } else {
    
  536.           currentWeight += childWeight;
    
  537.         }
    
  538.       }
    
  539.     }
    
  540. 
    
  541.     return ((currentElement: any): Element) || null;
    
  542.   }
    
  543. 
    
  544.   getElementIDAtIndex(index: number): number | null {
    
  545.     const element = this.getElementAtIndex(index);
    
  546.     return element === null ? null : element.id;
    
  547.   }
    
  548. 
    
  549.   getElementByID(id: number): Element | null {
    
  550.     const element = this._idToElement.get(id);
    
  551.     if (element === undefined) {
    
  552.       console.warn(`No element found with id "${id}"`);
    
  553.       return null;
    
  554.     }
    
  555. 
    
  556.     return element;
    
  557.   }
    
  558. 
    
  559.   // Returns a tuple of [id, index]
    
  560.   getElementsWithErrorsAndWarnings(): Array<{id: number, index: number}> {
    
  561.     if (this._cachedErrorAndWarningTuples !== null) {
    
  562.       return this._cachedErrorAndWarningTuples;
    
  563.     } else {
    
  564.       const errorAndWarningTuples: ErrorAndWarningTuples = [];
    
  565. 
    
  566.       this._errorsAndWarnings.forEach((_, id) => {
    
  567.         const index = this.getIndexOfElementID(id);
    
  568.         if (index !== null) {
    
  569.           let low = 0;
    
  570.           let high = errorAndWarningTuples.length;
    
  571.           while (low < high) {
    
  572.             const mid = (low + high) >> 1;
    
  573.             if (errorAndWarningTuples[mid].index > index) {
    
  574.               high = mid;
    
  575.             } else {
    
  576.               low = mid + 1;
    
  577.             }
    
  578.           }
    
  579. 
    
  580.           errorAndWarningTuples.splice(low, 0, {id, index});
    
  581.         }
    
  582.       });
    
  583. 
    
  584.       // Cache for later (at least until the tree changes again).
    
  585.       this._cachedErrorAndWarningTuples = errorAndWarningTuples;
    
  586. 
    
  587.       return errorAndWarningTuples;
    
  588.     }
    
  589.   }
    
  590. 
    
  591.   getErrorAndWarningCountForElementID(id: number): {
    
  592.     errorCount: number,
    
  593.     warningCount: number,
    
  594.   } {
    
  595.     return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0};
    
  596.   }
    
  597. 
    
  598.   getIndexOfElementID(id: number): number | null {
    
  599.     const element = this.getElementByID(id);
    
  600. 
    
  601.     if (element === null || element.parentID === 0) {
    
  602.       return null;
    
  603.     }
    
  604. 
    
  605.     // Walk up the tree to the root.
    
  606.     // Increment the index by one for each node we encounter,
    
  607.     // and by the weight of all nodes to the left of the current one.
    
  608.     // This should be a relatively fast way of determining the index of a node within the tree.
    
  609.     let previousID = id;
    
  610.     let currentID = element.parentID;
    
  611.     let index = 0;
    
  612.     while (true) {
    
  613.       const current = this._idToElement.get(currentID);
    
  614.       if (current === undefined) {
    
  615.         return null;
    
  616.       }
    
  617. 
    
  618.       const {children} = current;
    
  619.       for (let i = 0; i < children.length; i++) {
    
  620.         const childID = children[i];
    
  621.         if (childID === previousID) {
    
  622.           break;
    
  623.         }
    
  624. 
    
  625.         const child = this._idToElement.get(childID);
    
  626.         if (child === undefined) {
    
  627.           return null;
    
  628.         }
    
  629. 
    
  630.         index += child.isCollapsed ? 1 : child.weight;
    
  631.       }
    
  632. 
    
  633.       if (current.parentID === 0) {
    
  634.         // We found the root; stop crawling.
    
  635.         break;
    
  636.       }
    
  637. 
    
  638.       index++;
    
  639. 
    
  640.       previousID = current.id;
    
  641.       currentID = current.parentID;
    
  642.     }
    
  643. 
    
  644.     // At this point, the current ID is a root (from the previous loop).
    
  645.     // We also need to offset the index by previous root weights.
    
  646.     for (let i = 0; i < this._roots.length; i++) {
    
  647.       const rootID = this._roots[i];
    
  648.       if (rootID === currentID) {
    
  649.         break;
    
  650.       }
    
  651. 
    
  652.       const root = this._idToElement.get(rootID);
    
  653.       if (root === undefined) {
    
  654.         return null;
    
  655.       }
    
  656. 
    
  657.       index += root.weight;
    
  658.     }
    
  659. 
    
  660.     return index;
    
  661.   }
    
  662. 
    
  663.   getOwnersListForElement(ownerID: number): Array<Element> {
    
  664.     const list: Array<Element> = [];
    
  665.     const element = this._idToElement.get(ownerID);
    
  666.     if (element !== undefined) {
    
  667.       list.push({
    
  668.         ...element,
    
  669.         depth: 0,
    
  670.       });
    
  671. 
    
  672.       const unsortedIDs = this._ownersMap.get(ownerID);
    
  673.       if (unsortedIDs !== undefined) {
    
  674.         const depthMap: Map<number, number> = new Map([[ownerID, 0]]);
    
  675. 
    
  676.         // Items in a set are ordered based on insertion.
    
  677.         // This does not correlate with their order in the tree.
    
  678.         // So first we need to order them.
    
  679.         // I wish we could avoid this sorting operation; we could sort at insertion time,
    
  680.         // but then we'd have to pay sorting costs even if the owners list was never used.
    
  681.         // Seems better to defer the cost, since the set of ids is probably pretty small.
    
  682.         const sortedIDs = Array.from(unsortedIDs).sort(
    
  683.           (idA, idB) =>
    
  684.             (this.getIndexOfElementID(idA) || 0) -
    
  685.             (this.getIndexOfElementID(idB) || 0),
    
  686.         );
    
  687. 
    
  688.         // Next we need to determine the appropriate depth for each element in the list.
    
  689.         // The depth in the list may not correspond to the depth in the tree,
    
  690.         // because the list has been filtered to remove intermediate components.
    
  691.         // Perhaps the easiest way to do this is to walk up the tree until we reach either:
    
  692.         // (1) another node that's already in the tree, or (2) the root (owner)
    
  693.         // at which point, our depth is just the depth of that node plus one.
    
  694.         sortedIDs.forEach(id => {
    
  695.           const innerElement = this._idToElement.get(id);
    
  696.           if (innerElement !== undefined) {
    
  697.             let parentID = innerElement.parentID;
    
  698. 
    
  699.             let depth = 0;
    
  700.             while (parentID > 0) {
    
  701.               if (parentID === ownerID || unsortedIDs.has(parentID)) {
    
  702.                 // $FlowFixMe[unsafe-addition] addition with possible null/undefined value
    
  703.                 depth = depthMap.get(parentID) + 1;
    
  704.                 depthMap.set(id, depth);
    
  705.                 break;
    
  706.               }
    
  707.               const parent = this._idToElement.get(parentID);
    
  708.               if (parent === undefined) {
    
  709.                 break;
    
  710.               }
    
  711.               parentID = parent.parentID;
    
  712.             }
    
  713. 
    
  714.             if (depth === 0) {
    
  715.               this._throwAndEmitError(Error('Invalid owners list'));
    
  716.             }
    
  717. 
    
  718.             list.push({...innerElement, depth});
    
  719.           }
    
  720.         });
    
  721.       }
    
  722.     }
    
  723. 
    
  724.     return list;
    
  725.   }
    
  726. 
    
  727.   getRendererIDForElement(id: number): number | null {
    
  728.     let current = this._idToElement.get(id);
    
  729.     while (current !== undefined) {
    
  730.       if (current.parentID === 0) {
    
  731.         const rendererID = this._rootIDToRendererID.get(current.id);
    
  732.         return rendererID == null ? null : rendererID;
    
  733.       } else {
    
  734.         current = this._idToElement.get(current.parentID);
    
  735.       }
    
  736.     }
    
  737.     return null;
    
  738.   }
    
  739. 
    
  740.   getRootIDForElement(id: number): number | null {
    
  741.     let current = this._idToElement.get(id);
    
  742.     while (current !== undefined) {
    
  743.       if (current.parentID === 0) {
    
  744.         return current.id;
    
  745.       } else {
    
  746.         current = this._idToElement.get(current.parentID);
    
  747.       }
    
  748.     }
    
  749.     return null;
    
  750.   }
    
  751. 
    
  752.   isInsideCollapsedSubTree(id: number): boolean {
    
  753.     let current = this._idToElement.get(id);
    
  754.     while (current != null) {
    
  755.       if (current.parentID === 0) {
    
  756.         return false;
    
  757.       } else {
    
  758.         current = this._idToElement.get(current.parentID);
    
  759.         if (current != null && current.isCollapsed) {
    
  760.           return true;
    
  761.         }
    
  762.       }
    
  763.     }
    
  764.     return false;
    
  765.   }
    
  766. 
    
  767.   // TODO Maybe split this into two methods: expand() and collapse()
    
  768.   toggleIsCollapsed(id: number, isCollapsed: boolean): void {
    
  769.     let didMutate = false;
    
  770. 
    
  771.     const element = this.getElementByID(id);
    
  772.     if (element !== null) {
    
  773.       if (isCollapsed) {
    
  774.         if (element.type === ElementTypeRoot) {
    
  775.           this._throwAndEmitError(Error('Root nodes cannot be collapsed'));
    
  776.         }
    
  777. 
    
  778.         if (!element.isCollapsed) {
    
  779.           didMutate = true;
    
  780.           element.isCollapsed = true;
    
  781. 
    
  782.           const weightDelta = 1 - element.weight;
    
  783. 
    
  784.           let parentElement = this._idToElement.get(element.parentID);
    
  785.           while (parentElement !== undefined) {
    
  786.             // We don't need to break on a collapsed parent in the same way as the expand case below.
    
  787.             // That's because collapsing a node doesn't "bubble" and affect its parents.
    
  788.             parentElement.weight += weightDelta;
    
  789.             parentElement = this._idToElement.get(parentElement.parentID);
    
  790.           }
    
  791.         }
    
  792.       } else {
    
  793.         let currentElement: ?Element = element;
    
  794.         while (currentElement != null) {
    
  795.           const oldWeight = currentElement.isCollapsed
    
  796.             ? 1
    
  797.             : currentElement.weight;
    
  798. 
    
  799.           if (currentElement.isCollapsed) {
    
  800.             didMutate = true;
    
  801.             currentElement.isCollapsed = false;
    
  802. 
    
  803.             const newWeight = currentElement.isCollapsed
    
  804.               ? 1
    
  805.               : currentElement.weight;
    
  806.             const weightDelta = newWeight - oldWeight;
    
  807. 
    
  808.             let parentElement = this._idToElement.get(currentElement.parentID);
    
  809.             while (parentElement !== undefined) {
    
  810.               parentElement.weight += weightDelta;
    
  811.               if (parentElement.isCollapsed) {
    
  812.                 // It's important to break on a collapsed parent when expanding nodes.
    
  813.                 // That's because expanding a node "bubbles" up and expands all parents as well.
    
  814.                 // Breaking in this case prevents us from over-incrementing the expanded weights.
    
  815.                 break;
    
  816.               }
    
  817.               parentElement = this._idToElement.get(parentElement.parentID);
    
  818.             }
    
  819.           }
    
  820. 
    
  821.           currentElement =
    
  822.             currentElement.parentID !== 0
    
  823.               ? this.getElementByID(currentElement.parentID)
    
  824.               : null;
    
  825.         }
    
  826.       }
    
  827. 
    
  828.       // Only re-calculate weights and emit an "update" event if the store was mutated.
    
  829.       if (didMutate) {
    
  830.         let weightAcrossRoots = 0;
    
  831.         this._roots.forEach(rootID => {
    
  832.           const {weight} = ((this.getElementByID(rootID): any): Element);
    
  833.           weightAcrossRoots += weight;
    
  834.         });
    
  835.         this._weightAcrossRoots = weightAcrossRoots;
    
  836. 
    
  837.         // The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.
    
  838.         // In this  case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).
    
  839.         // Updating the selected search index later may require auto-expanding a collapsed subtree though.
    
  840.         this.emit('mutated', [[], new Map()]);
    
  841.       }
    
  842.     }
    
  843.   }
    
  844. 
    
  845.   _adjustParentTreeWeight: (
    
  846.     parentElement: ?Element,
    
  847.     weightDelta: number,
    
  848.   ) => void = (parentElement, weightDelta) => {
    
  849.     let isInsideCollapsedSubTree = false;
    
  850. 
    
  851.     while (parentElement != null) {
    
  852.       parentElement.weight += weightDelta;
    
  853. 
    
  854.       // Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.
    
  855.       // Their weight will bubble up when the parent is expanded.
    
  856.       if (parentElement.isCollapsed) {
    
  857.         isInsideCollapsedSubTree = true;
    
  858.         break;
    
  859.       }
    
  860. 
    
  861.       parentElement = this._idToElement.get(parentElement.parentID);
    
  862.     }
    
  863. 
    
  864.     // Additions and deletions within a collapsed subtree should not affect the overall number of elements.
    
  865.     if (!isInsideCollapsedSubTree) {
    
  866.       this._weightAcrossRoots += weightDelta;
    
  867.     }
    
  868.   };
    
  869. 
    
  870.   _recursivelyUpdateSubtree(
    
  871.     id: number,
    
  872.     callback: (element: Element) => void,
    
  873.   ): void {
    
  874.     const element = this._idToElement.get(id);
    
  875.     if (element) {
    
  876.       callback(element);
    
  877. 
    
  878.       element.children.forEach(child =>
    
  879.         this._recursivelyUpdateSubtree(child, callback),
    
  880.       );
    
  881.     }
    
  882.   }
    
  883. 
    
  884.   onBridgeNativeStyleEditorSupported: ({
    
  885.     isSupported: boolean,
    
  886.     validAttributes: ?$ReadOnlyArray<string>,
    
  887.   }) => void = ({isSupported, validAttributes}) => {
    
  888.     this._isNativeStyleEditorSupported = isSupported;
    
  889.     this._nativeStyleEditorValidAttributes = validAttributes || null;
    
  890. 
    
  891.     this.emit('supportsNativeStyleEditor');
    
  892.   };
    
  893. 
    
  894.   onBridgeOperations: (operations: Array<number>) => void = operations => {
    
  895.     if (__DEBUG__) {
    
  896.       console.groupCollapsed('onBridgeOperations');
    
  897.       debug('onBridgeOperations', operations.join(','));
    
  898.     }
    
  899. 
    
  900.     let haveRootsChanged = false;
    
  901.     let haveErrorsOrWarningsChanged = false;
    
  902. 
    
  903.     // The first two values are always rendererID and rootID
    
  904.     const rendererID = operations[0];
    
  905. 
    
  906.     const addedElementIDs: Array<number> = [];
    
  907.     // This is a mapping of removed ID -> parent ID:
    
  908.     const removedElementIDs: Map<number, number> = new Map();
    
  909.     // We'll use the parent ID to adjust selection if it gets deleted.
    
  910. 
    
  911.     let i = 2;
    
  912. 
    
  913.     // Reassemble the string table.
    
  914.     const stringTable: Array<string | null> = [
    
  915.       null, // ID = 0 corresponds to the null string.
    
  916.     ];
    
  917.     const stringTableSize = operations[i];
    
  918.     i++;
    
  919. 
    
  920.     const stringTableEnd = i + stringTableSize;
    
  921. 
    
  922.     while (i < stringTableEnd) {
    
  923.       const nextLength = operations[i];
    
  924.       i++;
    
  925. 
    
  926.       const nextString = utfDecodeString(operations.slice(i, i + nextLength));
    
  927.       stringTable.push(nextString);
    
  928.       i += nextLength;
    
  929.     }
    
  930. 
    
  931.     while (i < operations.length) {
    
  932.       const operation = operations[i];
    
  933.       switch (operation) {
    
  934.         case TREE_OPERATION_ADD: {
    
  935.           const id = operations[i + 1];
    
  936.           const type = ((operations[i + 2]: any): ElementType);
    
  937. 
    
  938.           i += 3;
    
  939. 
    
  940.           if (this._idToElement.has(id)) {
    
  941.             this._throwAndEmitError(
    
  942.               Error(
    
  943.                 `Cannot add node "${id}" because a node with that id is already in the Store.`,
    
  944.               ),
    
  945.             );
    
  946.           }
    
  947. 
    
  948.           if (type === ElementTypeRoot) {
    
  949.             if (__DEBUG__) {
    
  950.               debug('Add', `new root node ${id}`);
    
  951.             }
    
  952. 
    
  953.             const isStrictModeCompliant = operations[i] > 0;
    
  954.             i++;
    
  955. 
    
  956.             const supportsBasicProfiling =
    
  957.               (operations[i] & PROFILING_FLAG_BASIC_SUPPORT) !== 0;
    
  958.             const supportsTimeline =
    
  959.               (operations[i] & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0;
    
  960.             i++;
    
  961. 
    
  962.             let supportsStrictMode = false;
    
  963.             let hasOwnerMetadata = false;
    
  964. 
    
  965.             // If we don't know the bridge protocol, guess that we're dealing with the latest.
    
  966.             // If we do know it, we can take it into consideration when parsing operations.
    
  967.             if (
    
  968.               this._bridgeProtocol === null ||
    
  969.               this._bridgeProtocol.version >= 2
    
  970.             ) {
    
  971.               supportsStrictMode = operations[i] > 0;
    
  972.               i++;
    
  973. 
    
  974.               hasOwnerMetadata = operations[i] > 0;
    
  975.               i++;
    
  976.             }
    
  977. 
    
  978.             this._roots = this._roots.concat(id);
    
  979.             this._rootIDToRendererID.set(id, rendererID);
    
  980.             this._rootIDToCapabilities.set(id, {
    
  981.               supportsBasicProfiling,
    
  982.               hasOwnerMetadata,
    
  983.               supportsStrictMode,
    
  984.               supportsTimeline,
    
  985.             });
    
  986. 
    
  987.             // Not all roots support StrictMode;
    
  988.             // don't flag a root as non-compliant unless it also supports StrictMode.
    
  989.             const isStrictModeNonCompliant =
    
  990.               !isStrictModeCompliant && supportsStrictMode;
    
  991. 
    
  992.             this._idToElement.set(id, {
    
  993.               children: [],
    
  994.               depth: -1,
    
  995.               displayName: null,
    
  996.               hocDisplayNames: null,
    
  997.               id,
    
  998.               isCollapsed: false, // Never collapse roots; it would hide the entire tree.
    
  999.               isStrictModeNonCompliant,
    
  1000.               key: null,
    
  1001.               ownerID: 0,
    
  1002.               parentID: 0,
    
  1003.               type,
    
  1004.               weight: 0,
    
  1005.             });
    
  1006. 
    
  1007.             haveRootsChanged = true;
    
  1008.           } else {
    
  1009.             const parentID = operations[i];
    
  1010.             i++;
    
  1011. 
    
  1012.             const ownerID = operations[i];
    
  1013.             i++;
    
  1014. 
    
  1015.             const displayNameStringID = operations[i];
    
  1016.             const displayName = stringTable[displayNameStringID];
    
  1017.             i++;
    
  1018. 
    
  1019.             const keyStringID = operations[i];
    
  1020.             const key = stringTable[keyStringID];
    
  1021.             i++;
    
  1022. 
    
  1023.             if (__DEBUG__) {
    
  1024.               debug(
    
  1025.                 'Add',
    
  1026.                 `node ${id} (${displayName || 'null'}) as child of ${parentID}`,
    
  1027.               );
    
  1028.             }
    
  1029. 
    
  1030.             const parentElement = this._idToElement.get(parentID);
    
  1031.             if (parentElement === undefined) {
    
  1032.               this._throwAndEmitError(
    
  1033.                 Error(
    
  1034.                   `Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,
    
  1035.                 ),
    
  1036.               );
    
  1037. 
    
  1038.               continue;
    
  1039.             }
    
  1040. 
    
  1041.             parentElement.children.push(id);
    
  1042. 
    
  1043.             const [displayNameWithoutHOCs, hocDisplayNames] =
    
  1044.               separateDisplayNameAndHOCs(displayName, type);
    
  1045. 
    
  1046.             const element: Element = {
    
  1047.               children: [],
    
  1048.               depth: parentElement.depth + 1,
    
  1049.               displayName: displayNameWithoutHOCs,
    
  1050.               hocDisplayNames,
    
  1051.               id,
    
  1052.               isCollapsed: this._collapseNodesByDefault,
    
  1053.               isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant,
    
  1054.               key,
    
  1055.               ownerID,
    
  1056.               parentID,
    
  1057.               type,
    
  1058.               weight: 1,
    
  1059.             };
    
  1060. 
    
  1061.             this._idToElement.set(id, element);
    
  1062.             addedElementIDs.push(id);
    
  1063.             this._adjustParentTreeWeight(parentElement, 1);
    
  1064. 
    
  1065.             if (ownerID > 0) {
    
  1066.               let set = this._ownersMap.get(ownerID);
    
  1067.               if (set === undefined) {
    
  1068.                 set = new Set();
    
  1069.                 this._ownersMap.set(ownerID, set);
    
  1070.               }
    
  1071.               set.add(id);
    
  1072.             }
    
  1073.           }
    
  1074.           break;
    
  1075.         }
    
  1076.         case TREE_OPERATION_REMOVE: {
    
  1077.           const removeLength = operations[i + 1];
    
  1078.           i += 2;
    
  1079. 
    
  1080.           for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
    
  1081.             const id = operations[i];
    
  1082.             const element = this._idToElement.get(id);
    
  1083. 
    
  1084.             if (element === undefined) {
    
  1085.               this._throwAndEmitError(
    
  1086.                 Error(
    
  1087.                   `Cannot remove node "${id}" because no matching node was found in the Store.`,
    
  1088.                 ),
    
  1089.               );
    
  1090. 
    
  1091.               continue;
    
  1092.             }
    
  1093. 
    
  1094.             i += 1;
    
  1095. 
    
  1096.             const {children, ownerID, parentID, weight} = element;
    
  1097.             if (children.length > 0) {
    
  1098.               this._throwAndEmitError(
    
  1099.                 Error(`Node "${id}" was removed before its children.`),
    
  1100.               );
    
  1101.             }
    
  1102. 
    
  1103.             this._idToElement.delete(id);
    
  1104. 
    
  1105.             let parentElement: ?Element = null;
    
  1106.             if (parentID === 0) {
    
  1107.               if (__DEBUG__) {
    
  1108.                 debug('Remove', `node ${id} root`);
    
  1109.               }
    
  1110. 
    
  1111.               this._roots = this._roots.filter(rootID => rootID !== id);
    
  1112.               this._rootIDToRendererID.delete(id);
    
  1113.               this._rootIDToCapabilities.delete(id);
    
  1114. 
    
  1115.               haveRootsChanged = true;
    
  1116.             } else {
    
  1117.               if (__DEBUG__) {
    
  1118.                 debug('Remove', `node ${id} from parent ${parentID}`);
    
  1119.               }
    
  1120. 
    
  1121.               parentElement = this._idToElement.get(parentID);
    
  1122.               if (parentElement === undefined) {
    
  1123.                 this._throwAndEmitError(
    
  1124.                   Error(
    
  1125.                     `Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
    
  1126.                   ),
    
  1127.                 );
    
  1128. 
    
  1129.                 continue;
    
  1130.               }
    
  1131. 
    
  1132.               const index = parentElement.children.indexOf(id);
    
  1133.               parentElement.children.splice(index, 1);
    
  1134.             }
    
  1135. 
    
  1136.             this._adjustParentTreeWeight(parentElement, -weight);
    
  1137.             removedElementIDs.set(id, parentID);
    
  1138. 
    
  1139.             this._ownersMap.delete(id);
    
  1140.             if (ownerID > 0) {
    
  1141.               const set = this._ownersMap.get(ownerID);
    
  1142.               if (set !== undefined) {
    
  1143.                 set.delete(id);
    
  1144.               }
    
  1145.             }
    
  1146. 
    
  1147.             if (this._errorsAndWarnings.has(id)) {
    
  1148.               this._errorsAndWarnings.delete(id);
    
  1149.               haveErrorsOrWarningsChanged = true;
    
  1150.             }
    
  1151.           }
    
  1152. 
    
  1153.           break;
    
  1154.         }
    
  1155.         case TREE_OPERATION_REMOVE_ROOT: {
    
  1156.           i += 1;
    
  1157. 
    
  1158.           const id = operations[1];
    
  1159. 
    
  1160.           if (__DEBUG__) {
    
  1161.             debug(`Remove root ${id}`);
    
  1162.           }
    
  1163. 
    
  1164.           const recursivelyDeleteElements = (elementID: number) => {
    
  1165.             const element = this._idToElement.get(elementID);
    
  1166.             this._idToElement.delete(elementID);
    
  1167.             if (element) {
    
  1168.               // Mostly for Flow's sake
    
  1169.               for (let index = 0; index < element.children.length; index++) {
    
  1170.                 recursivelyDeleteElements(element.children[index]);
    
  1171.               }
    
  1172.             }
    
  1173.           };
    
  1174. 
    
  1175.           const root = ((this._idToElement.get(id): any): Element);
    
  1176.           recursivelyDeleteElements(id);
    
  1177. 
    
  1178.           this._rootIDToCapabilities.delete(id);
    
  1179.           this._rootIDToRendererID.delete(id);
    
  1180.           this._roots = this._roots.filter(rootID => rootID !== id);
    
  1181.           this._weightAcrossRoots -= root.weight;
    
  1182.           break;
    
  1183.         }
    
  1184.         case TREE_OPERATION_REORDER_CHILDREN: {
    
  1185.           const id = operations[i + 1];
    
  1186.           const numChildren = operations[i + 2];
    
  1187.           i += 3;
    
  1188. 
    
  1189.           const element = this._idToElement.get(id);
    
  1190.           if (element === undefined) {
    
  1191.             this._throwAndEmitError(
    
  1192.               Error(
    
  1193.                 `Cannot reorder children for node "${id}" because no matching node was found in the Store.`,
    
  1194.               ),
    
  1195.             );
    
  1196. 
    
  1197.             continue;
    
  1198.           }
    
  1199. 
    
  1200.           const children = element.children;
    
  1201.           if (children.length !== numChildren) {
    
  1202.             this._throwAndEmitError(
    
  1203.               Error(
    
  1204.                 `Children cannot be added or removed during a reorder operation.`,
    
  1205.               ),
    
  1206.             );
    
  1207.           }
    
  1208. 
    
  1209.           for (let j = 0; j < numChildren; j++) {
    
  1210.             const childID = operations[i + j];
    
  1211.             children[j] = childID;
    
  1212.             if (__DEV__) {
    
  1213.               // This check is more expensive so it's gated by __DEV__.
    
  1214.               const childElement = this._idToElement.get(childID);
    
  1215.               if (childElement == null || childElement.parentID !== id) {
    
  1216.                 console.error(
    
  1217.                   `Children cannot be added or removed during a reorder operation.`,
    
  1218.                 );
    
  1219.               }
    
  1220.             }
    
  1221.           }
    
  1222.           i += numChildren;
    
  1223. 
    
  1224.           if (__DEBUG__) {
    
  1225.             debug('Re-order', `Node ${id} children ${children.join(',')}`);
    
  1226.           }
    
  1227.           break;
    
  1228.         }
    
  1229.         case TREE_OPERATION_SET_SUBTREE_MODE: {
    
  1230.           const id = operations[i + 1];
    
  1231.           const mode = operations[i + 2];
    
  1232. 
    
  1233.           i += 3;
    
  1234. 
    
  1235.           // If elements have already been mounted in this subtree, update them.
    
  1236.           // (In practice, this likely only applies to the root element.)
    
  1237.           if (mode === StrictMode) {
    
  1238.             this._recursivelyUpdateSubtree(id, element => {
    
  1239.               element.isStrictModeNonCompliant = false;
    
  1240.             });
    
  1241.           }
    
  1242. 
    
  1243.           if (__DEBUG__) {
    
  1244.             debug(
    
  1245.               'Subtree mode',
    
  1246.               `Subtree with root ${id} set to mode ${mode}`,
    
  1247.             );
    
  1248.           }
    
  1249.           break;
    
  1250.         }
    
  1251.         case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
    
  1252.           // Base duration updates are only sent while profiling is in progress.
    
  1253.           // We can ignore them at this point.
    
  1254.           // The profiler UI uses them lazily in order to generate the tree.
    
  1255.           i += 3;
    
  1256.           break;
    
  1257.         case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
    
  1258.           const id = operations[i + 1];
    
  1259.           const errorCount = operations[i + 2];
    
  1260.           const warningCount = operations[i + 3];
    
  1261. 
    
  1262.           i += 4;
    
  1263. 
    
  1264.           if (errorCount > 0 || warningCount > 0) {
    
  1265.             this._errorsAndWarnings.set(id, {errorCount, warningCount});
    
  1266.           } else if (this._errorsAndWarnings.has(id)) {
    
  1267.             this._errorsAndWarnings.delete(id);
    
  1268.           }
    
  1269.           haveErrorsOrWarningsChanged = true;
    
  1270.           break;
    
  1271.         default:
    
  1272.           this._throwAndEmitError(
    
  1273.             new UnsupportedBridgeOperationError(
    
  1274.               `Unsupported Bridge operation "${operation}"`,
    
  1275.             ),
    
  1276.           );
    
  1277.       }
    
  1278.     }
    
  1279. 
    
  1280.     this._revision++;
    
  1281. 
    
  1282.     // Any time the tree changes (e.g. elements added, removed, or reordered) cached inidices may be invalid.
    
  1283.     this._cachedErrorAndWarningTuples = null;
    
  1284. 
    
  1285.     if (haveErrorsOrWarningsChanged) {
    
  1286.       let errorCount = 0;
    
  1287.       let warningCount = 0;
    
  1288. 
    
  1289.       this._errorsAndWarnings.forEach(entry => {
    
  1290.         errorCount += entry.errorCount;
    
  1291.         warningCount += entry.warningCount;
    
  1292.       });
    
  1293. 
    
  1294.       this._cachedErrorCount = errorCount;
    
  1295.       this._cachedWarningCount = warningCount;
    
  1296.     }
    
  1297. 
    
  1298.     if (haveRootsChanged) {
    
  1299.       const prevRootSupportsProfiling = this._rootSupportsBasicProfiling;
    
  1300.       const prevRootSupportsTimelineProfiling =
    
  1301.         this._rootSupportsTimelineProfiling;
    
  1302. 
    
  1303.       this._hasOwnerMetadata = false;
    
  1304.       this._rootSupportsBasicProfiling = false;
    
  1305.       this._rootSupportsTimelineProfiling = false;
    
  1306.       this._rootIDToCapabilities.forEach(
    
  1307.         ({supportsBasicProfiling, hasOwnerMetadata, supportsTimeline}) => {
    
  1308.           if (supportsBasicProfiling) {
    
  1309.             this._rootSupportsBasicProfiling = true;
    
  1310.           }
    
  1311.           if (hasOwnerMetadata) {
    
  1312.             this._hasOwnerMetadata = true;
    
  1313.           }
    
  1314.           if (supportsTimeline) {
    
  1315.             this._rootSupportsTimelineProfiling = true;
    
  1316.           }
    
  1317.         },
    
  1318.       );
    
  1319. 
    
  1320.       this.emit('roots');
    
  1321. 
    
  1322.       if (this._rootSupportsBasicProfiling !== prevRootSupportsProfiling) {
    
  1323.         this.emit('rootSupportsBasicProfiling');
    
  1324.       }
    
  1325. 
    
  1326.       if (
    
  1327.         this._rootSupportsTimelineProfiling !==
    
  1328.         prevRootSupportsTimelineProfiling
    
  1329.       ) {
    
  1330.         this.emit('rootSupportsTimelineProfiling');
    
  1331.       }
    
  1332.     }
    
  1333. 
    
  1334.     if (__DEBUG__) {
    
  1335.       console.log(printStore(this, true));
    
  1336.       console.groupEnd();
    
  1337.     }
    
  1338. 
    
  1339.     this.emit('mutated', [addedElementIDs, removedElementIDs]);
    
  1340.   };
    
  1341. 
    
  1342.   // Certain backends save filters on a per-domain basis.
    
  1343.   // In order to prevent filter preferences and applied filters from being out of sync,
    
  1344.   // this message enables the backend to override the frontend's current ("saved") filters.
    
  1345.   // This action should also override the saved filters too,
    
  1346.   // else reloading the frontend without reloading the backend would leave things out of sync.
    
  1347.   onBridgeOverrideComponentFilters: (
    
  1348.     componentFilters: Array<ComponentFilter>,
    
  1349.   ) => void = componentFilters => {
    
  1350.     this._componentFilters = componentFilters;
    
  1351. 
    
  1352.     setSavedComponentFilters(componentFilters);
    
  1353.   };
    
  1354. 
    
  1355.   onBridgeShutdown: () => void = () => {
    
  1356.     if (__DEBUG__) {
    
  1357.       debug('onBridgeShutdown', 'unsubscribing from Bridge');
    
  1358.     }
    
  1359. 
    
  1360.     const bridge = this._bridge;
    
  1361.     bridge.removeListener('operations', this.onBridgeOperations);
    
  1362.     bridge.removeListener(
    
  1363.       'overrideComponentFilters',
    
  1364.       this.onBridgeOverrideComponentFilters,
    
  1365.     );
    
  1366.     bridge.removeListener('shutdown', this.onBridgeShutdown);
    
  1367.     bridge.removeListener(
    
  1368.       'isBackendStorageAPISupported',
    
  1369.       this.onBackendStorageAPISupported,
    
  1370.     );
    
  1371.     bridge.removeListener(
    
  1372.       'isNativeStyleEditorSupported',
    
  1373.       this.onBridgeNativeStyleEditorSupported,
    
  1374.     );
    
  1375.     bridge.removeListener(
    
  1376.       'isSynchronousXHRSupported',
    
  1377.       this.onBridgeSynchronousXHRSupported,
    
  1378.     );
    
  1379.     bridge.removeListener(
    
  1380.       'unsupportedRendererVersion',
    
  1381.       this.onBridgeUnsupportedRendererVersion,
    
  1382.     );
    
  1383.     bridge.removeListener('backendVersion', this.onBridgeBackendVersion);
    
  1384.     bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
    
  1385.     bridge.removeListener('saveToClipboard', this.onSaveToClipboard);
    
  1386. 
    
  1387.     if (this._onBridgeProtocolTimeoutID !== null) {
    
  1388.       clearTimeout(this._onBridgeProtocolTimeoutID);
    
  1389.       this._onBridgeProtocolTimeoutID = null;
    
  1390.     }
    
  1391.   };
    
  1392. 
    
  1393.   onBackendStorageAPISupported: (
    
  1394.     isBackendStorageAPISupported: boolean,
    
  1395.   ) => void = isBackendStorageAPISupported => {
    
  1396.     this._isBackendStorageAPISupported = isBackendStorageAPISupported;
    
  1397. 
    
  1398.     this.emit('supportsReloadAndProfile');
    
  1399.   };
    
  1400. 
    
  1401.   onBridgeSynchronousXHRSupported: (
    
  1402.     isSynchronousXHRSupported: boolean,
    
  1403.   ) => void = isSynchronousXHRSupported => {
    
  1404.     this._isSynchronousXHRSupported = isSynchronousXHRSupported;
    
  1405. 
    
  1406.     this.emit('supportsReloadAndProfile');
    
  1407.   };
    
  1408. 
    
  1409.   onBridgeUnsupportedRendererVersion: () => void = () => {
    
  1410.     this._unsupportedRendererVersionDetected = true;
    
  1411. 
    
  1412.     this.emit('unsupportedRendererVersionDetected');
    
  1413.   };
    
  1414. 
    
  1415.   onBridgeBackendVersion: (backendVersion: string) => void = backendVersion => {
    
  1416.     this._backendVersion = backendVersion;
    
  1417.     this.emit('backendVersion');
    
  1418.   };
    
  1419. 
    
  1420.   onBridgeProtocol: (bridgeProtocol: BridgeProtocol) => void =
    
  1421.     bridgeProtocol => {
    
  1422.       if (this._onBridgeProtocolTimeoutID !== null) {
    
  1423.         clearTimeout(this._onBridgeProtocolTimeoutID);
    
  1424.         this._onBridgeProtocolTimeoutID = null;
    
  1425.       }
    
  1426. 
    
  1427.       this._bridgeProtocol = bridgeProtocol;
    
  1428. 
    
  1429.       if (bridgeProtocol.version !== currentBridgeProtocol.version) {
    
  1430.         // Technically newer versions of the frontend can, at least for now,
    
  1431.         // gracefully handle older versions of the backend protocol.
    
  1432.         // So for now we don't need to display the unsupported dialog.
    
  1433.       }
    
  1434.     };
    
  1435. 
    
  1436.   onBridgeProtocolTimeout: () => void = () => {
    
  1437.     this._onBridgeProtocolTimeoutID = null;
    
  1438. 
    
  1439.     // If we timed out, that indicates the backend predates the bridge protocol,
    
  1440.     // so we can set a fake version (0) to trigger the downgrade message.
    
  1441.     this._bridgeProtocol = BRIDGE_PROTOCOL[0];
    
  1442. 
    
  1443.     this.emit('unsupportedBridgeProtocolDetected');
    
  1444.   };
    
  1445. 
    
  1446.   onSaveToClipboard: (text: string) => void = text => {
    
  1447.     copy(text);
    
  1448.   };
    
  1449. 
    
  1450.   // The Store should never throw an Error without also emitting an event.
    
  1451.   // Otherwise Store errors will be invisible to users,
    
  1452.   // but the downstream errors they cause will be reported as bugs.
    
  1453.   // For example, https://github.com/facebook/react/issues/21402
    
  1454.   // Emitting an error event allows the ErrorBoundary to show the original error.
    
  1455.   _throwAndEmitError(error: Error): empty {
    
  1456.     this.emit('error', error);
    
  1457. 
    
  1458.     // Throwing is still valuable for local development
    
  1459.     // and for unit testing the Store itself.
    
  1460.     throw error;
    
  1461.   }
    
  1462. }