mirror of
https://github.com/facebook/react.git
synced 2026-02-24 12:43:00 +00:00
164 lines
5.0 KiB
JavaScript
164 lines
5.0 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
import LRU from 'lru-cache';
|
|
import {
|
|
convertInspectedElementBackendToFrontend,
|
|
hydrateHelper,
|
|
inspectElement as inspectElementAPI,
|
|
} from 'react-devtools-shared/src/backendAPI';
|
|
import {fillInPath} from 'react-devtools-shared/src/hydration';
|
|
|
|
import type {LRUCache} from 'react-devtools-shared/src/types';
|
|
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
|
import type {
|
|
InspectElementError,
|
|
InspectElementFullData,
|
|
InspectElementHydratedPath,
|
|
} from 'react-devtools-shared/src/backend/types';
|
|
import type {
|
|
Element,
|
|
InspectedElement as InspectedElementFrontend,
|
|
InspectedElementResponseType,
|
|
} from 'react-devtools-shared/src/devtools/views/Components/types';
|
|
|
|
// Maps element ID to inspected data.
|
|
// We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works.
|
|
// When the frontend polls the backend for an update on the element that's currently inspected,
|
|
// the backend will send a "no-change" message if the element hasn't updated (rendered) since the last time it was asked.
|
|
// In thid case, the frontend cache should reuse the previous (cached) value.
|
|
// Using a WeakMap keyed on Element generally works well for this, since Elements are mutable and stable in the Store.
|
|
// This doens't work properly though when component filters are changed,
|
|
// because this will cause the Store to dump all roots and re-initialize the tree (recreating the Element objects).
|
|
// So instead we key on Element ID (which is stable in this case) and use an LRU for eviction.
|
|
const inspectedElementCache: LRUCache<
|
|
number,
|
|
InspectedElementFrontend,
|
|
> = new LRU({
|
|
max: 25,
|
|
});
|
|
|
|
type Path = Array<string | number>;
|
|
|
|
type InspectElementReturnType = [
|
|
InspectedElementFrontend,
|
|
InspectedElementResponseType,
|
|
];
|
|
|
|
export function inspectElement({
|
|
bridge,
|
|
element,
|
|
path,
|
|
rendererID,
|
|
}: {|
|
|
bridge: FrontendBridge,
|
|
element: Element,
|
|
path: Path | null,
|
|
rendererID: number,
|
|
|}): Promise<InspectElementReturnType> {
|
|
const {id} = element;
|
|
|
|
// This could indicate that the DevTools UI has been closed and reopened.
|
|
// The in-memory cache will be clear but the backend still thinks we have cached data.
|
|
// In this case, we need to tell it to resend the full data.
|
|
const forceFullData = !inspectedElementCache.has(id);
|
|
|
|
return inspectElementAPI({
|
|
bridge,
|
|
forceFullData,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
}).then((data: any) => {
|
|
const {type} = data;
|
|
|
|
let inspectedElement;
|
|
switch (type) {
|
|
case 'error':
|
|
const {message, stack} = ((data: any): InspectElementError);
|
|
|
|
// The backend's stack (where the error originated) is more meaningful than this stack.
|
|
const error = new Error(message);
|
|
error.stack = stack;
|
|
|
|
throw error;
|
|
|
|
case 'no-change':
|
|
// This is a no-op for the purposes of our cache.
|
|
inspectedElement = inspectedElementCache.get(id);
|
|
if (inspectedElement != null) {
|
|
return [inspectedElement, type];
|
|
}
|
|
|
|
// We should only encounter this case in the event of a bug.
|
|
throw Error(`Cached data for element "${id}" not found`);
|
|
|
|
case 'not-found':
|
|
// This is effectively a no-op.
|
|
// If the Element is still in the Store, we can eagerly remove it from the Map.
|
|
inspectedElementCache.remove(id);
|
|
|
|
throw Error(`Element "${id}" not found`);
|
|
|
|
case 'full-data':
|
|
const fullData = ((data: any): InspectElementFullData);
|
|
|
|
// New data has come in.
|
|
// We should replace the data in our local mutable copy.
|
|
inspectedElement = convertInspectedElementBackendToFrontend(
|
|
fullData.value,
|
|
);
|
|
|
|
inspectedElementCache.set(id, inspectedElement);
|
|
|
|
return [inspectedElement, type];
|
|
|
|
case 'hydrated-path':
|
|
const hydratedPathData = ((data: any): InspectElementHydratedPath);
|
|
const {value} = hydratedPathData;
|
|
|
|
// A path has been hydrated.
|
|
// Merge it with the latest copy we have locally and resolve with the merged value.
|
|
inspectedElement = inspectedElementCache.get(id) || null;
|
|
if (inspectedElement !== null) {
|
|
// Clone element
|
|
inspectedElement = {...inspectedElement};
|
|
|
|
// Merge hydrated data
|
|
fillInPath(
|
|
inspectedElement,
|
|
value,
|
|
((path: any): Path),
|
|
hydrateHelper(value, ((path: any): Path)),
|
|
);
|
|
|
|
inspectedElementCache.set(id, inspectedElement);
|
|
|
|
return [inspectedElement, type];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Should never happen.
|
|
if (__DEV__) {
|
|
console.error(
|
|
`Unexpected inspected element response data: "${type}"`,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
|
|
throw Error(`Unable to inspect element with id "${id}"`);
|
|
});
|
|
}
|
|
|
|
export function clearCacheForTests(): void {
|
|
inspectedElementCache.reset();
|
|
}
|