From 6808516f6cb6780cc1684c3c7080c702ddb5599d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 23 Jun 2025 10:34:07 -0400 Subject: [PATCH 1/2] Only store Response on the ResolvedModelChunk It's the only one that needs it. --- .../react-client/src/ReactFlightClient.js | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 01e40cdc551ec..1346ca0975f12 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -165,7 +165,7 @@ type PendingChunk = { status: 'pending', value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, - _response: Response, + _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -174,7 +174,7 @@ type BlockedChunk = { status: 'blocked', value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, - _response: Response, + _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -192,7 +192,7 @@ type ResolvedModuleChunk = { status: 'resolved_module', value: ClientReference, reason: null, - _response: Response, + _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -201,7 +201,7 @@ type InitializedChunk = { status: 'fulfilled', value: T, reason: null | FlightStreamController, - _response: Response, + _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -212,7 +212,7 @@ type InitializedStreamChunk< status: 'fulfilled', value: T, reason: FlightStreamController, - _response: Response, + _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void, @@ -221,7 +221,7 @@ type ErroredChunk = { status: 'rejected', value: null, reason: mixed, - _response: Response, + _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -230,7 +230,7 @@ type HaltedChunk = { status: 'halted', value: null, reason: null, - _response: Response, + _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -249,7 +249,7 @@ function ReactPromise( status: any, value: any, reason: any, - response: Response, + response: null | Response, ) { this.status = status; this.value = value; @@ -401,12 +401,12 @@ export function getRoot(response: Response): Thenable { function createPendingChunk(response: Response): PendingChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(PENDING, null, null, response); + return new ReactPromise(PENDING, null, null, null); } function createBlockedChunk(response: Response): BlockedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(BLOCKED, null, null, response); + return new ReactPromise(BLOCKED, null, null, null); } function createErrorChunk( @@ -414,7 +414,7 @@ function createErrorChunk( error: mixed, ): ErroredChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(ERRORED, null, error, response); + return new ReactPromise(ERRORED, null, error, null); } function wakeChunk(listeners: Array<(T) => mixed>, value: T): void { @@ -494,7 +494,7 @@ function createResolvedModuleChunk( value: ClientReference, ): ResolvedModuleChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODULE, value, null, response); + return new ReactPromise(RESOLVED_MODULE, value, null, null); } function createInitializedTextChunk( @@ -502,7 +502,7 @@ function createInitializedTextChunk( value: string, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, null, response); + return new ReactPromise(INITIALIZED, value, null, null); } function createInitializedBufferChunk( @@ -510,7 +510,7 @@ function createInitializedBufferChunk( value: $ArrayBufferView | ArrayBuffer, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, null, response); + return new ReactPromise(INITIALIZED, value, null, null); } function createInitializedIteratorResultChunk( @@ -519,12 +519,7 @@ function createInitializedIteratorResultChunk( done: boolean, ): InitializedChunk> { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise( - INITIALIZED, - {done: done, value: value}, - null, - response, - ); + return new ReactPromise(INITIALIZED, {done: done, value: value}, null, null); } function createInitializedStreamChunk< @@ -537,7 +532,7 @@ function createInitializedStreamChunk< // We use the reason field to stash the controller since we already have that // field. It's a bit of a hack but efficient. // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, controller, response); + return new ReactPromise(INITIALIZED, value, controller, null); } function createResolvedIteratorResultChunk( @@ -553,6 +548,7 @@ function createResolvedIteratorResultChunk( } function resolveIteratorResultChunk( + response: Response, chunk: SomeChunk>, value: UninitializedModel, done: boolean, @@ -560,10 +556,11 @@ function resolveIteratorResultChunk( // To reuse code as much code as possible we add the wrapper element as part of the JSON. const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; - resolveModelChunk(chunk, iteratorResultJSON); + resolveModelChunk(response, chunk, iteratorResultJSON); } function resolveModelChunk( + response: Response, chunk: SomeChunk, value: UninitializedModel, ): void { @@ -580,6 +577,7 @@ function resolveModelChunk( const resolvedChunk: ResolvedModelChunk = (chunk: any); resolvedChunk.status = RESOLVED_MODEL; resolvedChunk.value = value; + resolvedChunk._response = response; if (resolveListeners !== null) { // This is unfortunate that we're reading this eagerly if // we already have listeners attached since they might no @@ -625,6 +623,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { initializingHandler = null; const resolvedModel = chunk.value; + const response = chunk._response; // 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 @@ -639,7 +638,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { } try { - const value: T = parseModel(chunk._response, resolvedModel); + const value: T = parseModel(response, resolvedModel); // Invoke any listeners added while resolving this model. I.e. cyclic // references. This may or may not fully resolve the model depending on // if they were blocked. @@ -1862,7 +1861,7 @@ function resolveModel( if (!chunk) { chunks.set(id, createResolvedModelChunk(response, model)); } else { - resolveModelChunk(chunk, model); + resolveModelChunk(response, chunk, model); } } @@ -2036,7 +2035,7 @@ function startReadableStream( // to synchronous emitting. previousBlockedChunk = null; } - resolveModelChunk(chunk, json); + resolveModelChunk(response, chunk, json); }); } }, @@ -2124,7 +2123,12 @@ function startAsyncIterable( false, ); } else { - resolveIteratorResultChunk(buffer[nextWriteIndex], value, false); + resolveIteratorResultChunk( + response, + buffer[nextWriteIndex], + value, + false, + ); } nextWriteIndex++; }, @@ -2137,12 +2141,18 @@ function startAsyncIterable( true, ); } else { - resolveIteratorResultChunk(buffer[nextWriteIndex], value, true); + resolveIteratorResultChunk( + response, + buffer[nextWriteIndex], + value, + true, + ); } nextWriteIndex++; while (nextWriteIndex < buffer.length) { // In generators, any extra reads from the iterator have the value undefined. resolveIteratorResultChunk( + response, buffer[nextWriteIndex++], '"$undefined"', true, @@ -2178,7 +2188,7 @@ function startAsyncIterable( INITIALIZED, {done: true, value: undefined}, null, - response, + null, ); } buffer[nextReadIndex] = @@ -2946,7 +2956,7 @@ function resolveIOInfo( chunks.set(id, chunk); initializeModelChunk(chunk); } else { - resolveModelChunk(chunk, model); + resolveModelChunk(response, chunk, model); if (chunk.status === RESOLVED_MODEL) { initializeModelChunk(chunk); } From 70dcc16276cddb5a7a504de6dfcc549ea9f5d45e Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 23 Jun 2025 10:43:09 -0400 Subject: [PATCH 2/2] Delete _response field and just use the reason instead The reason field is free to be reused for this purpose. --- .../react-client/src/ReactFlightClient.js | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 1346ca0975f12..cb19755734078 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -165,7 +165,6 @@ type PendingChunk = { status: 'pending', value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, - _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -174,7 +173,6 @@ type BlockedChunk = { status: 'blocked', value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, - _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -182,8 +180,7 @@ type BlockedChunk = { type ResolvedModelChunk = { status: 'resolved_model', value: UninitializedModel, - reason: null, - _response: Response, + reason: Response, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -192,7 +189,6 @@ type ResolvedModuleChunk = { status: 'resolved_module', value: ClientReference, reason: null, - _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -201,7 +197,6 @@ type InitializedChunk = { status: 'fulfilled', value: T, reason: null | FlightStreamController, - _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -212,7 +207,6 @@ type InitializedStreamChunk< status: 'fulfilled', value: T, reason: FlightStreamController, - _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void, @@ -221,7 +215,6 @@ type ErroredChunk = { status: 'rejected', value: null, reason: mixed, - _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -230,7 +223,6 @@ type HaltedChunk = { status: 'halted', value: null, reason: null, - _response: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -245,16 +237,10 @@ type SomeChunk = | HaltedChunk; // $FlowFixMe[missing-this-annot] -function ReactPromise( - status: any, - value: any, - reason: any, - response: null | Response, -) { +function ReactPromise(status: any, value: any, reason: any) { this.status = status; this.value = value; this.reason = reason; - this._response = response; if (enableProfilerTimer && enableComponentPerformanceTrack) { this._children = []; } @@ -401,12 +387,12 @@ export function getRoot(response: Response): Thenable { function createPendingChunk(response: Response): PendingChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(PENDING, null, null, null); + return new ReactPromise(PENDING, null, null); } function createBlockedChunk(response: Response): BlockedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(BLOCKED, null, null, null); + return new ReactPromise(BLOCKED, null, null); } function createErrorChunk( @@ -414,7 +400,7 @@ function createErrorChunk( error: mixed, ): ErroredChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(ERRORED, null, error, null); + return new ReactPromise(ERRORED, null, error); } function wakeChunk(listeners: Array<(T) => mixed>, value: T): void { @@ -486,7 +472,7 @@ function createResolvedModelChunk( value: UninitializedModel, ): ResolvedModelChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODEL, value, null, response); + return new ReactPromise(RESOLVED_MODEL, value, response); } function createResolvedModuleChunk( @@ -494,7 +480,7 @@ function createResolvedModuleChunk( value: ClientReference, ): ResolvedModuleChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODULE, value, null, null); + return new ReactPromise(RESOLVED_MODULE, value, null); } function createInitializedTextChunk( @@ -502,7 +488,7 @@ function createInitializedTextChunk( value: string, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, null, null); + return new ReactPromise(INITIALIZED, value, null); } function createInitializedBufferChunk( @@ -510,7 +496,7 @@ function createInitializedBufferChunk( value: $ArrayBufferView | ArrayBuffer, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, null, null); + return new ReactPromise(INITIALIZED, value, null); } function createInitializedIteratorResultChunk( @@ -519,7 +505,7 @@ function createInitializedIteratorResultChunk( done: boolean, ): InitializedChunk> { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, {done: done, value: value}, null, null); + return new ReactPromise(INITIALIZED, {done: done, value: value}, null); } function createInitializedStreamChunk< @@ -532,7 +518,7 @@ function createInitializedStreamChunk< // We use the reason field to stash the controller since we already have that // field. It's a bit of a hack but efficient. // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, controller, null); + return new ReactPromise(INITIALIZED, value, controller); } function createResolvedIteratorResultChunk( @@ -544,7 +530,7 @@ function createResolvedIteratorResultChunk( const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response); + return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, response); } function resolveIteratorResultChunk( @@ -577,7 +563,7 @@ function resolveModelChunk( const resolvedChunk: ResolvedModelChunk = (chunk: any); resolvedChunk.status = RESOLVED_MODEL; resolvedChunk.value = value; - resolvedChunk._response = response; + resolvedChunk.reason = response; if (resolveListeners !== null) { // This is unfortunate that we're reading this eagerly if // we already have listeners attached since they might no @@ -623,7 +609,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { initializingHandler = null; const resolvedModel = chunk.value; - const response = chunk._response; + const response = chunk.reason; // 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 @@ -2188,7 +2174,6 @@ function startAsyncIterable( INITIALIZED, {done: true, value: undefined}, null, - null, ); } buffer[nextReadIndex] =