forked from facebook/react
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinspectedElementMutableSource.js
175 lines (148 loc) · 5.53 KB
/
inspectedElementMutableSource.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import LRU from 'lru-cache';
import {
convertInspectedElementBackendToFrontend,
hydrateHelper,
inspectElement as inspectElementAPI,
} from 'react-devtools-shared/src/backendAPI';
import {fillInPath} from 'react-devtools-shared/src/hydration';
import type {LRUCache} from 'react-devtools-shared/src/types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {
InspectElementError,
InspectElementFullData,
InspectElementHydratedPath,
} from 'react-devtools-shared/src/backend/types';
import type {
Element,
InspectedElement as InspectedElementFrontend,
InspectedElementResponseType,
} from 'react-devtools-shared/src/devtools/views/Components/types';
import UserError from 'react-devtools-shared/src/errors/UserError';
import UnknownHookError from 'react-devtools-shared/src/errors/UnknownHookError';
// Maps element ID to inspected data.
// We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works.
// When the frontend polls the backend for an update on the element that's currently inspected,
// the backend will send a "no-change" message if the element hasn't updated (rendered) since the last time it was asked.
// In this case, the frontend cache should reuse the previous (cached) value.
// Using a WeakMap keyed on Element generally works well for this, since Elements are mutable and stable in the Store.
// This doens't work properly though when component filters are changed,
// because this will cause the Store to dump all roots and re-initialize the tree (recreating the Element objects).
// So instead we key on Element ID (which is stable in this case) and use an LRU for eviction.
const inspectedElementCache: LRUCache<
number,
InspectedElementFrontend,
> = new LRU({
max: 25,
});
type Path = Array<string | number>;
type InspectElementReturnType = [
InspectedElementFrontend,
InspectedElementResponseType,
];
export function inspectElement({
bridge,
element,
path,
rendererID,
}: {|
bridge: FrontendBridge,
element: Element,
path: Path | null,
rendererID: number,
|}): Promise<InspectElementReturnType> {
const {id} = element;
// This could indicate that the DevTools UI has been closed and reopened.
// The in-memory cache will be clear but the backend still thinks we have cached data.
// In this case, we need to tell it to resend the full data.
const forceFullData = !inspectedElementCache.has(id);
return inspectElementAPI({
bridge,
forceFullData,
id,
path,
rendererID,
}).then((data: any) => {
const {type} = data;
let inspectedElement;
switch (type) {
case 'error': {
const {message, stack, errorType} = ((data: any): InspectElementError);
// create a different error class for each error type
// and keep useful information from backend.
let error;
if (errorType === 'user') {
error = new UserError(message);
} else if (errorType === 'unknown-hook') {
error = new UnknownHookError(message);
} else {
error = new Error(message);
}
// The backend's stack (where the error originated) is more meaningful than this stack.
error.stack = stack || error.stack;
throw error;
}
case 'no-change':
// This is a no-op for the purposes of our cache.
inspectedElement = inspectedElementCache.get(id);
if (inspectedElement != null) {
return [inspectedElement, type];
}
// We should only encounter this case in the event of a bug.
throw Error(`Cached data for element "${id}" not found`);
case 'not-found':
// This is effectively a no-op.
// If the Element is still in the Store, we can eagerly remove it from the Map.
inspectedElementCache.del(id);
throw Error(`Element "${id}" not found`);
case 'full-data':
const fullData = ((data: any): InspectElementFullData);
// New data has come in.
// We should replace the data in our local mutable copy.
inspectedElement = convertInspectedElementBackendToFrontend(
fullData.value,
);
inspectedElementCache.set(id, inspectedElement);
return [inspectedElement, type];
case 'hydrated-path':
const hydratedPathData = ((data: any): InspectElementHydratedPath);
const {value} = hydratedPathData;
// A path has been hydrated.
// Merge it with the latest copy we have locally and resolve with the merged value.
inspectedElement = inspectedElementCache.get(id) || null;
if (inspectedElement !== null) {
// Clone element
inspectedElement = {...inspectedElement};
// Merge hydrated data
fillInPath(
inspectedElement,
value,
((path: any): Path),
hydrateHelper(value, ((path: any): Path)),
);
inspectedElementCache.set(id, inspectedElement);
return [inspectedElement, type];
}
break;
default:
// Should never happen.
if (__DEV__) {
console.error(
`Unexpected inspected element response data: "${type}"`,
);
}
break;
}
throw Error(`Unable to inspect element with id "${id}"`);
});
}
export function clearCacheForTests(): void {
inspectedElementCache.reset();
}