Skip to content
Open
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
58 changes: 45 additions & 13 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ type InitializationHandler = {
value: any,
reason: any,
deps: number,
debugDeps: number,
errored: boolean,
};
let initializingHandler: null | InitializationHandler = null;
Expand Down Expand Up @@ -1412,7 +1413,7 @@ function createElement(
}
return createLazyChunkWrapper(erroredChunk, validated);
}
if (handler.deps > 0) {
if (handler.deps > (__DEV__ ? handler.debugDeps : 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>> =
Expand All @@ -1427,6 +1428,9 @@ function createElement(
}
return lazyNode;
}
if (__DEV__ && handler.debugDeps > 0) {
handler.value = element;
}
}
if (__DEV__) {
initializeElement(response, element, null);
Expand Down Expand Up @@ -1669,6 +1673,19 @@ function fulfillReference(
if (handler.deps === 0) {
const chunk = handler.chunk;
if (chunk === null || chunk.status !== BLOCKED) {
if (__DEV__ && chunk === null && handler.debugDeps > 0) {
// All debug-only deps (owner/stack from debug channel) have resolved.
// Re-initialize the element so _debugStack gets normalized into an Error
// object now that the raw stack string is available.
const elementValue = handler.value;
if (
typeof elementValue === 'object' &&
elementValue !== null &&
elementValue.$$typeof === REACT_ELEMENT_TYPE
) {
initializeElement(response, elementValue, null);
}
}
return;
}
const resolveListeners = chunk.value;
Expand Down Expand Up @@ -1745,37 +1762,46 @@ function waitForReference<T>(
path: Array<string>,
isAwaitingDebugInfo: boolean, // DEV-only
): T {
if (
__DEV__ &&
(response._debugChannel === undefined ||
!response._debugChannel.hasReadable)
) {
let isDebugOnlyDep = false;
if (__DEV__) {
if (
referencedChunk.status === PENDING &&
parentObject[0] === REACT_ELEMENT_TYPE &&
(key === '4' || key === '5')
) {
// If the parent object is an unparsed React element tuple, and this is a reference
// to the owner or debug stack. Then we expect the chunk to have been emitted earlier
// in the stream. It might be blocked on other things but chunk should no longer be pending.
// If it's still pending that suggests that it was referencing an object in the debug
// channel, but no debug channel was wired up so it's missing. In this case we can just
// drop the debug info instead of halting the whole stream.
return (null: any);
// The parent object is an unparsed React element tuple and this is a reference
// to the owner (pos 4) or debug stack (pos 5) — dev-only metadata.
if (
response._debugChannel === undefined ||
!response._debugChannel.hasReadable
) {
// No debug channel is wired up so this data will never arrive.
// Drop the reference instead of halting the stream.
return (null: any);
}
// A debug channel with a readable is active: the data will arrive, but
// owner/stack must never block element construction because that would
// wrap the element in a lazy chunk ($$typeof: REACT_LAZY_TYPE) and break
// isValidElement(), cloneElement(), and hydration.
isDebugOnlyDep = true;
}
}

let handler: InitializationHandler;
if (initializingHandler) {
handler = initializingHandler;
handler.deps++;
if (__DEV__ && isDebugOnlyDep) {
handler.debugDeps++;
}
} else {
handler = initializingHandler = {
parent: null,
chunk: null,
value: null,
reason: null,
deps: 1,
debugDeps: __DEV__ && isDebugOnlyDep ? 1 : 0,
errored: false,
};
}
Expand Down Expand Up @@ -1865,6 +1891,7 @@ function loadServerReference<A: Iterable<any>, T>(
value: null,
reason: null,
deps: 1,
debugDeps: 0,
errored: false,
};
}
Expand Down Expand Up @@ -2108,6 +2135,7 @@ function getOutlinedModel<T>(
value: null,
reason: null,
deps: 1,
debugDeps: 0,
errored: false,
};
}
Expand All @@ -2127,6 +2155,7 @@ function getOutlinedModel<T>(
value: null,
reason: referencedChunk.reason,
deps: 0,
debugDeps: 0,
errored: true,
};
}
Expand Down Expand Up @@ -2205,6 +2234,7 @@ function getOutlinedModel<T>(
value: null,
reason: null,
deps: 1,
debugDeps: 0,
errored: false,
};
}
Expand All @@ -2224,6 +2254,7 @@ function getOutlinedModel<T>(
value: null,
reason: chunk.reason,
deps: 0,
debugDeps: 0,
errored: true,
};
}
Expand Down Expand Up @@ -2377,6 +2408,7 @@ function parseModelString(
value: null,
reason: null,
deps: 0,
debugDeps: 0,
errored: false,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1909,4 +1909,53 @@ describe('ReactFlightDOMNode', () => {
globalThis.eval = previousEval;
}
});

it('isValidElement returns true for elements deserialized with a delayed debugChannel', async () => {
function ServerApp() {
return ReactServer.createElement('div', null, 'hello');
}

const {delayedStream, resolveDelayedStream} = createDelayedStream();

const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
ReactServer.createElement(ServerApp, null),
webpackMap,
{
debugChannel: new Stream.Writable({
write(chunk, encoding, callback) {
delayedStream.write(chunk, encoding);
callback();
},
final() {
delayedStream.end();
},
}),
},
),
);

const readable = new Stream.PassThrough(streamOptions);
rscStream.pipe(readable);

const serverConsumerManifest = {
moduleMap: {},
moduleLoading: webpackModuleLoading,
};

const response = ReactServerDOMClient.createFromNodeStream(
readable,
serverConsumerManifest,
{debugChannel: delayedStream},
);

setTimeout(resolveDelayedStream);

let deserialized;
await serverAct(async () => {
deserialized = await response;
});

expect(React.isValidElement(deserialized)).toBe(true);
});
});