/** * Copyright (c) Meta Platforms, Inc. and 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/frontend/types'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type { InspectElementError, InspectElementFullData, InspectElementHydratedPath, } from 'react-devtools-shared/src/backend/types'; import UserError from 'react-devtools-shared/src/errors/UserError'; import UnknownHookError from 'react-devtools-shared/src/errors/UnknownHookError'; import type { Element, InspectedElement as InspectedElementFrontend, InspectedElementResponseType, InspectedElementPath, } from 'react-devtools-shared/src/frontend/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 this 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 = new LRU({ max: 25, }); type InspectElementReturnType = [ InspectedElementFrontend, InspectedElementResponseType, ]; export function inspectElement( bridge: FrontendBridge, element: Element, path: InspectedElementPath | null, rendererID: number, shouldListenToPauseEvents: boolean = false, ): Promise { 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, shouldListenToPauseEvents, ).then((data: any) => { const {type} = data; let inspectedElement; switch (type) { case 'error': { const {message, stack, errorType} = ((data: any): InspectElementError); // create a different error class for each error type // and keep useful information from backend. let error; if (errorType === 'user') { error = new UserError(message); } else if (errorType === 'unknown-hook') { error = new UnknownHookError(message); } else { error = new Error(message); } // The backend's stack (where the error originated) is more meaningful than this stack. error.stack = stack || error.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.del(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 if (path != null) { fillInPath( inspectedElement, value, path, hydrateHelper(value, 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(); }