| 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 {createContext, useCallback, useContext, useEffect} from 'react'; |
| 14 | import {createResource} from '../../cache'; |
| 15 | import {BridgeContext, StoreContext} from '../context'; |
| 16 | import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; |
| 17 | import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils'; |
| 18 | |
| 19 | import type {OwnersList} from 'react-devtools-shared/src/backend/types'; |
| 20 | import type { |
| 21 | Element, |
| 22 | SerializedElement, |
| 23 | } from 'react-devtools-shared/src/frontend/types'; |
| 24 | import type {Resource, Thenable} from '../../cache'; |
| 25 | |
| 26 | type Context = (id: number) => Array<SerializedElement> | null; |
| 27 | |
| 28 | const OwnersListContext: ReactContext<Context> = createContext<Context>( |
| 29 | null as any as Context, |
| 30 | ); |
| 31 | OwnersListContext.displayName = 'OwnersListContext'; |
| 32 | |
| 33 | type ResolveFn = (ownersList: Array<SerializedElement> | null) => void; |
| 34 | type InProgressRequest = { |
| 35 | promise: Thenable<Array<SerializedElement>>, |
| 36 | resolveFn: ResolveFn, |
| 37 | }; |
| 38 | |
| 39 | const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap(); |
| 40 | const resource: Resource< |
| 41 | Element, |
| 42 | Element, |
| 43 | Array<SerializedElement>, |
| 44 | > = createResource( |
| 45 | (element: Element) => { |
| 46 | const request = inProgressRequests.get(element); |
| 47 | if (request != null) { |
| 48 | return request.promise; |
| 49 | } |
| 50 | |
| 51 | let resolveFn: |
| 52 | | ResolveFn |
| 53 | | (( |
| 54 | result: Promise<Array<SerializedElement>> | Array<SerializedElement>, |
| 55 | ) => void) = null as any as ResolveFn; |
| 56 | const promise = new Promise(resolve => { |
| 57 | resolveFn = resolve; |
| 58 | }); |
| 59 | |
| 60 | // $FlowFixMe[incompatible-type] found when upgrading Flow |
| 61 | inProgressRequests.set(element, {promise, resolveFn}); |
| 62 | |
| 63 | return promise as $FlowFixMe; |
| 64 | }, |
| 65 | (element: Element) => element, |
| 66 | {useWeakMap: true}, |
| 67 | ); |
| 68 | |
| 69 | type Props = { |
| 70 | children: React$Node, |
| 71 | }; |
| 72 | |
| 73 | function useChangeOwnerAction(): (nextOwnerID: number) => void { |
| 74 | const bridge = useContext(BridgeContext); |
| 75 | const store = useContext(StoreContext); |
| 76 | const treeAction = useContext(TreeDispatcherContext); |
| 77 | |
| 78 | return useCallback( |
| 79 | function changeOwnerAction(nextOwnerID: number) { |
| 80 | treeAction({type: 'SELECT_OWNER', payload: nextOwnerID}); |
| 81 | |
| 82 | const element = store.getElementByID(nextOwnerID); |
| 83 | if (element !== null) { |
| 84 | if (!inProgressRequests.has(element)) { |
| 85 | let resolveFn: |
| 86 | | ResolveFn |
| 87 | | (( |
| 88 | result: |
| 89 | | Promise<Array<SerializedElement>> |
| 90 | | Array<SerializedElement>, |
| 91 | ) => void) = null as any as ResolveFn; |
| 92 | const promise = new Promise(resolve => { |
| 93 | resolveFn = resolve; |
| 94 | }); |
| 95 | |
| 96 | // $FlowFixMe[incompatible-type] found when upgrading Flow |
| 97 | inProgressRequests.set(element, {promise, resolveFn}); |
| 98 | } |
| 99 | |
| 100 | const rendererID = store.getRendererIDForElement(nextOwnerID); |
| 101 | if (rendererID !== null) { |
| 102 | bridge.send('getOwnersList', {id: nextOwnerID, rendererID}); |
| 103 | } |
| 104 | } |
| 105 | }, |
| 106 | [bridge, store], |
| 107 | ); |
| 108 | } |
| 109 | |
| 110 | function OwnersListContextController({children}: Props): React.Node { |
| 111 | const bridge = useContext(BridgeContext); |
| 112 | const store = useContext(StoreContext); |
| 113 | const {ownerID} = useContext(TreeStateContext); |
| 114 | |
| 115 | const read = useCallback( |
| 116 | (id: number) => { |
| 117 | const element = store.getElementByID(id); |
| 118 | if (element !== null) { |
| 119 | return resource.read(element); |
| 120 | } else { |
| 121 | return null; |
| 122 | } |
| 123 | }, |
| 124 | [store], |
| 125 | ); |
| 126 | |
| 127 | useEffect(() => { |
| 128 | const onOwnersList = (ownersList: OwnersList) => { |
| 129 | const id = ownersList.id; |
| 130 | |
| 131 | const element = store.getElementByID(id); |
| 132 | if (element !== null) { |
| 133 | const request = inProgressRequests.get(element); |
| 134 | if (request != null) { |
| 135 | request.resolveFn( |
| 136 | ownersList.owners === null |
| 137 | ? null |
| 138 | : ownersList.owners.map(backendToFrontendSerializedElementMapper), |
| 139 | ); |
| 140 | } |
| 141 | } |
| 142 | }; |
| 143 | |
| 144 | bridge.addListener('ownersList', onOwnersList); |
| 145 | return () => bridge.removeListener('ownersList', onOwnersList); |
| 146 | }, [bridge, store]); |
| 147 | |
| 148 | // This effect requests an updated owners list any time the selected owner changes |
| 149 | useEffect(() => { |
| 150 | if (ownerID !== null) { |
| 151 | const rendererID = store.getRendererIDForElement(ownerID); |
| 152 | if (rendererID !== null) { |
| 153 | bridge.send('getOwnersList', {id: ownerID, rendererID}); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | return () => {}; |
| 158 | }, [bridge, ownerID, store]); |
| 159 | |
| 160 | return ( |
| 161 | <OwnersListContext.Provider value={read}> |
| 162 | {children} |
| 163 | </OwnersListContext.Provider> |
| 164 | ); |
| 165 | } |
| 166 | |
| 167 | export {OwnersListContext, OwnersListContextController, useChangeOwnerAction}; |