Skip to content
Draft
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
335 changes: 223 additions & 112 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2320,90 +2320,196 @@ function renderElement(
return renderClientElement(request, task, type, key, props, validated);
}

function visitAsyncNode(
request: Request,
task: Task,
node: AsyncSequence,
visited: Map<
AsyncSequence | ReactDebugInfo,
void | null | PromiseNode | IONode,
>,
cutOff: number,
): void | null | PromiseNode | IONode {
if (visited.has(node)) {
// It's possible to visit them same node twice when it's part of both an "awaited" path
// and a "previous" path. This also gracefully handles cycles which would be a bug.
return visited.get(node);
}
// Set it as visited early in case we see ourselves before returning.
visited.set(node, null);
const result = visitAsyncNodeImpl(request, task, node, visited, cutOff);
if (result !== null) {
// If we ended up with a value, let's use that value for future visits.
visited.set(node, result);
}
return result;
}
// Frame phases for iterative traversal
const VISIT_NODE = 0;
const PROCESS_PREVIOUS = 1;
const PROCESS_AWAITED = 2;

function visitAsyncNodeImpl(
function visitAsyncNode(
request: Request,
task: Task,
node: AsyncSequence,
startNode: AsyncSequence,
visited: Map<
AsyncSequence | ReactDebugInfo,
void | null | PromiseNode | IONode,
>,
cutOff: number,
): void | null | PromiseNode | IONode {
if (node.end >= 0 && node.end <= request.timeOrigin) {
// This was already resolved when we started this render. It must have been either something
// that's part of a start up sequence or externally cached data. We exclude that information.
// The technique for debugging the effects of uncached data on the render is to simply uncache it.
return null;
}
// Explicit stack to avoid recursion. Each frame tracks the node being processed,
// the current phase, and intermediate results.
const stack: Array<{
node: AsyncSequence,
phase: number,
previousIONode: void | null | PromiseNode | IONode,
}> = [];
// Result passed between frames (simulates return values from recursive calls)
let result: void | null | PromiseNode | IONode;

stack.push({node: startNode, phase: VISIT_NODE, previousIONode: null});

while (stack.length > 0) {
const frame = stack[stack.length - 1];
const node = frame.node;

if (frame.phase === VISIT_NODE) {
// Check memoization
if (visited.has(node)) {
// It's possible to visit the same node twice when it's part of both an "awaited" path
// and a "previous" path. This also gracefully handles cycles which would be a bug.
result = visited.get(node);
stack.pop();
continue;
}
// Set it as visited early in case we see ourselves before returning.
visited.set(node, null);

// Early return: already resolved before render started
if (node.end >= 0 && node.end <= request.timeOrigin) {
// This was already resolved when we started this render. It must have been either something
// that's part of a start up sequence or externally cached data. We exclude that information.
// The technique for debugging the effects of uncached data on the render is to simply uncache it.
result = null;
stack.pop();
continue;
}

let previousIONode: void | null | PromiseNode | IONode = null;
// First visit anything that blocked this sequence to start in the first place.
if (node.previous !== null) {
previousIONode = visitAsyncNode(
request,
task,
node.previous,
visited,
cutOff,
);
if (previousIONode === undefined) {
// Undefined is used as a signal that we found a suitable aborted node and we don't have to find
// further aborted nodes.
return undefined;
// Visit previous if it exists
if (node.previous !== null) {
frame.phase = PROCESS_PREVIOUS;
stack.push({node: node.previous, phase: VISIT_NODE, previousIONode: null});
continue;
} else {
frame.previousIONode = null;
frame.phase = PROCESS_PREVIOUS;
// Fall through to PROCESS_PREVIOUS
}
}
}

// `found` represents the return value of the following switch statement.
// We can't use multiple `return` statements in the switch statement
// since that prevents Closure compiler from inlining `visitAsyncImpl`
// thus doubling the call stack size.
let found: void | null | PromiseNode | IONode;
switch (node.tag) {
case IO_NODE: {
found = node;
break;
}
case UNRESOLVED_PROMISE_NODE: {
found = previousIONode;
break;
if (frame.phase === PROCESS_PREVIOUS) {
// Get result from visiting previous (if we visited it)
if (node.previous !== null) {
frame.previousIONode = result;
}
const previousIONode = frame.previousIONode;

if (previousIONode === undefined) {
// Undefined is used as a signal that we found a suitable aborted node and we don't have to find
// further aborted nodes.
result = undefined;
stack.pop();
continue;
}

// Process based on node tag
switch (node.tag) {
case IO_NODE: {
result = node;
if (result !== null) {
visited.set(node, result);
}
stack.pop();
continue;
}
case UNRESOLVED_PROMISE_NODE: {
result = previousIONode;
if (result !== null) {
visited.set(node, result);
}
stack.pop();
continue;
}
case PROMISE_NODE: {
const awaited = node.awaited;
if (awaited !== null) {
frame.phase = PROCESS_AWAITED;
stack.push({node: awaited, phase: VISIT_NODE, previousIONode: null});
continue;
} else {
// No awaited - check aborting case and forward debug info
let match: void | null | PromiseNode | IONode = previousIONode;
const promise = node.promise.deref();
if (request.status === ABORTING) {
if (node.start < request.abortTime && node.end > request.abortTime) {
if (
(node.stack !== null &&
hasUnfilteredFrame(request, node.stack)) ||
(promise !== undefined &&
// $FlowFixMe[prop-missing]
typeof promise.displayName === 'string')
) {
match = node;
}
}
}
if (promise !== undefined) {
const debugInfo = promise._debugInfo;
if (debugInfo != null && !visited.has(debugInfo)) {
visited.set(debugInfo, null);
forwardDebugInfo(request, task, debugInfo);
}
}
result = match;
if (result !== null) {
visited.set(node, result);
}
stack.pop();
continue;
}
}
case UNRESOLVED_AWAIT_NODE: {
result = previousIONode;
if (result !== null) {
visited.set(node, result);
}
stack.pop();
continue;
}
case AWAIT_NODE: {
const awaited = node.awaited;
if (awaited !== null) {
frame.phase = PROCESS_AWAITED;
stack.push({node: awaited, phase: VISIT_NODE, previousIONode: null});
continue;
} else {
// No awaited - just forward debug info
const promise = node.promise.deref();
if (promise !== undefined) {
const debugInfo = promise._debugInfo;
if (debugInfo != null && !visited.has(debugInfo)) {
visited.set(debugInfo, null);
forwardDebugInfo(request, task, debugInfo);
}
}
result = previousIONode;
if (result !== null) {
visited.set(node, result);
}
stack.pop();
continue;
}
}
default: {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Unknown AsyncSequence tag. This is a bug in React.');
}
}
}
case PROMISE_NODE: {
const awaited = node.awaited;
let match: void | null | PromiseNode | IONode = previousIONode;
const promise = node.promise.deref();
if (awaited !== null) {
const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff);

if (frame.phase === PROCESS_AWAITED) {
const previousIONode = frame.previousIONode;
const ioNode = result; // Result from visiting awaited

if (node.tag === PROMISE_NODE) {
// PROMISE_NODE processing with awaited result
let match: void | null | PromiseNode | IONode = previousIONode;
const promise = node.promise.deref();

if (ioNode === undefined) {
// Undefined is used as a signal that we found a suitable aborted node and we don't have to find
// further aborted nodes.
found = undefined;
break;
result = undefined;
stack.pop();
continue;
} else if (ioNode !== null) {
// This Promise was blocked on I/O. That's a signal that this Promise is interesting to log.
// We don't log it yet though. We return it to be logged by the point where it's awaited.
Expand Down Expand Up @@ -2450,33 +2556,32 @@ function visitAsyncNodeImpl(
}
}
}
}
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
// the thing that generated this node and its virtual children.
if (promise !== undefined) {
const debugInfo = promise._debugInfo;
if (debugInfo != null && !visited.has(debugInfo)) {
visited.set(debugInfo, null);
forwardDebugInfo(request, task, debugInfo);
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
// the thing that generated this node and its virtual children.
if (promise !== undefined) {
const debugInfo = promise._debugInfo;
if (debugInfo != null && !visited.has(debugInfo)) {
visited.set(debugInfo, null);
forwardDebugInfo(request, task, debugInfo);
}
}
}
found = match;
break;
}
case UNRESOLVED_AWAIT_NODE: {
found = previousIONode;
break;
}
case AWAIT_NODE: {
const awaited = node.awaited;
let match: void | null | PromiseNode | IONode = previousIONode;
if (awaited !== null) {
const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff);
result = match;
if (result !== null) {
visited.set(node, result);
}
stack.pop();
continue;
} else {
// AWAIT_NODE processing with awaited result
let match: void | null | PromiseNode | IONode = previousIONode;
const awaited = node.awaited;

if (ioNode === undefined) {
// Undefined is used as a signal that we found a suitable aborted node and we don't have to find
// further aborted nodes.
found = undefined;
break;
result = undefined;
stack.pop();
continue;
} else if (ioNode !== null) {
const startTime: number = node.start;
const endTime: number = node.end;
Expand All @@ -2497,10 +2602,11 @@ function visitAsyncNodeImpl(
// but if we do it now we can override the promise value of the I/O entry to the
// one observed by this await which will be a better value than the internals of
// the I/O entry. If it's still alive that is.
const promise =
awaited.promise === null ? undefined : awaited.promise.deref();
if (promise !== undefined) {
serializeIONode(request, ioNode, awaited.promise);
if (awaited !== null && awaited.promise !== null) {
const promise = awaited.promise.deref();
if (promise !== undefined) {
serializeIONode(request, ioNode, awaited.promise);
}
}
}
} else {
Expand All @@ -2524,7 +2630,8 @@ function visitAsyncNodeImpl(
// processed through various awaits in the internals of the third party code.
// Therefore we don't use the inner most Promise as the conceptual value but the
// Promise that was ultimately awaited by the user space await.
serializeIONode(request, ioNode, awaited.promise);
// We know awaited is non-null here because we only reach PROCESS_AWAITED when awaited !== null.
serializeIONode(request, ioNode, awaited === null ? null : awaited.promise);

// If we ever visit this I/O node again, skip it because we already emitted this
// exact entry and we don't need two awaits on the same thing.
Expand Down Expand Up @@ -2561,26 +2668,30 @@ function visitAsyncNodeImpl(
}
}
}
}
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
// the thing that generated this node and its virtual children.
const promise = node.promise.deref();
if (promise !== undefined) {
const debugInfo = promise._debugInfo;
if (debugInfo != null && !visited.has(debugInfo)) {
visited.set(debugInfo, null);
forwardDebugInfo(request, task, debugInfo);
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
// the thing that generated this node and its virtual children.
// Note: node.promise is non-null for AWAIT_NODE (and PROMISE_NODE) but we need the check for Flow.
if (node.promise !== null) {
const promise = node.promise.deref();
if (promise !== undefined) {
const debugInfo = promise._debugInfo;
if (debugInfo != null && !visited.has(debugInfo)) {
visited.set(debugInfo, null);
forwardDebugInfo(request, task, debugInfo);
}
}
}
result = match;
if (result !== null) {
visited.set(node, result);
}
stack.pop();
continue;
}
found = match;
break;
}
default: {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Unknown AsyncSequence tag. This is a bug in React.');
}
}
return found;

return result;
}

function emitAsyncSequence(
Expand Down
Loading