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 LRU from 'lru-cache';
    
  11. import {
    
  12.   convertInspectedElementBackendToFrontend,
    
  13.   hydrateHelper,
    
  14.   inspectElement as inspectElementAPI,
    
  15. } from 'react-devtools-shared/src/backendAPI';
    
  16. import {fillInPath} from 'react-devtools-shared/src/hydration';
    
  17. 
    
  18. import type {LRUCache} from 'react-devtools-shared/src/frontend/types';
    
  19. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  20. import type {
    
  21.   InspectElementError,
    
  22.   InspectElementFullData,
    
  23.   InspectElementHydratedPath,
    
  24. } from 'react-devtools-shared/src/backend/types';
    
  25. import UserError from 'react-devtools-shared/src/errors/UserError';
    
  26. import UnknownHookError from 'react-devtools-shared/src/errors/UnknownHookError';
    
  27. import type {
    
  28.   Element,
    
  29.   InspectedElement as InspectedElementFrontend,
    
  30.   InspectedElementResponseType,
    
  31.   InspectedElementPath,
    
  32. } from 'react-devtools-shared/src/frontend/types';
    
  33. 
    
  34. // Maps element ID to inspected data.
    
  35. // We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works.
    
  36. // When the frontend polls the backend for an update on the element that's currently inspected,
    
  37. // the backend will send a "no-change" message if the element hasn't updated (rendered) since the last time it was asked.
    
  38. // In this case, the frontend cache should reuse the previous (cached) value.
    
  39. // Using a WeakMap keyed on Element generally works well for this, since Elements are mutable and stable in the Store.
    
  40. // This doens't work properly though when component filters are changed,
    
  41. // because this will cause the Store to dump all roots and re-initialize the tree (recreating the Element objects).
    
  42. // So instead we key on Element ID (which is stable in this case) and use an LRU for eviction.
    
  43. const inspectedElementCache: LRUCache<number, InspectedElementFrontend> =
    
  44.   new LRU({
    
  45.     max: 25,
    
  46.   });
    
  47. 
    
  48. type InspectElementReturnType = [
    
  49.   InspectedElementFrontend,
    
  50.   InspectedElementResponseType,
    
  51. ];
    
  52. 
    
  53. export function inspectElement(
    
  54.   bridge: FrontendBridge,
    
  55.   element: Element,
    
  56.   path: InspectedElementPath | null,
    
  57.   rendererID: number,
    
  58.   shouldListenToPauseEvents: boolean = false,
    
  59. ): Promise<InspectElementReturnType> {
    
  60.   const {id} = element;
    
  61. 
    
  62.   // This could indicate that the DevTools UI has been closed and reopened.
    
  63.   // The in-memory cache will be clear but the backend still thinks we have cached data.
    
  64.   // In this case, we need to tell it to resend the full data.
    
  65.   const forceFullData = !inspectedElementCache.has(id);
    
  66. 
    
  67.   return inspectElementAPI(
    
  68.     bridge,
    
  69.     forceFullData,
    
  70.     id,
    
  71.     path,
    
  72.     rendererID,
    
  73.     shouldListenToPauseEvents,
    
  74.   ).then((data: any) => {
    
  75.     const {type} = data;
    
  76. 
    
  77.     let inspectedElement;
    
  78.     switch (type) {
    
  79.       case 'error': {
    
  80.         const {message, stack, errorType} = ((data: any): InspectElementError);
    
  81. 
    
  82.         // create a different error class for each error type
    
  83.         // and keep useful information from backend.
    
  84.         let error;
    
  85.         if (errorType === 'user') {
    
  86.           error = new UserError(message);
    
  87.         } else if (errorType === 'unknown-hook') {
    
  88.           error = new UnknownHookError(message);
    
  89.         } else {
    
  90.           error = new Error(message);
    
  91.         }
    
  92.         // The backend's stack (where the error originated) is more meaningful than this stack.
    
  93.         error.stack = stack || error.stack;
    
  94. 
    
  95.         throw error;
    
  96.       }
    
  97. 
    
  98.       case 'no-change':
    
  99.         // This is a no-op for the purposes of our cache.
    
  100.         inspectedElement = inspectedElementCache.get(id);
    
  101.         if (inspectedElement != null) {
    
  102.           return [inspectedElement, type];
    
  103.         }
    
  104. 
    
  105.         // We should only encounter this case in the event of a bug.
    
  106.         throw Error(`Cached data for element "${id}" not found`);
    
  107. 
    
  108.       case 'not-found':
    
  109.         // This is effectively a no-op.
    
  110.         // If the Element is still in the Store, we can eagerly remove it from the Map.
    
  111.         inspectedElementCache.del(id);
    
  112. 
    
  113.         throw Error(`Element "${id}" not found`);
    
  114. 
    
  115.       case 'full-data':
    
  116.         const fullData = ((data: any): InspectElementFullData);
    
  117. 
    
  118.         // New data has come in.
    
  119.         // We should replace the data in our local mutable copy.
    
  120.         inspectedElement = convertInspectedElementBackendToFrontend(
    
  121.           fullData.value,
    
  122.         );
    
  123. 
    
  124.         inspectedElementCache.set(id, inspectedElement);
    
  125. 
    
  126.         return [inspectedElement, type];
    
  127. 
    
  128.       case 'hydrated-path':
    
  129.         const hydratedPathData = ((data: any): InspectElementHydratedPath);
    
  130.         const {value} = hydratedPathData;
    
  131. 
    
  132.         // A path has been hydrated.
    
  133.         // Merge it with the latest copy we have locally and resolve with the merged value.
    
  134.         inspectedElement = inspectedElementCache.get(id) || null;
    
  135.         if (inspectedElement !== null) {
    
  136.           // Clone element
    
  137.           inspectedElement = {...inspectedElement};
    
  138. 
    
  139.           // Merge hydrated data
    
  140.           if (path != null) {
    
  141.             fillInPath(
    
  142.               inspectedElement,
    
  143.               value,
    
  144.               path,
    
  145.               hydrateHelper(value, path),
    
  146.             );
    
  147.           }
    
  148. 
    
  149.           inspectedElementCache.set(id, inspectedElement);
    
  150. 
    
  151.           return [inspectedElement, type];
    
  152.         }
    
  153.         break;
    
  154. 
    
  155.       default:
    
  156.         // Should never happen.
    
  157.         if (__DEV__) {
    
  158.           console.error(
    
  159.             `Unexpected inspected element response data: "${type}"`,
    
  160.           );
    
  161.         }
    
  162.         break;
    
  163.     }
    
  164. 
    
  165.     throw Error(`Unable to inspect element with id "${id}"`);
    
  166.   });
    
  167. }
    
  168. 
    
  169. export function clearCacheForTests(): void {
    
  170.   inspectedElementCache.reset();
    
  171. }