2019-08-28 01:54:01 +08:00
|
|
|
/**
|
|
|
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
|
|
|
*
|
|
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
|
|
|
|
*
|
|
|
|
|
* @flow
|
|
|
|
|
*/
|
2019-01-23 03:04:37 +08:00
|
|
|
|
2020-03-26 01:26:40 +08:00
|
|
|
import EventEmitter from '../events';
|
2019-08-14 08:58:03 +08:00
|
|
|
import {inspect} from 'util';
|
2019-01-29 07:50:48 +08:00
|
|
|
import {
|
|
|
|
|
TREE_OPERATION_ADD,
|
|
|
|
|
TREE_OPERATION_REMOVE,
|
2020-12-23 00:09:29 +08:00
|
|
|
TREE_OPERATION_REMOVE_ROOT,
|
2019-04-19 08:59:53 +08:00
|
|
|
TREE_OPERATION_REORDER_CHILDREN,
|
2020-12-23 00:09:29 +08:00
|
|
|
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
2019-03-16 11:05:52 +08:00
|
|
|
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
2019-01-29 07:50:48 +08:00
|
|
|
} from '../constants';
|
2019-08-14 08:58:03 +08:00
|
|
|
import {ElementTypeRoot} from '../types';
|
2019-04-29 06:33:39 +08:00
|
|
|
import {
|
2019-05-02 01:43:40 +08:00
|
|
|
getSavedComponentFilters,
|
|
|
|
|
saveComponentFilters,
|
2019-06-01 23:24:24 +08:00
|
|
|
separateDisplayNameAndHOCs,
|
2019-06-11 06:36:29 +08:00
|
|
|
shallowDiffers,
|
2019-04-29 06:33:39 +08:00
|
|
|
utfDecodeString,
|
|
|
|
|
} from '../utils';
|
2019-08-14 08:58:03 +08:00
|
|
|
import {localStorageGetItem, localStorageSetItem} from '../storage';
|
|
|
|
|
import {__DEBUG__} from '../constants';
|
|
|
|
|
import {printStore} from './utils';
|
2019-05-22 21:05:25 +08:00
|
|
|
import ProfilerStore from './ProfilerStore';
|
2019-01-29 07:50:48 +08:00
|
|
|
|
2019-08-14 08:58:03 +08:00
|
|
|
import type {Element} from './views/Components/types';
|
|
|
|
|
import type {ComponentFilter, ElementType} from '../types';
|
|
|
|
|
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
2019-01-23 03:04:37 +08:00
|
|
|
|
2019-01-24 00:27:40 +08:00
|
|
|
const debug = (methodName, ...args) => {
|
2019-01-30 05:48:35 +08:00
|
|
|
if (__DEBUG__) {
|
|
|
|
|
console.log(
|
|
|
|
|
`%cStore %c${methodName}`,
|
|
|
|
|
'color: green; font-weight: bold;',
|
|
|
|
|
'font-weight: bold;',
|
2019-08-14 08:58:03 +08:00
|
|
|
...args,
|
2019-01-30 05:48:35 +08:00
|
|
|
);
|
|
|
|
|
}
|
2019-01-24 00:27:40 +08:00
|
|
|
};
|
|
|
|
|
|
2019-04-17 04:59:36 +08:00
|
|
|
const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
|
|
|
|
|
'React::DevTools::collapseNodesByDefault';
|
2019-06-08 06:38:26 +08:00
|
|
|
const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
|
|
|
|
|
'React::DevTools::recordChangeDescriptions';
|
2019-04-04 05:32:16 +08:00
|
|
|
|
2019-03-28 00:41:12 +08:00
|
|
|
type Config = {|
|
2019-04-03 02:53:18 +08:00
|
|
|
isProfiling?: boolean,
|
2019-07-14 01:05:04 +08:00
|
|
|
supportsNativeInspection?: boolean,
|
2019-03-28 00:41:12 +08:00
|
|
|
supportsReloadAndProfile?: boolean,
|
|
|
|
|
supportsProfiling?: boolean,
|
2019-10-04 02:07:18 +08:00
|
|
|
supportsTraceUpdates?: boolean,
|
2019-03-28 00:41:12 +08:00
|
|
|
|};
|
|
|
|
|
|
2019-03-11 00:01:05 +08:00
|
|
|
export type Capabilities = {|
|
2019-04-12 05:00:27 +08:00
|
|
|
hasOwnerMetadata: boolean,
|
2019-03-11 00:01:05 +08:00
|
|
|
supportsProfiling: boolean,
|
|
|
|
|
|};
|
|
|
|
|
|
2019-01-23 03:04:37 +08:00
|
|
|
/**
|
|
|
|
|
* The store is the single source of truth for updates from the backend.
|
|
|
|
|
* ContextProviders can subscribe to the Store for specific things they want to provide.
|
|
|
|
|
*/
|
2019-06-10 06:50:17 +08:00
|
|
|
export default class Store extends EventEmitter<{|
|
|
|
|
|
collapseNodesByDefault: [],
|
|
|
|
|
componentFilters: [],
|
|
|
|
|
mutated: [[Array<number>, Map<number, number>]],
|
|
|
|
|
recordChangeDescriptions: [],
|
|
|
|
|
roots: [],
|
2019-07-14 01:05:04 +08:00
|
|
|
supportsNativeStyleEditor: [],
|
2019-06-10 06:50:17 +08:00
|
|
|
supportsProfiling: [],
|
|
|
|
|
supportsReloadAndProfile: [],
|
2019-09-26 23:41:46 +08:00
|
|
|
unsupportedRendererVersionDetected: [],
|
2019-06-10 06:50:17 +08:00
|
|
|
|}> {
|
2019-07-21 05:08:23 +08:00
|
|
|
_bridge: FrontendBridge;
|
2019-02-15 03:45:11 +08:00
|
|
|
|
2020-12-23 00:09:29 +08:00
|
|
|
// Computed whenever _errorsAndWarnings Map changes.
|
|
|
|
|
_cachedErrorCount: number = 0;
|
|
|
|
|
_cachedWarningCount: number = 0;
|
|
|
|
|
_cachedErrorAndWarningTuples: Array<{|id: number, index: number|}> = [];
|
|
|
|
|
|
2019-04-17 04:59:36 +08:00
|
|
|
// Should new nodes be collapsed by default when added to the tree?
|
|
|
|
|
_collapseNodesByDefault: boolean = true;
|
|
|
|
|
|
2019-05-02 01:43:40 +08:00
|
|
|
_componentFilters: Array<ComponentFilter>;
|
2019-04-29 06:33:39 +08:00
|
|
|
|
2020-12-23 00:09:29 +08:00
|
|
|
// Map of ID to number of recorded error and warning message IDs.
|
|
|
|
|
_errorsAndWarnings: Map<
|
|
|
|
|
number,
|
|
|
|
|
{|errorCount: number, warningCount: number|},
|
|
|
|
|
> = new Map();
|
|
|
|
|
|
2019-04-12 05:00:27 +08:00
|
|
|
// At least one of the injected renderers contains (DEV only) owner metadata.
|
|
|
|
|
_hasOwnerMetadata: boolean = false;
|
|
|
|
|
|
2019-04-24 09:04:25 +08:00
|
|
|
// Map of ID to (mutable) Element.
|
|
|
|
|
// Elements are mutated to avoid excessive cloning during tree updates.
|
Update DevTools to use getCacheForType API (#20548)
DevTools was built with a fork of an early idea for how Suspense cache might work. This idea is incompatible with newer APIs like `useTransition` which unfortunately prevented me from making certain UX improvements. This PR swaps out the primary usage of this cache (there are a few) in favor of the newer `unstable_getCacheForType` and `unstable_useCacheRefresh` APIs. We can go back and update the others in follow up PRs.
### Messaging changes
I've refactored the way the frontend loads component props/state/etc to hopefully make it better match the Suspense+cache model. Doing this gave up some of the small optimizations I'd added but hopefully the actual performance impact of that is minor and the overall ergonomic improvements of working with the cache API make this worth it.
The backend no longer remembers inspected paths. Instead, the frontend sends them every time and the backend sends a response with those paths. I've also added a new "force" parameter that the frontend can use to tell the backend to send a response even if the component hasn't rendered since the last time it asked. (This is used to get data for newly inspected paths.)
_Initial inspection..._
```
front | | back
| -- "inspect" (id:1, paths:[], force:true) ---------> |
| <------------------------ "inspected" (full-data) -- |
```
_1 second passes with no updates..._
```
| -- "inspect" (id:1, paths:[], force:false) --------> |
| <------------------------ "inspected" (no-change) -- |
```
_User clicks to expand a path, aka hydrate..._
```
| -- "inspect" (id:1, paths:['foo'], force:true) ----> |
| <------------------------ "inspected" (full-data) -- |
```
_1 second passes during which there is an update..._
```
| -- "inspect" (id:1, paths:['foo'], force:false) ---> |
| <----------------- "inspectedElement" (full-data) -- |
```
### Clear errors/warnings transition
Previously this meant there would be a delay after clicking the "clear" button. The UX after this change is much improved.
### Hydrating paths transition
I also added a transition to hydration (expanding "dehyrated" paths).
### Better error boundaries
I also added a lower-level error boundary in case the new suspense operation ever failed. It provides a better "retry" mechanism (select a new element) so DevTools doesn't become entirely useful. Here I'm intentionally causing an error every time I select an element.
### Improved snapshot tests
I also migrated several of the existing snapshot tests to use inline snapshots and added a new serializer for dehydrated props. Inline snapshots are easier to verify and maintain and the new serializer means dehydrated props will be formatted in a way that makes sense rather than being empty (in external snapshots) or super verbose (default inline snapshot format).
2021-01-19 22:51:32 +08:00
|
|
|
// The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.
|
2019-01-29 07:50:48 +08:00
|
|
|
_idToElement: Map<number, Element> = new Map();
|
2019-01-28 08:04:17 +08:00
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
// Should the React Native style editor panel be shown?
|
|
|
|
|
_isNativeStyleEditorSupported: boolean = false;
|
|
|
|
|
|
2019-06-08 04:08:49 +08:00
|
|
|
// Can the backend use the Storage API (e.g. localStorage)?
|
|
|
|
|
// If not, features like reload-and-profile will not work correctly and must be disabled.
|
|
|
|
|
_isBackendStorageAPISupported: boolean = false;
|
|
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
_nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;
|
|
|
|
|
|
2019-04-26 03:16:13 +08:00
|
|
|
// Map of element (id) to the set of elements (ids) it owns.
|
|
|
|
|
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
|
|
|
|
|
_ownersMap: Map<number, Set<number>> = new Map();
|
|
|
|
|
|
2019-05-22 21:05:25 +08:00
|
|
|
_profilerStore: ProfilerStore;
|
2019-03-12 02:26:30 +08:00
|
|
|
|
2019-06-08 06:38:26 +08:00
|
|
|
_recordChangeDescriptions: boolean = false;
|
|
|
|
|
|
2019-02-09 04:37:52 +08:00
|
|
|
// Incremented each time the store is mutated.
|
|
|
|
|
// This enables a passive effect to detect a mutation between render and commit phase.
|
|
|
|
|
_revision: number = 0;
|
|
|
|
|
|
2019-01-27 23:58:09 +08:00
|
|
|
// This Array must be treated as immutable!
|
|
|
|
|
// Passive effects will check it for changes between render and mount.
|
2019-01-29 07:50:48 +08:00
|
|
|
_roots: $ReadOnlyArray<number> = [];
|
2019-01-23 03:04:37 +08:00
|
|
|
|
2019-03-11 00:01:05 +08:00
|
|
|
_rootIDToCapabilities: Map<number, Capabilities> = new Map();
|
|
|
|
|
|
2019-02-05 01:49:30 +08:00
|
|
|
// Renderer ID is needed to support inspection fiber props, state, and hooks.
|
|
|
|
|
_rootIDToRendererID: Map<number, number> = new Map();
|
|
|
|
|
|
2019-03-31 05:16:34 +08:00
|
|
|
// These options may be initially set by a confiugraiton option when constructing the Store.
|
|
|
|
|
// In the case of "supportsProfiling", the option may be updated based on the injected renderers.
|
2019-08-06 01:09:26 +08:00
|
|
|
_supportsNativeInspection: boolean = true;
|
2019-03-11 00:01:05 +08:00
|
|
|
_supportsProfiling: boolean = false;
|
2019-03-28 00:41:12 +08:00
|
|
|
_supportsReloadAndProfile: boolean = false;
|
2019-10-04 02:07:18 +08:00
|
|
|
_supportsTraceUpdates: boolean = false;
|
2019-03-28 00:41:12 +08:00
|
|
|
|
2019-09-26 23:41:46 +08:00
|
|
|
_unsupportedRendererVersionDetected: boolean = false;
|
|
|
|
|
|
2019-04-10 22:31:34 +08:00
|
|
|
// Total number of visible elements (within all roots).
|
|
|
|
|
// Used for windowing purposes.
|
|
|
|
|
_weightAcrossRoots: number = 0;
|
|
|
|
|
|
2019-07-21 05:08:23 +08:00
|
|
|
constructor(bridge: FrontendBridge, config?: Config) {
|
2019-01-23 03:04:37 +08:00
|
|
|
super();
|
|
|
|
|
|
2019-04-05 07:03:17 +08:00
|
|
|
if (__DEBUG__) {
|
|
|
|
|
debug('constructor', 'subscribing to Bridge');
|
|
|
|
|
}
|
2019-02-01 06:31:12 +08:00
|
|
|
|
2019-04-17 04:59:36 +08:00
|
|
|
this._collapseNodesByDefault =
|
2019-07-25 02:25:11 +08:00
|
|
|
localStorageGetItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) ===
|
|
|
|
|
'true';
|
2019-04-17 04:59:36 +08:00
|
|
|
|
2019-06-08 06:38:26 +08:00
|
|
|
this._recordChangeDescriptions =
|
|
|
|
|
localStorageGetItem(LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
|
|
|
|
|
'true';
|
|
|
|
|
|
2019-05-02 01:43:40 +08:00
|
|
|
this._componentFilters = getSavedComponentFilters();
|
2019-04-29 06:33:39 +08:00
|
|
|
|
2019-05-22 21:05:25 +08:00
|
|
|
let isProfiling = false;
|
2019-03-28 00:41:12 +08:00
|
|
|
if (config != null) {
|
2019-05-22 21:05:25 +08:00
|
|
|
isProfiling = config.isProfiling === true;
|
|
|
|
|
|
2019-03-31 05:16:34 +08:00
|
|
|
const {
|
2019-07-14 01:05:04 +08:00
|
|
|
supportsNativeInspection,
|
2019-03-31 05:16:34 +08:00
|
|
|
supportsProfiling,
|
|
|
|
|
supportsReloadAndProfile,
|
2019-10-04 02:07:18 +08:00
|
|
|
supportsTraceUpdates,
|
2019-03-31 05:16:34 +08:00
|
|
|
} = config;
|
2019-07-14 01:05:04 +08:00
|
|
|
this._supportsNativeInspection = supportsNativeInspection !== false;
|
2019-03-31 05:16:34 +08:00
|
|
|
if (supportsProfiling) {
|
2019-03-28 00:41:12 +08:00
|
|
|
this._supportsProfiling = true;
|
|
|
|
|
}
|
2019-03-31 05:16:34 +08:00
|
|
|
if (supportsReloadAndProfile) {
|
2019-03-28 00:41:12 +08:00
|
|
|
this._supportsReloadAndProfile = true;
|
|
|
|
|
}
|
2019-10-04 02:07:18 +08:00
|
|
|
if (supportsTraceUpdates) {
|
|
|
|
|
this._supportsTraceUpdates = true;
|
|
|
|
|
}
|
2019-03-28 00:41:12 +08:00
|
|
|
}
|
|
|
|
|
|
2019-02-15 03:45:11 +08:00
|
|
|
this._bridge = bridge;
|
2019-03-12 02:26:30 +08:00
|
|
|
bridge.addListener('operations', this.onBridgeOperations);
|
2019-07-14 01:05:04 +08:00
|
|
|
bridge.addListener(
|
|
|
|
|
'overrideComponentFilters',
|
2019-08-14 08:58:03 +08:00
|
|
|
this.onBridgeOverrideComponentFilters,
|
2019-07-14 01:05:04 +08:00
|
|
|
);
|
2019-03-12 02:26:30 +08:00
|
|
|
bridge.addListener('shutdown', this.onBridgeShutdown);
|
2019-06-08 04:08:49 +08:00
|
|
|
bridge.addListener(
|
|
|
|
|
'isBackendStorageAPISupported',
|
2019-08-14 08:58:03 +08:00
|
|
|
this.onBridgeStorageSupported,
|
2019-06-08 04:08:49 +08:00
|
|
|
);
|
2019-07-14 01:05:04 +08:00
|
|
|
bridge.addListener(
|
|
|
|
|
'isNativeStyleEditorSupported',
|
2019-08-14 08:58:03 +08:00
|
|
|
this.onBridgeNativeStyleEditorSupported,
|
2019-07-14 01:05:04 +08:00
|
|
|
);
|
2019-09-26 23:41:46 +08:00
|
|
|
bridge.addListener(
|
|
|
|
|
'unsupportedRendererVersion',
|
|
|
|
|
this.onBridgeUnsupportedRendererVersion,
|
|
|
|
|
);
|
2019-03-12 02:26:30 +08:00
|
|
|
|
2019-05-22 21:05:25 +08:00
|
|
|
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
|
2019-03-13 06:50:29 +08:00
|
|
|
}
|
|
|
|
|
|
2019-05-04 00:30:48 +08:00
|
|
|
// This is only used in tests to avoid memory leaks.
|
2019-05-05 00:36:13 +08:00
|
|
|
assertExpectedRootMapSizes() {
|
|
|
|
|
if (this.roots.length === 0) {
|
|
|
|
|
// The only safe time to assert these maps are empty is when the store is empty.
|
|
|
|
|
this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');
|
|
|
|
|
this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These maps should always be the same size as the number of roots
|
|
|
|
|
this.assertMapSizeMatchesRootCount(
|
|
|
|
|
this._rootIDToCapabilities,
|
2019-08-14 08:58:03 +08:00
|
|
|
'_rootIDToCapabilities',
|
2019-05-01 16:44:07 +08:00
|
|
|
);
|
2019-05-05 00:36:13 +08:00
|
|
|
this.assertMapSizeMatchesRootCount(
|
|
|
|
|
this._rootIDToRendererID,
|
2019-08-14 08:58:03 +08:00
|
|
|
'_rootIDToRendererID',
|
2019-05-01 16:44:07 +08:00
|
|
|
);
|
2019-05-04 00:30:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This is only used in tests to avoid memory leaks.
|
2019-05-05 00:36:13 +08:00
|
|
|
assertMapSizeMatchesRootCount(map: Map<any, any>, mapName: string) {
|
|
|
|
|
const expectedSize = this.roots.length;
|
|
|
|
|
if (map.size !== expectedSize) {
|
2019-05-04 00:30:48 +08:00
|
|
|
throw new Error(
|
2019-05-05 00:36:13 +08:00
|
|
|
`Expected ${mapName} to contain ${expectedSize} items, but it contains ${
|
|
|
|
|
map.size
|
|
|
|
|
} items\n\n${inspect(map, {
|
2019-05-04 05:59:20 +08:00
|
|
|
depth: 20,
|
2019-08-14 08:58:03 +08:00
|
|
|
})}`,
|
2019-05-04 00:30:48 +08:00
|
|
|
);
|
|
|
|
|
}
|
2019-04-27 00:47:00 +08:00
|
|
|
}
|
|
|
|
|
|
2019-04-17 04:59:36 +08:00
|
|
|
get collapseNodesByDefault(): boolean {
|
|
|
|
|
return this._collapseNodesByDefault;
|
|
|
|
|
}
|
|
|
|
|
set collapseNodesByDefault(value: boolean): void {
|
|
|
|
|
this._collapseNodesByDefault = value;
|
|
|
|
|
|
2019-06-08 01:51:01 +08:00
|
|
|
localStorageSetItem(
|
2019-04-17 04:59:36 +08:00
|
|
|
LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY,
|
2019-08-14 08:58:03 +08:00
|
|
|
value ? 'true' : 'false',
|
2019-04-17 04:59:36 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.emit('collapseNodesByDefault');
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:43:40 +08:00
|
|
|
get componentFilters(): Array<ComponentFilter> {
|
|
|
|
|
return this._componentFilters;
|
2019-04-29 06:33:39 +08:00
|
|
|
}
|
2019-05-02 01:43:40 +08:00
|
|
|
set componentFilters(value: Array<ComponentFilter>): void {
|
2019-05-22 21:05:25 +08:00
|
|
|
if (this._profilerStore.isProfiling) {
|
2019-05-01 00:48:15 +08:00
|
|
|
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
|
|
|
|
|
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
|
|
|
|
throw Error('Cannot modify filter preferences while profiling');
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-11 06:36:29 +08:00
|
|
|
// Filter updates are expensive to apply (since they impact the entire tree).
|
|
|
|
|
// Let's determine if they've changed and avoid doing this work if they haven't.
|
|
|
|
|
const prevEnabledComponentFilters = this._componentFilters.filter(
|
2019-08-14 08:58:03 +08:00
|
|
|
filter => filter.isEnabled,
|
2019-06-11 06:36:29 +08:00
|
|
|
);
|
|
|
|
|
const nextEnabledComponentFilters = value.filter(
|
2019-08-14 08:58:03 +08:00
|
|
|
filter => filter.isEnabled,
|
2019-06-11 06:36:29 +08:00
|
|
|
);
|
|
|
|
|
let haveEnabledFiltersChanged =
|
|
|
|
|
prevEnabledComponentFilters.length !== nextEnabledComponentFilters.length;
|
|
|
|
|
if (!haveEnabledFiltersChanged) {
|
|
|
|
|
for (let i = 0; i < nextEnabledComponentFilters.length; i++) {
|
|
|
|
|
const prevFilter = prevEnabledComponentFilters[i];
|
|
|
|
|
const nextFilter = nextEnabledComponentFilters[i];
|
|
|
|
|
if (shallowDiffers(prevFilter, nextFilter)) {
|
|
|
|
|
haveEnabledFiltersChanged = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:43:40 +08:00
|
|
|
this._componentFilters = value;
|
2019-04-29 06:33:39 +08:00
|
|
|
|
2019-05-01 03:52:43 +08:00
|
|
|
// Update persisted filter preferences stored in localStorage.
|
2019-05-02 01:43:40 +08:00
|
|
|
saveComponentFilters(value);
|
2019-04-29 06:33:39 +08:00
|
|
|
|
2019-05-01 03:52:43 +08:00
|
|
|
// Notify the renderer that filter prefernces have changed.
|
2019-06-11 06:36:29 +08:00
|
|
|
// This is an expensive opreation; it unmounts and remounts the entire tree,
|
|
|
|
|
// so only do it if the set of enabled component filters has changed.
|
|
|
|
|
if (haveEnabledFiltersChanged) {
|
|
|
|
|
this._bridge.send('updateComponentFilters', value);
|
|
|
|
|
}
|
2019-04-29 06:33:39 +08:00
|
|
|
|
2019-05-02 01:43:40 +08:00
|
|
|
this.emit('componentFilters');
|
2019-04-29 06:33:39 +08:00
|
|
|
}
|
|
|
|
|
|
2020-12-23 00:09:29 +08:00
|
|
|
get errorCount(): number {
|
|
|
|
|
return this._cachedErrorCount;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-12 05:00:27 +08:00
|
|
|
get hasOwnerMetadata(): boolean {
|
|
|
|
|
return this._hasOwnerMetadata;
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
get nativeStyleEditorValidAttributes(): $ReadOnlyArray<string> | null {
|
|
|
|
|
return this._nativeStyleEditorValidAttributes;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-29 07:50:48 +08:00
|
|
|
get numElements(): number {
|
2019-04-10 22:31:34 +08:00
|
|
|
return this._weightAcrossRoots;
|
2019-01-29 07:50:48 +08:00
|
|
|
}
|
2019-01-28 08:04:17 +08:00
|
|
|
|
2019-05-22 21:05:25 +08:00
|
|
|
get profilerStore(): ProfilerStore {
|
|
|
|
|
return this._profilerStore;
|
2019-03-16 11:05:52 +08:00
|
|
|
}
|
|
|
|
|
|
2019-06-08 06:38:26 +08:00
|
|
|
get recordChangeDescriptions(): boolean {
|
|
|
|
|
return this._recordChangeDescriptions;
|
|
|
|
|
}
|
|
|
|
|
set recordChangeDescriptions(value: boolean): void {
|
|
|
|
|
this._recordChangeDescriptions = value;
|
|
|
|
|
|
|
|
|
|
localStorageSetItem(
|
|
|
|
|
LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
2019-08-14 08:58:03 +08:00
|
|
|
value ? 'true' : 'false',
|
2019-06-08 06:38:26 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.emit('recordChangeDescriptions');
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-09 04:37:52 +08:00
|
|
|
get revision(): number {
|
|
|
|
|
return this._revision;
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-22 21:05:25 +08:00
|
|
|
get rootIDToRendererID(): Map<number, number> {
|
|
|
|
|
return this._rootIDToRendererID;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-29 07:50:48 +08:00
|
|
|
get roots(): $ReadOnlyArray<number> {
|
|
|
|
|
return this._roots;
|
2019-01-23 03:04:37 +08:00
|
|
|
}
|
|
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
get supportsNativeInspection(): boolean {
|
|
|
|
|
return this._supportsNativeInspection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get supportsNativeStyleEditor(): boolean {
|
|
|
|
|
return this._isNativeStyleEditorSupported;
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-11 00:01:05 +08:00
|
|
|
get supportsProfiling(): boolean {
|
|
|
|
|
return this._supportsProfiling;
|
|
|
|
|
}
|
2019-03-28 00:41:12 +08:00
|
|
|
get supportsReloadAndProfile(): boolean {
|
2019-06-08 04:08:49 +08:00
|
|
|
// Does the DevTools shell support reloading and eagerly injecting the renderer interface?
|
|
|
|
|
// And if so, can the backend use the localStorage API?
|
|
|
|
|
// Both of these are required for the reload-and-profile feature to work.
|
|
|
|
|
return this._supportsReloadAndProfile && this._isBackendStorageAPISupported;
|
2019-03-28 00:41:12 +08:00
|
|
|
}
|
|
|
|
|
|
2019-10-04 02:07:18 +08:00
|
|
|
get supportsTraceUpdates(): boolean {
|
|
|
|
|
return this._supportsTraceUpdates;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 23:41:46 +08:00
|
|
|
get unsupportedRendererVersionDetected(): boolean {
|
|
|
|
|
return this._unsupportedRendererVersionDetected;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-23 00:09:29 +08:00
|
|
|
get warningCount(): number {
|
|
|
|
|
return this._cachedWarningCount;
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-09 09:23:54 +08:00
|
|
|
containsElement(id: number): boolean {
|
|
|
|
|
return this._idToElement.get(id) != null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-01 07:59:08 +08:00
|
|
|
getElementAtIndex(index: number): Element | null {
|
2019-01-28 08:04:17 +08:00
|
|
|
if (index < 0 || index >= this.numElements) {
|
2019-02-01 07:59:08 +08:00
|
|
|
console.warn(
|
2020-01-09 21:54:11 +08:00
|
|
|
`Invalid index ${index} specified; store contains ${this.numElements} items.`,
|
2019-02-01 01:42:07 +08:00
|
|
|
);
|
2019-02-01 07:59:08 +08:00
|
|
|
|
|
|
|
|
return null;
|
2019-01-28 08:04:17 +08:00
|
|
|
}
|
|
|
|
|
|
2020-06-16 07:59:44 +08:00
|
|
|
// Find which root this element is in...
|
2019-01-28 08:04:17 +08:00
|
|
|
let rootID;
|
|
|
|
|
let root;
|
|
|
|
|
let rootWeight = 0;
|
2019-01-29 07:50:48 +08:00
|
|
|
for (let i = 0; i < this._roots.length; i++) {
|
|
|
|
|
rootID = this._roots[i];
|
|
|
|
|
root = ((this._idToElement.get(rootID): any): Element);
|
2019-02-14 03:22:22 +08:00
|
|
|
if (root.children.length === 0) {
|
|
|
|
|
continue;
|
|
|
|
|
} else if (rootWeight + root.weight > index) {
|
2019-01-28 08:04:17 +08:00
|
|
|
break;
|
2019-01-29 07:50:48 +08:00
|
|
|
} else {
|
|
|
|
|
rootWeight += root.weight;
|
2019-01-28 08:04:17 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-01 06:31:12 +08:00
|
|
|
// Find the element in the tree using the weight of each node...
|
|
|
|
|
// Skip over the root itself, because roots aren't visible in the Elements tree.
|
2019-04-17 22:45:33 +08:00
|
|
|
let currentElement = ((root: any): Element);
|
|
|
|
|
let currentWeight = rootWeight - 1;
|
2019-01-28 08:04:17 +08:00
|
|
|
while (index !== currentWeight) {
|
2019-04-10 09:10:17 +08:00
|
|
|
const numChildren = currentElement.children.length;
|
|
|
|
|
for (let i = 0; i < numChildren; i++) {
|
2019-01-28 08:04:17 +08:00
|
|
|
const childID = currentElement.children[i];
|
2019-01-29 07:50:48 +08:00
|
|
|
const child = ((this._idToElement.get(childID): any): Element);
|
2019-04-10 09:10:17 +08:00
|
|
|
const childWeight = child.isCollapsed ? 1 : child.weight;
|
|
|
|
|
|
|
|
|
|
if (index <= currentWeight + childWeight) {
|
2019-01-28 08:04:17 +08:00
|
|
|
currentWeight++;
|
|
|
|
|
currentElement = child;
|
|
|
|
|
break;
|
|
|
|
|
} else {
|
2019-04-10 09:10:17 +08:00
|
|
|
currentWeight += childWeight;
|
2019-01-28 08:04:17 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-28 05:49:46 +08:00
|
|
|
return ((currentElement: any): Element) || null;
|
2019-01-28 08:04:17 +08:00
|
|
|
}
|
|
|
|
|
|
2019-02-09 03:44:07 +08:00
|
|
|
getElementIDAtIndex(index: number): number | null {
|
|
|
|
|
const element: Element | null = this.getElementAtIndex(index);
|
|
|
|
|
return element === null ? null : element.id;
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-03 05:57:32 +08:00
|
|
|
getElementByID(id: number): Element | null {
|
2019-01-28 08:04:17 +08:00
|
|
|
const element = this._idToElement.get(id);
|
|
|
|
|
if (element == null) {
|
2019-02-17 00:29:26 +08:00
|
|
|
console.warn(`No element found with id "${id}"`);
|
2019-02-03 05:57:32 +08:00
|
|
|
return null;
|
2019-01-28 08:04:17 +08:00
|
|
|
}
|
|
|
|
|
|
2019-02-03 05:57:32 +08:00
|
|
|
return element;
|
2019-01-28 08:04:17 +08:00
|
|
|
}
|
|
|
|
|
|
2020-12-23 00:09:29 +08:00
|
|
|
// Returns a tuple of [id, index]
|
|
|
|
|
getElementsWithErrorsAndWarnings(): Array<{|id: number, index: number|}> {
|
|
|
|
|
return this._cachedErrorAndWarningTuples;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getErrorAndWarningCountForElementID(
|
|
|
|
|
id: number,
|
|
|
|
|
): {|errorCount: number, warningCount: number|} {
|
|
|
|
|
return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0};
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-05 01:49:30 +08:00
|
|
|
getIndexOfElementID(id: number): number | null {
|
|
|
|
|
const element = this.getElementByID(id);
|
|
|
|
|
|
2019-02-28 05:49:46 +08:00
|
|
|
if (element === null || element.parentID === 0) {
|
2019-02-05 01:49:30 +08:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Walk up the tree to the root.
|
|
|
|
|
// Increment the index by one for each node we encounter,
|
|
|
|
|
// and by the weight of all nodes to the left of the current one.
|
|
|
|
|
// This should be a relatively fast way of determining the index of a node within the tree.
|
|
|
|
|
let previousID = id;
|
|
|
|
|
let currentID = element.parentID;
|
|
|
|
|
let index = 0;
|
|
|
|
|
while (true) {
|
|
|
|
|
const current = ((this._idToElement.get(currentID): any): Element);
|
|
|
|
|
|
2019-08-14 08:58:03 +08:00
|
|
|
const {children} = current;
|
2019-02-05 01:49:30 +08:00
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
|
const childID = children[i];
|
|
|
|
|
if (childID === previousID) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const child = ((this._idToElement.get(childID): any): Element);
|
2019-04-11 00:23:24 +08:00
|
|
|
index += child.isCollapsed ? 1 : child.weight;
|
2019-02-05 01:49:30 +08:00
|
|
|
}
|
|
|
|
|
|
2019-05-03 06:12:57 +08:00
|
|
|
if (current.parentID === 0) {
|
|
|
|
|
// We found the root; stop crawling.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
index++;
|
|
|
|
|
|
2019-02-05 01:49:30 +08:00
|
|
|
previousID = current.id;
|
|
|
|
|
currentID = current.parentID;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// At this point, the current ID is a root (from the previous loop).
|
|
|
|
|
// We also need to offset the index by previous root weights.
|
|
|
|
|
for (let i = 0; i < this._roots.length; i++) {
|
|
|
|
|
const rootID = this._roots[i];
|
|
|
|
|
if (rootID === currentID) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const root = ((this._idToElement.get(rootID): any): Element);
|
|
|
|
|
index += root.weight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return index;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-26 03:16:13 +08:00
|
|
|
getOwnersListForElement(ownerID: number): Array<Element> {
|
2019-04-25 06:19:13 +08:00
|
|
|
const list = [];
|
2020-04-02 03:35:52 +08:00
|
|
|
const element = this._idToElement.get(ownerID);
|
2019-04-26 03:16:13 +08:00
|
|
|
if (element != null) {
|
|
|
|
|
list.push({
|
|
|
|
|
...element,
|
|
|
|
|
depth: 0,
|
|
|
|
|
});
|
2019-04-25 06:19:13 +08:00
|
|
|
|
2019-04-26 03:16:13 +08:00
|
|
|
const unsortedIDs = this._ownersMap.get(ownerID);
|
|
|
|
|
if (unsortedIDs !== undefined) {
|
|
|
|
|
const depthMap: Map<number, number> = new Map([[ownerID, 0]]);
|
|
|
|
|
|
|
|
|
|
// Items in a set are ordered based on insertion.
|
|
|
|
|
// This does not correlate with their order in the tree.
|
|
|
|
|
// So first we need to order them.
|
|
|
|
|
// I wish we could avoid this sorting operation; we could sort at insertion time,
|
|
|
|
|
// but then we'd have to pay sorting costs even if the owners list was never used.
|
|
|
|
|
// Seems better to defer the cost, since the set of ids is probably pretty small.
|
|
|
|
|
const sortedIDs = Array.from(unsortedIDs).sort(
|
|
|
|
|
(idA, idB) =>
|
|
|
|
|
((this.getIndexOfElementID(idA): any): number) -
|
2019-08-14 08:58:03 +08:00
|
|
|
((this.getIndexOfElementID(idB): any): number),
|
2019-04-26 03:16:13 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Next we need to determine the appropriate depth for each element in the list.
|
|
|
|
|
// The depth in the list may not correspond to the depth in the tree,
|
|
|
|
|
// because the list has been filtered to remove intermediate components.
|
|
|
|
|
// Perhaps the easiest way to do this is to walk up the tree until we reach either:
|
|
|
|
|
// (1) another node that's already in the tree, or (2) the root (owner)
|
|
|
|
|
// at which point, our depth is just the depth of that node plus one.
|
|
|
|
|
sortedIDs.forEach(id => {
|
2019-08-14 12:59:07 +08:00
|
|
|
const innerElement = this._idToElement.get(id);
|
|
|
|
|
if (innerElement != null) {
|
|
|
|
|
let parentID = innerElement.parentID;
|
2019-04-26 03:16:13 +08:00
|
|
|
|
|
|
|
|
let depth = 0;
|
|
|
|
|
while (parentID > 0) {
|
|
|
|
|
if (parentID === ownerID || unsortedIDs.has(parentID)) {
|
|
|
|
|
depth = depthMap.get(parentID) + 1;
|
|
|
|
|
depthMap.set(id, depth);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const parent = this._idToElement.get(parentID);
|
|
|
|
|
if (parent == null) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
parentID = parent.parentID;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (depth === 0) {
|
|
|
|
|
throw Error('Invalid owners list');
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-14 12:59:07 +08:00
|
|
|
list.push({...innerElement, depth});
|
2019-04-26 03:16:13 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-04-25 06:19:13 +08:00
|
|
|
|
|
|
|
|
return list;
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-05 01:49:30 +08:00
|
|
|
getRendererIDForElement(id: number): number | null {
|
|
|
|
|
let current = this._idToElement.get(id);
|
|
|
|
|
while (current != null) {
|
|
|
|
|
if (current.parentID === 0) {
|
|
|
|
|
const rendererID = this._rootIDToRendererID.get(current.id);
|
2019-04-28 18:44:36 +08:00
|
|
|
return rendererID == null ? null : rendererID;
|
2019-02-05 01:49:30 +08:00
|
|
|
} else {
|
|
|
|
|
current = this._idToElement.get(current.parentID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 06:50:29 +08:00
|
|
|
getRootIDForElement(id: number): number | null {
|
|
|
|
|
let current = this._idToElement.get(id);
|
|
|
|
|
while (current != null) {
|
|
|
|
|
if (current.parentID === 0) {
|
|
|
|
|
return current.id;
|
|
|
|
|
} else {
|
|
|
|
|
current = this._idToElement.get(current.parentID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-20 23:20:03 +08:00
|
|
|
isInsideCollapsedSubTree(id: number): boolean {
|
|
|
|
|
let current = this._idToElement.get(id);
|
|
|
|
|
while (current != null) {
|
|
|
|
|
if (current.parentID === 0) {
|
|
|
|
|
return false;
|
|
|
|
|
} else {
|
|
|
|
|
current = this._idToElement.get(current.parentID);
|
|
|
|
|
if (current != null && current.isCollapsed) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-17 22:45:33 +08:00
|
|
|
// TODO Maybe split this into two methods: expand() and collapse()
|
2019-04-10 09:10:17 +08:00
|
|
|
toggleIsCollapsed(id: number, isCollapsed: boolean): void {
|
2019-04-22 03:20:23 +08:00
|
|
|
let didMutate = false;
|
|
|
|
|
|
2019-04-10 09:10:17 +08:00
|
|
|
const element = this.getElementByID(id);
|
|
|
|
|
if (element !== null) {
|
2019-04-17 22:45:33 +08:00
|
|
|
if (isCollapsed) {
|
|
|
|
|
if (element.type === ElementTypeRoot) {
|
|
|
|
|
throw Error('Root nodes cannot be collapsed');
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-22 03:20:23 +08:00
|
|
|
if (!element.isCollapsed) {
|
|
|
|
|
didMutate = true;
|
|
|
|
|
element.isCollapsed = true;
|
2019-04-10 09:10:17 +08:00
|
|
|
|
2019-04-22 03:20:23 +08:00
|
|
|
const weightDelta = 1 - element.weight;
|
2019-04-10 09:10:17 +08:00
|
|
|
|
2019-04-22 03:20:23 +08:00
|
|
|
let parentElement = ((this._idToElement.get(
|
2019-08-14 08:58:03 +08:00
|
|
|
element.parentID,
|
2019-04-22 03:20:23 +08:00
|
|
|
): any): Element);
|
|
|
|
|
while (parentElement != null) {
|
|
|
|
|
// We don't need to break on a collapsed parent in the same way as the expand case below.
|
|
|
|
|
// That's because collapsing a node doesn't "bubble" and affect its parents.
|
|
|
|
|
parentElement.weight += weightDelta;
|
|
|
|
|
parentElement = this._idToElement.get(parentElement.parentID);
|
|
|
|
|
}
|
2019-04-17 22:45:33 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let currentElement = element;
|
|
|
|
|
while (currentElement != null) {
|
|
|
|
|
const oldWeight = currentElement.isCollapsed
|
|
|
|
|
? 1
|
|
|
|
|
: currentElement.weight;
|
|
|
|
|
|
2019-04-22 03:20:23 +08:00
|
|
|
if (currentElement.isCollapsed) {
|
|
|
|
|
didMutate = true;
|
|
|
|
|
currentElement.isCollapsed = false;
|
|
|
|
|
|
|
|
|
|
const newWeight = currentElement.isCollapsed
|
|
|
|
|
? 1
|
|
|
|
|
: currentElement.weight;
|
|
|
|
|
const weightDelta = newWeight - oldWeight;
|
|
|
|
|
|
|
|
|
|
let parentElement = ((this._idToElement.get(
|
2019-08-14 08:58:03 +08:00
|
|
|
currentElement.parentID,
|
2019-04-22 03:20:23 +08:00
|
|
|
): any): Element);
|
|
|
|
|
while (parentElement != null) {
|
|
|
|
|
parentElement.weight += weightDelta;
|
|
|
|
|
if (parentElement.isCollapsed) {
|
|
|
|
|
// It's important to break on a collapsed parent when expanding nodes.
|
|
|
|
|
// That's because expanding a node "bubbles" up and expands all parents as well.
|
|
|
|
|
// Breaking in this case prevents us from over-incrementing the expanded weights.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
parentElement = this._idToElement.get(parentElement.parentID);
|
2019-04-17 22:45:33 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentElement =
|
|
|
|
|
currentElement.parentID !== 0
|
|
|
|
|
? this.getElementByID(currentElement.parentID)
|
|
|
|
|
: null;
|
|
|
|
|
}
|
2019-04-10 09:10:17 +08:00
|
|
|
}
|
|
|
|
|
|
2019-04-22 03:20:23 +08:00
|
|
|
// Only re-calculate weights and emit an "update" event if the store was mutated.
|
|
|
|
|
if (didMutate) {
|
|
|
|
|
let weightAcrossRoots = 0;
|
|
|
|
|
this._roots.forEach(rootID => {
|
2019-08-14 08:58:03 +08:00
|
|
|
const {weight} = ((this.getElementByID(rootID): any): Element);
|
2019-04-22 03:20:23 +08:00
|
|
|
weightAcrossRoots += weight;
|
|
|
|
|
});
|
|
|
|
|
this._weightAcrossRoots = weightAcrossRoots;
|
|
|
|
|
|
|
|
|
|
// The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.
|
|
|
|
|
// In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).
|
|
|
|
|
// Updating the selected search index later may require auto-expanding a collapsed subtree though.
|
2019-04-24 23:29:23 +08:00
|
|
|
this.emit('mutated', [[], new Map()]);
|
2019-04-22 03:20:23 +08:00
|
|
|
}
|
2019-04-10 09:10:17 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-19 08:09:57 +08:00
|
|
|
_adjustParentTreeWeight = (
|
|
|
|
|
parentElement: Element | null,
|
2019-08-14 08:58:03 +08:00
|
|
|
weightDelta: number,
|
2019-04-19 08:09:57 +08:00
|
|
|
) => {
|
|
|
|
|
let isInsideCollapsedSubTree = false;
|
|
|
|
|
|
|
|
|
|
while (parentElement != null) {
|
|
|
|
|
parentElement.weight += weightDelta;
|
|
|
|
|
|
|
|
|
|
// Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.
|
|
|
|
|
// Their weight will bubble up when the parent is expanded.
|
|
|
|
|
if (parentElement.isCollapsed) {
|
|
|
|
|
isInsideCollapsedSubTree = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parentElement = ((this._idToElement.get(
|
2019-08-14 08:58:03 +08:00
|
|
|
parentElement.parentID,
|
2019-04-19 08:09:57 +08:00
|
|
|
): any): Element);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Additions and deletions within a collapsed subtree should not affect the overall number of elements.
|
|
|
|
|
if (!isInsideCollapsedSubTree) {
|
|
|
|
|
this._weightAcrossRoots += weightDelta;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
onBridgeNativeStyleEditorSupported = ({
|
|
|
|
|
isSupported,
|
|
|
|
|
validAttributes,
|
|
|
|
|
}: {|
|
|
|
|
|
isSupported: boolean,
|
2019-07-21 05:08:23 +08:00
|
|
|
validAttributes: ?$ReadOnlyArray<string>,
|
2019-07-14 01:05:04 +08:00
|
|
|
|}) => {
|
|
|
|
|
this._isNativeStyleEditorSupported = isSupported;
|
|
|
|
|
this._nativeStyleEditorValidAttributes = validAttributes || null;
|
|
|
|
|
|
|
|
|
|
this.emit('supportsNativeStyleEditor');
|
|
|
|
|
};
|
2019-01-30 05:17:37 +08:00
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
onBridgeOperations = (operations: Array<number>) => {
|
2019-04-05 07:03:17 +08:00
|
|
|
if (__DEBUG__) {
|
2019-04-18 00:39:54 +08:00
|
|
|
console.groupCollapsed('onBridgeOperations');
|
2019-04-19 00:38:41 +08:00
|
|
|
debug('onBridgeOperations', operations.join(','));
|
2019-04-05 07:03:17 +08:00
|
|
|
}
|
2019-01-29 07:50:48 +08:00
|
|
|
|
|
|
|
|
let haveRootsChanged = false;
|
2020-12-23 00:09:29 +08:00
|
|
|
let haveErrorsOrWarningsChanged = false;
|
2019-01-29 07:50:48 +08:00
|
|
|
|
2019-05-22 21:05:25 +08:00
|
|
|
// The first two values are always rendererID and rootID
|
2019-02-05 01:49:30 +08:00
|
|
|
const rendererID = operations[0];
|
2019-03-12 02:26:30 +08:00
|
|
|
|
2019-04-19 20:17:17 +08:00
|
|
|
const addedElementIDs: Array<number> = [];
|
2019-04-24 23:29:23 +08:00
|
|
|
// This is a mapping of removed ID -> parent ID:
|
|
|
|
|
const removedElementIDs: Map<number, number> = new Map();
|
|
|
|
|
// We'll use the parent ID to adjust selection if it gets deleted.
|
2019-02-07 00:31:02 +08:00
|
|
|
|
2019-03-13 06:50:29 +08:00
|
|
|
let i = 2;
|
2019-04-26 00:11:24 +08:00
|
|
|
|
|
|
|
|
// Reassemble the string table.
|
2019-04-26 00:38:55 +08:00
|
|
|
const stringTable = [
|
2019-04-26 00:11:24 +08:00
|
|
|
null, // ID = 0 corresponds to the null string.
|
|
|
|
|
];
|
|
|
|
|
const stringTableSize = operations[i++];
|
|
|
|
|
const stringTableEnd = i + stringTableSize;
|
|
|
|
|
while (i < stringTableEnd) {
|
2019-04-26 00:38:55 +08:00
|
|
|
const nextLength = operations[i++];
|
|
|
|
|
const nextString = utfDecodeString(
|
2019-08-14 08:58:03 +08:00
|
|
|
(operations.slice(i, i + nextLength): any),
|
2019-04-26 00:11:24 +08:00
|
|
|
);
|
|
|
|
|
stringTable.push(nextString);
|
|
|
|
|
i += nextLength;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-29 07:50:48 +08:00
|
|
|
while (i < operations.length) {
|
|
|
|
|
const operation = operations[i];
|
|
|
|
|
switch (operation) {
|
2019-04-19 08:09:57 +08:00
|
|
|
case TREE_OPERATION_ADD: {
|
|
|
|
|
const id = ((operations[i + 1]: any): number);
|
|
|
|
|
const type = ((operations[i + 2]: any): ElementType);
|
2019-01-29 07:50:48 +08:00
|
|
|
|
2019-08-14 12:59:07 +08:00
|
|
|
i += 3;
|
2019-01-29 07:50:48 +08:00
|
|
|
|
2019-04-06 01:41:13 +08:00
|
|
|
if (this._idToElement.has(id)) {
|
2019-04-19 06:40:50 +08:00
|
|
|
throw Error(
|
2019-08-14 08:58:03 +08:00
|
|
|
`Cannot add node ${id} because a node with that id is already in the Store.`,
|
2019-04-06 01:41:13 +08:00
|
|
|
);
|
2019-04-06 01:25:22 +08:00
|
|
|
}
|
|
|
|
|
|
2019-04-19 08:09:57 +08:00
|
|
|
let ownerID: number = 0;
|
|
|
|
|
let parentID: number = ((null: any): number);
|
2019-03-01 00:52:46 +08:00
|
|
|
if (type === ElementTypeRoot) {
|
2019-04-05 07:03:17 +08:00
|
|
|
if (__DEBUG__) {
|
2019-04-19 06:40:50 +08:00
|
|
|
debug('Add', `new root node ${id}`);
|
2019-04-05 07:03:17 +08:00
|
|
|
}
|
2019-01-29 07:50:48 +08:00
|
|
|
|
2019-04-06 01:18:39 +08:00
|
|
|
const supportsProfiling = operations[i] > 0;
|
|
|
|
|
i++;
|
|
|
|
|
|
2019-04-12 05:00:27 +08:00
|
|
|
const hasOwnerMetadata = operations[i] > 0;
|
|
|
|
|
i++;
|
|
|
|
|
|
2019-04-06 01:18:39 +08:00
|
|
|
this._roots = this._roots.concat(id);
|
|
|
|
|
this._rootIDToRendererID.set(id, rendererID);
|
2019-04-12 05:00:27 +08:00
|
|
|
this._rootIDToCapabilities.set(id, {
|
|
|
|
|
hasOwnerMetadata,
|
|
|
|
|
supportsProfiling,
|
|
|
|
|
});
|
2019-04-06 01:18:39 +08:00
|
|
|
|
|
|
|
|
this._idToElement.set(id, {
|
|
|
|
|
children: [],
|
|
|
|
|
depth: -1,
|
|
|
|
|
displayName: null,
|
2019-06-01 23:24:24 +08:00
|
|
|
hocDisplayNames: null,
|
2019-04-06 01:18:39 +08:00
|
|
|
id,
|
2019-04-17 05:10:06 +08:00
|
|
|
isCollapsed: false, // Never collapse roots; it would hide the entire tree.
|
2019-04-06 01:18:39 +08:00
|
|
|
key: null,
|
|
|
|
|
ownerID: 0,
|
|
|
|
|
parentID: 0,
|
|
|
|
|
type,
|
|
|
|
|
weight: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
haveRootsChanged = true;
|
2019-01-29 07:50:48 +08:00
|
|
|
} else {
|
2019-03-01 00:52:46 +08:00
|
|
|
parentID = ((operations[i]: any): number);
|
|
|
|
|
i++;
|
|
|
|
|
|
2019-02-07 22:34:24 +08:00
|
|
|
ownerID = ((operations[i]: any): number);
|
|
|
|
|
i++;
|
|
|
|
|
|
2019-04-26 00:11:24 +08:00
|
|
|
const displayNameStringID = operations[i];
|
|
|
|
|
const displayName = stringTable[displayNameStringID];
|
2019-01-29 07:50:48 +08:00
|
|
|
i++;
|
2019-04-26 00:11:24 +08:00
|
|
|
|
|
|
|
|
const keyStringID = operations[i];
|
|
|
|
|
const key = stringTable[keyStringID];
|
2019-01-29 07:50:48 +08:00
|
|
|
i++;
|
|
|
|
|
|
2019-04-05 07:03:17 +08:00
|
|
|
if (__DEBUG__) {
|
|
|
|
|
debug(
|
|
|
|
|
'Add',
|
2019-08-14 08:58:03 +08:00
|
|
|
`node ${id} (${displayName || 'null'}) as child of ${parentID}`,
|
2019-04-19 06:40:50 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this._idToElement.has(parentID)) {
|
|
|
|
|
throw Error(
|
2019-08-14 08:58:03 +08:00
|
|
|
`Cannot add child ${id} to parent ${parentID} because parent node was not found in the Store.`,
|
2019-04-05 07:03:17 +08:00
|
|
|
);
|
|
|
|
|
}
|
2019-01-29 07:50:48 +08:00
|
|
|
|
2019-04-19 08:09:57 +08:00
|
|
|
const parentElement = ((this._idToElement.get(
|
2019-08-14 08:58:03 +08:00
|
|
|
parentID,
|
2019-04-19 08:09:57 +08:00
|
|
|
): any): Element);
|
2019-04-25 21:39:28 +08:00
|
|
|
parentElement.children.push(id);
|
2019-04-06 01:18:39 +08:00
|
|
|
|
2019-06-01 23:24:24 +08:00
|
|
|
const [
|
|
|
|
|
displayNameWithoutHOCs,
|
|
|
|
|
hocDisplayNames,
|
|
|
|
|
] = separateDisplayNameAndHOCs(displayName, type);
|
|
|
|
|
|
2019-04-06 01:18:39 +08:00
|
|
|
const element: Element = {
|
|
|
|
|
children: [],
|
|
|
|
|
depth: parentElement.depth + 1,
|
2019-06-01 23:24:24 +08:00
|
|
|
displayName: displayNameWithoutHOCs,
|
|
|
|
|
hocDisplayNames,
|
2019-04-06 01:18:39 +08:00
|
|
|
id,
|
2019-04-17 04:59:36 +08:00
|
|
|
isCollapsed: this._collapseNodesByDefault,
|
2019-04-06 01:18:39 +08:00
|
|
|
key,
|
|
|
|
|
ownerID,
|
|
|
|
|
parentID: parentElement.id,
|
|
|
|
|
type,
|
|
|
|
|
weight: 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this._idToElement.set(id, element);
|
2019-04-19 08:13:25 +08:00
|
|
|
addedElementIDs.push(id);
|
2019-04-19 08:09:57 +08:00
|
|
|
this._adjustParentTreeWeight(parentElement, 1);
|
2019-04-26 03:16:13 +08:00
|
|
|
|
|
|
|
|
if (ownerID > 0) {
|
|
|
|
|
let set = this._ownersMap.get(ownerID);
|
|
|
|
|
if (set === undefined) {
|
|
|
|
|
set = new Set();
|
|
|
|
|
this._ownersMap.set(ownerID, set);
|
|
|
|
|
}
|
|
|
|
|
set.add(id);
|
|
|
|
|
}
|
2019-01-29 07:50:48 +08:00
|
|
|
}
|
|
|
|
|
break;
|
2019-04-19 08:09:57 +08:00
|
|
|
}
|
2019-04-06 04:48:50 +08:00
|
|
|
case TREE_OPERATION_REMOVE: {
|
2019-04-19 08:09:57 +08:00
|
|
|
const removeLength = ((operations[i + 1]: any): number);
|
2019-08-14 12:59:07 +08:00
|
|
|
i += 2;
|
2019-01-28 08:04:17 +08:00
|
|
|
|
2019-04-19 08:09:57 +08:00
|
|
|
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
|
|
|
|
|
const id = ((operations[i]: any): number);
|
2019-01-23 03:04:37 +08:00
|
|
|
|
2019-04-19 08:09:57 +08:00
|
|
|
if (!this._idToElement.has(id)) {
|
|
|
|
|
throw Error(
|
2019-08-14 08:58:03 +08:00
|
|
|
`Cannot remove node ${id} because no matching node was found in the Store.`,
|
2019-04-19 08:09:57 +08:00
|
|
|
);
|
2019-04-05 07:03:17 +08:00
|
|
|
}
|
2019-02-14 03:22:22 +08:00
|
|
|
|
2019-08-14 12:59:07 +08:00
|
|
|
i += 1;
|
2019-03-11 00:01:05 +08:00
|
|
|
|
2019-04-19 08:09:57 +08:00
|
|
|
const element = ((this._idToElement.get(id): any): Element);
|
2019-08-14 08:58:03 +08:00
|
|
|
const {children, ownerID, parentID, weight} = element;
|
2019-04-26 03:16:13 +08:00
|
|
|
if (children.length > 0) {
|
2019-04-19 20:09:01 +08:00
|
|
|
throw new Error(`Node ${id} was removed before its children.`);
|
2019-04-05 07:03:17 +08:00
|
|
|
}
|
2019-04-19 08:09:57 +08:00
|
|
|
|
2019-05-04 05:59:20 +08:00
|
|
|
this._idToElement.delete(id);
|
|
|
|
|
|
2019-04-19 08:09:57 +08:00
|
|
|
let parentElement = null;
|
|
|
|
|
if (parentID === 0) {
|
|
|
|
|
if (__DEBUG__) {
|
2019-04-19 08:41:00 +08:00
|
|
|
debug('Remove', `node ${id} root`);
|
2019-04-19 08:09:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._roots = this._roots.filter(rootID => rootID !== id);
|
|
|
|
|
this._rootIDToRendererID.delete(id);
|
|
|
|
|
this._rootIDToCapabilities.delete(id);
|
|
|
|
|
|
|
|
|
|
haveRootsChanged = true;
|
|
|
|
|
} else {
|
|
|
|
|
if (__DEBUG__) {
|
2019-04-19 08:41:00 +08:00
|
|
|
debug('Remove', `node ${id} from parent ${parentID}`);
|
2019-04-19 08:09:57 +08:00
|
|
|
}
|
|
|
|
|
parentElement = ((this._idToElement.get(parentID): any): Element);
|
|
|
|
|
if (parentElement === undefined) {
|
|
|
|
|
throw Error(
|
2019-08-14 08:58:03 +08:00
|
|
|
`Cannot remove node ${id} from parent ${parentID} because no matching node was found in the Store.`,
|
2019-04-19 08:09:57 +08:00
|
|
|
);
|
|
|
|
|
}
|
2019-04-25 21:39:28 +08:00
|
|
|
const index = parentElement.children.indexOf(id);
|
|
|
|
|
parentElement.children.splice(index, 1);
|
2019-04-18 23:02:08 +08:00
|
|
|
}
|
2019-02-06 01:14:06 +08:00
|
|
|
|
2019-04-26 03:16:13 +08:00
|
|
|
this._adjustParentTreeWeight(parentElement, -weight);
|
2019-04-24 23:29:23 +08:00
|
|
|
removedElementIDs.set(id, parentID);
|
2019-04-26 03:16:13 +08:00
|
|
|
|
|
|
|
|
this._ownersMap.delete(id);
|
|
|
|
|
if (ownerID > 0) {
|
|
|
|
|
const set = this._ownersMap.get(ownerID);
|
|
|
|
|
if (set !== undefined) {
|
|
|
|
|
set.delete(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-23 00:09:29 +08:00
|
|
|
|
|
|
|
|
if (this._errorsAndWarnings.has(id)) {
|
|
|
|
|
this._errorsAndWarnings.delete(id);
|
|
|
|
|
haveErrorsOrWarningsChanged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case TREE_OPERATION_REMOVE_ROOT: {
|
|
|
|
|
i += 1;
|
|
|
|
|
|
|
|
|
|
const id = operations[1];
|
|
|
|
|
|
|
|
|
|
if (__DEBUG__) {
|
|
|
|
|
debug(`Remove root ${id}`);
|
2019-04-19 08:09:57 +08:00
|
|
|
}
|
2020-12-23 00:09:29 +08:00
|
|
|
|
|
|
|
|
const recursivelyDeleteElements = elementID => {
|
|
|
|
|
const element = this._idToElement.get(elementID);
|
|
|
|
|
this._idToElement.delete(elementID);
|
|
|
|
|
if (element) {
|
|
|
|
|
// Mostly for Flow's sake
|
|
|
|
|
for (let index = 0; index < element.children.length; index++) {
|
|
|
|
|
recursivelyDeleteElements(element.children[index]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const root = ((this._idToElement.get(id): any): Element);
|
|
|
|
|
recursivelyDeleteElements(id);
|
|
|
|
|
|
|
|
|
|
this._rootIDToCapabilities.delete(id);
|
|
|
|
|
this._rootIDToRendererID.delete(id);
|
|
|
|
|
this._roots = this._roots.filter(rootID => rootID !== id);
|
|
|
|
|
this._weightAcrossRoots -= root.weight;
|
2019-01-29 07:50:48 +08:00
|
|
|
break;
|
2019-04-06 04:48:50 +08:00
|
|
|
}
|
2019-04-19 20:09:01 +08:00
|
|
|
case TREE_OPERATION_REORDER_CHILDREN: {
|
2019-04-19 08:09:57 +08:00
|
|
|
const id = ((operations[i + 1]: any): number);
|
2019-01-29 07:50:48 +08:00
|
|
|
const numChildren = ((operations[i + 2]: any): number);
|
2019-08-14 12:59:07 +08:00
|
|
|
i += 3;
|
2019-04-19 06:40:50 +08:00
|
|
|
|
|
|
|
|
if (!this._idToElement.has(id)) {
|
|
|
|
|
throw Error(
|
2019-08-14 08:58:03 +08:00
|
|
|
`Cannot reorder children for node ${id} because no matching node was found in the Store.`,
|
2019-04-19 06:40:50 +08:00
|
|
|
);
|
2019-04-05 07:03:17 +08:00
|
|
|
}
|
2019-01-28 08:04:17 +08:00
|
|
|
|
2019-04-19 08:09:57 +08:00
|
|
|
const element = ((this._idToElement.get(id): any): Element);
|
2019-04-25 21:39:28 +08:00
|
|
|
const children = element.children;
|
|
|
|
|
if (children.length !== numChildren) {
|
2019-04-19 08:58:48 +08:00
|
|
|
throw Error(
|
2019-08-14 08:58:03 +08:00
|
|
|
`Children cannot be added or removed during a reorder operation.`,
|
2019-04-19 07:29:08 +08:00
|
|
|
);
|
|
|
|
|
}
|
2019-04-25 21:39:28 +08:00
|
|
|
|
|
|
|
|
for (let j = 0; j < numChildren; j++) {
|
|
|
|
|
const childID = operations[i + j];
|
|
|
|
|
children[j] = childID;
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
// This check is more expensive so it's gated by __DEV__.
|
|
|
|
|
const childElement = this._idToElement.get(childID);
|
|
|
|
|
if (childElement == null || childElement.parentID !== id) {
|
|
|
|
|
console.error(
|
2019-08-14 08:58:03 +08:00
|
|
|
`Children cannot be added or removed during a reorder operation.`,
|
2019-04-25 21:39:28 +08:00
|
|
|
);
|
|
|
|
|
}
|
2019-04-19 20:09:01 +08:00
|
|
|
}
|
|
|
|
|
}
|
2019-08-14 12:59:07 +08:00
|
|
|
i += numChildren;
|
2019-04-25 21:39:28 +08:00
|
|
|
|
|
|
|
|
if (__DEBUG__) {
|
|
|
|
|
debug('Re-order', `Node ${id} children ${children.join(',')}`);
|
|
|
|
|
}
|
2019-01-29 07:50:48 +08:00
|
|
|
break;
|
2019-04-19 08:09:57 +08:00
|
|
|
}
|
2019-03-16 11:05:52 +08:00
|
|
|
case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
|
|
|
|
|
// Base duration updates are only sent while profiling is in progress.
|
|
|
|
|
// We can ignore them at this point.
|
|
|
|
|
// The profiler UI uses them lazily in order to generate the tree.
|
2019-08-14 12:59:07 +08:00
|
|
|
i += 3;
|
2019-03-16 11:05:52 +08:00
|
|
|
break;
|
2020-12-23 00:09:29 +08:00
|
|
|
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
|
|
|
|
|
const id = operations[i + 1];
|
|
|
|
|
const errorCount = operations[i + 2];
|
|
|
|
|
const warningCount = operations[i + 3];
|
|
|
|
|
|
|
|
|
|
i += 4;
|
|
|
|
|
|
|
|
|
|
if (errorCount > 0 || warningCount > 0) {
|
|
|
|
|
this._errorsAndWarnings.set(id, {errorCount, warningCount});
|
|
|
|
|
} else if (this._errorsAndWarnings.has(id)) {
|
|
|
|
|
this._errorsAndWarnings.delete(id);
|
|
|
|
|
}
|
|
|
|
|
haveErrorsOrWarningsChanged = true;
|
|
|
|
|
break;
|
2019-01-29 07:50:48 +08:00
|
|
|
default:
|
|
|
|
|
throw Error(`Unsupported Bridge operation ${operation}`);
|
2019-01-28 08:04:17 +08:00
|
|
|
}
|
2019-01-29 07:50:48 +08:00
|
|
|
}
|
2019-01-28 08:04:17 +08:00
|
|
|
|
2019-02-09 04:37:52 +08:00
|
|
|
this._revision++;
|
|
|
|
|
|
2020-12-23 00:09:29 +08:00
|
|
|
if (haveErrorsOrWarningsChanged) {
|
|
|
|
|
let errorCount = 0;
|
|
|
|
|
let warningCount = 0;
|
|
|
|
|
|
|
|
|
|
this._errorsAndWarnings.forEach(entry => {
|
|
|
|
|
errorCount += entry.errorCount;
|
|
|
|
|
warningCount += entry.warningCount;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._cachedErrorCount = errorCount;
|
|
|
|
|
this._cachedWarningCount = warningCount;
|
|
|
|
|
|
|
|
|
|
const errorAndWarningTuples: Array<{|id: number, index: number|}> = [];
|
|
|
|
|
|
|
|
|
|
this._errorsAndWarnings.forEach((_, id) => {
|
|
|
|
|
const index = this.getIndexOfElementID(id);
|
|
|
|
|
if (index !== null) {
|
|
|
|
|
let low = 0;
|
|
|
|
|
let high = errorAndWarningTuples.length;
|
|
|
|
|
while (low < high) {
|
|
|
|
|
const mid = (low + high) >> 1;
|
|
|
|
|
if (errorAndWarningTuples[mid].index > index) {
|
|
|
|
|
high = mid;
|
|
|
|
|
} else {
|
|
|
|
|
low = mid + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errorAndWarningTuples.splice(low, 0, {id, index});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._cachedErrorAndWarningTuples = errorAndWarningTuples;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-29 07:50:48 +08:00
|
|
|
if (haveRootsChanged) {
|
2019-05-24 00:09:35 +08:00
|
|
|
const prevSupportsProfiling = this._supportsProfiling;
|
|
|
|
|
|
2019-04-12 05:00:27 +08:00
|
|
|
this._hasOwnerMetadata = false;
|
2019-05-24 00:09:35 +08:00
|
|
|
this._supportsProfiling = false;
|
2019-04-12 05:00:27 +08:00
|
|
|
this._rootIDToCapabilities.forEach(
|
2019-08-14 08:58:03 +08:00
|
|
|
({hasOwnerMetadata, supportsProfiling}) => {
|
2019-04-12 05:00:27 +08:00
|
|
|
if (hasOwnerMetadata) {
|
|
|
|
|
this._hasOwnerMetadata = true;
|
|
|
|
|
}
|
2019-05-24 00:09:35 +08:00
|
|
|
if (supportsProfiling) {
|
2019-04-12 05:00:27 +08:00
|
|
|
this._supportsProfiling = true;
|
|
|
|
|
}
|
2019-08-14 08:58:03 +08:00
|
|
|
},
|
2019-04-12 05:00:27 +08:00
|
|
|
);
|
2019-03-11 00:01:05 +08:00
|
|
|
|
2019-01-23 03:04:37 +08:00
|
|
|
this.emit('roots');
|
2019-05-24 00:09:35 +08:00
|
|
|
|
|
|
|
|
if (this._supportsProfiling !== prevSupportsProfiling) {
|
|
|
|
|
this.emit('supportsProfiling');
|
|
|
|
|
}
|
2019-01-23 03:04:37 +08:00
|
|
|
}
|
2019-01-28 08:04:17 +08:00
|
|
|
|
2019-04-18 00:39:54 +08:00
|
|
|
if (__DEBUG__) {
|
2019-04-25 06:19:13 +08:00
|
|
|
console.log(printStore(this, true));
|
2019-04-18 00:39:54 +08:00
|
|
|
console.groupEnd();
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-24 23:29:23 +08:00
|
|
|
this.emit('mutated', [addedElementIDs, removedElementIDs]);
|
2019-01-29 07:50:48 +08:00
|
|
|
};
|
2019-01-23 03:04:37 +08:00
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
// Certain backends save filters on a per-domain basis.
|
|
|
|
|
// In order to prevent filter preferences and applied filters from being out of sync,
|
|
|
|
|
// this message enables the backend to override the frontend's current ("saved") filters.
|
|
|
|
|
// This action should also override the saved filters too,
|
|
|
|
|
// else reloading the frontend without reloading the backend would leave things out of sync.
|
|
|
|
|
onBridgeOverrideComponentFilters = (
|
2019-08-14 08:58:03 +08:00
|
|
|
componentFilters: Array<ComponentFilter>,
|
2019-07-14 01:05:04 +08:00
|
|
|
) => {
|
|
|
|
|
this._componentFilters = componentFilters;
|
|
|
|
|
|
|
|
|
|
saveComponentFilters(componentFilters);
|
|
|
|
|
};
|
|
|
|
|
|
2019-02-14 03:22:22 +08:00
|
|
|
onBridgeShutdown = () => {
|
2019-04-05 07:03:17 +08:00
|
|
|
if (__DEBUG__) {
|
|
|
|
|
debug('onBridgeShutdown', 'unsubscribing from Bridge');
|
|
|
|
|
}
|
2019-02-14 03:22:22 +08:00
|
|
|
|
2019-02-15 05:32:30 +08:00
|
|
|
this._bridge.removeListener('operations', this.onBridgeOperations);
|
|
|
|
|
this._bridge.removeListener('shutdown', this.onBridgeShutdown);
|
2019-06-08 04:08:49 +08:00
|
|
|
this._bridge.removeListener(
|
|
|
|
|
'isBackendStorageAPISupported',
|
2019-08-14 08:58:03 +08:00
|
|
|
this.onBridgeStorageSupported,
|
2019-06-08 04:08:49 +08:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onBridgeStorageSupported = (isBackendStorageAPISupported: boolean) => {
|
|
|
|
|
this._isBackendStorageAPISupported = isBackendStorageAPISupported;
|
|
|
|
|
|
|
|
|
|
this.emit('supportsReloadAndProfile');
|
2019-02-14 03:22:22 +08:00
|
|
|
};
|
2019-09-26 23:41:46 +08:00
|
|
|
|
|
|
|
|
onBridgeUnsupportedRendererVersion = () => {
|
|
|
|
|
this._unsupportedRendererVersionDetected = true;
|
|
|
|
|
|
|
|
|
|
this.emit('unsupportedRendererVersionDetected');
|
|
|
|
|
};
|
2019-01-23 03:04:37 +08:00
|
|
|
}
|