diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index ec64995bd..35509c646 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -27,13 +27,12 @@ import {Controls} from './Controls'; import {Button, Spin} from 'antd'; import {QueryClientProvider} from 'react-query'; import {Tree2} from './Tree'; +import {StreamInterceptorErrorView} from './StreamInterceptorErrorView'; export function Component() { const instance = usePlugin(plugin); const rootId = useValue(instance.rootId); - const streamInterceptorError = useValue( - instance.uiState.streamInterceptorError, - ); + const streamState = useValue(instance.uiState.streamState); const visualiserWidth = useValue(instance.uiState.visualiserWidth); const nodes: Map = useValue(instance.nodes); const metadata: Map = useValue(instance.metadata); @@ -54,13 +53,28 @@ export function Component() { setBottomPanelComponent(undefined); }; - if (showPerfStats) return ; - - if (streamInterceptorError != null) { - return streamInterceptorError; + if (streamState.state === 'UnrecoverableError') { + return ( + + ); } - if (rootId == null || nodes.size == 0) { + if (streamState.state === 'StreamInterceptorRetryableError') { + return ( + + ); + } + + if (showPerfStats) return ; + + if (rootId == null || streamState.state === 'RetryingAfterError') { return ( diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index dd4b14e12..8c348f19c 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -24,6 +24,7 @@ import { PerformanceStatsEvent, Snapshot, StreamInterceptorError, + StreamState, SubtreeUpdateEvent, UINode, } from './types'; @@ -31,8 +32,6 @@ import {Draft} from 'immer'; import {QueryClient, setLogger} from 'react-query'; import {tracker} from './tracker'; import {getStreamInterceptor} from './fb-stubs/StreamInterceptor'; -import React from 'react'; -import {StreamInterceptorErrorView} from './components/StreamInterceptorErrorView'; type SnapshotInfo = {nodeId: Id; base64Image: Snapshot}; type LiveClientState = { @@ -40,9 +39,14 @@ type LiveClientState = { nodes: Map; }; +type PendingData = { + metadata: Record; + frame: SubtreeUpdateEvent | null; +}; + type UIState = { isPaused: Atom; - streamInterceptorError: Atom; + streamState: Atom; searchTerm: Atom; isContextMenuOpen: Atom; hoveredNodes: Atom; @@ -71,15 +75,68 @@ export function plugin(client: PluginClient) { }); }); - client.onMessage('metadataUpdate', (event) => { + async function processMetadata( + incomingMetadata: Record, + ) { + const mappedMeta = await Promise.all( + Object.values(incomingMetadata).map((metadata) => + streamInterceptor.transformMetadata(metadata), + ), + ); + + metadata.update((draft) => { + for (const metadata of mappedMeta) { + draft.set(metadata.id, metadata); + } + }); + } + + //this holds pending any pending data that needs to be applied in the event of a stream interceptor error + //while in the error state more metadata or a more recent frame may come in so both cases need to apply the same darta + const pendingData: PendingData = {frame: null, metadata: {}}; + + function handleStreamError(source: 'Frame' | 'Metadata', error: any) { + if (error instanceof StreamInterceptorError) { + const retryCallback = async () => { + uiState.streamState.set({state: 'RetryingAfterError'}); + + await processMetadata(pendingData.metadata); + if (pendingData.frame != null) { + await processSubtreeUpdate(pendingData.frame); + } + uiState.streamState.set({state: 'Ok'}); + pendingData.frame = null; + pendingData.metadata = {}; + }; + + uiState.streamState.set({ + state: 'StreamInterceptorRetryableError', + retryCallback: retryCallback, + error: error, + }); + } else { + console.error( + `[ui-debugger] Unexpected Error processing ${source}`, + error, + ); + + uiState.streamState.set({state: 'UnrecoverableError'}); + } + } + + client.onMessage('metadataUpdate', async (event) => { if (!event.attributeMetadata) { return; } - metadata.update((draft) => { - for (const [_key, value] of Object.entries(event.attributeMetadata)) { - draft.set(value.id, value); + + try { + await processMetadata(event.attributeMetadata); + } catch (error) { + for (const metadata of Object.values(event.attributeMetadata)) { + pendingData.metadata[metadata.id] = metadata; } - }); + handleStreamError('Metadata', error); + } }); const perfEvents = createDataSource([], { @@ -124,7 +181,7 @@ export function plugin(client: PluginClient) { //used to disabled hover effects which cause rerenders and mess up the existing context menu isContextMenuOpen: createState(false), - streamInterceptorError: createState(undefined), + streamState: createState({state: 'Ok'}), visualiserWidth: createState(Math.min(window.innerWidth / 4.5, 500)), highlightedNodes, @@ -171,7 +228,7 @@ export function plugin(client: PluginClient) { }; const seenNodes = new Set(); - const subTreeUpdateCallBack = async (subtreeUpdate: SubtreeUpdateEvent) => { + const processSubtreeUpdate = async (subtreeUpdate: SubtreeUpdateEvent) => { try { const processedNodes = await streamInterceptor.transformNodes( new Map(subtreeUpdate.nodes.map((node) => [node.id, {...node}])), @@ -182,36 +239,9 @@ export function plugin(client: PluginClient) { }); applyFrameworkEvents(subtreeUpdate); - - uiState.streamInterceptorError.set(undefined); } catch (error) { - if (error instanceof StreamInterceptorError) { - const retryCallback = () => { - uiState.streamInterceptorError.set(undefined); - //wipe the internal state so loading indicator appears - applyFrameData(new Map(), null); - subTreeUpdateCallBack(subtreeUpdate); - }; - uiState.streamInterceptorError.set( - , - ); - } else { - console.error( - `[ui-debugger] Unexpected Error processing frame from ${client.appName}`, - error, - ); - - uiState.streamInterceptorError.set( - , - ); - } + pendingData.frame = subtreeUpdate; + handleStreamError('Frame', error); } }; @@ -258,7 +288,6 @@ export function plugin(client: PluginClient) { } draft.nodes = nodes; - setParentPointers(rootId.get()!!, undefined, draft.nodes); }); uiState.expandedNodes.update((draft) => { @@ -283,7 +312,7 @@ export function plugin(client: PluginClient) { checkFocusedNodeStillActive(uiState, nodesAtom.get()); } } - client.onMessage('subtreeUpdate', subTreeUpdateCallBack); + client.onMessage('subtreeUpdate', processSubtreeUpdate); const queryClient = new QueryClient({}); @@ -302,21 +331,6 @@ export function plugin(client: PluginClient) { }; } -function setParentPointers( - cur: Id, - parent: Id | undefined, - nodes: Map, -) { - const node = nodes.get(cur); - if (node == null) { - return; - } - node.parent = parent; - node.children.forEach((child) => { - setParentPointers(child, cur, nodes); - }); -} - type UIActions = { onHoverNode: (node: Id) => void; onFocusNode: (focused?: Id) => void; diff --git a/desktop/plugins/public/ui-debugger/types.tsx b/desktop/plugins/public/ui-debugger/types.tsx index f46c02aff..447e4fda4 100644 --- a/desktop/plugins/public/ui-debugger/types.tsx +++ b/desktop/plugins/public/ui-debugger/types.tsx @@ -7,6 +7,18 @@ * @format */ +export type StreamState = + | {state: 'Ok'} + | {state: 'RetryingAfterError'} + | { + state: 'StreamInterceptorRetryableError'; + error: StreamInterceptorError; + retryCallback: () => Promise; + } + | { + state: 'UnrecoverableError'; + }; + export type Events = { init: InitEvent; subtreeUpdate: SubtreeUpdateEvent; @@ -32,7 +44,11 @@ export type FrameworkEventMetadata = { documentation: string; }; -type JSON = string | number | boolean | null | JSON[] | {[key: string]: JSON}; +type JsonObject = { + [key: string]: JSON; +}; + +type JSON = string | number | boolean | null | JSON[] | JsonObject; type Stacktrace = {type: 'stacktrace'; stacktrace: string[]}; type Reason = {type: 'reason'; reason: string}; @@ -163,7 +179,14 @@ export type Id = number; export type MetadataId = number; export type TreeState = {expandedNodes: Id[]}; -export type Tag = 'Native' | 'Declarative' | 'Android' | 'Litho' | 'CK' | 'iOS'; +export type Tag = + | 'Native' + | 'Declarative' + | 'Android' + | 'Litho' + | 'CK' + | 'iOS' + | 'BloksBoundTree'; export type Inspectable = | InspectableObject