Skip to content
Prev Previous commit
Next Next commit
Ensure blocked console log entries resolves in order
  • Loading branch information
sebmarkbage committed Jul 18, 2025
commit 378a3234e4fe00f05ab0a982519711d3afb7d71d
92 changes: 60 additions & 32 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ type Response = {
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannelCallback, // DEV-only
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
_replayConsole: boolean, // DEV-only
_rootEnvironmentName: string, // DEV-only, the requested environment name.
};
Expand Down Expand Up @@ -2222,6 +2223,7 @@ function ResponseInstance(
}
this._debugFindSourceMapURL = findSourceMapURL;
this._debugChannel = debugChannel;
this._blockedConsole = null;
this._replayConsole = replayConsole;
this._rootEnvironmentName = rootEnv;
if (debugChannel) {
Expand Down Expand Up @@ -3329,12 +3331,14 @@ function getCurrentStackInDEV(): string {
const replayConsoleWithCallStack = {
react_stack_bottom_frame: function (
response: Response,
methodName: string,
stackTrace: ReactStackTrace,
owner: null | ReactComponentInfo,
env: string,
args: Array<mixed>,
payload: ConsoleEntry,
): void {
const methodName = payload[0];
const stackTrace = payload[1];
const owner = payload[2];
const env = payload[3];
const args = payload.slice(4);

// There really shouldn't be anything else on the stack atm.
const prevStack = ReactSharedInternals.getCurrentStack;
ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
Expand Down Expand Up @@ -3372,21 +3376,25 @@ const replayConsoleWithCallStack = {

const replayConsoleWithCallStackInDEV: (
response: Response,
methodName: string,
stackTrace: ReactStackTrace,
owner: null | ReactComponentInfo,
env: string,
args: Array<mixed>,
payload: ConsoleEntry,
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(replayConsoleWithCallStack.react_stack_bottom_frame.bind(
replayConsoleWithCallStack,
): any)
: (null: any);

type ConsoleEntry = [
string,
ReactStackTrace,
null | ReactComponentInfo,
string,
mixed,
];
Comment on lines +3572 to +3578
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type ConsoleEntry = [
string,
ReactStackTrace,
null | ReactComponentInfo,
string,
mixed,
];
type ConsoleEntry = [
/* eslint-disable no-undef -- eslint does not understand Flow tuple syntax. */
methodName: string,
stackTrace: ReactStackTrace,
owner: null | ReactComponentInfo,
env: string,
args: mixed,
/* eslint-enable no-undef */
];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this error? I'm not seeing any lint errors.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your version it does not error. I wanted to suggest adding the tuple labels. And then it would error unless we disable eslint. Devon did the same here:

export type ImportMetadata = [
// eslint does not understand Flow tuple syntax.
/* eslint-disable */
id: string,
name: string,
bundles: Array<string>,
importMap?: {[string]: string},
/* eslint-enable */
];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given how much trouble we've had with parsers I'm not sure I trust that this won't break linting elsewhere.


function resolveConsoleEntry(
response: Response,
value: UninitializedModel,
json: UninitializedModel,
): void {
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
Expand All @@ -3400,27 +3408,47 @@ function resolveConsoleEntry(
return;
}

const payload: [
string,
ReactStackTrace,
null | ReactComponentInfo,
string,
mixed,
] = parseModel(response, value);
const methodName = payload[0];
const stackTrace = payload[1];
const owner = payload[2];
const env = payload[3];
const args = payload.slice(4);

replayConsoleWithCallStackInDEV(
response,
methodName,
stackTrace,
owner,
env,
args,
);
const blockedChunk = response._blockedConsole;
if (blockedChunk == null) {
// If we're not blocked on any other chunks, we can try to eagerly initialize
// this as a fast-path to avoid awaiting them.
const chunk: ResolvedModelChunk<ConsoleEntry> = createResolvedModelChunk(
response,
json,
);
initializeModelChunk(chunk);
const initializedChunk: SomeChunk<ConsoleEntry> = chunk;
if (initializedChunk.status === INITIALIZED) {
replayConsoleWithCallStackInDEV(response, initializedChunk.value);
} else {
chunk.then(
v => replayConsoleWithCallStackInDEV(response, v),
e => {
// Ignore console errors for now. Unnecessary noise.
},
);
response._blockedConsole = chunk;
}
} else {
// We're still waiting on a previous chunk so we can't enqueue quite yet.
const chunk: SomeChunk<ConsoleEntry> = createPendingChunk(response);
chunk.then(
v => replayConsoleWithCallStackInDEV(response, v),
e => {
// Ignore console errors for now. Unnecessary noise.
},
);
response._blockedConsole = chunk;
const unblock = () => {
if (response._blockedConsole === chunk) {
// We were still the last chunk so we can now clear the queue and return
// to synchronous emitting.
response._blockedConsole = null;
}
resolveModelChunk(response, chunk, json);
};
blockedChunk.then(unblock, unblock);
}
}

function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
Expand Down