mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[Flight] Let Errored/Blocked Direct References Turn Nearest Element Lazy (#29823)
Stacked on #29807. This lets the nearest Suspense/Error Boundary handle it even if that boundary is defined by the model itself. It also ensures that when we have an error during serialization of properties, those can be associated with the nearest JSX element and since we have a stack/owner for that element we can use it to point to the source code of that line. We can't track the source of any nested arbitrary objects deeper inside since objects don’t track their stacks but close enough. Ideally we have the property path but we don’t have that right now. We have a partial in the message itself. <img width="813" alt="Screenshot 2024-06-09 at 10 08 27 PM" src="https://github.com/facebook/react/assets/63648/917fbe0c-053c-4204-93db-d68a66e3e874"> Note: The component name (Counter) is lost in the first message because we don't print it in the Task. We use `"use client"` instead because we expect the next stack frame to have the name. We also don't include it in the actual error message because the Server doesn't know the component name yet. Ideally Client References should be able to have a name. If the nearest is a Host Component then we do use the name though. However, it's not actually inside that Component that the error happens it's in App and that points to the right line number. An interesting case is that if something that's actually going to be consumed by the props to a Suspense/Error Boundary or the Client Component that wraps them fails, then it can't be handled by the boundary. However, a counter intuitive case might be when that's on the `children` props. E.g. `<ErrorBoundary>{clientReferenceOrInvalidSerialization}</ErrorBoundary>`. This value can be inspected by the boundary so it's not safe to pass it so if it's errored it is not caught. ## Implementation The first insight is that this is best solved on the Client rather than in the Server because that way it also covers Client References that end up erroring. The key insight is that while we don't have a true stack when using `JSON.parse` and therefore no begin/complete we can still infer these phases for Elements because the first child of an Element is always `'$'` which is also a leaf. In depth first that's our begin phase. When the Element itself completes, we have the complete phase. Anything in between is within the Element. Using this idea I was able to refactor the blocking tracking mechanism to stash the blocked information on `initializingHandler` and then on the way up do we let whatever is nearest handle it - whether that's an Element or the root Chunk. It's kind of like an Algebraic Effect. cc @unstubbable This is something you might want to deep dive into to find more edge cases. I'm sure I've missed something. --------- Co-authored-by: eps1lon <sebastian.silbermann@vercel.com>
This commit is contained in:
parent
383b2a1845
commit
fb57fc5a8a
|
|
@ -571,6 +571,7 @@ module.exports = {
|
||||||
TimeoutID: 'readonly',
|
TimeoutID: 'readonly',
|
||||||
WheelEventHandler: 'readonly',
|
WheelEventHandler: 'readonly',
|
||||||
FinalizationRegistry: 'readonly',
|
FinalizationRegistry: 'readonly',
|
||||||
|
Omit: 'readonly',
|
||||||
|
|
||||||
spyOnDev: 'readonly',
|
spyOnDev: 'readonly',
|
||||||
spyOnDevAndProd: 'readonly',
|
spyOnDevAndProd: 'readonly',
|
||||||
|
|
|
||||||
306
packages/react-client/src/ReactFlightClient.js
vendored
306
packages/react-client/src/ReactFlightClient.js
vendored
|
|
@ -72,6 +72,8 @@ import {
|
||||||
|
|
||||||
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
||||||
|
|
||||||
|
import isArray from 'shared/isArray';
|
||||||
|
|
||||||
export type {CallServerCallback, EncodeFormActionCallback};
|
export type {CallServerCallback, EncodeFormActionCallback};
|
||||||
|
|
||||||
interface FlightStreamController {
|
interface FlightStreamController {
|
||||||
|
|
@ -101,7 +103,6 @@ type RowParserState = 0 | 1 | 2 | 3 | 4;
|
||||||
|
|
||||||
const PENDING = 'pending';
|
const PENDING = 'pending';
|
||||||
const BLOCKED = 'blocked';
|
const BLOCKED = 'blocked';
|
||||||
const CYCLIC = 'cyclic';
|
|
||||||
const RESOLVED_MODEL = 'resolved_model';
|
const RESOLVED_MODEL = 'resolved_model';
|
||||||
const RESOLVED_MODULE = 'resolved_module';
|
const RESOLVED_MODULE = 'resolved_module';
|
||||||
const INITIALIZED = 'fulfilled';
|
const INITIALIZED = 'fulfilled';
|
||||||
|
|
@ -123,14 +124,6 @@ type BlockedChunk<T> = {
|
||||||
_debugInfo?: null | ReactDebugInfo,
|
_debugInfo?: null | ReactDebugInfo,
|
||||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||||
};
|
};
|
||||||
type CyclicChunk<T> = {
|
|
||||||
status: 'cyclic',
|
|
||||||
value: null | Array<(T) => mixed>,
|
|
||||||
reason: null | Array<(mixed) => mixed>,
|
|
||||||
_response: Response,
|
|
||||||
_debugInfo?: null | ReactDebugInfo,
|
|
||||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
|
||||||
};
|
|
||||||
type ResolvedModelChunk<T> = {
|
type ResolvedModelChunk<T> = {
|
||||||
status: 'resolved_model',
|
status: 'resolved_model',
|
||||||
value: UninitializedModel,
|
value: UninitializedModel,
|
||||||
|
|
@ -176,7 +169,6 @@ type ErroredChunk<T> = {
|
||||||
type SomeChunk<T> =
|
type SomeChunk<T> =
|
||||||
| PendingChunk<T>
|
| PendingChunk<T>
|
||||||
| BlockedChunk<T>
|
| BlockedChunk<T>
|
||||||
| CyclicChunk<T>
|
|
||||||
| ResolvedModelChunk<T>
|
| ResolvedModelChunk<T>
|
||||||
| ResolvedModuleChunk<T>
|
| ResolvedModuleChunk<T>
|
||||||
| InitializedChunk<T>
|
| InitializedChunk<T>
|
||||||
|
|
@ -218,7 +210,6 @@ Chunk.prototype.then = function <T>(
|
||||||
break;
|
break;
|
||||||
case PENDING:
|
case PENDING:
|
||||||
case BLOCKED:
|
case BLOCKED:
|
||||||
case CYCLIC:
|
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
if (chunk.value === null) {
|
if (chunk.value === null) {
|
||||||
chunk.value = ([]: Array<(T) => mixed>);
|
chunk.value = ([]: Array<(T) => mixed>);
|
||||||
|
|
@ -278,7 +269,6 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
|
||||||
return chunk.value;
|
return chunk.value;
|
||||||
case PENDING:
|
case PENDING:
|
||||||
case BLOCKED:
|
case BLOCKED:
|
||||||
case CYCLIC:
|
|
||||||
// eslint-disable-next-line no-throw-literal
|
// eslint-disable-next-line no-throw-literal
|
||||||
throw ((chunk: any): Thenable<T>);
|
throw ((chunk: any): Thenable<T>);
|
||||||
default:
|
default:
|
||||||
|
|
@ -327,7 +317,6 @@ function wakeChunkIfInitialized<T>(
|
||||||
break;
|
break;
|
||||||
case PENDING:
|
case PENDING:
|
||||||
case BLOCKED:
|
case BLOCKED:
|
||||||
case CYCLIC:
|
|
||||||
if (chunk.value) {
|
if (chunk.value) {
|
||||||
for (let i = 0; i < resolveListeners.length; i++) {
|
for (let i = 0; i < resolveListeners.length; i++) {
|
||||||
chunk.value.push(resolveListeners[i]);
|
chunk.value.push(resolveListeners[i]);
|
||||||
|
|
@ -501,51 +490,61 @@ function resolveModuleChunk<T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let initializingChunk: ResolvedModelChunk<any> = (null: any);
|
type InitializationHandler = {
|
||||||
let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
|
parent: null | InitializationHandler,
|
||||||
|
chunk: null | BlockedChunk<any>,
|
||||||
|
value: any,
|
||||||
|
deps: number,
|
||||||
|
errored: boolean,
|
||||||
|
};
|
||||||
|
let initializingHandler: null | InitializationHandler = null;
|
||||||
|
|
||||||
function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||||
const prevChunk = initializingChunk;
|
const prevHandler = initializingHandler;
|
||||||
const prevBlocked = initializingChunkBlockedModel;
|
initializingHandler = null;
|
||||||
initializingChunk = chunk;
|
|
||||||
initializingChunkBlockedModel = null;
|
|
||||||
|
|
||||||
const resolvedModel = chunk.value;
|
const resolvedModel = chunk.value;
|
||||||
|
|
||||||
// We go to the CYCLIC state until we've fully resolved this.
|
// We go to the BLOCKED state until we've fully resolved this.
|
||||||
// We do this before parsing in case we try to initialize the same chunk
|
// We do this before parsing in case we try to initialize the same chunk
|
||||||
// while parsing the model. Such as in a cyclic reference.
|
// while parsing the model. Such as in a cyclic reference.
|
||||||
const cyclicChunk: CyclicChunk<T> = (chunk: any);
|
const cyclicChunk: BlockedChunk<T> = (chunk: any);
|
||||||
cyclicChunk.status = CYCLIC;
|
cyclicChunk.status = BLOCKED;
|
||||||
cyclicChunk.value = null;
|
cyclicChunk.value = null;
|
||||||
cyclicChunk.reason = null;
|
cyclicChunk.reason = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const value: T = parseModel(chunk._response, resolvedModel);
|
const value: T = parseModel(chunk._response, resolvedModel);
|
||||||
if (
|
// Invoke any listeners added while resolving this model. I.e. cyclic
|
||||||
initializingChunkBlockedModel !== null &&
|
// references. This may or may not fully resolve the model depending on
|
||||||
initializingChunkBlockedModel.deps > 0
|
// if they were blocked.
|
||||||
) {
|
|
||||||
initializingChunkBlockedModel.value = value;
|
|
||||||
// We discovered new dependencies on modules that are not yet resolved.
|
|
||||||
// We have to go the BLOCKED state until they're resolved.
|
|
||||||
const blockedChunk: BlockedChunk<T> = (chunk: any);
|
|
||||||
blockedChunk.status = BLOCKED;
|
|
||||||
} else {
|
|
||||||
const resolveListeners = cyclicChunk.value;
|
const resolveListeners = cyclicChunk.value;
|
||||||
|
if (resolveListeners !== null) {
|
||||||
|
cyclicChunk.value = null;
|
||||||
|
cyclicChunk.reason = null;
|
||||||
|
wakeChunk(resolveListeners, value);
|
||||||
|
}
|
||||||
|
if (initializingHandler !== null) {
|
||||||
|
if (initializingHandler.errored) {
|
||||||
|
throw initializingHandler.value;
|
||||||
|
}
|
||||||
|
if (initializingHandler.deps > 0) {
|
||||||
|
// We discovered new dependencies on modules that are not yet resolved.
|
||||||
|
// We have to keep the BLOCKED state until they're resolved.
|
||||||
|
initializingHandler.value = value;
|
||||||
|
initializingHandler.chunk = cyclicChunk;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
||||||
initializedChunk.status = INITIALIZED;
|
initializedChunk.status = INITIALIZED;
|
||||||
initializedChunk.value = value;
|
initializedChunk.value = value;
|
||||||
if (resolveListeners !== null) {
|
|
||||||
wakeChunk(resolveListeners, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
||||||
erroredChunk.status = ERRORED;
|
erroredChunk.status = ERRORED;
|
||||||
erroredChunk.reason = error;
|
erroredChunk.reason = error;
|
||||||
} finally {
|
} finally {
|
||||||
initializingChunk = prevChunk;
|
initializingHandler = prevHandler;
|
||||||
initializingChunkBlockedModel = prevBlocked;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -626,7 +625,9 @@ function createElement(
|
||||||
owner: null | ReactComponentInfo, // DEV-only
|
owner: null | ReactComponentInfo, // DEV-only
|
||||||
stack: null | string, // DEV-only
|
stack: null | string, // DEV-only
|
||||||
validated: number, // DEV-only
|
validated: number, // DEV-only
|
||||||
): React$Element<any> {
|
):
|
||||||
|
| React$Element<any>
|
||||||
|
| LazyComponent<React$Element<any>, SomeChunk<React$Element<any>>> {
|
||||||
let element: any;
|
let element: any;
|
||||||
if (__DEV__ && enableRefAsProp) {
|
if (__DEV__ && enableRefAsProp) {
|
||||||
// `ref` is non-enumerable in dev
|
// `ref` is non-enumerable in dev
|
||||||
|
|
@ -723,15 +724,60 @@ function createElement(
|
||||||
value: task,
|
value: task,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initializingHandler !== null) {
|
||||||
|
const handler = initializingHandler;
|
||||||
|
// We pop the stack to the previous outer handler before leaving the Element.
|
||||||
|
// This is effectively the complete phase.
|
||||||
|
initializingHandler = handler.parent;
|
||||||
|
if (handler.errored) {
|
||||||
|
// Something errored inside this Element's props. We can turn this Element
|
||||||
|
// into a Lazy so that we can still render up until that Lazy is rendered.
|
||||||
|
const erroredChunk: ErroredChunk<React$Element<any>> = createErrorChunk(
|
||||||
|
response,
|
||||||
|
handler.value,
|
||||||
|
);
|
||||||
|
if (__DEV__) {
|
||||||
|
// Conceptually the error happened inside this Element but right before
|
||||||
|
// it was rendered. We don't have a client side component to render but
|
||||||
|
// we can add some DebugInfo to explain that this was conceptually a
|
||||||
|
// Server side error that errored inside this element. That way any stack
|
||||||
|
// traces will point to the nearest JSX that errored - e.g. during
|
||||||
|
// serialization.
|
||||||
|
const erroredComponent: ReactComponentInfo = {
|
||||||
|
name: getComponentNameFromType(element.type) || '',
|
||||||
|
owner: element._owner,
|
||||||
|
};
|
||||||
|
if (enableOwnerStacks) {
|
||||||
|
// $FlowFixMe[cannot-write]
|
||||||
|
erroredComponent.stack = element._debugStack;
|
||||||
|
// $FlowFixMe[cannot-write]
|
||||||
|
erroredComponent.task = element._debugTask;
|
||||||
|
}
|
||||||
|
erroredChunk._debugInfo = [erroredComponent];
|
||||||
|
}
|
||||||
|
return createLazyChunkWrapper(erroredChunk);
|
||||||
|
}
|
||||||
|
if (handler.deps > 0) {
|
||||||
|
// We have blocked references inside this Element but we can turn this into
|
||||||
|
// a Lazy node referencing this Element to let everything around it proceed.
|
||||||
|
const blockedChunk: BlockedChunk<React$Element<any>> =
|
||||||
|
createBlockedChunk(response);
|
||||||
|
handler.value = element;
|
||||||
|
handler.chunk = blockedChunk;
|
||||||
|
if (__DEV__) {
|
||||||
|
const freeze = Object.freeze.bind(Object, element.props);
|
||||||
|
blockedChunk.then(freeze, freeze);
|
||||||
|
}
|
||||||
|
return createLazyChunkWrapper(blockedChunk);
|
||||||
|
}
|
||||||
|
} else if (__DEV__) {
|
||||||
// TODO: We should be freezing the element but currently, we might write into
|
// TODO: We should be freezing the element but currently, we might write into
|
||||||
// _debugInfo later. We could move it into _store which remains mutable.
|
// _debugInfo later. We could move it into _store which remains mutable.
|
||||||
if (initializingChunkBlockedModel !== null) {
|
|
||||||
const freeze = Object.freeze.bind(Object, element.props);
|
|
||||||
initializingChunk.then(freeze, freeze);
|
|
||||||
} else {
|
|
||||||
Object.freeze(element.props);
|
Object.freeze(element.props);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -762,57 +808,129 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createModelResolver<T>(
|
function waitForReference<T>(
|
||||||
chunk: SomeChunk<T>,
|
referencedChunk: PendingChunk<T> | BlockedChunk<T>,
|
||||||
parentObject: Object,
|
parentObject: Object,
|
||||||
key: string,
|
key: string,
|
||||||
cyclic: boolean,
|
|
||||||
response: Response,
|
response: Response,
|
||||||
map: (response: Response, model: any) => T,
|
map: (response: Response, model: any) => T,
|
||||||
path: Array<string>,
|
path: Array<string>,
|
||||||
): (value: any) => void {
|
): T {
|
||||||
let blocked;
|
let handler: InitializationHandler;
|
||||||
if (initializingChunkBlockedModel) {
|
if (initializingHandler) {
|
||||||
blocked = initializingChunkBlockedModel;
|
handler = initializingHandler;
|
||||||
if (!cyclic) {
|
handler.deps++;
|
||||||
blocked.deps++;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
blocked = initializingChunkBlockedModel = {
|
handler = initializingHandler = {
|
||||||
deps: cyclic ? 0 : 1,
|
parent: null,
|
||||||
value: (null: any),
|
chunk: null,
|
||||||
|
value: null,
|
||||||
|
deps: 1,
|
||||||
|
errored: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return value => {
|
|
||||||
|
function fulfill(value: any): void {
|
||||||
for (let i = 1; i < path.length; i++) {
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
while (value.$$typeof === REACT_LAZY_TYPE) {
|
||||||
|
// We never expect to see a Lazy node on this path because we encode those as
|
||||||
|
// separate models. This must mean that we have inserted an extra lazy node
|
||||||
|
// e.g. to replace a blocked element. We must instead look for it inside.
|
||||||
|
const chunk: SomeChunk<any> = value._payload;
|
||||||
|
if (chunk === handler.chunk) {
|
||||||
|
// This is a reference to the thing we're currently blocking. We can peak
|
||||||
|
// inside of it to get the value.
|
||||||
|
value = handler.value;
|
||||||
|
continue;
|
||||||
|
} else if (chunk.status === INITIALIZED) {
|
||||||
|
value = chunk.value;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// If we're not yet initialized we need to skip what we've already drilled
|
||||||
|
// through and then wait for the next value to become available.
|
||||||
|
path.splice(0, i - 1);
|
||||||
|
chunk.then(fulfill, reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
value = value[path[i]];
|
value = value[path[i]];
|
||||||
}
|
}
|
||||||
parentObject[key] = map(response, value);
|
parentObject[key] = map(response, value);
|
||||||
|
|
||||||
// If this is the root object for a model reference, where `blocked.value`
|
// If this is the root object for a model reference, where `handler.value`
|
||||||
// is a stale `null`, the resolved value can be used directly.
|
// is a stale `null`, the resolved value can be used directly.
|
||||||
if (key === '' && blocked.value === null) {
|
if (key === '' && handler.value === null) {
|
||||||
blocked.value = parentObject[key];
|
handler.value = parentObject[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked.deps--;
|
handler.deps--;
|
||||||
if (blocked.deps === 0) {
|
|
||||||
if (chunk.status !== BLOCKED) {
|
if (handler.deps === 0) {
|
||||||
|
const chunk = handler.chunk;
|
||||||
|
if (chunk === null || chunk.status !== BLOCKED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resolveListeners = chunk.value;
|
const resolveListeners = chunk.value;
|
||||||
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
||||||
initializedChunk.status = INITIALIZED;
|
initializedChunk.status = INITIALIZED;
|
||||||
initializedChunk.value = blocked.value;
|
initializedChunk.value = handler.value;
|
||||||
if (resolveListeners !== null) {
|
if (resolveListeners !== null) {
|
||||||
wakeChunk(resolveListeners, blocked.value);
|
wakeChunk(resolveListeners, handler.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
|
function reject(error: mixed): void {
|
||||||
return (error: mixed) => triggerErrorOnChunk(chunk, error);
|
if (handler.errored) {
|
||||||
|
// We've already errored. We could instead build up an AggregateError
|
||||||
|
// but if there are multiple errors we just take the first one like
|
||||||
|
// Promise.all.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blockedValue = handler.value;
|
||||||
|
handler.errored = true;
|
||||||
|
handler.value = error;
|
||||||
|
const chunk = handler.chunk;
|
||||||
|
if (chunk === null || chunk.status !== BLOCKED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
if (
|
||||||
|
typeof blockedValue === 'object' &&
|
||||||
|
blockedValue !== null &&
|
||||||
|
blockedValue.$$typeof === REACT_ELEMENT_TYPE
|
||||||
|
) {
|
||||||
|
const element = blockedValue;
|
||||||
|
// Conceptually the error happened inside this Element but right before
|
||||||
|
// it was rendered. We don't have a client side component to render but
|
||||||
|
// we can add some DebugInfo to explain that this was conceptually a
|
||||||
|
// Server side error that errored inside this element. That way any stack
|
||||||
|
// traces will point to the nearest JSX that errored - e.g. during
|
||||||
|
// serialization.
|
||||||
|
const erroredComponent: ReactComponentInfo = {
|
||||||
|
name: getComponentNameFromType(element.type) || '',
|
||||||
|
owner: element._owner,
|
||||||
|
};
|
||||||
|
if (enableOwnerStacks) {
|
||||||
|
// $FlowFixMe[cannot-write]
|
||||||
|
erroredComponent.stack = element._debugStack;
|
||||||
|
// $FlowFixMe[cannot-write]
|
||||||
|
erroredComponent.task = element._debugTask;
|
||||||
|
}
|
||||||
|
const chunkDebugInfo: ReactDebugInfo =
|
||||||
|
chunk._debugInfo || (chunk._debugInfo = []);
|
||||||
|
chunkDebugInfo.push(erroredComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerErrorOnChunk(chunk, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
referencedChunk.then(fulfill, reject);
|
||||||
|
|
||||||
|
// Return a place holder value for now.
|
||||||
|
return (null: any);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createServerReferenceProxy<A: Iterable<any>, T>(
|
function createServerReferenceProxy<A: Iterable<any>, T>(
|
||||||
|
|
@ -880,7 +998,7 @@ function getOutlinedModel<T>(
|
||||||
if (
|
if (
|
||||||
typeof chunkValue === 'object' &&
|
typeof chunkValue === 'object' &&
|
||||||
chunkValue !== null &&
|
chunkValue !== null &&
|
||||||
(Array.isArray(chunkValue) ||
|
(isArray(chunkValue) ||
|
||||||
typeof chunkValue[ASYNC_ITERATOR] === 'function' ||
|
typeof chunkValue[ASYNC_ITERATOR] === 'function' ||
|
||||||
chunkValue.$$typeof === REACT_ELEMENT_TYPE) &&
|
chunkValue.$$typeof === REACT_ELEMENT_TYPE) &&
|
||||||
!chunkValue._debugInfo
|
!chunkValue._debugInfo
|
||||||
|
|
@ -898,23 +1016,24 @@ function getOutlinedModel<T>(
|
||||||
return chunkValue;
|
return chunkValue;
|
||||||
case PENDING:
|
case PENDING:
|
||||||
case BLOCKED:
|
case BLOCKED:
|
||||||
case CYCLIC:
|
return waitForReference(chunk, parentObject, key, response, map, path);
|
||||||
const parentChunk = initializingChunk;
|
|
||||||
chunk.then(
|
|
||||||
createModelResolver(
|
|
||||||
parentChunk,
|
|
||||||
parentObject,
|
|
||||||
key,
|
|
||||||
chunk.status === CYCLIC,
|
|
||||||
response,
|
|
||||||
map,
|
|
||||||
path,
|
|
||||||
),
|
|
||||||
createModelReject(parentChunk),
|
|
||||||
);
|
|
||||||
return (null: any);
|
|
||||||
default:
|
default:
|
||||||
throw chunk.reason;
|
// This is an error. Instead of erroring directly, we're going to encode this on
|
||||||
|
// an initialization handler so that we can catch it at the nearest Element.
|
||||||
|
if (initializingHandler) {
|
||||||
|
initializingHandler.errored = true;
|
||||||
|
initializingHandler.value = chunk.reason;
|
||||||
|
} else {
|
||||||
|
initializingHandler = {
|
||||||
|
parent: null,
|
||||||
|
chunk: null,
|
||||||
|
value: chunk.reason,
|
||||||
|
deps: 0,
|
||||||
|
errored: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Placeholder
|
||||||
|
return (null: any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -962,6 +1081,19 @@ function parseModelString(
|
||||||
if (value[0] === '$') {
|
if (value[0] === '$') {
|
||||||
if (value === '$') {
|
if (value === '$') {
|
||||||
// A very common symbol.
|
// A very common symbol.
|
||||||
|
if (initializingHandler !== null && key === '0') {
|
||||||
|
// We we already have an initializing handler and we're abound to enter
|
||||||
|
// a new element, we need to shadow it because we're now in a new scope.
|
||||||
|
// This is effectively the "begin" or "push" phase of Element parsing.
|
||||||
|
// We'll pop later when we parse the array itself.
|
||||||
|
initializingHandler = {
|
||||||
|
parent: initializingHandler,
|
||||||
|
chunk: null,
|
||||||
|
value: null,
|
||||||
|
deps: 0,
|
||||||
|
errored: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
return REACT_ELEMENT_TYPE;
|
return REACT_ELEMENT_TYPE;
|
||||||
}
|
}
|
||||||
switch (value[1]) {
|
switch (value[1]) {
|
||||||
|
|
|
||||||
|
|
@ -1104,6 +1104,46 @@ describe('ReactFlight', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle serialization errors in element inside error boundary', async () => {
|
||||||
|
const ClientErrorBoundary = clientReference(ErrorBoundary);
|
||||||
|
|
||||||
|
const expectedStack = __DEV__
|
||||||
|
? '\n in div' + '\n in ErrorBoundary (at **)' + '\n in App'
|
||||||
|
: '\n in ErrorBoundary (at **)';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ClientErrorBoundary
|
||||||
|
expectedMessage="Event handlers cannot be passed to Client Component props."
|
||||||
|
expectedStack={expectedStack}>
|
||||||
|
<div onClick={function () {}} />
|
||||||
|
</ClientErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transport = ReactNoopFlightServer.render(<App />, {
|
||||||
|
onError(x) {
|
||||||
|
if (__DEV__) {
|
||||||
|
return 'a dev digest';
|
||||||
|
}
|
||||||
|
if (x instanceof Error) {
|
||||||
|
return `digest("${x.message}")`;
|
||||||
|
} else if (Array.isArray(x)) {
|
||||||
|
return `digest([])`;
|
||||||
|
} else if (typeof x === 'object' && x !== null) {
|
||||||
|
return `digest({})`;
|
||||||
|
}
|
||||||
|
return `digest(${String(x)})`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
startTransition(() => {
|
||||||
|
ReactNoop.render(ReactNoopFlightClient.read(transport));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should include server components in warning stacks', async () => {
|
it('should include server components in warning stacks', async () => {
|
||||||
function Component() {
|
function Component() {
|
||||||
// Trigger key warning
|
// Trigger key warning
|
||||||
|
|
|
||||||
|
|
@ -4129,7 +4129,7 @@ function beginWork(
|
||||||
}
|
}
|
||||||
case Throw: {
|
case Throw: {
|
||||||
// This represents a Component that threw in the reconciliation phase.
|
// This represents a Component that threw in the reconciliation phase.
|
||||||
// So we'll rethrow here. This might be
|
// So we'll rethrow here. This might be a Thenable.
|
||||||
throw workInProgress.pendingProps;
|
throw workInProgress.pendingProps;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
|
||||||
break;
|
break;
|
||||||
case Throw: {
|
case Throw: {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// For an error in child position we use the of the inner most parent component.
|
// For an error in child position we use the name of the inner most parent component.
|
||||||
// Whether a Server Component or the parent Fiber.
|
// Whether a Server Component or the parent Fiber.
|
||||||
const debugInfo = fiber._debugInfo;
|
const debugInfo = fiber._debugInfo;
|
||||||
if (debugInfo != null) {
|
if (debugInfo != null) {
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,53 @@ describe('ReactFlightDOMBrowser', () => {
|
||||||
expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
|
expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should resolve deduped objects that are themselves blocked', async () => {
|
||||||
|
let resolveClientComponentChunk;
|
||||||
|
|
||||||
|
const Client = clientExports(
|
||||||
|
[4, 5],
|
||||||
|
'42',
|
||||||
|
'/test.js',
|
||||||
|
new Promise(resolve => (resolveClientComponentChunk = resolve)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const shared = [1, 2, 3, Client];
|
||||||
|
|
||||||
|
const stream = await serverAct(() =>
|
||||||
|
ReactServerDOMServer.renderToReadableStream(
|
||||||
|
<div>
|
||||||
|
<Suspense fallback="Loading">
|
||||||
|
<span>
|
||||||
|
{shared /* this will serialize first and block nearest element */}
|
||||||
|
</span>
|
||||||
|
</Suspense>
|
||||||
|
{shared /* this will be referenced inside the blocked element */}
|
||||||
|
</div>,
|
||||||
|
webpackMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
function ClientRoot({response}) {
|
||||||
|
return use(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = ReactServerDOMClient.createFromReadableStream(stream);
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const root = ReactDOMClient.createRoot(container);
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
root.render(<ClientRoot response={response} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.innerHTML).toBe('');
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
resolveClientComponentChunk();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.innerHTML).toBe('<div><span>12345</span>12345</div>');
|
||||||
|
});
|
||||||
|
|
||||||
it('should progressively reveal server components', async () => {
|
it('should progressively reveal server components', async () => {
|
||||||
let reportedErrors = [];
|
let reportedErrors = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -224,14 +224,12 @@ describe('ReactFlightDOMEdge', () => {
|
||||||
this: {is: 'a large objected'},
|
this: {is: 'a large objected'},
|
||||||
with: {many: 'properties in it'},
|
with: {many: 'properties in it'},
|
||||||
};
|
};
|
||||||
const props = {
|
const props = {root: <div>{new Array(30).fill(obj)}</div>};
|
||||||
items: new Array(30).fill(obj),
|
|
||||||
};
|
|
||||||
const stream = ReactServerDOMServer.renderToReadableStream(props);
|
const stream = ReactServerDOMServer.renderToReadableStream(props);
|
||||||
const [stream1, stream2] = passThrough(stream).tee();
|
const [stream1, stream2] = passThrough(stream).tee();
|
||||||
|
|
||||||
const serializedContent = await readResult(stream1);
|
const serializedContent = await readResult(stream1);
|
||||||
expect(serializedContent.length).toBeLessThan(470);
|
expect(serializedContent.length).toBeLessThan(1100);
|
||||||
|
|
||||||
const result = await ReactServerDOMClient.createFromReadableStream(
|
const result = await ReactServerDOMClient.createFromReadableStream(
|
||||||
stream2,
|
stream2,
|
||||||
|
|
@ -242,10 +240,13 @@ describe('ReactFlightDOMEdge', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// TODO: Cyclic references currently cause a Lazy wrapper which is not ideal.
|
||||||
|
const resultElement = result.root._init(result.root._payload);
|
||||||
// Should still match the result when parsed
|
// Should still match the result when parsed
|
||||||
expect(result).toEqual(props);
|
expect(resultElement).toEqual(props.root);
|
||||||
expect(result.items[5]).toBe(result.items[10]); // two random items are the same instance
|
expect(resultElement.props.children[5]).toBe(
|
||||||
// TODO: items[0] is not the same as the others in this case
|
resultElement.props.children[10],
|
||||||
|
); // two random items are the same instance
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute repeated server components only once', async () => {
|
it('should execute repeated server components only once', async () => {
|
||||||
|
|
|
||||||
50
packages/react-server/src/ReactFlightServer.js
vendored
50
packages/react-server/src/ReactFlightServer.js
vendored
|
|
@ -1054,13 +1054,14 @@ function renderFunctionComponent<Props>(
|
||||||
request.pendingChunks++;
|
request.pendingChunks++;
|
||||||
|
|
||||||
const componentDebugID = debugID;
|
const componentDebugID = debugID;
|
||||||
componentDebugInfo = {
|
componentDebugInfo = ({
|
||||||
name: componentName,
|
name: componentName,
|
||||||
env: request.environmentName,
|
env: request.environmentName,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
};
|
}: ReactComponentInfo);
|
||||||
if (enableOwnerStacks) {
|
if (enableOwnerStacks) {
|
||||||
(componentDebugInfo: any).stack = stack;
|
// $FlowFixMe[cannot-write]
|
||||||
|
componentDebugInfo.stack = stack;
|
||||||
}
|
}
|
||||||
// We outline this model eagerly so that we can refer to by reference as an owner.
|
// We outline this model eagerly so that we can refer to by reference as an owner.
|
||||||
// If we had a smarter way to dedupe we might not have to do this if there ends up
|
// If we had a smarter way to dedupe we might not have to do this if there ends up
|
||||||
|
|
@ -2076,20 +2077,19 @@ function renderModel(
|
||||||
task.keyPath = prevKeyPath;
|
task.keyPath = prevKeyPath;
|
||||||
task.implicitSlot = prevImplicitSlot;
|
task.implicitSlot = prevImplicitSlot;
|
||||||
|
|
||||||
if (wasReactNode) {
|
|
||||||
// Something errored. We'll still send everything we have up until this point.
|
// Something errored. We'll still send everything we have up until this point.
|
||||||
// We'll replace this element with a lazy reference that throws on the client
|
|
||||||
// once it gets rendered.
|
|
||||||
request.pendingChunks++;
|
request.pendingChunks++;
|
||||||
const errorId = request.nextChunkId++;
|
const errorId = request.nextChunkId++;
|
||||||
const digest = logRecoverableError(request, x);
|
const digest = logRecoverableError(request, x);
|
||||||
emitErrorChunk(request, errorId, digest, x);
|
emitErrorChunk(request, errorId, digest, x);
|
||||||
|
if (wasReactNode) {
|
||||||
|
// We'll replace this element with a lazy reference that throws on the client
|
||||||
|
// once it gets rendered.
|
||||||
return serializeLazyID(errorId);
|
return serializeLazyID(errorId);
|
||||||
}
|
}
|
||||||
// Something errored but it was not in a React Node. There's no need to serialize
|
// If we don't know if it was a React Node we render a direct reference and let
|
||||||
// it by value because it'll just error the whole parent row anyway so we can
|
// the client deal with it.
|
||||||
// just stop any siblings and error the whole parent row.
|
return serializeByValueID(errorId);
|
||||||
throw x;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2117,6 +2117,7 @@ function renderModelDestructive(
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
switch ((value: any).$$typeof) {
|
switch ((value: any).$$typeof) {
|
||||||
case REACT_ELEMENT_TYPE: {
|
case REACT_ELEMENT_TYPE: {
|
||||||
|
let elementReference = null;
|
||||||
const writtenObjects = request.writtenObjects;
|
const writtenObjects = request.writtenObjects;
|
||||||
if (task.keyPath !== null || task.implicitSlot) {
|
if (task.keyPath !== null || task.implicitSlot) {
|
||||||
// If we're in some kind of context we can't reuse the result of this render or
|
// If we're in some kind of context we can't reuse the result of this render or
|
||||||
|
|
@ -2145,10 +2146,8 @@ function renderModelDestructive(
|
||||||
if (parentReference !== undefined) {
|
if (parentReference !== undefined) {
|
||||||
// If the parent has a reference, we can refer to this object indirectly
|
// If the parent has a reference, we can refer to this object indirectly
|
||||||
// through the property name inside that parent.
|
// through the property name inside that parent.
|
||||||
writtenObjects.set(
|
elementReference = parentReference + ':' + parentPropertyName;
|
||||||
value,
|
writtenObjects.set(value, elementReference);
|
||||||
parentReference + ':' + parentPropertyName,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2183,7 +2182,7 @@ function renderModelDestructive(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to render the Server Component.
|
// Attempt to render the Server Component.
|
||||||
return renderElement(
|
const newChild = renderElement(
|
||||||
request,
|
request,
|
||||||
task,
|
task,
|
||||||
element.type,
|
element.type,
|
||||||
|
|
@ -2199,6 +2198,18 @@ function renderModelDestructive(
|
||||||
: null,
|
: null,
|
||||||
__DEV__ && enableOwnerStacks ? element._store.validated : 0,
|
__DEV__ && enableOwnerStacks ? element._store.validated : 0,
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
|
typeof newChild === 'object' &&
|
||||||
|
newChild !== null &&
|
||||||
|
elementReference !== null
|
||||||
|
) {
|
||||||
|
// If this element renders another object, we can now refer to that object through
|
||||||
|
// the same location as this element.
|
||||||
|
if (!writtenObjects.has(newChild)) {
|
||||||
|
writtenObjects.set(newChild, elementReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newChild;
|
||||||
}
|
}
|
||||||
case REACT_LAZY_TYPE: {
|
case REACT_LAZY_TYPE: {
|
||||||
// Reset the task's thenable state before continuing. If there was one, it was
|
// Reset the task's thenable state before continuing. If there was one, it was
|
||||||
|
|
@ -2478,15 +2489,16 @@ function renderModelDestructive(
|
||||||
) {
|
) {
|
||||||
// This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we
|
// This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we
|
||||||
// need to omit it before serializing.
|
// need to omit it before serializing.
|
||||||
const componentDebugInfo = {
|
const componentDebugInfo: Omit<ReactComponentInfo, 'task'> = {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
env: value.env,
|
env: value.env,
|
||||||
owner: value.owner,
|
owner: (value: any).owner,
|
||||||
};
|
};
|
||||||
if (enableOwnerStacks) {
|
if (enableOwnerStacks) {
|
||||||
(componentDebugInfo: any).stack = (value: any).stack;
|
// $FlowFixMe[cannot-write]
|
||||||
|
componentDebugInfo.stack = (value: any).stack;
|
||||||
}
|
}
|
||||||
return (componentDebugInfo: any);
|
return componentDebugInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (objectName(value) !== 'Object') {
|
if (objectName(value) !== 'Object') {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user