Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -974,12 +974,8 @@ describe('Store', () => {
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
`);

const rendererID = getRendererID();
const rootID = store.getRootIDForElement(store.getElementIDAtIndex(0));
await actAsync(() => {
agent.overrideSuspenseMilestone({
rendererID,
rootID,
suspendedSet: [
store.getElementIDAtIndex(4),
store.getElementIDAtIndex(8),
Expand Down Expand Up @@ -1009,8 +1005,6 @@ describe('Store', () => {

await actAsync(() => {
agent.overrideSuspenseMilestone({
rendererID,
rootID,
suspendedSet: [],
});
});
Expand Down
276 changes: 263 additions & 13 deletions packages/react-devtools-shared/src/backend/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
*/

import EventEmitter from '../events';
import {SESSION_STORAGE_LAST_SELECTION_KEY, __DEBUG__} from '../constants';
import {
SESSION_STORAGE_LAST_SELECTION_KEY,
UNKNOWN_SUSPENDERS_NONE,
__DEBUG__,
} from '../constants';
import setupHighlighter from './views/Highlighter';
import {
initialize as setupTraceUpdates,
Expand All @@ -26,8 +30,13 @@ import type {
RendererID,
RendererInterface,
DevToolsHookSettings,
InspectedElement,
} from './types';
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
import type {
ComponentFilter,
DehydratedData,
ElementType,
} from 'react-devtools-shared/src/frontend/types';
import type {GroupItem} from './views/TraceUpdates/canvas';
import {gte, isReactNativeEnvironment} from './utils';
import {
Expand Down Expand Up @@ -73,6 +82,13 @@ type InspectElementParams = {
requestID: number,
};

type InspectScreenParams = {
forceFullData: boolean,
id: number,
path: Array<string | number> | null,
requestID: number,
};

type OverrideHookParams = {
id: number,
hookID: number,
Expand Down Expand Up @@ -131,8 +147,6 @@ type OverrideSuspenseParams = {
};

type OverrideSuspenseMilestoneParams = {
rendererID: number,
rootID: number,
suspendedSet: Array<number>,
};

Expand All @@ -141,6 +155,111 @@ type PersistedSelection = {
path: Array<PathFrame>,
};

function createEmptyInspectedScreen(
arbitraryRootID: number,
type: ElementType,
): InspectedElement {
const suspendedBy: DehydratedData = {
cleaned: [],
data: [],
unserializable: [],
};
return {
// invariants
id: arbitraryRootID,
type: type,
// Properties we merge
isErrored: false,
errors: [],
warnings: [],
suspendedBy,
suspendedByRange: null,
// TODO: How to merge these?
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
// Properties where merging doesn't make sense so we ignore them entirely in the UI
rootType: null,
plugins: {stylex: null},
nativeTag: null,
env: null,
source: null,
stack: null,
rendererPackageName: null,
rendererVersion: null,
// These don't make sense for a Root. They're just bottom values.
key: null,
canEditFunctionProps: false,
canEditHooks: false,
canEditFunctionPropsDeletePaths: false,
canEditFunctionPropsRenamePaths: false,
canEditHooksAndDeletePaths: false,
canEditHooksAndRenamePaths: false,
canToggleError: false,
canToggleSuspense: false,
isSuspended: false,
hasLegacyContext: false,
context: null,
hooks: null,
props: null,
state: null,
owners: null,
};
}

function mergeRoots(
left: InspectedElement,
right: InspectedElement,
suspendedByOffset: number,
): void {
const leftSuspendedByRange = left.suspendedByRange;
const rightSuspendedByRange = right.suspendedByRange;

if (right.isErrored) {
left.isErrored = true;
}
for (let i = 0; i < right.errors.length; i++) {
left.errors.push(right.errors[i]);
}
for (let i = 0; i < right.warnings.length; i++) {
left.warnings.push(right.warnings[i]);
}

const leftSuspendedBy: DehydratedData = left.suspendedBy;
const {data, cleaned, unserializable} = (right.suspendedBy: DehydratedData);
const leftSuspendedByData = ((leftSuspendedBy.data: any): Array<mixed>);
const rightSuspendedByData = ((data: any): Array<mixed>);
for (let i = 0; i < rightSuspendedByData.length; i++) {
leftSuspendedByData.push(rightSuspendedByData[i]);
}
for (let i = 0; i < cleaned.length; i++) {
leftSuspendedBy.cleaned.push(
[suspendedByOffset + cleaned[i][0]].concat(cleaned[i].slice(1)),
);
}
for (let i = 0; i < unserializable.length; i++) {
leftSuspendedBy.unserializable.push(
[suspendedByOffset + unserializable[i][0]].concat(
unserializable[i].slice(1),
),
);
}

if (rightSuspendedByRange !== null) {
if (leftSuspendedByRange === null) {
left.suspendedByRange = [
rightSuspendedByRange[0],
rightSuspendedByRange[1],
];
} else {
if (rightSuspendedByRange[0] < leftSuspendedByRange[0]) {
leftSuspendedByRange[0] = rightSuspendedByRange[0];
}
if (rightSuspendedByRange[1] > leftSuspendedByRange[1]) {
leftSuspendedByRange[1] = rightSuspendedByRange[1];
}
}
}
}

export default class Agent extends EventEmitter<{
hideNativeHighlight: [],
showNativeHighlight: [HostInstance],
Expand Down Expand Up @@ -201,6 +320,7 @@ export default class Agent extends EventEmitter<{
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
bridge.addListener('getOwnersList', this.getOwnersList);
bridge.addListener('inspectElement', this.inspectElement);
bridge.addListener('inspectScreen', this.inspectScreen);
bridge.addListener('logElementToConsole', this.logElementToConsole);
bridge.addListener('overrideError', this.overrideError);
bridge.addListener('overrideSuspense', this.overrideSuspense);
Expand Down Expand Up @@ -531,6 +651,138 @@ export default class Agent extends EventEmitter<{
}
};

inspectScreen: InspectScreenParams => void = ({
requestID,
id,
forceFullData,
path: screenPath,
}) => {
let inspectedScreen: InspectedElement | null = null;
let found = false;
// the suspendedBy index will be from the previously merged roots.
// We need to keep track of how many suspendedBy we've already seen to know
// to which renderer the index belongs.
let suspendedByOffset = 0;
let suspendedByPathIndex: number | null = null;
// The path to hydrate for a specific renderer
let rendererPath: InspectElementParams['path'] = null;
if (screenPath !== null && screenPath.length > 1) {
const secondaryCategory = screenPath[0];
if (secondaryCategory !== 'suspendedBy') {
throw new Error(
'Only hydrating suspendedBy paths is supported. This is a bug.',
);
}
if (typeof screenPath[1] !== 'number') {
throw new Error(
`Expected suspendedBy index to be a number. Received '${screenPath[1]}' instead. This is a bug.`,
);
}
suspendedByPathIndex = screenPath[1];
rendererPath = screenPath.slice(2);
}

for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
let path: InspectElementParams['path'] = null;
if (suspendedByPathIndex !== null && rendererPath !== null) {
const suspendedByPathRendererIndex =
suspendedByPathIndex - suspendedByOffset;
const rendererHasRequestedSuspendedByPath =
renderer.getElementAttributeByPath(id, [
'suspendedBy',
suspendedByPathRendererIndex,
]) !== undefined;
if (rendererHasRequestedSuspendedByPath) {
path = ['suspendedBy', suspendedByPathRendererIndex].concat(
rendererPath,
);
}
}

const inspectedRootsPayload = renderer.inspectElement(
requestID,
id,
path,
forceFullData,
);
switch (inspectedRootsPayload.type) {
case 'hydrated-path':
// The path will be relative to the Roots of this renderer. We adjust it
// to be relative to all Roots of this implementation.
inspectedRootsPayload.path[1] += suspendedByOffset;
// TODO: Hydration logic is flawed since the Frontend path is not based
// on the original backend data but rather its own representation of it (e.g. due to reorder).
// So we can receive null here instead when hydration fails
if (inspectedRootsPayload.value !== null) {
for (
let i = 0;
i < inspectedRootsPayload.value.cleaned.length;
i++
) {
inspectedRootsPayload.value.cleaned[i][1] += suspendedByOffset;
}
}
this._bridge.send('inspectedScreen', inspectedRootsPayload);
// If we hydrated a path, it must've been in a specific renderer so we can stop here.
return;
case 'full-data':
const inspectedRoots = inspectedRootsPayload.value;
if (inspectedScreen === null) {
inspectedScreen = createEmptyInspectedScreen(
inspectedRoots.id,
inspectedRoots.type,
);
}
mergeRoots(inspectedScreen, inspectedRoots, suspendedByOffset);
const dehydratedSuspendedBy: DehydratedData =
inspectedRoots.suspendedBy;
const suspendedBy = ((dehydratedSuspendedBy.data: any): Array<mixed>);
suspendedByOffset += suspendedBy.length;
found = true;
break;
case 'no-change':
found = true;
const rootsSuspendedBy: Array<mixed> =
(renderer.getElementAttributeByPath(id, ['suspendedBy']): any);
suspendedByOffset += rootsSuspendedBy.length;
break;
case 'not-found':
break;
case 'error':
// bail out and show the error
// TODO: aggregate errors
this._bridge.send('inspectedScreen', inspectedRootsPayload);
return;
}
}

if (inspectedScreen === null) {
if (found) {
this._bridge.send('inspectedScreen', {
type: 'no-change',
responseID: requestID,
id,
});
} else {
this._bridge.send('inspectedScreen', {
type: 'not-found',
responseID: requestID,
id,
});
}
} else {
this._bridge.send('inspectedScreen', {
type: 'full-data',
responseID: requestID,
id,
value: inspectedScreen,
});
}
};

logElementToConsole: ElementAndRendererID => void = ({id, rendererID}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
Expand Down Expand Up @@ -567,17 +819,15 @@ export default class Agent extends EventEmitter<{
};

overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
rendererID,
rootID,
suspendedSet,
}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(
`Invalid renderer id "${rendererID}" to override suspense milestone`,
);
} else {
renderer.overrideSuspenseMilestone(rootID, suspendedSet);
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
if (renderer.supportsTogglingSuspense) {
renderer.overrideSuspenseMilestone(suspendedSet);
}
}
};

Expand Down
Loading
Loading