mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[Flight] Fix stranded row content under Node stream backpressure (#36516)
The Flight Server emits Text and TypedArray rows as two chunks: a header that gives the row's id, type, and content length, followed by the content itself. These two chunks were pushed into `completedRegularChunks` (and `completedDebugChunks` in DEV) as separate items, so when the destination signaled backpressure between them, the flush would write the header and then break out before reaching the content. The content chunk was left stranded at the head of the queue. Async work running while the destination was paused appended new rows to `completedImportChunks` / `completedHintChunks`, and the next drain flushed those queues first — splicing the newly-arrived bytes into the position the Flight Client expects to read as the original row's content. From there the Flight Client read rows from the wrong byte offsets and the model failed to deserialize. This only surfaced on the Node stream path. `createFakeWritableFromReadableStreamController`, used by `renderToReadableStream`, always returns `true` from `write()`, so the flush loop never saw backpressure. The fix pushes a `NEXT_TWO_CHUNKS_ARE_ATOMIC` sentinel ahead of each `headerChunk` / `contentChunk` pair in `completedRegularChunks` and `completedDebugChunks`. The flush loops detect the sentinel and write the two chunks that follow it together before re-checking backpressure, so backpressure can still break between rows but never within one. ### Alternatives considered - **Pushing the pair as a `[headerChunk, contentChunk]` tuple.** Simpler but allocates an array per row. The required `isArray` branch in the flush hot path is likely comparable with the symbol check. This also violates the opaque `Chunk` type boundary. - **Concatenating header and content into one chunk.** Bad for memory — typed-array content can be large. - **Storing atomic groups in a separate queue.** Conceptually wrong and risks breaking reference-ordering assumptions between rows. - **Ignoring backpressure until the regulars queue is empty.** Defeats the point of backpressure. - **Wrapping the tuple behind a host-config API** (`writeAdjacentChunks` / `isAdjacentChunks` / `chunksToAdjacentChunks` / `getAdjacentChunksLength`). Keeps the implementation opaque but adds four exports per host config. Also has the tuple overhead. - **Begin/end sentinels for variable-length atomic groups.** Not needed — only Text and TypedArray rows use this pattern, and both are pairs.
This commit is contained in:
@@ -585,5 +585,6 @@
|
||||
"597": "The module \"%s\" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.",
|
||||
"598": "Maximum update depth exceeded. This could be an infinite loop. This can happen when a component repeatedly calls setState during render phase or inside useLayoutEffect, causing infinite render loop. React limits the number of nested updates to prevent infinite loops.",
|
||||
"599": "Expected an initialized chunk but got an initialized stream chunk instead. This payload may have been submitted by an older version of React.",
|
||||
"600": "A rejected Promise was passed to React without a `reason` property. React threw a generic error from where the Promise was used to assist in identifying the problematic Promise. Make sure that instrumented Promises correctly set the `reason` property when setting `status` to `'rejected'`."
|
||||
"600": "A rejected Promise was passed to React without a `reason` property. React threw a generic error from where the Promise was used to assist in identifying the problematic Promise. Make sure that instrumented Promises correctly set the `reason` property when setting `status` to `'rejected'`.",
|
||||
"601": "A chunk pair is incomplete. This is a bug in React."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user