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 type {ReactContext} from 'shared/ReactTypes';
    
  11. 
    
  12. import * as React from 'react';
    
  13. import {
    
  14.   createContext,
    
  15.   useCallback,
    
  16.   useContext,
    
  17.   useEffect,
    
  18.   useMemo,
    
  19.   useState,
    
  20. } from 'react';
    
  21. import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
    
  22. import {createResource} from 'react-devtools-shared/src/devtools/cache';
    
  23. import {
    
  24.   BridgeContext,
    
  25.   StoreContext,
    
  26. } from 'react-devtools-shared/src/devtools/views/context';
    
  27. import {TreeStateContext} from '../TreeContext';
    
  28. 
    
  29. import type {StateContext} from '../TreeContext';
    
  30. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  31. import type Store from 'react-devtools-shared/src/devtools/store';
    
  32. import type {StyleAndLayout as StyleAndLayoutBackend} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
    
  33. import type {StyleAndLayout as StyleAndLayoutFrontend} from './types';
    
  34. import type {Element} from 'react-devtools-shared/src/frontend/types';
    
  35. import type {
    
  36.   Resource,
    
  37.   Thenable,
    
  38. } from 'react-devtools-shared/src/devtools/cache';
    
  39. 
    
  40. export type GetStyleAndLayout = (id: number) => StyleAndLayoutFrontend | null;
    
  41. 
    
  42. type Context = {
    
  43.   getStyleAndLayout: GetStyleAndLayout,
    
  44. };
    
  45. 
    
  46. const NativeStyleContext: ReactContext<Context> = createContext<Context>(
    
  47.   ((null: any): Context),
    
  48. );
    
  49. NativeStyleContext.displayName = 'NativeStyleContext';
    
  50. 
    
  51. type ResolveFn = (styleAndLayout: StyleAndLayoutFrontend) => void;
    
  52. type InProgressRequest = {
    
  53.   promise: Thenable<StyleAndLayoutFrontend>,
    
  54.   resolveFn: ResolveFn,
    
  55. };
    
  56. 
    
  57. const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
    
  58. const resource: Resource<Element, Element, StyleAndLayoutFrontend> =
    
  59.   createResource(
    
  60.     (element: Element) => {
    
  61.       const request = inProgressRequests.get(element);
    
  62.       if (request != null) {
    
  63.         return request.promise;
    
  64.       }
    
  65. 
    
  66.       let resolveFn:
    
  67.         | ResolveFn
    
  68.         | ((
    
  69.             result: Promise<StyleAndLayoutFrontend> | StyleAndLayoutFrontend,
    
  70.           ) => void) = ((null: any): ResolveFn);
    
  71.       const promise = new Promise(resolve => {
    
  72.         resolveFn = resolve;
    
  73.       });
    
  74. 
    
  75.       inProgressRequests.set(element, ({promise, resolveFn}: $FlowFixMe));
    
  76. 
    
  77.       return (promise: $FlowFixMe);
    
  78.     },
    
  79.     (element: Element) => element,
    
  80.     {useWeakMap: true},
    
  81.   );
    
  82. 
    
  83. type Props = {
    
  84.   children: React$Node,
    
  85. };
    
  86. 
    
  87. function NativeStyleContextController({children}: Props): React.Node {
    
  88.   const bridge = useContext<FrontendBridge>(BridgeContext);
    
  89.   const store = useContext<Store>(StoreContext);
    
  90. 
    
  91.   const getStyleAndLayout = useCallback<GetStyleAndLayout>(
    
  92.     (id: number) => {
    
  93.       const element = store.getElementByID(id);
    
  94.       if (element !== null) {
    
  95.         return resource.read(element);
    
  96.       } else {
    
  97.         return null;
    
  98.       }
    
  99.     },
    
  100.     [store],
    
  101.   );
    
  102. 
    
  103.   // It's very important that this context consumes selectedElementID and not NativeStyleID.
    
  104.   // Otherwise the effect that sends the "inspect" message across the bridge-
    
  105.   // would itself be blocked by the same render that suspends (waiting for the data).
    
  106.   const {selectedElementID} = useContext<StateContext>(TreeStateContext);
    
  107. 
    
  108.   const [currentStyleAndLayout, setCurrentStyleAndLayout] =
    
  109.     useState<StyleAndLayoutFrontend | null>(null);
    
  110. 
    
  111.   // This effect handler invalidates the suspense cache and schedules rendering updates with React.
    
  112.   useEffect(() => {
    
  113.     const onStyleAndLayout = ({id, layout, style}: StyleAndLayoutBackend) => {
    
  114.       const element = store.getElementByID(id);
    
  115.       if (element !== null) {
    
  116.         const styleAndLayout: StyleAndLayoutFrontend = {
    
  117.           layout,
    
  118.           style,
    
  119.         };
    
  120.         const request = inProgressRequests.get(element);
    
  121.         if (request != null) {
    
  122.           inProgressRequests.delete(element);
    
  123.           batchedUpdates(() => {
    
  124.             request.resolveFn(styleAndLayout);
    
  125.             setCurrentStyleAndLayout(styleAndLayout);
    
  126.           });
    
  127.         } else {
    
  128.           resource.write(element, styleAndLayout);
    
  129. 
    
  130.           // Schedule update with React if the currently-selected element has been invalidated.
    
  131.           if (id === selectedElementID) {
    
  132.             setCurrentStyleAndLayout(styleAndLayout);
    
  133.           }
    
  134.         }
    
  135.       }
    
  136.     };
    
  137. 
    
  138.     bridge.addListener('NativeStyleEditor_styleAndLayout', onStyleAndLayout);
    
  139.     return () =>
    
  140.       bridge.removeListener(
    
  141.         'NativeStyleEditor_styleAndLayout',
    
  142.         onStyleAndLayout,
    
  143.       );
    
  144.   }, [bridge, currentStyleAndLayout, selectedElementID, store]);
    
  145. 
    
  146.   // This effect handler polls for updates on the currently selected element.
    
  147.   useEffect(() => {
    
  148.     if (selectedElementID === null) {
    
  149.       return () => {};
    
  150.     }
    
  151. 
    
  152.     const rendererID = store.getRendererIDForElement(selectedElementID);
    
  153. 
    
  154.     let timeoutID: TimeoutID | null = null;
    
  155. 
    
  156.     const sendRequest = () => {
    
  157.       timeoutID = null;
    
  158. 
    
  159.       if (rendererID !== null) {
    
  160.         bridge.send('NativeStyleEditor_measure', {
    
  161.           id: selectedElementID,
    
  162.           rendererID,
    
  163.         });
    
  164.       }
    
  165.     };
    
  166. 
    
  167.     // Send the initial measurement request.
    
  168.     // We'll poll for an update in the response handler below.
    
  169.     sendRequest();
    
  170. 
    
  171.     const onStyleAndLayout = ({id}: StyleAndLayoutBackend) => {
    
  172.       // If this is the element we requested, wait a little bit and then ask for another update.
    
  173.       if (id === selectedElementID) {
    
  174.         if (timeoutID !== null) {
    
  175.           clearTimeout(timeoutID);
    
  176.         }
    
  177.         timeoutID = setTimeout(sendRequest, 1000);
    
  178.       }
    
  179.     };
    
  180. 
    
  181.     bridge.addListener('NativeStyleEditor_styleAndLayout', onStyleAndLayout);
    
  182. 
    
  183.     return () => {
    
  184.       bridge.removeListener(
    
  185.         'NativeStyleEditor_styleAndLayout',
    
  186.         onStyleAndLayout,
    
  187.       );
    
  188. 
    
  189.       if (timeoutID !== null) {
    
  190.         clearTimeout(timeoutID);
    
  191.       }
    
  192.     };
    
  193.   }, [bridge, selectedElementID, store]);
    
  194. 
    
  195.   const value = useMemo(
    
  196.     () => ({getStyleAndLayout}),
    
  197.     // NativeStyle is used to invalidate the cache and schedule an update with React.
    
  198.     [currentStyleAndLayout, getStyleAndLayout],
    
  199.   );
    
  200. 
    
  201.   return (
    
  202.     <NativeStyleContext.Provider value={value}>
    
  203.       {children}
    
  204.     </NativeStyleContext.Provider>
    
  205.   );
    
  206. }
    
  207. 
    
  208. export {NativeStyleContext, NativeStyleContextController};