Repository navigation
fix: inline Suspense boundaries that complete before the stream is read - #112
BleedingDev wants to merge 1 commit into
Conversation
Write streamed SSR output when the destination starts reading, as React does, instead of enqueueing the shell as soon as it renders. Pending Suspense boundaries stay addressable in the buffered output: those that complete or fail before their bytes are written are inlined as `$` or `$E`/`$!` boundaries with their resources hoisted to the shell, and only boundaries still pending at the write stream a hidden segment and reveal script. A reader that waits for `allReady` therefore receives a complete document that renders without JavaScript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 5ecdf0c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Retain boundary parts packages/redact/src/server/walk.ts |
HtmlPart and SuspendedBoundary retain fallback and completed content as parts. Boundary markers distinguish completed, pending, and failed output. |
Serialize completed and pending boundaries packages/redact/src/server/stream.ts, .changeset/ssr-inline-completed-boundaries.md, scripts/size-check.mjs, tests/ssr-inline-completed-boundaries.test.tsx |
streamHtml defers shell output and serializes completed or failed boundaries inline. Pending boundaries retain their fallback and can later emit a hidden segment and reveal script. Tests cover hydration, IDs, errors, stylesheets, and replaced fallbacks. The changeset and size budget are updated. |
Start and close output streams packages/redact/src/server/stream.ts, tests/ssr-inline-completed-boundaries.test.tsx |
Readable-stream pulls and pipeable destinations start output flow. The readable stream closes after rendering when it is flowing. Tests compare readable and pipeable output. |
Priority:
Estimated code review effort: 3 (Moderate) | ~25 minutes
Change: Bug fix
Sequence Diagram(s)
sequenceDiagram
participant walk
participant streamHtml
participant ReadableStreamConsumer
participant SuspendedBoundary
walk->>streamHtml: Emit HtmlPart values
streamHtml->>streamHtml: Buffer shell and boundary parts
ReadableStreamConsumer->>streamHtml: pull or pipe starts flow
streamHtml->>ReadableStreamConsumer: Flush shell with pending fallback
SuspendedBoundary->>streamHtml: Complete after shell flush
streamHtml->>ReadableStreamConsumer: Write hidden segment and reveal script
Possibly related issues
- SSR: an error thrown inside <Suspense> rejects renderToReadableStream instead of falling back to client rendering #71: The changes cover server-side Suspense failure output, including inline failed boundaries and their markers.
Possibly related PRs
- TanStack/redact#103: Both changes modify streamed Suspense boundary representation and output.
- TanStack/redact#104: Both changes affect the server-to-client streaming protocol for pending boundaries and reveal instructions.
- TanStack/redact#110: Both changes concern pending boundaries and segments emitted after the shell.
Suggested reviewers: tannerlinsley, southpolesteve
Merge Risk: 
Minimal · up to 5ecdf
-
No actionable issue identified; the change is ready to merge after normal checks.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passedFull details: Docstring Coverage
-
Explanation
Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 8 functions across 4 files. (1 skipped: 1 unsupported.)
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
Why the change
Redact writes the streamed shell the moment it renders and always outlines every suspended boundary, so a reader that waits for
allReady(the usual bot and no-JavaScript path) gets fallbacks plus hidden segments that only a script can reveal, while React writes when the reader starts pulling and inlines every boundary that completed by then.Special things to note
pull()ofrenderToReadableStream(highWaterMark: 0) orpipe()forrenderToPipeableStream. A consumer that reads right after the shell still gets the old streaming behavior, apart from boundaries that finished in the meantime.$E/$!with their fallback and no$RBscript. Boundaries inside a fallback that was replaced before the write are dropped, with no orphan segment. The reveal runtime is only added when something is still pending or bootstrap scripts are present.progressiveChunkSizeis still ignored, so completed boundaries are inlined at any size. React outlines completed boundaries larger than 12,800 bytes by default.Change outline
Repro, on
mainat37c051c: one boundary inside a wrapper, resolved before the reader waits forallReady.The cause is that the shell is serialized and enqueued in
start()right after the walk (stream.ts:167,:357), and each resolved boundary is emitted as a segment straight away (:239). The walk has already flattened the fallback into the string (walk.ts:488), so no later step can put completed content back in place.The walk now emits a pending boundary as itself. The stream keeps it addressable until the bytes are written.
renderToReadableStream / renderToPipeableStream streamHtml walk shell -> HtmlPart[] - drain hints into <head>, append runtime, emit shell + state.flush = on first pull() / pipe(): + drain hints into <head> // includes resources of boundaries inlined below + partsHtml(shell) // completed -> <!--$N-->content, failed -> <!--$EN-->fallback, + // pending -> fallback, mark written + append runtime only if something is pending or bootstrap scripts exist streamBoundary(b) on resolve render b.children -> parts - emit hints + <div hidden id=S:N> + $RC + if b.written: emit hints + segment(partsHtml(parts)) + $RC + else: b.content = parts // inlined when its parent is written allReady resolves when the work is done, whether or not anyone has read yetTests are in
tests/ssr-inline-completed-boundaries.test.tsx. They cover:All seven tests fail on
mainexcept the useId one, which already passes because inlining does not change the render order.pnpm testpasses 185 files: 2,009 tests passed, 91 skipped.test:types, the build andsize:checkalso pass.redact/servergzipOnly the server cap changes. It goes up by the exact deficit, 188/191 B, through
INLINE_BOUNDARY_SIZE_ALLOWANCE. Client entries do not change.🤖 Generated with Claude Code
Summary by CodeRabbit