8000 [Flight] Serialize already resolved Promises as debug models by sebmarkbage · Pull Request #33588 · facebook/react · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[Flight] Serialize already resolved Promises as debug models #33588

New issue

Have a question about this project? Sign up for a free 8000 GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 22, 2025
Merged
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
42 changes: 37 additions & 5 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ const RESOLVED_MODEL = 'resolved_model';
const RESOLVED_MODULE = 'resolved_module';
const INITIALIZED = 'fulfilled';
const ERRORED = 'rejected';
const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes.

type PendingChunk<T> = {
status: 'pending',
Expand Down Expand Up @@ -221,13 +222,23 @@ type ErroredChunk<T> = {
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type HaltedChunk<T> = {
status: 'halted',
value: null,
reason: null,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type SomeChunk<T> =
| PendingChunk<T>
| BlockedChunk<T>
| ResolvedModelChunk<T>
| ResolvedModuleChunk<T>
| InitializedChunk<T>
| ErroredChunk<T>;
| ErroredChunk<T>
| HaltedChunk<T>;

// $FlowFixMe[missing-this-annot]
function ReactPromise(
Expand Down Expand Up @@ -311,6 +322,9 @@ ReactPromise.prototype.then = function <T>(
chunk.reason.push(reject);
}
break;
case HALTED: {
break;
}
default:
if (reject) {
reject(chunk.reason);
Expand Down Expand Up @@ -368,6 +382,7 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
return chunk.value;
case PENDING:
case BLOCKED:
case HALTED:
// eslint-disable-next-line no-throw-literal
throw ((chunk: any): Thenable<T>);
default:
Expand Down Expand Up @@ -1367,6 +1382,7 @@ function getOutlinedModel<T>(
return chunkValue;
case PENDING:
case BLOCKED:
case HALTED:
return waitForReference(chunk, parentObject, key, response, map, path);
default:
// This is an error. Instead of erroring directly, we're going to encode this on
Expand Down Expand Up @@ -1470,10 +1486,6 @@ function parseModelString(
}
case '@': {
// Promise
if (value.length === 2) {
// Infinite promise that never resolves.
return new Promise(() => {});
}
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
if (enableProfilerTimer && enableComponentPerformanceTrack) {
Expand Down Expand Up @@ -1769,6 +1781,22 @@ export function createResponse(
);
}

function resolveDebugHalt(response: Response, id: number): void {
const chunks = response._chunks;
let chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, (chunk = createPendingChunk(response)));
} else {
}
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
return;
}
const haltedChunk: HaltedChunk<any> = (chunk: any);
haltedChunk.status = HALTED;
haltedChunk.value = null;
haltedChunk.reason = null;
}

function resolveModel(
response: Response,
id: number,
Expand Down Expand Up @@ -3337,6 +3365,10 @@ function processFullStringRow(
}
// Fallthrough
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
if (__DEV__ && row === '') {
resolveDebugHalt(response, id);
return;
}
// We assume anything else is JSON.
resolveModel(response, id, row);
return;
Expand Down
27 changes: 25 additions & 2 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3213,7 +3213,8 @@ describe('ReactFlight', () => {
prop: 123,
fn: foo,
map: new Map([['foo', foo]]),
promise: new Promise(() => {}),
promise: Promise.resolve('yo'),
infinitePromise: new Promise(() => {}),
});
throw new Error('err');
}
Expand Down Expand Up @@ -3258,9 +3259,14 @@ describe('ReactFlight', () => {
});
ownerStacks = [];

// Let the Promises resolve.
await 0;
await 0;
await 0;

// The error should not actually get logged because we're not awaiting the root
// so it's not thrown but the server log also shouldn't be replayed.
await ReactNoopFlightClient.read(transport);
await ReactNoopFlightClient.read(transport, {close: true});

expect(mockConsoleLog).toHaveBeenCalledTimes(1);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
Expand All @@ -3280,6 +3286,23 @@ describe('ReactFlight', () => {

const promise = mockConsoleLog.mock.calls[0][1].promise;
expect(promise).toBeInstanceOf(Promise);
expect(await promise).toBe('yo');

const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise;
expect(infinitePromise).toBeInstanceOf(Promise);
let resolved = false;
infinitePromise.then(
() => (resolved = true),
x => {
console.error(x);
resolved = true;
},
);
await 0;
await 0;
await 0;
// This should not reject upon aborting the stream.
expect(resolved).toBe(false);

expect(ownerStacks).toEqual(['\n in App (at **)']);
});
Expand Down
6 changes: 5 additions & 1 deletion packages/react-noop-renderer/src/ReactNoopFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Source = Array<Uint8Array>;

const decoderOptions = {stream: true};

const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
createStringDecoder() {
return new TextDecoder();
},
Expand Down Expand Up @@ -56,6 +56,7 @@ const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({

type ReadOptions = {|
findSourceMapURL?: FindSourceMapURLCallback,
close?: boolean,
|};

function read<T>(source: Source, options: ReadOptions): Thenable<T> {
Expand All @@ -74,6 +75,9 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
for (let i = 0; i < source.length; i++) {
processBinaryChunk(response, source[i], 0);
}
if (options !== undefined && options.close) {
close(response);
}
return getRoot(response);
}

Expand Down
Loading
Loading
0