@samitouri / QOS-React-1 / commits / fb2177c153

[Flight] Fix pending chunks count for streams & async iterables in DEV (#35143)

In DEV, we need to prevent the response from being GC'd while there are still pending chunks for ReadableSteams or pending results for AsyncIterables. Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>

Hendrik Liebau committed Nov 14, 2025 at 23:52 UTC fb2177c153353621c5e343b0386993e5084f641e
1 file changed +14 -1
packages/react-client/src/ReactFlightClient.js
+14 -1
@@ -813,6 +813,12 @@ function createInitializedStreamChunk<
813 value: T,
814 controller: FlightStreamController,
815 ): InitializedChunk<T> {
816 + if (__DEV__) {
817 + // Retain a strong reference to the Response while we wait for chunks.
818 + if (response._pendingChunks++ === 0) {
819 + response._weakResponse.response = response;
820 + }
821 + }
822 // We use the reason field to stash the controller since we already have that
823 // field. It's a bit of a hack but efficient.
824 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
@@ -3075,7 +3081,6 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
3081 // We already resolved. We didn't expect to see this.
3082 return;
3083 }
3078 - releasePendingChunk(response, chunk);
3084
3085 const resolveListeners = chunk.value;
3086
@@ -3375,6 +3380,14 @@ function stopStream(
3380 // We didn't expect not to have an existing stream;
3381 return;
3382 }
3383 + if (__DEV__) {
3384 + if (--response._pendingChunks === 0) {
3385 + // We're no longer waiting for any more chunks. We can release the strong
3386 + // reference to the response. We'll regain it if we ask for any more data
3387 + // later on.
3388 + response._weakResponse.response = null;
3389 + }
3390 + }
3391 const streamChunk: InitializedStreamChunk<any> = (chunk: any);
3392 const controller = streamChunk.reason;
3393 controller.close(row === '' ? '"$undefined"' : row);