[Flight] Keep an element pending while a referenced row is resolving (#37542)
A row that is still parsing can hand its partially built value to references that were registered on it during that parse. `initializeModelChunk` fulfilled every such listener, on the assumption that all of them are cyclic references back into the parsing row. Only some are. A listener from a nested parse that is not part of a cycle belongs to a handler that does not wait on the parsing row, so fulfilling it early completes that handler with an object that still has references outstanding. When that handler owns an element, `initializeElement` runs on incomplete props. In DEV the props are frozen, so the write that arrives later throws `Cannot assign to read only property`, and `rejectReference` escalates the error into the rows that wait on the element, up to the root. Only the debug tree can produce this shape. The RSC stream writes element props inline, while the debug channel outlines a props object that an element shares with its own componentInfo into a separate row, and that row can still wait on a client module. `resolveBlockedCycle` already tells a cyclic reference from any other, but it returned `null` for mid-parse listeners because `handler.chunk` was assigned after the drain loop. This change assigns it before the loop and classifies each listener. A reference whose handler is transitively waiting on the parsing row is a genuine cycle and receives the value now, because neither side can complete before the other. Every other listener is queued back on the parsing row and fulfilled when that row completes, like any reference into a blocked row. A row whose own parse fails used to hand the partial value to its mid-parse listeners before it threw. It now errors through `triggerErrorOnChunk`, which rejects them the same way a reference into any other errored row is rejected. The `if (handler.errored) throw` after the loop is removed. It also caught a rejection during the loop, which now reaches `triggerErrorOnChunk` on its own because `handler.chunk` is set. One behaviour changes beyond the reported bug. When `initializeDebugChunk` errors a chunk before `parseModel` runs, the old code set `INITIALIZED` over that status if the model had no pending references, and left it `ERRORED` otherwise. The chunk now stays `ERRORED` in both cases. That is what the `triggerErrorOnChunk` call in `initializeDebugChunk` intends, and the TODO above the `parseModel` call already notes that the chunk can be `ERRORED` there. PR #37398 deferred `Object.freeze(element.props)` until the outstanding references have resolved. That removes the exception but not the cause: the element is still initialized on an incomplete object and is visible through `_debugInfo` with a `null` placeholder until the late write lands. With the early release fixed, the freeze needs no change. **Alternatives Considered** - Deferring every listener whose handler is not the parsing row's own deadlocks `foo ↔ bar` in `can deduped outlined references inside promises`. One side of a genuine cycle has to accept the partial object. - Holding an element back while its props row is `BLOCKED` breaks `should handle deduped props of re-used elements in fragments`, where the row is blocked on an unrelated module and the props object itself is complete. - A per-object count of pending writes plus a reverse `dependents` edge works, but adds a second dependency graph next to `deps` and special-cases elements. Fixes #37361 Closes #37398
H
Hendrik Liebau committed
6c0e1047e392da04bccb36e84c04e1d1412901d6
Parent: 9b7a0d4
Committed by GitHub <noreply@github.com>
on 9/8/2026, 11:31:40 AM