main
js 2,585 lines 82.5 KB
Raw
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_PERFORMANCE_TRACKS_SUPPORT,
16 TREE_OPERATION_ADD,
17 TREE_OPERATION_REMOVE,
18 TREE_OPERATION_REORDER_CHILDREN,
19 TREE_OPERATION_SET_SUBTREE_MODE,
20 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
21 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
22 TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
23 SUSPENSE_TREE_OPERATION_ADD,
24 SUSPENSE_TREE_OPERATION_REMOVE,
25 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
26 SUSPENSE_TREE_OPERATION_RESIZE,
27 SUSPENSE_TREE_OPERATION_SUSPENDERS,
28 } from '../constants';
29 import {
30 ElementTypeClass,
31 ElementTypeContext,
32 ElementTypeFunction,
33 ElementTypeForwardRef,
34 ElementTypeHostComponent,
35 ElementTypeMemo,
36 ElementTypeOtherOrUnknown,
37 ElementTypeProfiler,
38 ElementTypeRoot,
39 ElementTypeSuspense,
40 ElementTypeSuspenseList,
41 ElementTypeTracingMarker,
42 ElementTypeVirtual,
43 ElementTypeViewTransition,
44 ElementTypeActivity,
45 ComponentFilterActivitySlice,
46 } from '../frontend/types';
47 import {
48 getSavedComponentFilters,
49 setSavedComponentFilters,
50 shallowDiffers,
51 utfDecodeStringWithRanges,
52 parseElementDisplayNameFromBackend,
53 unionOfTwoArrays,
54 } from '../utils';
55 import {localStorageGetItem, localStorageSetItem} from '../storage';
56 import {__DEBUG__} from '../constants';
57 import {printStore} from './utils';
58 import ProfilerStore from './ProfilerStore';
59 import {
60 BRIDGE_PROTOCOL,
61 currentBridgeProtocol,
62 } from 'react-devtools-shared/src/bridge';
63 import {
64 StrictMode,
65 ActivityHiddenMode,
66 ActivityVisibleMode,
67 } from 'react-devtools-shared/src/frontend/types';
68 import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
69
70 import type {
71 Element,
72 ComponentFilter,
73 ElementType,
74 SuspenseNode,
75 SuspenseTimelineStep,
76 Rect,
77 } from 'react-devtools-shared/src/frontend/types';
78 import type {
79 FrontendBridge,
80 BridgeProtocol,
81 } from 'react-devtools-shared/src/bridge';
82 import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';
83 import type {DevToolsHookSettings} from '../backend/types';
84
85 import RBush from 'rbush';
86
87 // Custom version which works with our Rect data structure.
88 class RectRBush extends RBush<Rect> {
89 toBBox(rect: Rect): {
90 minX: number,
91 minY: number,
92 maxX: number,
93 maxY: number,
94 } {
95 return {
96 minX: rect.x,
97 minY: rect.y,
98 maxX: rect.x + rect.width,
99 maxY: rect.y + rect.height,
100 };
101 }
102 compareMinX(a: Rect, b: Rect): number {
103 return a.x - b.x;
104 }
105 compareMinY(a: Rect, b: Rect): number {
106 return a.y - b.y;
107 }
108 }
109
110 const debug = (methodName: string, ...args: Array<string>) => {
111 // $FlowFixMe[constant-condition]
112 if (__DEBUG__) {
113 console.log(
114 `%cStore %c${methodName}`,
115 'color: green; font-weight: bold;',
116 'font-weight: bold;',
117 ...args,
118 );
119 }
120 };
121
122 const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
123 'React::DevTools::collapseNodesByDefault';
124 const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
125 'React::DevTools::recordChangeDescriptions';
126
127 type ErrorAndWarningTuples = Array<{id: number, index: number}>;
128
129 export type Config = {
130 checkBridgeProtocolCompatibility?: boolean,
131 isProfiling?: boolean,
132 supportsInspectMatchingDOMElement?: boolean,
133 supportsClickToInspect?: boolean,
134 supportsReloadAndProfile?: boolean,
135 supportsTraceUpdates?: boolean,
136 };
137
138 const ADVANCED_PROFILING_NONE = 0;
139 const ADVANCED_PROFILING_PERFORMANCE_TRACKS = 2;
140 type AdvancedProfiling = 0 | 2;
141
142 export type Capabilities = {
143 supportsBasicProfiling: boolean,
144 hasOwnerMetadata: boolean,
145 supportsStrictMode: boolean,
146 supportsAdvancedProfiling: AdvancedProfiling,
147 };
148
149 function isNonZeroRect(rect: Rect) {
150 return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0;
151 }
152
153 function parseElementType(value: number): ElementType | null {
154 // Cast before switching so Flow checks exhaustiveness while the default rejects unknown bridge values.
155 const type = value as any as ElementType;
156 switch (type) {
157 case ElementTypeClass:
158 case ElementTypeContext:
159 case ElementTypeFunction:
160 case ElementTypeForwardRef:
161 case ElementTypeHostComponent:
162 case ElementTypeMemo:
163 case ElementTypeOtherOrUnknown:
164 case ElementTypeProfiler:
165 case ElementTypeRoot:
166 case ElementTypeSuspense:
167 case ElementTypeSuspenseList:
168 case ElementTypeTracingMarker:
169 case ElementTypeVirtual:
170 case ElementTypeViewTransition:
171 case ElementTypeActivity:
172 return type;
173 default:
174 (type) as empty;
175 return null;
176 }
177 }
178
179 /**
180 * The store is the single source of truth for updates from the backend.
181 * ContextProviders can subscribe to the Store for specific things they want to provide.
182 */
183 export default class Store extends EventEmitter<{
184 backendVersion: [],
185 collapseNodesByDefault: [],
186 componentFilters: [],
187 error: [Error],
188 hookSettings: [$ReadOnly<DevToolsHookSettings>],
189 hostInstanceSelected: [Element['id'] | null],
190 settingsUpdated: [$ReadOnly<DevToolsHookSettings>, Array<ComponentFilter>],
191 mutated: [
192 [
193 Array<Element['id']>,
194 Map<Element['id'], Element['id']>,
195 Element['id'] | null,
196 ],
197 ],
198 recordChangeDescriptions: [],
199 roots: [],
200 rootSupportsBasicProfiling: [],
201 rootSupportsPerformanceTracks: [],
202 suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],
203 supportsNativeStyleEditor: [],
204 supportsReloadAndProfile: [],
205 unsupportedBridgeProtocolDetected: [],
206 unsupportedRendererVersionDetected: [],
207 }> {
208 // If the backend version is new enough to report its (NPM) version, this is it.
209 // This version may be displayed by the frontend for debugging purposes.
210 _backendVersion: string | null = null;
211
212 _bridge: FrontendBridge;
213
214 // Computed whenever _errorsAndWarnings Map changes.
215 _cachedComponentWithErrorCount: number = 0;
216 _cachedComponentWithWarningCount: number = 0;
217 _cachedErrorAndWarningTuples: ErrorAndWarningTuples | null = null;
218
219 // Should new nodes be collapsed by default when added to the tree?
220 _collapseNodesByDefault: boolean = true;
221
222 _componentFilters: Array<ComponentFilter>;
223
224 // Map of ID to number of recorded error and warning message IDs.
225 _errorsAndWarnings: Map<
226 Element['id'],
227 {errorCount: number, warningCount: number},
228 > = new Map();
229
230 _focusedTransition: 0 | Element['id'] = 0;
231
232 // At least one of the injected renderers contains (DEV only) owner metadata.
233 _hasOwnerMetadata: boolean = false;
234
235 // Map of ID to (mutable) Element.
236 // Elements are mutated to avoid excessive cloning during tree updates.
237 // The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.
238 _idToElement: Map<Element['id'], Element> = new Map();
239
240 _idToSuspense: Map<SuspenseNode['id'], SuspenseNode> = new Map();
241
242 // Should the React Native style editor panel be shown?
243 _isNativeStyleEditorSupported: boolean = false;
244
245 _nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;
246
247 // Older backends don't support an explicit bridge protocol,
248 // so we should timeout eventually and show a downgrade message.
249 _onBridgeProtocolTimeoutID: TimeoutID | null = null;
250
251 // Map of element (id) to the set of elements (ids) it owns.
252 // This map enables getOwnersListForElement() to avoid traversing the entire tree.
253 _ownersMap: Map<Element['id'], Set<Element['id']>> = new Map();
254
255 _profilerStore: ProfilerStore;
256
257 _recordChangeDescriptions: boolean = false;
258
259 // Incremented each time the store is mutated.
260 // This enables a passive effect to detect a mutation between render and commit phase.
261 _revision: number = 0;
262 _revisionSuspense: number = 0;
263
264 // This Array must be treated as immutable!
265 // Passive effects will check it for changes between render and mount.
266 _roots: $ReadOnlyArray<Element['id']> = [];
267
268 _rootIDToCapabilities: Map<Element['id'], Capabilities> = new Map();
269
270 // Renderer ID is needed to support inspection fiber props, state, and hooks.
271 _rootIDToRendererID: Map<Element['id'], number> = new Map();
272
273 // Stores all the SuspenseNode rects in an R-tree to make it fast to find overlaps.
274 _rtree: RBush<Rect> = new RectRBush();
275
276 // These options may be initially set by a configuration option when constructing the Store.
277 _supportsInspectMatchingDOMElement: boolean = false;
278 _supportsClickToInspect: boolean = false;
279 _supportsTraceUpdates: boolean = false;
280
281 _isReloadAndProfileFrontendSupported: boolean = false;
282 _isReloadAndProfileBackendSupported: boolean = false;
283
284 // These options default to false but may be updated as roots are added and removed.
285 _rootSupportsBasicProfiling: boolean = false;
286 _rootSupportsPerformanceTracks: boolean = false;
287
288 _bridgeProtocol: BridgeProtocol | null = null;
289 _unsupportedBridgeProtocolDetected: boolean = false;
290 _unsupportedRendererVersionDetected: boolean = false;
291
292 // Total number of visible elements (within all roots).
293 // Used for windowing purposes.
294 _weightAcrossRoots: number = 0;
295
296 _shouldCheckBridgeProtocolCompatibility: boolean = false;
297 _hookSettings: $ReadOnly<DevToolsHookSettings> | null = null;
298 _shouldShowWarningsAndErrors: boolean = false;
299
300 // Only used in browser extension for synchronization with built-in Elements panel.
301 _lastSelectedHostInstanceElementId: Element['id'] | null = null;
302
303 // Maximum recorded node depth during the lifetime of this Store.
304 // Can only increase: not guaranteed to return maximal value for currently recorded elements.
305 _maximumRecordedDepth = 0;
306
307 constructor(bridge: FrontendBridge, config?: Config) {
308 super();
309
310 // $FlowFixMe[constant-condition]
311 if (__DEBUG__) {
312 debug('constructor', 'subscribing to Bridge');
313 }
314
315 this._collapseNodesByDefault =
316 localStorageGetItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) ===
317 'true';
318
319 this._recordChangeDescriptions =
320 localStorageGetItem(LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
321 'true';
322
323 this._componentFilters = getSavedComponentFilters();
324
325 let isProfiling = false;
326 if (config != null) {
327 isProfiling = config.isProfiling === true;
328
329 const {
330 supportsInspectMatchingDOMElement,
331 supportsClickToInspect,
332 supportsReloadAndProfile,
333 supportsTraceUpdates,
334 checkBridgeProtocolCompatibility,
335 } = config;
336 if (supportsInspectMatchingDOMElement) {
337 this._supportsInspectMatchingDOMElement = true;
338 }
339 if (supportsClickToInspect) {
340 this._supportsClickToInspect = true;
341 }
342 if (supportsReloadAndProfile) {
343 this._isReloadAndProfileFrontendSupported = true;
344 }
345 if (supportsTraceUpdates) {
346 this._supportsTraceUpdates = true;
347 }
348 if (checkBridgeProtocolCompatibility) {
349 this._shouldCheckBridgeProtocolCompatibility = true;
350 }
351 }
352
353 this._bridge = bridge;
354 bridge.addListener('operations', this.onBridgeOperations);
355 bridge.addListener('shutdown', this.onBridgeShutdown);
356 bridge.addListener(
357 'isReloadAndProfileSupportedByBackend',
358 this.onBackendReloadAndProfileSupported,
359 );
360 bridge.addListener(
361 'isNativeStyleEditorSupported',
362 this.onBridgeNativeStyleEditorSupported,
363 );
364 bridge.addListener(
365 'unsupportedRendererVersion',
366 this.onBridgeUnsupportedRendererVersion,
367 );
368
369 this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
370
371 bridge.addListener('backendVersion', this.onBridgeBackendVersion);
372 bridge.addListener('saveToClipboard', this.onSaveToClipboard);
373 bridge.addListener('hookSettings', this.onHookSettings);
374 bridge.addListener('backendInitialized', this.onBackendInitialized);
375 bridge.addListener('selectElement', this.onHostInstanceSelected);
376 }
377
378 // This is only used in tests to avoid memory leaks.
379 assertExpectedRootMapSizes() {
380 if (this.roots.length === 0) {
381 // The only safe time to assert these maps are empty is when the store is empty.
382 this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');
383 this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');
384 }
385
386 // These maps should always be the same size as the number of roots
387 this.assertMapSizeMatchesRootCount(
388 this._rootIDToCapabilities,
389 '_rootIDToCapabilities',
390 );
391 this.assertMapSizeMatchesRootCount(
392 this._rootIDToRendererID,
393 '_rootIDToRendererID',
394 );
395 }
396
397 // This is only used in tests to avoid memory leaks.
398 assertMapSizeMatchesRootCount<K, V>(map: Map<K, V>, mapName: string) {
399 const expectedSize = this.roots.length;
400 if (map.size !== expectedSize) {
401 this._throwAndEmitError(
402 Error(
403 `Expected ${mapName} to contain ${expectedSize} items, but it contains ${
404 map.size
405 } items\n\n${inspect(map, {
406 depth: 20,
407 })}`,
408 ),
409 );
410 }
411 }
412
413 get backendVersion(): string | null {
414 return this._backendVersion;
415 }
416
417 get collapseNodesByDefault(): boolean {
418 return this._collapseNodesByDefault;
419 }
420 set collapseNodesByDefault(value: boolean): void {
421 this._collapseNodesByDefault = value;
422
423 localStorageSetItem(
424 LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY,
425 value ? 'true' : 'false',
426 );
427
428 this.emit('collapseNodesByDefault');
429 }
430
431 get componentFilters(): Array<ComponentFilter> {
432 return this._componentFilters;
433 }
434 set componentFilters(value: Array<ComponentFilter>): void {
435 if (this._profilerStore.isProfilingBasedOnUserInput) {
436 // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
437 // If necessary, we could support this- but it doesn't seem like a necessary use case.
438 this._throwAndEmitError(
439 Error('Cannot modify filter preferences while profiling'),
440 );
441 }
442
443 // Filter updates are expensive to apply (since they impact the entire tree).
444 // Let's determine if they've changed and avoid doing this work if they haven't.
445 const prevEnabledComponentFilters = this._componentFilters.filter(
446 filter => filter.isEnabled,
447 );
448 const nextEnabledComponentFilters = value.filter(
449 filter => filter.isEnabled,
450 );
451 let haveEnabledFiltersChanged =
452 prevEnabledComponentFilters.length !== nextEnabledComponentFilters.length;
453 if (!haveEnabledFiltersChanged) {
454 for (let i = 0; i < nextEnabledComponentFilters.length; i++) {
455 const prevFilter = prevEnabledComponentFilters[i];
456 const nextFilter = nextEnabledComponentFilters[i];
457 if (shallowDiffers(prevFilter, nextFilter)) {
458 haveEnabledFiltersChanged = true;
459 break;
460 }
461 }
462 }
463
464 this._componentFilters = value;
465
466 // Update persisted filter preferences
467 setSavedComponentFilters(value);
468 if (this._hookSettings === null) {
469 // We changed filters before we got the hook settings.
470 // Wait for hook settings before persisting component filters to not overwrite
471 // persisted hook settings with defaults.
472 // This exists purely as a type safety check; in practice the hook settings
473 // should have arrived before any filter changes could be made.
474 const onHookSettings = (settings: $ReadOnly<DevToolsHookSettings>) => {
475 this._bridge.removeListener('hookSettings', onHookSettings);
476 this.emit('settingsUpdated', settings, value);
477 };
478 this._bridge.addListener('hookSettings', onHookSettings);
479 this._bridge.send('getHookSettings');
480 } else {
481 this.emit('settingsUpdated', this._hookSettings, value);
482 }
483
484 // Notify the renderer that filter preferences have changed.
485 // This is an expensive operation; it unmounts and remounts the entire tree,
486 // so only do it if the set of enabled component filters has changed.
487 if (haveEnabledFiltersChanged) {
488 this._bridge.send('updateComponentFilters', value);
489 }
490
491 this.emit('componentFilters');
492 }
493
494 get bridgeProtocol(): BridgeProtocol | null {
495 return this._bridgeProtocol;
496 }
497
498 get componentWithErrorCount(): number {
499 if (!this._shouldShowWarningsAndErrors) {
500 return 0;
501 }
502
503 return this._cachedComponentWithErrorCount;
504 }
505
506 get componentWithWarningCount(): number {
507 if (!this._shouldShowWarningsAndErrors) {
508 return 0;
509 }
510
511 return this._cachedComponentWithWarningCount;
512 }
513
514 get displayingErrorsAndWarningsEnabled(): boolean {
515 return this._shouldShowWarningsAndErrors;
516 }
517
518 get hasOwnerMetadata(): boolean {
519 return this._hasOwnerMetadata;
520 }
521
522 get nativeStyleEditorValidAttributes(): $ReadOnlyArray<string> | null {
523 return this._nativeStyleEditorValidAttributes;
524 }
525
526 get numElements(): number {
527 return this._weightAcrossRoots;
528 }
529
530 get profilerStore(): ProfilerStore {
531 return this._profilerStore;
532 }
533
534 get recordChangeDescriptions(): boolean {
535 return this._recordChangeDescriptions;
536 }
537 set recordChangeDescriptions(value: boolean): void {
538 this._recordChangeDescriptions = value;
539
540 localStorageSetItem(
541 LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
542 value ? 'true' : 'false',
543 );
544
545 this.emit('recordChangeDescriptions');
546 }
547
548 get revision(): number {
549 return this._revision;
550 }
551 get revisionSuspense(): number {
552 return this._revisionSuspense;
553 }
554
555 get rootIDToRendererID(): Map<number, number> {
556 return this._rootIDToRendererID;
557 }
558
559 get roots(): $ReadOnlyArray<number> {
560 return this._roots;
561 }
562
563 // At least one of the currently mounted roots support the Legacy profiler.
564 get rootSupportsBasicProfiling(): boolean {
565 return this._rootSupportsBasicProfiling;
566 }
567
568 // At least one of the currently mounted roots support performance tracks.
569 get rootSupportsPerformanceTracks(): boolean {
570 return this._rootSupportsPerformanceTracks;
571 }
572
573 get supportsInspectMatchingDOMElement(): boolean {
574 return this._supportsInspectMatchingDOMElement;
575 }
576
577 get supportsClickToInspect(): boolean {
578 return this._supportsClickToInspect;
579 }
580
581 get supportsNativeStyleEditor(): boolean {
582 return this._isNativeStyleEditorSupported;
583 }
584
585 get supportsReloadAndProfile(): boolean {
586 return (
587 this._isReloadAndProfileFrontendSupported &&
588 this._isReloadAndProfileBackendSupported
589 );
590 }
591
592 get supportsTraceUpdates(): boolean {
593 return this._supportsTraceUpdates;
594 }
595
596 get unsupportedBridgeProtocolDetected(): boolean {
597 return this._unsupportedBridgeProtocolDetected;
598 }
599
600 get unsupportedRendererVersionDetected(): boolean {
601 return this._unsupportedRendererVersionDetected;
602 }
603
604 get lastSelectedHostInstanceElementId(): Element['id'] | null {
605 return this._lastSelectedHostInstanceElementId;
606 }
607
608 containsElement(id: number): boolean {
609 return this._idToElement.has(id);
610 }
611
612 getElementAtIndex(index: number): Element | null {
613 if (index < 0 || index >= this.numElements) {
614 console.warn(
615 `Invalid index ${index} specified; store contains ${this.numElements} items.`,
616 );
617
618 return null;
619 }
620
621 // Find which root this element is in...
622 let root;
623 let rootWeight = 0;
624 for (let i = 0; i < this._roots.length; i++) {
625 const rootID = this._roots[i];
626 root = this._idToElement.get(rootID);
627
628 if (root === undefined) {
629 // We should never reach this. This is a bug in the backend renderer.
630 return this._throwAndEmitError(
631 Error(
632 `Couldn't find root with id "${rootID}": no matching node was found in the Store.`,
633 ),
634 );
635 }
636
637 if (root.children.length === 0) {
638 continue;
639 }
640
641 if (rootWeight + root.weight > index) {
642 break;
643 } else {
644 rootWeight += root.weight;
645 }
646 }
647
648 if (root === undefined) {
649 return this._throwAndEmitError(
650 Error(`Could not find an element at index "${index}" in the Store.`),
651 );
652 }
653
654 // Find the element in the tree using the weight of each node...
655 // Skip over the root itself, because roots aren't visible in the Elements tree.
656 let currentElement: Element = root;
657 let currentWeight = rootWeight - 1;
658
659 while (index !== currentWeight) {
660 const numChildren = currentElement.children.length;
661 let didFindChild = false;
662 for (let i = 0; i < numChildren; i++) {
663 const childID = currentElement.children[i];
664 const child = this._idToElement.get(childID);
665
666 if (child === undefined) {
667 // We should never reach this. This is a bug in the backend renderer.
668 return this._throwAndEmitError(
669 Error(
670 `Couldn't child element with id "${childID}": no matching node was found in the Store.`,
671 ),
672 );
673 }
674
675 const childWeight = child.isCollapsed ? 1 : child.weight;
676
677 if (index <= currentWeight + childWeight) {
678 currentWeight++;
679 currentElement = child;
680 didFindChild = true;
681 break;
682 } else {
683 currentWeight += childWeight;
684 }
685 }
686
687 if (!didFindChild) {
688 return this._throwAndEmitError(
689 Error(
690 `Could not find an element at index "${index}" because the Store tree weights are invalid.`,
691 ),
692 );
693 }
694 }
695
696 return currentElement;
697 }
698
699 getElementIDAtIndex(index: number): number | null {
700 const element = this.getElementAtIndex(index);
701 return element === null ? null : element.id;
702 }
703
704 getElementByID(id: number): Element | null {
705 const element = this._idToElement.get(id);
706 if (element === undefined) {
707 console.warn(`No element found with id "${id}"`);
708 return null;
709 }
710
711 return element;
712 }
713
714 _getElementByIDOrThrow(id: Element['id']): Element {
715 const element = this._idToElement.get(id);
716 if (element === undefined) {
717 return this._throwAndEmitError(
718 Error(
719 `Could not find element with id "${id}": no matching node was found in the Store.`,
720 ),
721 );
722 }
723 return element;
724 }
725
726 _recalculateWeightAcrossRoots(): void {
727 let weightAcrossRoots = 0;
728 this._roots.forEach(rootID => {
729 weightAcrossRoots += this._getElementByIDOrThrow(rootID).weight;
730 });
731 this._weightAcrossRoots = weightAcrossRoots;
732 }
733
734 containsSuspense(id: SuspenseNode['id']): boolean {
735 return this._idToSuspense.has(id);
736 }
737
738 getSuspenseByID(id: SuspenseNode['id']): SuspenseNode | null {
739 const suspense = this._idToSuspense.get(id);
740 if (suspense === undefined) {
741 console.warn(`No suspense found with id "${id}"`);
742 return null;
743 }
744
745 return suspense;
746 }
747
748 // Returns a tuple of [id, index]
749 getElementsWithErrorsAndWarnings(): ErrorAndWarningTuples {
750 if (!this._shouldShowWarningsAndErrors) {
751 return [];
752 }
753
754 if (this._cachedErrorAndWarningTuples !== null) {
755 return this._cachedErrorAndWarningTuples;
756 }
757
758 const errorAndWarningTuples: ErrorAndWarningTuples = [];
759
760 this._errorsAndWarnings.forEach((_, id) => {
761 const index = this.getIndexOfElementID(id);
762 if (index !== null) {
763 let low = 0;
764 let high = errorAndWarningTuples.length;
765 while (low < high) {
766 const mid = (low + high) >> 1;
767 if (errorAndWarningTuples[mid].index > index) {
768 high = mid;
769 } else {
770 low = mid + 1;
771 }
772 }
773
774 errorAndWarningTuples.splice(low, 0, {id, index});
775 }
776 });
777
778 // Cache for later (at least until the tree changes again).
779 this._cachedErrorAndWarningTuples = errorAndWarningTuples;
780 return errorAndWarningTuples;
781 }
782
783 getErrorAndWarningCountForElementID(id: number): {
784 errorCount: number,
785 warningCount: number,
786 } {
787 if (!this._shouldShowWarningsAndErrors) {
788 return {errorCount: 0, warningCount: 0};
789 }
790
791 return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0};
792 }
793
794 getIndexOfElementID(id: number): number | null {
795 const element = this.getElementByID(id);
796
797 if (element === null || element.parentID === 0) {
798 return null;
799 }
800
801 // Walk up the tree to the root.
802 // Increment the index by one for each node we encounter,
803 // and by the weight of all nodes to the left of the current one.
804 // This should be a relatively fast way of determining the index of a node within the tree.
805 let previousID = id;
806 let currentID = element.parentID;
807 let index = 0;
808 while (true) {
809 const current = this._idToElement.get(currentID);
810 if (current === undefined) {
811 return null;
812 }
813
814 const {children} = current;
815 for (let i = 0; i < children.length; i++) {
816 const childID = children[i];
817 if (childID === previousID) {
818 break;
819 }
820
821 const child = this._idToElement.get(childID);
822 if (child === undefined) {
823 return null;
824 }
825
826 index += child.isCollapsed ? 1 : child.weight;
827 }
828
829 if (current.parentID === 0) {
830 // We found the root; stop crawling.
831 break;
832 }
833
834 index++;
835
836 previousID = current.id;
837 currentID = current.parentID;
838 }
839
840 // At this point, the current ID is a root (from the previous loop).
841 // We also need to offset the index by previous root weights.
842 for (let i = 0; i < this._roots.length; i++) {
843 const rootID = this._roots[i];
844 if (rootID === currentID) {
845 break;
846 }
847
848 const root = this._idToElement.get(rootID);
849 if (root === undefined) {
850 return null;
851 }
852
853 index += root.weight;
854 }
855
856 return index;
857 }
858
859 isDescendantOf(parentId: number, descendantId: number): boolean {
860 if (descendantId === 0) {
861 return false;
862 }
863
864 const descendant = this.getElementByID(descendantId);
865 if (descendant === null) {
866 return false;
867 }
868
869 if (descendant.parentID === parentId) {
870 return true;
871 }
872
873 const parent = this.getElementByID(parentId);
874 if (!parent || parent.depth >= descendant.depth) {
875 return false;
876 }
877
878 return this.isDescendantOf(parentId, descendant.parentID);
879 }
880
881 /**
882 * Returns index of the lowest descendant element, if available.
883 * May not be the deepest element, the lowest is used in a sense of bottom-most from UI Tree representation perspective.
884 */
885 getIndexOfLowestDescendantElement(element: Element): number | null {
886 let current: null | Element = element;
887 while (current !== null) {
888 if (current.isCollapsed || current.children.length === 0) {
889 if (current === element) {
890 return null;
891 }
892
893 return this.getIndexOfElementID(current.id);
894 } else {
895 const lastChildID = current.children[current.children.length - 1];
896 current = this.getElementByID(lastChildID);
897 }
898 }
899
900 return null;
901 }
902
903 getOwnersListForElement(ownerID: number): Array<Element> {
904 const list: Array<Element> = [];
905 const element = this._idToElement.get(ownerID);
906 if (element !== undefined) {
907 list.push({
908 ...element,
909 depth: 0,
910 });
911
912 const unsortedIDs = this._ownersMap.get(ownerID);
913 if (unsortedIDs !== undefined) {
914 const depthMap: Map<number, number> = new Map([[ownerID, 0]]);
915
916 // Items in a set are ordered based on insertion.
917 // This does not correlate with their order in the tree.
918 // So first we need to order them.
919 // I wish we could avoid this sorting operation; we could sort at insertion time,
920 // but then we'd have to pay sorting costs even if the owners list was never used.
921 // Seems better to defer the cost, since the set of ids is probably pretty small.
922 const sortedIDs = Array.from(unsortedIDs).sort(
923 (idA, idB) =>
924 (this.getIndexOfElementID(idA) || 0) -
925 (this.getIndexOfElementID(idB) || 0),
926 );
927
928 // Next we need to determine the appropriate depth for each element in the list.
929 // The depth in the list may not correspond to the depth in the tree,
930 // because the list has been filtered to remove intermediate components.
931 // Perhaps the easiest way to do this is to walk up the tree until we reach either:
932 // (1) another node that's already in the tree, or (2) the root (owner)
933 // at which point, our depth is just the depth of that node plus one.
934 sortedIDs.forEach(id => {
935 const innerElement = this._idToElement.get(id);
936 if (innerElement !== undefined) {
937 let parentID = innerElement.parentID;
938
939 let depth = 0;
940 while (parentID > 0) {
941 if (parentID === ownerID || unsortedIDs.has(parentID)) {
942 const parentDepth = depthMap.get(parentID);
943 if (parentDepth === undefined) {
944 return this._throwAndEmitError(
945 Error(
946 `Invalid owners list: owner depth for element "${parentID}" was not found.`,
947 ),
948 );
949 }
950 depth = parentDepth + 1;
951 depthMap.set(id, depth);
952 break;
953 }
954 const parent = this._idToElement.get(parentID);
955 if (parent === undefined) {
956 break;
957 }
958 parentID = parent.parentID;
959 }
960
961 if (depth === 0) {
962 this._throwAndEmitError(Error('Invalid owners list'));
963 }
964
965 list.push({...innerElement, depth});
966 }
967 });
968 }
969 }
970
971 return list;
972 }
973
974 getSuspenseLineage(
975 suspenseID: SuspenseNode['id'],
976 ): $ReadOnlyArray<SuspenseNode['id']> {
977 const lineage: Array<SuspenseNode['id']> = [];
978 let next: null | SuspenseNode = this.getSuspenseByID(suspenseID);
979 while (next !== null) {
980 if (next.parentID === 0) {
981 next = null;
982 } else {
983 lineage.unshift(next.id);
984 next = this.getSuspenseByID(next.parentID);
985 }
986 }
987
988 return lineage;
989 }
990
991 /**
992 * Like {@link getRootIDForElement} but should be used for traversing Suspense since it works with disconnected nodes.
993 */
994 getSuspenseRootIDForSuspense(id: SuspenseNode['id']): number | null {
995 let current = this._idToSuspense.get(id);
996 while (current !== undefined) {
997 if (current.parentID === 0) {
998 return current.id;
999 } else {
1000 current = this._idToSuspense.get(current.parentID);
1001 }
1002 }
1003 return null;
1004 }
1005
1006 /**
1007 * @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
1008 */
1009 getSuspendableDocumentOrderSuspenseInitialPaint(
1010 uniqueSuspendersOnly: boolean,
1011 ): Array<SuspenseTimelineStep> {
1012 const target: Array<SuspenseTimelineStep> = [];
1013 const roots = this.roots;
1014 let rootStep: null | SuspenseTimelineStep = null;
1015 for (let i = 0; i < roots.length; i++) {
1016 const rootID = roots[i];
1017 this._getElementByIDOrThrow(rootID);
1018 const rendererID = this._rootIDToRendererID.get(rootID);
1019 if (rendererID === undefined) {
1020 return this._throwAndEmitError(
1021 Error(
1022 'Failed to find renderer ID for root. This is a bug in React DevTools.',
1023 ),
1024 );
1025 }
1026 // TODO: This includes boundaries that can't be suspended due to no support from the renderer.
1027
1028 const suspense = this.getSuspenseByID(rootID);
1029 if (suspense !== null) {
1030 const environments = suspense.environments;
1031 const environmentName =
1032 environments.length > 0
1033 ? environments[environments.length - 1]
1034 : null;
1035 if (rootStep === null) {
1036 // Arbitrarily use the first root as the root step id.
1037 rootStep = {
1038 id: suspense.id,
1039 environment: environmentName,
1040 endTime: suspense.endTime,
1041 rendererID,
1042 };
1043 target.push(rootStep);
1044 } else {
1045 if (rootStep.environment === null) {
1046 // If any root has an environment name, then let's use it.
1047 rootStep.environment = environmentName;
1048 }
1049 if (suspense.endTime > rootStep.endTime) {
1050 // If any root has a higher end time, let's use that.
1051 rootStep.endTime = suspense.endTime;
1052 }
1053 }
1054 this.pushTimelineStepsInDocumentOrder(
1055 suspense.children,
1056 target,
1057 uniqueSuspendersOnly,
1058 environments,
1059 0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
1060 rendererID,
1061 );
1062 }
1063 }
1064
1065 return target;
1066 }
1067
1068 _pushSuspenseChildrenInDocumentOrder(
1069 children: Array<Element['id']>,
1070 target: Array<SuspenseNode['id']>,
1071 ): void {
1072 for (let i = 0; i < children.length; i++) {
1073 const childID = children[i];
1074 const suspense = this._idToSuspense.get(childID);
1075 if (suspense !== undefined) {
1076 target.push(suspense.id);
1077 } else {
1078 const childElement = this._idToElement.get(childID);
1079 if (childElement !== undefined) {
1080 this._pushSuspenseChildrenInDocumentOrder(
1081 childElement.children,
1082 target,
1083 );
1084 }
1085 }
1086 }
1087 }
1088
1089 getSuspenseChildren(id: Element['id']): Array<SuspenseNode['id']> {
1090 const transitionChildren: Array<SuspenseNode['id']> = [];
1091
1092 const root = this._idToElement.get(id);
1093 if (root === undefined) {
1094 return transitionChildren;
1095 }
1096
1097 this._pushSuspenseChildrenInDocumentOrder(
1098 root.children,
1099 transitionChildren,
1100 );
1101
1102 return transitionChildren;
1103 }
1104
1105 /**
1106 * @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
1107 */
1108 getSuspendableDocumentOrderSuspenseTransition(
1109 uniqueSuspendersOnly: boolean,
1110 rendererID: number,
1111 ): Array<SuspenseTimelineStep> {
1112 const target: Array<SuspenseTimelineStep> = [];
1113 const focusedTransitionID = this._focusedTransition;
1114 if (focusedTransitionID === 0) {
1115 return this._throwAndEmitError(
1116 Error(
1117 'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.',
1118 ),
1119 );
1120 }
1121
1122 target.push({
1123 id: focusedTransitionID,
1124 // TODO: Get environment for Activity
1125 environment: null,
1126 endTime: 0,
1127 rendererID,
1128 });
1129
1130 const transitionChildren = this.getSuspenseChildren(focusedTransitionID);
1131
1132 this.pushTimelineStepsInDocumentOrder(
1133 transitionChildren,
1134 target,
1135 uniqueSuspendersOnly,
1136 // TODO: Get environment for Activity
1137 [],
1138 0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
1139 rendererID,
1140 );
1141
1142 return target;
1143 }
1144
1145 pushTimelineStepsInDocumentOrder(
1146 children: Array<SuspenseNode['id']>,
1147 target: Array<SuspenseTimelineStep>,
1148 uniqueSuspendersOnly: boolean,
1149 parentEnvironments: Array<string>,
1150 parentEndTime: number,
1151 rendererID: number,
1152 ): void {
1153 for (let i = 0; i < children.length; i++) {
1154 const child = this.getSuspenseByID(children[i]);
1155 if (child === null) {
1156 continue;
1157 }
1158 // Ignore any suspense boundaries that has no visual representation as this is not
1159 // part of the visible loading sequence.
1160 // TODO: Consider making visible meta data and other side-effects get virtual rects.
1161 const hasRects =
1162 child.rects !== null &&
1163 child.rects.length > 0 &&
1164 child.rects.some(isNonZeroRect);
1165 const childEnvironments = child.environments;
1166 // Since children are blocked on the parent, they're also blocked by the parent environments.
1167 // Only if we discover a novel environment do we add that and it becomes the name we use.
1168 const unionEnvironments = unionOfTwoArrays(
1169 parentEnvironments,
1170 childEnvironments,
1171 );
1172 const environmentName =
1173 unionEnvironments.length > 0
1174 ? unionEnvironments[unionEnvironments.length - 1]
1175 : null;
1176 // The end time of a child boundary can in effect never be earlier than its parent even if
1177 // everything unsuspended before that.
1178 const maxEndTime =
1179 parentEndTime > child.endTime ? parentEndTime : child.endTime;
1180 if (hasRects && (!uniqueSuspendersOnly || child.hasUniqueSuspenders)) {
1181 target.push({
1182 id: child.id,
1183 environment: environmentName,
1184 endTime: maxEndTime,
1185 rendererID,
1186 });
1187 }
1188 this.pushTimelineStepsInDocumentOrder(
1189 child.children,
1190 target,
1191 uniqueSuspendersOnly,
1192 unionEnvironments,
1193 maxEndTime,
1194 rendererID,
1195 );
1196 }
1197 }
1198
1199 getEndTimeOrDocumentOrderSuspense(
1200 uniqueSuspendersOnly: boolean,
1201 ): $ReadOnlyArray<SuspenseTimelineStep> {
1202 let timeline: SuspenseTimelineStep[];
1203 if (this._focusedTransition === 0) {
1204 timeline =
1205 this.getSuspendableDocumentOrderSuspenseInitialPaint(
1206 uniqueSuspendersOnly,
1207 );
1208 } else {
1209 const focusedTransitionRootID = this.getRootIDForElement(
1210 this._focusedTransition,
1211 );
1212 if (focusedTransitionRootID === null) {
1213 return this._throwAndEmitError(
1214 Error(
1215 'Failed to find root ID for focused transition. This is a bug in React DevTools.',
1216 ),
1217 );
1218 }
1219 const rendererID = this._rootIDToRendererID.get(focusedTransitionRootID);
1220 if (rendererID === undefined) {
1221 return this._throwAndEmitError(
1222 Error(
1223 'Failed to find renderer ID for focused transition root. This is a bug in React DevTools.',
1224 ),
1225 );
1226 }
1227 timeline = this.getSuspendableDocumentOrderSuspenseTransition(
1228 uniqueSuspendersOnly,
1229 rendererID,
1230 );
1231 }
1232
1233 if (timeline.length === 0) {
1234 return timeline;
1235 }
1236 const root = timeline[0];
1237 // We mutate in place since we assume we've got a fresh array.
1238 timeline.sort((a, b) => {
1239 // Root is always first
1240 return a === root ? -1 : b === root ? 1 : a.endTime - b.endTime;
1241 });
1242 return timeline;
1243 }
1244
1245 getActivities(): Array<{id: Element['id'], depth: number}> {
1246 const target: Array<{id: Element['id'], depth: number}> = [];
1247 // TODO: Keep a live tree in the backend so we don't need to recalculate
1248 // this each time while also including filtered Activities.
1249 this._pushActivitiesInDocumentOrder(this.roots, target, 0);
1250 return target;
1251 }
1252
1253 _pushActivitiesInDocumentOrder(
1254 children: $ReadOnlyArray<Element['id']>,
1255 target: Array<{id: Element['id'], depth: number}>,
1256 depth: number,
1257 ): void {
1258 for (let i = 0; i < children.length; i++) {
1259 const child = this._idToElement.get(children[i]);
1260 if (child === undefined) {
1261 continue;
1262 }
1263 if (child.type === ElementTypeActivity && child.nameProp !== null) {
1264 target.push({id: child.id, depth});
1265 this._pushActivitiesInDocumentOrder(child.children, target, depth + 1);
1266 } else {
1267 this._pushActivitiesInDocumentOrder(child.children, target, depth);
1268 }
1269 }
1270 }
1271
1272 getRendererIDForElement(id: number): number | null {
1273 let current = this._idToElement.get(id);
1274 while (current !== undefined) {
1275 if (current.parentID === 0) {
1276 const rendererID = this._rootIDToRendererID.get(current.id);
1277 return rendererID == null ? null : rendererID;
1278 } else {
1279 current = this._idToElement.get(current.parentID);
1280 }
1281 }
1282 return null;
1283 }
1284
1285 getRootIDForElement(id: number): number | null {
1286 let current = this._idToElement.get(id);
1287 while (current !== undefined) {
1288 if (current.parentID === 0) {
1289 return current.id;
1290 } else {
1291 current = this._idToElement.get(current.parentID);
1292 }
1293 }
1294 return null;
1295 }
1296
1297 isInsideCollapsedSubTree(id: number): boolean {
1298 let current = this._idToElement.get(id);
1299 while (current != null) {
1300 if (current.parentID === 0) {
1301 return false;
1302 } else {
1303 current = this._idToElement.get(current.parentID);
1304 if (current != null && current.isCollapsed) {
1305 return true;
1306 }
1307 }
1308 }
1309 return false;
1310 }
1311
1312 // TODO Maybe split this into two methods: expand() and collapse()
1313 toggleIsCollapsed(id: number, isCollapsed: boolean): void {
1314 let didMutate = false;
1315
1316 const element = this.getElementByID(id);
1317 if (element !== null) {
1318 if (isCollapsed) {
1319 if (element.type === ElementTypeRoot) {
1320 this._throwAndEmitError(Error('Root nodes cannot be collapsed'));
1321 }
1322
1323 if (!element.isCollapsed) {
1324 didMutate = true;
1325 element.isCollapsed = true;
1326
1327 const weightDelta = 1 - element.weight;
1328
1329 let parentElement = this._idToElement.get(element.parentID);
1330 while (parentElement !== undefined) {
1331 // We don't need to break on a collapsed parent in the same way as the expand case below.
1332 // That's because collapsing a node doesn't "bubble" and affect its parents.
1333 parentElement.weight += weightDelta;
1334 parentElement = this._idToElement.get(parentElement.parentID);
1335 }
1336 }
1337 } else {
1338 let currentElement: ?Element = element;
1339 while (currentElement != null) {
1340 const oldWeight = currentElement.isCollapsed
1341 ? 1
1342 : currentElement.weight;
1343
1344 if (currentElement.isCollapsed) {
1345 didMutate = true;
1346 currentElement.isCollapsed = false;
1347
1348 const newWeight = currentElement.isCollapsed
1349 ? 1
1350 : currentElement.weight;
1351 const weightDelta = newWeight - oldWeight;
1352
1353 let parentElement = this._idToElement.get(currentElement.parentID);
1354 while (parentElement !== undefined) {
1355 parentElement.weight += weightDelta;
1356 if (parentElement.isCollapsed) {
1357 // It's important to break on a collapsed parent when expanding nodes.
1358 // That's because expanding a node "bubbles" up and expands all parents as well.
1359 // Breaking in this case prevents us from over-incrementing the expanded weights.
1360 break;
1361 }
1362 parentElement = this._idToElement.get(parentElement.parentID);
1363 }
1364 }
1365
1366 currentElement =
1367 currentElement.parentID !== 0
1368 ? this.getElementByID(currentElement.parentID)
1369 : null;
1370 }
1371 }
1372
1373 // Only re-calculate weights and emit an "update" event if the store was mutated.
1374 if (didMutate) {
1375 this._recalculateWeightAcrossRoots();
1376
1377 // The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.
1378 // In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).
1379 // Updating the selected search index later may require auto-expanding a collapsed subtree though.
1380 this.emit('mutated', [[], new Map(), null]);
1381 }
1382 }
1383 }
1384
1385 _adjustParentTreeWeight: (
1386 parentElement: ?Element,
1387 weightDelta: number,
1388 ) => void = (parentElement, weightDelta) => {
1389 let isInsideCollapsedSubTree = false;
1390
1391 while (parentElement != null) {
1392 parentElement.weight += weightDelta;
1393
1394 // Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.
1395 // Their weight will bubble up when the parent is expanded.
1396 if (parentElement.isCollapsed) {
1397 isInsideCollapsedSubTree = true;
1398 break;
1399 }
1400
1401 parentElement = this._idToElement.get(parentElement.parentID);
1402 }
1403
1404 // Additions and deletions within a collapsed subtree should not affect the overall number of elements.
1405 if (!isInsideCollapsedSubTree) {
1406 this._weightAcrossRoots += weightDelta;
1407 }
1408 };
1409
1410 _recursivelyUpdateSubtree(
1411 id: number,
1412 callback: (element: Element) => void,
1413 ): void {
1414 const element = this._idToElement.get(id);
1415 if (element) {
1416 callback(element);
1417
1418 element.children.forEach(child =>
1419 this._recursivelyUpdateSubtree(child, callback),
1420 );
1421 }
1422 }
1423
1424 onBridgeNativeStyleEditorSupported: ({
1425 isSupported: boolean,
1426 validAttributes: ?$ReadOnlyArray<string>,
1427 }) => void = ({isSupported, validAttributes}) => {
1428 this._isNativeStyleEditorSupported = isSupported;
1429 this._nativeStyleEditorValidAttributes = validAttributes || null;
1430
1431 this.emit('supportsNativeStyleEditor');
1432 };
1433
1434 onBridgeOperations: (operations: Array<number>) => void = operations => {
1435 // $FlowFixMe[constant-condition]
1436 if (__DEBUG__) {
1437 console.groupCollapsed('onBridgeOperations');
1438 debug('onBridgeOperations', operations.join(','));
1439 }
1440
1441 let haveRootsChanged = false;
1442 let haveErrorsOrWarningsChanged = false;
1443 let hasSuspenseTreeChanged = false;
1444
1445 // The first two values are always rendererID and rootID
1446 const rendererID = operations[0];
1447
1448 const addedElementIDs: Array<number> = [];
1449 // This is a mapping of removed ID -> parent ID:
1450 // We'll use the parent ID to adjust selection if it gets deleted.
1451 const removedElementIDs: Map<number, number> = new Map();
1452 const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
1453 new Map();
1454 let nextActivitySliceID: Element['id'] | null = null;
1455
1456 let i = 2;
1457
1458 // Reassemble the string table.
1459 const stringTable: Array<string | null> = [
1460 null, // ID = 0 corresponds to the null string.
1461 ];
1462 const stringTableSize = operations[i];
1463 i++;
1464
1465 const stringTableEnd = i + stringTableSize;
1466
1467 while (i < stringTableEnd) {
1468 const nextLength = operations[i];
1469 i++;
1470
1471 const nextString = utfDecodeStringWithRanges(
1472 operations,
1473 i,
1474 i + nextLength - 1,
1475 );
1476 stringTable.push(nextString);
1477 i += nextLength;
1478 }
1479
1480 while (i < operations.length) {
1481 const operation = operations[i];
1482 switch (operation) {
1483 case TREE_OPERATION_ADD: {
1484 const id = operations[i + 1];
1485 const rawType = operations[i + 2];
1486 const type = parseElementType(rawType);
1487
1488 if (type === null) {
1489 return this._throwAndEmitError(
1490 Error(
1491 `Cannot add node "${id}" because "${rawType}" is not a valid element type.`,
1492 ),
1493 );
1494 }
1495
1496 i += 3;
1497
1498 if (this._idToElement.has(id)) {
1499 // We should never reach this. This is a bug in the backend renderer.
1500 return this._throwAndEmitError(
1501 Error(
1502 `Cannot add node "${id}" because a node with that id is already in the Store.`,
1503 ),
1504 );
1505 }
1506
1507 if (type === ElementTypeRoot) {
1508 // $FlowFixMe[constant-condition]
1509 if (__DEBUG__) {
1510 debug('Add', `new root node ${id}`);
1511 }
1512
1513 const isStrictModeCompliant = operations[i] > 0;
1514 i++;
1515
1516 const profilerFlags = operations[i++];
1517 const supportsBasicProfiling =
1518 (profilerFlags & PROFILING_FLAG_BASIC_SUPPORT) !== 0;
1519 const supportsPerformanceTracks =
1520 (profilerFlags & PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT) !== 0;
1521 let supportsAdvancedProfiling: AdvancedProfiling =
1522 ADVANCED_PROFILING_NONE;
1523 if (supportsPerformanceTracks) {
1524 supportsAdvancedProfiling = ADVANCED_PROFILING_PERFORMANCE_TRACKS;
1525 }
1526
1527 let supportsStrictMode = false;
1528 let hasOwnerMetadata = false;
1529
1530 // If we don't know the bridge protocol, guess that we're dealing with the latest.
1531 // If we do know it, we can take it into consideration when parsing operations.
1532 if (
1533 this._bridgeProtocol === null ||
1534 this._bridgeProtocol.version >= 2
1535 ) {
1536 supportsStrictMode = operations[i] > 0;
1537 i++;
1538
1539 hasOwnerMetadata = operations[i] > 0;
1540 i++;
1541 }
1542
1543 this._roots = this._roots.concat(id);
1544 this._rootIDToRendererID.set(id, rendererID);
1545 this._rootIDToCapabilities.set(id, {
1546 supportsBasicProfiling,
1547 hasOwnerMetadata,
1548 supportsStrictMode,
1549 supportsAdvancedProfiling,
1550 });
1551
1552 // Not all roots support StrictMode;
1553 // don't flag a root as non-compliant unless it also supports StrictMode.
1554 const isStrictModeNonCompliant =
1555 !isStrictModeCompliant && supportsStrictMode;
1556
1557 this._idToElement.set(id, {
1558 children: [],
1559 depth: -1,
1560 displayName: null,
1561 hocDisplayNames: null,
1562 id,
1563 isCollapsed: false, // Never collapse roots; it would hide the entire tree.
1564 isStrictModeNonCompliant,
1565 isActivityHidden: false,
1566 isInsideHiddenActivity: false,
1567 key: null,
1568 nameProp: null,
1569 ownerID: 0,
1570 parentID: 0,
1571 type,
1572 weight: 0,
1573 compiledWithForget: false,
1574 });
1575
1576 haveRootsChanged = true;
1577 } else {
1578 const parentID = operations[i];
1579 i++;
1580
1581 const ownerID = operations[i];
1582 i++;
1583
1584 const displayNameStringID = operations[i];
1585 const displayName = stringTable[displayNameStringID];
1586 i++;
1587
1588 const keyStringID = operations[i];
1589 const key = stringTable[keyStringID];
1590 i++;
1591
1592 const namePropStringID = operations[i];
1593 const nameProp = stringTable[namePropStringID];
1594 i++;
1595
1596 // $FlowFixMe[constant-condition]
1597 if (__DEBUG__) {
1598 debug(
1599 'Add',
1600 `node ${id} (${displayName || 'null'}) as child of ${parentID}`,
1601 );
1602 }
1603
1604 const parentElement = this._idToElement.get(parentID);
1605 if (parentElement === undefined) {
1606 // We should never reach this. This is a bug in the backend renderer.
1607 return this._throwAndEmitError(
1608 Error(
1609 `Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,
1610 ),
1611 );
1612 }
1613
1614 parentElement.children.push(id);
1615
1616 const {
1617 formattedDisplayName: displayNameWithoutHOCs,
1618 hocDisplayNames,
1619 compiledWithForget,
1620 } = parseElementDisplayNameFromBackend(displayName, type);
1621
1622 const elementDepth = parentElement.depth + 1;
1623 this._maximumRecordedDepth = Math.max(
1624 this._maximumRecordedDepth,
1625 elementDepth,
1626 );
1627
1628 const element: Element = {
1629 children: [],
1630 depth: elementDepth,
1631 displayName: displayNameWithoutHOCs,
1632 hocDisplayNames,
1633 id,
1634 isCollapsed: this._collapseNodesByDefault,
1635 isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant,
1636 isActivityHidden: false,
1637 isInsideHiddenActivity:
1638 parentElement.isInsideHiddenActivity ||
1639 parentElement.isActivityHidden,
1640 key,
1641 nameProp,
1642 ownerID,
1643 parentID,
1644 type,
1645 weight: 1,
1646 compiledWithForget,
1647 };
1648
1649 this._idToElement.set(id, element);
1650 addedElementIDs.push(id);
1651 this._adjustParentTreeWeight(parentElement, 1);
1652
1653 if (ownerID > 0) {
1654 let set = this._ownersMap.get(ownerID);
1655 if (set === undefined) {
1656 set = new Set();
1657 this._ownersMap.set(ownerID, set);
1658 }
1659 set.add(id);
1660 }
1661
1662 const suspense = this._idToSuspense.get(id);
1663 if (suspense !== undefined) {
1664 // We're reconnecting a node.
1665 if (suspense.name === null) {
1666 suspense.name = this._guessSuspenseName(element);
1667 }
1668 }
1669 }
1670 break;
1671 }
1672 case TREE_OPERATION_REMOVE: {
1673 const removeLength = operations[i + 1];
1674 i += 2;
1675
1676 for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
1677 const id = operations[i];
1678 const element = this._idToElement.get(id);
1679
1680 if (element === undefined) {
1681 // We should never reach this. This is a bug in the backend renderer.
1682 return this._throwAndEmitError(
1683 Error(
1684 `Cannot remove node "${id}" because no matching node was found in the Store.`,
1685 ),
1686 );
1687 }
1688
1689 i += 1;
1690
1691 const {children, ownerID, parentID, weight} = element;
1692 if (children.length > 0) {
1693 // We should never reach this. This is a bug in the backend renderer.
1694 return this._throwAndEmitError(
1695 Error(`Node "${id}" was removed before its children.`),
1696 );
1697 }
1698
1699 let parentElement: ?Element = null;
1700 if (parentID === 0) {
1701 // $FlowFixMe[constant-condition]
1702 if (__DEBUG__) {
1703 debug('Remove', `node ${id} root`);
1704 }
1705
1706 this._roots = this._roots.filter(rootID => rootID !== id);
1707 this._rootIDToRendererID.delete(id);
1708 this._rootIDToCapabilities.delete(id);
1709
1710 haveRootsChanged = true;
1711 } else {
1712 // $FlowFixMe[constant-condition]
1713 if (__DEBUG__) {
1714 debug('Remove', `node ${id} from parent ${parentID}`);
1715 }
1716
1717 parentElement = this._idToElement.get(parentID);
1718 if (parentElement === undefined) {
1719 // We should never reach this. This is a bug in the backend renderer.
1720 return this._throwAndEmitError(
1721 Error(
1722 `Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
1723 ),
1724 );
1725 }
1726
1727 const index = parentElement.children.indexOf(id);
1728 if (index === -1) {
1729 return this._throwAndEmitError(
1730 Error(
1731 `Cannot remove node "${id}" from parent "${parentID}" because it is not a child of the parent.`,
1732 ),
1733 );
1734 }
1735 parentElement.children.splice(index, 1);
1736 }
1737
1738 this._idToElement.delete(id);
1739
1740 this._adjustParentTreeWeight(parentElement, -weight);
1741 removedElementIDs.set(id, parentID);
1742
1743 this._ownersMap.delete(id);
1744 if (ownerID > 0) {
1745 const set = this._ownersMap.get(ownerID);
1746 if (set !== undefined) {
1747 set.delete(id);
1748 }
1749 }
1750
1751 if (this._errorsAndWarnings.has(id)) {
1752 this._errorsAndWarnings.delete(id);
1753 haveErrorsOrWarningsChanged = true;
1754 }
1755 }
1756
1757 break;
1758 }
1759 case TREE_OPERATION_REORDER_CHILDREN: {
1760 const id = operations[i + 1];
1761 const numChildren = operations[i + 2];
1762 i += 3;
1763
1764 const element = this._idToElement.get(id);
1765 if (element === undefined) {
1766 // We should never reach this. This is a bug in the backend renderer.
1767 return this._throwAndEmitError(
1768 Error(
1769 `Cannot reorder children for node "${id}" because no matching node was found in the Store.`,
1770 ),
1771 );
1772 }
1773
1774 const children = element.children;
1775 if (children.length !== numChildren) {
1776 // We should never reach this. This is a bug in the backend renderer.
1777 return this._throwAndEmitError(
1778 Error(
1779 `Children cannot be added or removed during a reorder operation.`,
1780 ),
1781 );
1782 }
1783
1784 const reorderedChildIDs: Set<Element['id']> = new Set();
1785 for (let j = 0; j < numChildren; j++) {
1786 const childID = operations[i + j];
1787 const childElement = this._idToElement.get(childID);
1788 if (
1789 childElement === undefined ||
1790 childElement.parentID !== id ||
1791 reorderedChildIDs.has(childID)
1792 ) {
1793 return this._throwAndEmitError(
1794 Error(
1795 `Children cannot be added or removed during a reorder operation.`,
1796 ),
1797 );
1798 }
1799 reorderedChildIDs.add(childID);
1800 }
1801 for (let j = 0; j < numChildren; j++) {
1802 children[j] = operations[i + j];
1803 }
1804 i += numChildren;
1805
1806 // $FlowFixMe[constant-condition]
1807 if (__DEBUG__) {
1808 debug('Re-order', `Node ${id} children ${children.join(',')}`);
1809 }
1810 break;
1811 }
1812 case TREE_OPERATION_SET_SUBTREE_MODE: {
1813 const id = operations[i + 1];
1814 const mode = operations[i + 2];
1815
1816 i += 3;
1817
1818 // If elements have already been mounted in this subtree, update them.
1819 // (In practice, this likely only applies to the root element.)
1820 if (mode === StrictMode) {
1821 this._recursivelyUpdateSubtree(id, element => {
1822 element.isStrictModeNonCompliant = false;
1823 });
1824 } else if (mode === ActivityHiddenMode) {
1825 const element = this._idToElement.get(id);
1826 if (element != null) {
1827 element.isActivityHidden = true;
1828 element.children.forEach(childID =>
1829 this._recursivelyUpdateSubtree(childID, child => {
1830 child.isInsideHiddenActivity = true;
1831 }),
1832 );
1833 // Collapse hidden Activity subtrees by default.
1834 if (!element.isCollapsed) {
1835 element.isCollapsed = true;
1836 if (element.children.length > 0) {
1837 const weightDelta = 1 - element.weight;
1838 const parentElement = this._idToElement.get(element.parentID);
1839 this._adjustParentTreeWeight(parentElement, weightDelta);
1840 }
1841 }
1842 }
1843 } else if (mode === ActivityVisibleMode) {
1844 const element = this._idToElement.get(id);
1845 if (element != null) {
1846 element.isActivityHidden = false;
1847 element.children.forEach(childID =>
1848 this._recursivelyUpdateSubtree(childID, child => {
1849 child.isInsideHiddenActivity = false;
1850 }),
1851 );
1852 // Expand Activity subtree when it becomes visible.
1853 if (element.isCollapsed && element.children.length > 0) {
1854 element.isCollapsed = false;
1855 const weightDelta = element.weight - 1;
1856 const parentElement = this._idToElement.get(element.parentID);
1857 this._adjustParentTreeWeight(parentElement, weightDelta);
1858 }
1859 }
1860 }
1861
1862 // $FlowFixMe[constant-condition]
1863 if (__DEBUG__) {
1864 debug(
1865 'Subtree mode',
1866 `Subtree with root ${id} set to mode ${mode}`,
1867 );
1868 }
1869 break;
1870 }
1871 case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
1872 // Base duration updates are only sent while profiling is in progress.
1873 // We can ignore them at this point.
1874 // The profiler UI uses them lazily in order to generate the tree.
1875 i += 3;
1876 break;
1877 case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
1878 const id = operations[i + 1];
1879 const errorCount = operations[i + 2];
1880 const warningCount = operations[i + 3];
1881
1882 i += 4;
1883
1884 if (errorCount > 0 || warningCount > 0) {
1885 this._errorsAndWarnings.set(id, {errorCount, warningCount});
1886 } else if (this._errorsAndWarnings.has(id)) {
1887 this._errorsAndWarnings.delete(id);
1888 }
1889 haveErrorsOrWarningsChanged = true;
1890 break;
1891 }
1892 case SUSPENSE_TREE_OPERATION_ADD: {
1893 const id = operations[i + 1];
1894 const parentID = operations[i + 2];
1895 const nameStringID = operations[i + 3];
1896 const isSuspended = operations[i + 4] === 1;
1897 const numRects = operations[i + 5];
1898 let name = stringTable[nameStringID];
1899
1900 if (this._idToSuspense.has(id)) {
1901 // We should never reach this. This is a bug in the backend renderer.
1902 return this._throwAndEmitError(
1903 Error(
1904 `Cannot add suspense node "${id}" because a suspense node with that id is already in the Store.`,
1905 ),
1906 );
1907 }
1908
1909 const element = this._idToElement.get(id);
1910 if (element === undefined) {
1911 // This element isn't connected yet.
1912 } else {
1913 if (name === null) {
1914 // The boundary isn't explicitly named.
1915 // Pick a sensible default.
1916 if (parentID === 0) {
1917 // For Roots we use their display name.
1918 name = element.displayName;
1919 } else {
1920 name = this._guessSuspenseName(element);
1921 }
1922 }
1923 }
1924
1925 i += 6;
1926 let rects: SuspenseNode['rects'];
1927 if (numRects === -1) {
1928 rects = null;
1929 } else {
1930 rects = [];
1931 for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
1932 const x = operations[i + 0] / 1000;
1933 const y = operations[i + 1] / 1000;
1934 const width = operations[i + 2] / 1000;
1935 const height = operations[i + 3] / 1000;
1936 const rect = {x, y, width, height};
1937 if (parentID !== 0) {
1938 // Track all rects except the root.
1939 this._rtree.insert(rect);
1940 }
1941 rects.push(rect);
1942 i += 4;
1943 }
1944 }
1945
1946 // $FlowFixMe[constant-condition]
1947 if (__DEBUG__) {
1948 debug('Suspense Add', `node ${id} as child of ${parentID}`);
1949 }
1950
1951 if (parentID !== 0) {
1952 const parentSuspense = this._idToSuspense.get(parentID);
1953 if (parentSuspense === undefined) {
1954 // We should never reach this. This is a bug in the backend renderer.
1955 return this._throwAndEmitError(
1956 Error(
1957 `Cannot add suspense child "${id}" to parent suspense "${parentID}" because parent suspense node was not found in the Store.`,
1958 ),
1959 );
1960 }
1961
1962 parentSuspense.children.push(id);
1963 }
1964
1965 this._idToSuspense.set(id, {
1966 id,
1967 parentID,
1968 children: [],
1969 name,
1970 rects,
1971 hasUniqueSuspenders: false,
1972 isSuspended: isSuspended,
1973 environments: [],
1974 endTime: 0,
1975 });
1976
1977 hasSuspenseTreeChanged = true;
1978 break;
1979 }
1980 case SUSPENSE_TREE_OPERATION_REMOVE: {
1981 const removeLength = operations[i + 1];
1982 i += 2;
1983
1984 for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
1985 const id = operations[i];
1986 const suspense = this._idToSuspense.get(id);
1987
1988 if (suspense === undefined) {
1989 // We should never reach this. This is a bug in the backend renderer.
1990 return this._throwAndEmitError(
1991 Error(
1992 `Cannot remove suspense node "${id}" because no matching node was found in the Store.`,
1993 ),
1994 );
1995 }
1996
1997 i += 1;
1998
1999 const {children, parentID, rects} = suspense;
2000 if (children.length > 0) {
2001 // We should never reach this. This is a bug in the backend renderer.
2002 return this._throwAndEmitError(
2003 Error(`Suspense node "${id}" was removed before its children.`),
2004 );
2005 }
2006
2007 let parentSuspense: SuspenseNode | null = null;
2008 let parentIndex = -1;
2009 if (parentID !== 0) {
2010 parentSuspense = this._idToSuspense.get(parentID) || null;
2011 if (parentSuspense === null) {
2012 return this._throwAndEmitError(
2013 Error(
2014 `Cannot remove suspense node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
2015 ),
2016 );
2017 }
2018
2019 parentIndex = parentSuspense.children.indexOf(id);
2020 if (parentIndex === -1) {
2021 return this._throwAndEmitError(
2022 Error(
2023 `Cannot remove suspense node "${id}" from parent "${parentID}" because it is not a child of the parent.`,
2024 ),
2025 );
2026 }
2027 }
2028
2029 if (rects !== null && parentID !== 0) {
2030 // Delete all the existing rects from the R-tree
2031 for (let j = 0; j < rects.length; j++) {
2032 this._rtree.remove(rects[j]);
2033 }
2034 }
2035
2036 this._idToSuspense.delete(id);
2037 removedSuspenseIDs.set(id, parentID);
2038
2039 if (parentSuspense === null) {
2040 // $FlowFixMe[constant-condition]
2041 if (__DEBUG__) {
2042 debug('Suspense remove', `node ${id} root`);
2043 }
2044 } else {
2045 // $FlowFixMe[constant-condition]
2046 if (__DEBUG__) {
2047 debug('Suspense Remove', `node ${id} from parent ${parentID}`);
2048 }
2049
2050 parentSuspense.children.splice(parentIndex, 1);
2051 }
2052 }
2053
2054 hasSuspenseTreeChanged = true;
2055 break;
2056 }
2057 case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: {
2058 const id = operations[i + 1];
2059 const numChildren = operations[i + 2];
2060 i += 3;
2061
2062 const suspense = this._idToSuspense.get(id);
2063 if (suspense === undefined) {
2064 // We should never reach this. This is a bug in the backend renderer.
2065 return this._throwAndEmitError(
2066 Error(
2067 `Cannot reorder children for suspense node "${id}" because no matching node was found in the Store.`,
2068 ),
2069 );
2070 }
2071
2072 const children = suspense.children;
2073 if (children.length !== numChildren) {
2074 // We should never reach this. This is a bug in the backend renderer.
2075 return this._throwAndEmitError(
2076 Error(
2077 `Suspense children cannot be added or removed during a reorder operation.`,
2078 ),
2079 );
2080 }
2081
2082 const reorderedChildIDs: Set<SuspenseNode['id']> = new Set();
2083 for (let j = 0; j < numChildren; j++) {
2084 const childID = operations[i + j];
2085 const childSuspense = this._idToSuspense.get(childID);
2086 if (
2087 childSuspense === undefined ||
2088 childSuspense.parentID !== id ||
2089 reorderedChildIDs.has(childID)
2090 ) {
2091 return this._throwAndEmitError(
2092 Error(
2093 `Suspense children cannot be added or removed during a reorder operation.`,
2094 ),
2095 );
2096 }
2097 reorderedChildIDs.add(childID);
2098 }
2099 for (let j = 0; j < numChildren; j++) {
2100 children[j] = operations[i + j];
2101 }
2102 i += numChildren;
2103
2104 // $FlowFixMe[constant-condition]
2105 if (__DEBUG__) {
2106 debug(
2107 'Re-order',
2108 `Suspense node ${id} children ${children.join(',')}`,
2109 );
2110 }
2111
2112 hasSuspenseTreeChanged = true;
2113 break;
2114 }
2115 case SUSPENSE_TREE_OPERATION_RESIZE: {
2116 const id = operations[i + 1];
2117 const numRects = operations[i + 2];
2118 i += 3;
2119
2120 const suspense = this._idToSuspense.get(id);
2121 if (suspense === undefined) {
2122 // We should never reach this. This is a bug in the backend renderer.
2123 return this._throwAndEmitError(
2124 Error(
2125 `Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`,
2126 ),
2127 );
2128 }
2129
2130 const prevRects = suspense.rects;
2131 if (prevRects !== null && suspense.parentID !== 0) {
2132 // Delete all the existing rects from the R-tree
2133 for (let j = 0; j < prevRects.length; j++) {
2134 this._rtree.remove(prevRects[j]);
2135 }
2136 }
2137
2138 let nextRects: SuspenseNode['rects'];
2139 if (numRects === -1) {
2140 nextRects = null;
2141 } else {
2142 nextRects = [];
2143 for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
2144 const x = operations[i + 0] / 1000;
2145 const y = operations[i + 1] / 1000;
2146 const width = operations[i + 2] / 1000;
2147 const height = operations[i + 3] / 1000;
2148
2149 const rect = {x, y, width, height};
2150 if (suspense.parentID !== 0) {
2151 // Track all rects except the root.
2152 this._rtree.insert(rect);
2153 }
2154 nextRects.push(rect);
2155
2156 i += 4;
2157 }
2158 }
2159
2160 suspense.rects = nextRects;
2161
2162 // $FlowFixMe[constant-condition]
2163 if (__DEBUG__) {
2164 debug(
2165 'Resize',
2166 `Suspense node ${id} resize to ${
2167 nextRects === null
2168 ? 'null'
2169 : nextRects
2170 .map(
2171 rect =>
2172 `(${rect.x},${rect.y},${rect.width},${rect.height})`,
2173 )
2174 .join(',')
2175 }`,
2176 );
2177 }
2178
2179 hasSuspenseTreeChanged = true;
2180
2181 break;
2182 }
2183 case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
2184 i++;
2185 const changeLength = operations[i++];
2186
2187 for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
2188 const id = operations[i++];
2189 const hasUniqueSuspenders = operations[i++] === 1;
2190 const endTime = operations[i++] / 1000;
2191 const isSuspended = operations[i++] === 1;
2192 const environmentNamesLength = operations[i++];
2193 const environmentNames = [];
2194 for (
2195 let envIndex = 0;
2196 envIndex < environmentNamesLength;
2197 envIndex++
2198 ) {
2199 const environmentNameStringID = operations[i++];
2200 const environmentName = stringTable[environmentNameStringID];
2201 if (environmentName != null) {
2202 environmentNames.push(environmentName);
2203 }
2204 }
2205 const suspense = this._idToSuspense.get(id);
2206
2207 if (suspense === undefined) {
2208 // We should never reach this. This is a bug in the backend renderer.
2209 return this._throwAndEmitError(
2210 Error(
2211 `Cannot update suspenders of suspense node "${id}" because no matching node was found in the Store.`,
2212 ),
2213 );
2214 }
2215
2216 // $FlowFixMe[constant-condition]
2217 if (__DEBUG__) {
2218 const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders;
2219 debug(
2220 'Suspender changes',
2221 `Suspense node ${id} unique suspenders set to ${String(
2222 hasUniqueSuspenders,
2223 )} (was ${String(previousHasUniqueSuspenders)})`,
2224 );
2225 }
2226
2227 suspense.hasUniqueSuspenders = hasUniqueSuspenders;
2228 suspense.endTime = endTime;
2229 suspense.isSuspended = isSuspended;
2230 suspense.environments = environmentNames;
2231 }
2232
2233 hasSuspenseTreeChanged = true;
2234
2235 break;
2236 }
2237 case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
2238 i++;
2239 nextActivitySliceID = operations[i++];
2240 break;
2241 }
2242 default:
2243 return this._throwAndEmitError(
2244 new UnsupportedBridgeOperationError(
2245 `Unsupported Bridge operation "${operation}"`,
2246 ),
2247 );
2248 }
2249 }
2250
2251 this._revision++;
2252 if (hasSuspenseTreeChanged) {
2253 this._revisionSuspense++;
2254 }
2255
2256 // Any time the tree changes (e.g. elements added, removed, or reordered) cached indices may be invalid.
2257 this._cachedErrorAndWarningTuples = null;
2258
2259 if (haveErrorsOrWarningsChanged) {
2260 let componentWithErrorCount = 0;
2261 let componentWithWarningCount = 0;
2262
2263 this._errorsAndWarnings.forEach(entry => {
2264 if (entry.errorCount > 0) {
2265 componentWithErrorCount++;
2266 }
2267
2268 if (entry.warningCount > 0) {
2269 componentWithWarningCount++;
2270 }
2271 });
2272
2273 this._cachedComponentWithErrorCount = componentWithErrorCount;
2274 this._cachedComponentWithWarningCount = componentWithWarningCount;
2275 }
2276
2277 if (haveRootsChanged) {
2278 const prevRootSupportsProfiling = this._rootSupportsBasicProfiling;
2279 const prevRootSupportsPerformanceTracks =
2280 this._rootSupportsPerformanceTracks;
2281
2282 this._hasOwnerMetadata = false;
2283 this._rootSupportsBasicProfiling = false;
2284 this._rootSupportsPerformanceTracks = false;
2285 this._rootIDToCapabilities.forEach(
2286 ({
2287 supportsBasicProfiling,
2288 hasOwnerMetadata,
2289 supportsAdvancedProfiling,
2290 }) => {
2291 if (supportsBasicProfiling) {
2292 this._rootSupportsBasicProfiling = true;
2293 }
2294 if (hasOwnerMetadata) {
2295 this._hasOwnerMetadata = true;
2296 }
2297 if (
2298 supportsAdvancedProfiling === ADVANCED_PROFILING_PERFORMANCE_TRACKS
2299 ) {
2300 this._rootSupportsPerformanceTracks = true;
2301 }
2302 },
2303 );
2304
2305 this.emit('roots');
2306
2307 if (this._rootSupportsBasicProfiling !== prevRootSupportsProfiling) {
2308 this.emit('rootSupportsBasicProfiling');
2309 }
2310
2311 if (
2312 this._rootSupportsPerformanceTracks !==
2313 prevRootSupportsPerformanceTracks
2314 ) {
2315 this.emit('rootSupportsPerformanceTracks');
2316 }
2317 }
2318
2319 if (hasSuspenseTreeChanged) {
2320 this.emit('suspenseTreeMutated', [removedSuspenseIDs]);
2321 }
2322
2323 // $FlowFixMe[constant-condition]
2324 if (__DEBUG__) {
2325 console.log(printStore(this, true));
2326 console.groupEnd();
2327 }
2328
2329 if (nextActivitySliceID !== null && nextActivitySliceID !== 0) {
2330 let didCollapse = false;
2331 // The backend filtered everything above the Activity slice.
2332 // We need to hide everything below the Activity slice by collapsing
2333 // the Activities that are descendants of the next Activity slice.
2334 const nextActivitySlice = this._idToElement.get(nextActivitySliceID);
2335 if (nextActivitySlice === undefined) {
2336 return this._throwAndEmitError(
2337 Error('Next Activity slice not found in Store.'),
2338 );
2339 }
2340
2341 for (let j = 0; j < nextActivitySlice.children.length; j++) {
2342 didCollapse ||= this._collapseActivitiesRecursively(
2343 nextActivitySlice.children[j],
2344 );
2345 }
2346
2347 if (didCollapse) {
2348 this._recalculateWeightAcrossRoots();
2349 }
2350 }
2351
2352 for (let j = 0; j < this._componentFilters.length; j++) {
2353 const filter = this._componentFilters[j];
2354 // If we're focusing an Activity, IDs may have changed.
2355 if (filter.type === ComponentFilterActivitySlice) {
2356 if (nextActivitySliceID === null || nextActivitySliceID === 0) {
2357 filter.isValid = false;
2358 } else {
2359 filter.activityID = nextActivitySliceID;
2360 }
2361 }
2362 }
2363
2364 if (nextActivitySliceID !== null) {
2365 this._focusedTransition = nextActivitySliceID;
2366 }
2367
2368 this.emit('mutated', [
2369 addedElementIDs,
2370 removedElementIDs,
2371 nextActivitySliceID,
2372 ]);
2373 };
2374
2375 _collapseActivitiesRecursively(elementID: number): boolean {
2376 let didMutate = false;
2377 const element = this._idToElement.get(elementID);
2378 if (element === undefined) {
2379 return this._throwAndEmitError(Error('Element not found in Store.'));
2380 }
2381
2382 if (element.type === ElementTypeActivity) {
2383 if (!element.isCollapsed) {
2384 element.isCollapsed = true;
2385
2386 const weightDelta = 1 - element.weight;
2387
2388 let parentElement = this._idToElement.get(element.parentID);
2389 while (parentElement !== undefined) {
2390 parentElement.weight += weightDelta;
2391 parentElement = this._idToElement.get(parentElement.parentID);
2392 }
2393 return true;
2394 }
2395 return false;
2396 }
2397
2398 for (let i = 0; i < element.children.length; i++) {
2399 didMutate ||= this._collapseActivitiesRecursively(element.children[i]);
2400 }
2401 return didMutate;
2402 }
2403
2404 onBridgeShutdown: () => void = () => {
2405 // $FlowFixMe[constant-condition]
2406 if (__DEBUG__) {
2407 debug('onBridgeShutdown', 'unsubscribing from Bridge');
2408 }
2409
2410 const bridge = this._bridge;
2411 bridge.removeListener('operations', this.onBridgeOperations);
2412 bridge.removeListener('shutdown', this.onBridgeShutdown);
2413 bridge.removeListener(
2414 'isReloadAndProfileSupportedByBackend',
2415 this.onBackendReloadAndProfileSupported,
2416 );
2417 bridge.removeListener(
2418 'isNativeStyleEditorSupported',
2419 this.onBridgeNativeStyleEditorSupported,
2420 );
2421 bridge.removeListener(
2422 'unsupportedRendererVersion',
2423 this.onBridgeUnsupportedRendererVersion,
2424 );
2425 bridge.removeListener('backendVersion', this.onBridgeBackendVersion);
2426 bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
2427 bridge.removeListener('saveToClipboard', this.onSaveToClipboard);
2428 bridge.removeListener('selectElement', this.onHostInstanceSelected);
2429
2430 if (this._onBridgeProtocolTimeoutID !== null) {
2431 clearTimeout(this._onBridgeProtocolTimeoutID);
2432 this._onBridgeProtocolTimeoutID = null;
2433 }
2434 };
2435
2436 onBackendReloadAndProfileSupported: (
2437 isReloadAndProfileSupported: boolean,
2438 ) => void = isReloadAndProfileSupported => {
2439 this._isReloadAndProfileBackendSupported = isReloadAndProfileSupported;
2440
2441 this.emit('supportsReloadAndProfile');
2442 };
2443
2444 onBridgeUnsupportedRendererVersion: () => void = () => {
2445 this._unsupportedRendererVersionDetected = true;
2446
2447 this.emit('unsupportedRendererVersionDetected');
2448 };
2449
2450 onBridgeBackendVersion: (backendVersion: string) => void = backendVersion => {
2451 this._backendVersion = backendVersion;
2452 this.emit('backendVersion');
2453 };
2454
2455 onBridgeProtocol: (bridgeProtocol: BridgeProtocol) => void =
2456 bridgeProtocol => {
2457 if (this._onBridgeProtocolTimeoutID !== null) {
2458 clearTimeout(this._onBridgeProtocolTimeoutID);
2459 this._onBridgeProtocolTimeoutID = null;
2460 }
2461
2462 this._bridgeProtocol = bridgeProtocol;
2463
2464 if (bridgeProtocol.version !== currentBridgeProtocol.version) {
2465 // Technically newer versions of the frontend can, at least for now,
2466 // gracefully handle older versions of the backend protocol.
2467 // So for now we don't need to display the unsupported dialog.
2468 }
2469 };
2470
2471 onBridgeProtocolTimeout: () => void = () => {
2472 this._onBridgeProtocolTimeoutID = null;
2473
2474 // If we timed out, that indicates the backend predates the bridge protocol,
2475 // so we can set a fake version (0) to trigger the downgrade message.
2476 this._bridgeProtocol = BRIDGE_PROTOCOL[0];
2477
2478 this.emit('unsupportedBridgeProtocolDetected');
2479 };
2480
2481 onSaveToClipboard: (text: string) => void = text => {
2482 withPermissionsCheck({permissions: ['clipboardWrite']}, () => copy(text))();
2483 };
2484
2485 onBackendInitialized: () => void = () => {
2486 // Verify that the frontend version is compatible with the connected backend.
2487 // See github.com/facebook/react/issues/21326
2488 if (this._shouldCheckBridgeProtocolCompatibility) {
2489 // Older backends don't support an explicit bridge protocol,
2490 // so we should timeout eventually and show a downgrade message.
2491 this._onBridgeProtocolTimeoutID = setTimeout(
2492 this.onBridgeProtocolTimeout,
2493 10000,
2494 );
2495
2496 this._bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
2497 this._bridge.send('getBridgeProtocol');
2498 }
2499
2500 this._bridge.send('getBackendVersion');
2501 this._bridge.send('getIfHasUnsupportedRendererVersion');
2502 this._bridge.send('getHookSettings'); // Warm up cached hook settings
2503 };
2504
2505 onHostInstanceSelected: (elementId: number | null) => void = elementId => {
2506 if (
2507 this._lastSelectedHostInstanceElementId === elementId &&
2508 // Force clear selection e.g. when we inspect an element in the Components panel
2509 // and then switch to the browser's Elements panel.
2510 // We wouldn't want to stay on the inspected element if we're inspecting
2511 // an element not owned by React when switching to the browser's Elements panel.
2512 elementId !== null
2513 ) {
2514 return;
2515 }
2516
2517 this._lastSelectedHostInstanceElementId = elementId;
2518 // By the time we emit this, there is no guarantee that TreeContext is rendered.
2519 this.emit('hostInstanceSelected', elementId);
2520 };
2521
2522 getHookSettings: () => void = () => {
2523 if (this._hookSettings != null) {
2524 this.emit('hookSettings', this._hookSettings);
2525 } else {
2526 this._bridge.send('getHookSettings');
2527 }
2528 };
2529
2530 /**
2531 * Maximum recorded node depth during the lifetime of this Store.
2532 * Can only increase: not guaranteed to return maximal value for currently recorded elements.
2533 */
2534 getMaximumRecordedDepth(): number {
2535 return this._maximumRecordedDepth;
2536 }
2537
2538 updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
2539 settings => {
2540 this._hookSettings = settings;
2541
2542 this._bridge.send('updateHookSettings', settings);
2543 this.emit('settingsUpdated', settings, this._componentFilters);
2544 };
2545
2546 onHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
2547 settings => {
2548 this._hookSettings = settings;
2549
2550 this.setShouldShowWarningsAndErrors(settings.showInlineWarningsAndErrors);
2551 this.emit('hookSettings', settings);
2552 };
2553
2554 setShouldShowWarningsAndErrors(status: boolean): void {
2555 const previousStatus = this._shouldShowWarningsAndErrors;
2556 this._shouldShowWarningsAndErrors = status;
2557
2558 if (previousStatus !== status) {
2559 // Propagate to subscribers, although tree state has not changed
2560 this.emit('mutated', [[], new Map(), null]);
2561 }
2562 }
2563
2564 // The Store should never throw an Error without also emitting an event.
2565 // Otherwise Store errors will be invisible to users,
2566 // but the downstream errors they cause will be reported as bugs.
2567 // For example, https://github.com/facebook/react/issues/21402
2568 // Emitting an error event allows the ErrorBoundary to show the original error.
2569 _throwAndEmitError(error: Error): empty {
2570 this.emit('error', error);
2571
2572 // Throwing is still valuable for local development
2573 // and for unit testing the Store itself.
2574 throw error;
2575 }
2576
2577 _guessSuspenseName(element: Element): string | null {
2578 const owner = this._idToElement.get(element.ownerID);
2579 if (owner !== undefined && owner.displayName !== null) {
2580 return owner.displayName;
2581 }
2582
2583 return null;
2584 }
2585 }