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 {
    
  11.   unstable_getCacheForType as getCacheForType,
    
  12.   startTransition,
    
  13. } from 'react';
    
  14. import Store from 'react-devtools-shared/src/devtools/store';
    
  15. import {inspectElement as inspectElementMutableSource} from 'react-devtools-shared/src/inspectedElementMutableSource';
    
  16. import ElementPollingCancellationError from 'react-devtools-shared/src//errors/ElementPollingCancellationError';
    
  17. 
    
  18. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  19. import type {Wakeable} from 'shared/ReactTypes';
    
  20. import type {
    
  21.   Element,
    
  22.   InspectedElement as InspectedElementFrontend,
    
  23.   InspectedElementResponseType,
    
  24.   InspectedElementPath,
    
  25. } from 'react-devtools-shared/src/frontend/types';
    
  26. 
    
  27. const Pending = 0;
    
  28. const Resolved = 1;
    
  29. const Rejected = 2;
    
  30. 
    
  31. type PendingRecord = {
    
  32.   status: 0,
    
  33.   value: Wakeable,
    
  34. };
    
  35. 
    
  36. type ResolvedRecord<T> = {
    
  37.   status: 1,
    
  38.   value: T,
    
  39. };
    
  40. 
    
  41. type RejectedRecord = {
    
  42.   status: 2,
    
  43.   value: Error | string,
    
  44. };
    
  45. 
    
  46. type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
    
  47. 
    
  48. function readRecord<T>(record: Record<T>): ResolvedRecord<T> {
    
  49.   if (record.status === Resolved) {
    
  50.     // This is just a type refinement.
    
  51.     return record;
    
  52.   } else {
    
  53.     throw record.value;
    
  54.   }
    
  55. }
    
  56. 
    
  57. type InspectedElementMap = WeakMap<Element, Record<InspectedElementFrontend>>;
    
  58. type CacheSeedKey = () => InspectedElementMap;
    
  59. 
    
  60. function createMap(): InspectedElementMap {
    
  61.   return new WeakMap();
    
  62. }
    
  63. 
    
  64. function getRecordMap(): WeakMap<Element, Record<InspectedElementFrontend>> {
    
  65.   return getCacheForType(createMap);
    
  66. }
    
  67. 
    
  68. function createCacheSeed(
    
  69.   element: Element,
    
  70.   inspectedElement: InspectedElementFrontend,
    
  71. ): [CacheSeedKey, InspectedElementMap] {
    
  72.   const newRecord: Record<InspectedElementFrontend> = {
    
  73.     status: Resolved,
    
  74.     value: inspectedElement,
    
  75.   };
    
  76.   const map = createMap();
    
  77.   map.set(element, newRecord);
    
  78.   return [createMap, map];
    
  79. }
    
  80. 
    
  81. /**
    
  82.  * Fetches element props and state from the backend for inspection.
    
  83.  * This method should be called during render; it will suspend if data has not yet been fetched.
    
  84.  */
    
  85. export function inspectElement(
    
  86.   element: Element,
    
  87.   path: InspectedElementPath | null,
    
  88.   store: Store,
    
  89.   bridge: FrontendBridge,
    
  90. ): InspectedElementFrontend | null {
    
  91.   const map = getRecordMap();
    
  92.   let record = map.get(element);
    
  93.   if (!record) {
    
  94.     const callbacks = new Set<() => mixed>();
    
  95.     const wakeable: Wakeable = {
    
  96.       then(callback: () => mixed) {
    
  97.         callbacks.add(callback);
    
  98.       },
    
  99. 
    
  100.       // Optional property used by Timeline:
    
  101.       displayName: `Inspecting ${element.displayName || 'Unknown'}`,
    
  102.     };
    
  103. 
    
  104.     const wake = () => {
    
  105.       // This assumes they won't throw.
    
  106.       callbacks.forEach(callback => callback());
    
  107.       callbacks.clear();
    
  108.     };
    
  109.     const newRecord: Record<InspectedElementFrontend> = (record = {
    
  110.       status: Pending,
    
  111.       value: wakeable,
    
  112.     });
    
  113. 
    
  114.     const rendererID = store.getRendererIDForElement(element.id);
    
  115.     if (rendererID == null) {
    
  116.       const rejectedRecord = ((newRecord: any): RejectedRecord);
    
  117.       rejectedRecord.status = Rejected;
    
  118.       rejectedRecord.value = new Error(
    
  119.         `Could not inspect element with id "${element.id}". No renderer found.`,
    
  120.       );
    
  121. 
    
  122.       map.set(element, record);
    
  123. 
    
  124.       return null;
    
  125.     }
    
  126. 
    
  127.     inspectElementMutableSource(bridge, element, path, rendererID).then(
    
  128.       ([inspectedElement]: [
    
  129.         InspectedElementFrontend,
    
  130.         InspectedElementResponseType,
    
  131.       ]) => {
    
  132.         const resolvedRecord =
    
  133.           ((newRecord: any): ResolvedRecord<InspectedElementFrontend>);
    
  134.         resolvedRecord.status = Resolved;
    
  135.         resolvedRecord.value = inspectedElement;
    
  136. 
    
  137.         wake();
    
  138.       },
    
  139. 
    
  140.       error => {
    
  141.         console.error(error);
    
  142. 
    
  143.         const rejectedRecord = ((newRecord: any): RejectedRecord);
    
  144.         rejectedRecord.status = Rejected;
    
  145.         rejectedRecord.value = error;
    
  146. 
    
  147.         wake();
    
  148.       },
    
  149.     );
    
  150. 
    
  151.     map.set(element, record);
    
  152.   }
    
  153. 
    
  154.   const response = readRecord(record).value;
    
  155.   return response;
    
  156. }
    
  157. 
    
  158. type RefreshFunction = (
    
  159.   seedKey: CacheSeedKey,
    
  160.   cacheMap: InspectedElementMap,
    
  161. ) => void;
    
  162. 
    
  163. /**
    
  164.  * Asks the backend for updated props and state from an expected element.
    
  165.  * This method should never be called during render; call it from an effect or event handler.
    
  166.  * This method will schedule an update if updated information is returned.
    
  167.  */
    
  168. export function checkForUpdate({
    
  169.   bridge,
    
  170.   element,
    
  171.   refresh,
    
  172.   store,
    
  173. }: {
    
  174.   bridge: FrontendBridge,
    
  175.   element: Element,
    
  176.   refresh: RefreshFunction,
    
  177.   store: Store,
    
  178. }): void | Promise<void> {
    
  179.   const {id} = element;
    
  180.   const rendererID = store.getRendererIDForElement(id);
    
  181. 
    
  182.   if (rendererID == null) {
    
  183.     return;
    
  184.   }
    
  185. 
    
  186.   return inspectElementMutableSource(
    
  187.     bridge,
    
  188.     element,
    
  189.     null,
    
  190.     rendererID,
    
  191.     true,
    
  192.   ).then(
    
  193.     ([inspectedElement, responseType]: [
    
  194.       InspectedElementFrontend,
    
  195.       InspectedElementResponseType,
    
  196.     ]) => {
    
  197.       if (responseType === 'full-data') {
    
  198.         startTransition(() => {
    
  199.           const [key, value] = createCacheSeed(element, inspectedElement);
    
  200.           refresh(key, value);
    
  201.         });
    
  202.       }
    
  203.     },
    
  204.   );
    
  205. }
    
  206. 
    
  207. function createPromiseWhichResolvesInOneSecond() {
    
  208.   return new Promise(resolve => setTimeout(resolve, 1000));
    
  209. }
    
  210. 
    
  211. type PollingStatus = 'idle' | 'running' | 'paused' | 'aborted';
    
  212. 
    
  213. export function startElementUpdatesPolling({
    
  214.   bridge,
    
  215.   element,
    
  216.   refresh,
    
  217.   store,
    
  218. }: {
    
  219.   bridge: FrontendBridge,
    
  220.   element: Element,
    
  221.   refresh: RefreshFunction,
    
  222.   store: Store,
    
  223. }): {abort: () => void, pause: () => void, resume: () => void} {
    
  224.   let status: PollingStatus = 'idle';
    
  225. 
    
  226.   function abort() {
    
  227.     status = 'aborted';
    
  228.   }
    
  229. 
    
  230.   function resume() {
    
  231.     if (status === 'running' || status === 'aborted') {
    
  232.       return;
    
  233.     }
    
  234. 
    
  235.     status = 'idle';
    
  236.     poll();
    
  237.   }
    
  238. 
    
  239.   function pause() {
    
  240.     if (status === 'paused' || status === 'aborted') {
    
  241.       return;
    
  242.     }
    
  243. 
    
  244.     status = 'paused';
    
  245.   }
    
  246. 
    
  247.   function poll(): Promise<void> {
    
  248.     status = 'running';
    
  249. 
    
  250.     return Promise.allSettled([
    
  251.       checkForUpdate({bridge, element, refresh, store}),
    
  252.       createPromiseWhichResolvesInOneSecond(),
    
  253.     ])
    
  254.       .then(([{status: updateStatus, reason}]) => {
    
  255.         // There isn't much to do about errors in this case,
    
  256.         // but we should at least log them, so they aren't silent.
    
  257.         // Log only if polling is still active, we can't handle the case when
    
  258.         // request was sent, and then bridge was remounted (for example, when user did navigate to a new page),
    
  259.         // but at least we can mark that polling was aborted
    
  260.         if (updateStatus === 'rejected' && status !== 'aborted') {
    
  261.           // This is expected Promise rejection, no need to log it
    
  262.           if (reason instanceof ElementPollingCancellationError) {
    
  263.             return;
    
  264.           }
    
  265. 
    
  266.           console.error(reason);
    
  267.         }
    
  268.       })
    
  269.       .finally(() => {
    
  270.         const shouldContinuePolling =
    
  271.           status !== 'aborted' && status !== 'paused';
    
  272. 
    
  273.         status = 'idle';
    
  274. 
    
  275.         if (shouldContinuePolling) {
    
  276.           return poll();
    
  277.         }
    
  278.       });
    
  279.   }
    
  280. 
    
  281.   poll();
    
  282. 
    
  283.   return {abort, resume, pause};
    
  284. }
    
  285. 
    
  286. export function clearCacheBecauseOfError(refresh: RefreshFunction): void {
    
  287.   startTransition(() => {
    
  288.     const map = createMap();
    
  289.     refresh(createMap, map);
    
  290.   });
    
  291. }