Skip to content

fix: preserve solid deferred stream order#7515

Open
brenelz wants to merge 3 commits into
mainfrom
fix/solid-stream-boundary-flush-main
Open

fix: preserve solid deferred stream order#7515
brenelz wants to merge 3 commits into
mainfrom
fix/solid-stream-boundary-flush-main

Conversation

@brenelz
Copy link
Copy Markdown
Contributor

@brenelz brenelz commented May 30, 2026

Follow up to this #7512

Summary by CodeRabbit

  • Tests

    • Added end-to-end tests verifying deferred route regions stream and finish independently.
  • Improvements

    • Improved HTML streaming to correctly detect closing tags and preserve ordering of deferred content.
    • Adjusted deferred-content streaming behavior so deferred subregions render more predictably during streaming.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2887a5b1-e4b9-404b-9864-27a2f61b0dab

📥 Commits

Reviewing files that changed from the base of the PR and between 4955201 and ea7b2aa.

📒 Files selected for processing (1)
  • packages/solid-router/src/awaited.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/solid-router/src/awaited.tsx

📝 Walkthrough

Walkthrough

The PR enhances SSR streaming to detect and properly handle the </html> closing boundary, introduces a tail-completion flag to control buffering, adds a HoldingTail path that flushes router-injected HTML in order once the closing tag end is observed, removes a deferStream option from Solid Await, and adds Playwright e2e tests validating independent deferred-region streaming for React and Solid.

Changes

Independent Deferred Boundary Streaming

Layer / File(s) Summary
HTML end-tag detection helper
packages/router-core/src/ssr/transformStreamWithRouter.ts
New internal helper findHtmlEndTagEnd(str, searchFrom) scans text for case-insensitive </html> closing tags and returns the index immediately after the tag (or -1 if not found).
Tail completion state management
packages/router-core/src/ssr/transformStreamWithRouter.ts
Introduces pendingTailComplete flag to differentiate buffering after router tail handling vs. after full </html> observation; cleanup logic resets this flag alongside other pending buffers.
HoldingTail streaming loop and transition
packages/router-core/src/ssr/transformStreamWithRouter.ts
Main transform loop adds a HoldingTail branch that scans streamed chunks for the </html> end, buffers until found, then flushes pending router HTML and writes post-boundary bytes while resuming barrier marker tracking. Transition integrates findHtmlEndTagEnd to determine completion.
Solid Await resource deferred option removal
packages/solid-router/src/awaited.tsx
Await component's Solid.createResource call removes the { deferStream: true } options argument when fallback is not provided.
E2E tests for independent boundary streaming
e2e/react-start/basic/tests/streaming.spec.ts, e2e/solid-start/basic/tests/streaming.spec.ts
Parallel Playwright tests validate deferred routes stream independently by checking immediate regular content, intermediate loading placeholders, and final deferred-person and deferred-stuff content.

Possibly Related PRs

  • TanStack/router#7497: Modifies the same transformStreamWithRouter.ts SSR streaming logic around HTML boundary and tail handling that this PR extends with completion-state tracking and end-tag detection.

Suggested Labels

package: router-core, package: solid-router

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes


"A rabbit hops past the closing tag,
holding tails until the stream's in flag;
deferred bits race to their own cue,
tests cheer as each subtree comes through 🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main objective of the PR: fixing the order preservation of Solid deferred streams in the router's streaming logic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/solid-stream-boundary-flush-main

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 30, 2026

View your CI Pipeline Execution ↗ for commit 4955201

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 8m 55s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 15s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-31 00:08:28 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@github-actions
Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: fca55ce537f3
  • Measured at: 2026-05-31T00:00:40.749Z
  • Baseline source: history:9a6c12596ff6
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.25 KiB 0 B (0.00%) 87.12 KiB 273.94 KiB 75.84 KiB ███████▁▁▁▁
react-router.full 90.69 KiB 0 B (0.00%) 90.55 KiB 285.30 KiB 78.83 KiB ███████▁▁▁▁
solid-router.minimal 35.47 KiB 0 B (0.00%) 35.35 KiB 106.24 KiB 32.00 KiB ███████▁▁▁▁
solid-router.full 40.17 KiB -14 B (-0.03%) 40.04 KiB 120.42 KiB 36.16 KiB ███████▂▂▂▂▁
vue-router.minimal 52.97 KiB 0 B (0.00%) 52.84 KiB 150.26 KiB 47.66 KiB ███████▁▁▁▁
vue-router.full 58.60 KiB 0 B (0.00%) 58.47 KiB 167.99 KiB 52.51 KiB ███████▁▁▁▁
react-start.minimal 101.88 KiB 0 B (0.00%) 101.74 KiB 322.26 KiB 88.15 KiB ███████▁▁▁▁
react-start.deferred-hydration 102.61 KiB 0 B (0.00%) 101.76 KiB 323.63 KiB 88.82 KiB ███████▁▁▁▁
react-start.full 105.26 KiB 0 B (0.00%) 105.12 KiB 332.57 KiB 91.09 KiB ███████▁▁▁▁
react-start.rsbuild.minimal 99.58 KiB 0 B (0.00%) 99.41 KiB 316.74 KiB 85.74 KiB ███████▁▁▄▄
react-start.rsbuild.full 102.85 KiB 0 B (0.00%) 102.68 KiB 327.13 KiB 88.45 KiB ███████▁▁▄▄
solid-start.minimal 49.58 KiB 0 B (0.00%) 49.45 KiB 152.31 KiB 43.78 KiB ███████▁▁▁▁
solid-start.deferred-hydration 52.84 KiB 0 B (0.00%) 49.51 KiB 160.35 KiB 46.76 KiB ███████▁▁▁▁
solid-start.full 55.36 KiB -7 B (-0.01%) 55.23 KiB 169.22 KiB 48.79 KiB ███████▂▂▂▂▁

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/solid-start/basic/tests/streaming.spec.ts`:
- Around line 31-45: The SSR-streaming specific assertions in the test 'deferred
route streams boundaries independently' should be skipped when running in SPA
mode: wrap the block of expectations that assert commit-time streamed HTML (the
checks for "Loading person...", "Loading stuff...", and the deferred element
contents like page.getByTestId('deferred-person') and
page.getByTestId('deferred-stuff')) in a guard that only runs when not in SPA
mode (e.g., if (!isSpaMode()) { ... }) or conditionally call test.skip when
isSpaMode() is true; reuse or add a project helper named isSpaMode() (or the
existing SPA-mode detection utility) to detect SPA runs and ensure the streaming
assertions are bypassed in SPA mode.

In `@packages/router-core/src/ssr/transformStreamWithRouter.ts`:
- Around line 813-836: The HTML end-tag detection fails when the tail is split
across chunks (e.g., previous leftover ends with "</ht" and current chunk begins
"ml>") because the code only calls findHtmlEndTagEnd on chunkString; change the
logic in the pending-tail branch to search the concatenation of the buffered
tail/leftover plus the current chunk (use the same findHtmlEndTagEnd on leftover
+ chunkString), adjust appendTail and slicing indices to account for the
combined string (so appendTail receives only the part up to htmlEndTagEnd from
the combined string, and afterClosingTags is the remainder from the chunk
portion), and ensure leftover is cleared/updated correctly so
flushPendingRouterHtml, noteBarrierMarker and subsequent writeChunk flows behave
the same as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c41419b5-f19b-4a40-b612-6a6cde9ebd9d

📥 Commits

Reviewing files that changed from the base of the PR and between 9a6c125 and 4955201.

📒 Files selected for processing (4)
  • e2e/react-start/basic/tests/streaming.spec.ts
  • e2e/solid-start/basic/tests/streaming.spec.ts
  • packages/router-core/src/ssr/transformStreamWithRouter.ts
  • packages/solid-router/src/awaited.tsx

Comment on lines +31 to +45
test('deferred route streams boundaries independently', async ({ page }) => {
await page.goto('/deferred', { waitUntil: 'commit' })

await expect(page.getByTestId('regular-person')).toContainText('John Doe')
await expect(page.getByText('Loading person...')).toBeVisible()
await expect(page.getByText('Loading stuff...')).toBeVisible()

await expect(page.getByTestId('deferred-person')).toContainText(
'Tanner Linsley',
)
await expect(page.getByText('Loading stuff...')).toBeVisible()
await expect(page.getByTestId('deferred-stuff')).toContainText(
'Hello deferred!',
)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skip this SSR-streaming assertion in SPA mode.

This test exercises commit-time streamed HTML behavior, so it has the same SPA-mode constraint as the existing SSR fallback test later in this file. Without the guard, the Solid suite can fail in SPA mode even though the app is behaving correctly.

💡 Suggested fix
 test('deferred route streams boundaries independently', async ({ page }) => {
+  test.skip(isSpaMode, 'SPA mode does not render streamed SSR HTML')
   await page.goto('/deferred', { waitUntil: 'commit' })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/solid-start/basic/tests/streaming.spec.ts` around lines 31 - 45, The
SSR-streaming specific assertions in the test 'deferred route streams boundaries
independently' should be skipped when running in SPA mode: wrap the block of
expectations that assert commit-time streamed HTML (the checks for "Loading
person...", "Loading stuff...", and the deferred element contents like
page.getByTestId('deferred-person') and page.getByTestId('deferred-stuff')) in a
guard that only runs when not in SPA mode (e.g., if (!isSpaMode()) { ... }) or
conditionally call test.skip when isSpaMode() is true; reuse or add a project
helper named isSpaMode() (or the existing SPA-mode detection utility) to detect
SPA runs and ensure the streaming assertions are bypassed in SPA mode.

Comment on lines +813 to +836
if (!pendingTailComplete) {
const htmlEndTagEnd = findHtmlEndTagEnd(chunkString, 0)
if (htmlEndTagEnd === -1) {
appendTail(chunkString)
leftover = ''
continue
}

appendTail(chunkString.slice(0, htmlEndTagEnd))
pendingTailComplete = true

const afterClosingTags = chunkString.slice(htmlEndTagEnd)
flushPendingRouterHtml()
if (afterClosingTags) {
writeChunk(afterClosingTags)
if (cleanedUp || isDone()) return
noteBarrierMarker(afterClosingTags)
liftBarrierAfterBoundary()
if (cleanedUp || isDone()) return
flushPendingRouterHtml()
}

leftover = ''
continue
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle </html> split across chunk boundaries.

Once HoldingTail starts, this scan only looks at the current chunkString. If the previous tail ended with something like </ht and the next chunk starts with ml>, the closing tag is never detected, so pendingTailComplete stays false and the tail keeps buffering until EOF.

💡 Suggested fix
         if (state >= MergeState.HoldingTail) {
           if (!pendingTailComplete) {
-            const htmlEndTagEnd = findHtmlEndTagEnd(chunkString, 0)
+            const overlapChars = Math.min(6, pendingTail.length)
+            const overlap = pendingTail.slice(-overlapChars) + chunkString
+            const htmlEndTagEnd = findHtmlEndTagEnd(overlap, 0)
             if (htmlEndTagEnd === -1) {
               appendTail(chunkString)
               leftover = ''
               continue
             }
 
-            appendTail(chunkString.slice(0, htmlEndTagEnd))
+            const splitIndex = Math.max(0, htmlEndTagEnd - overlapChars)
+            appendTail(chunkString.slice(0, splitIndex))
             pendingTailComplete = true
 
-            const afterClosingTags = chunkString.slice(htmlEndTagEnd)
+            const afterClosingTags = chunkString.slice(splitIndex)
             flushPendingRouterHtml()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/src/ssr/transformStreamWithRouter.ts` around lines 813 -
836, The HTML end-tag detection fails when the tail is split across chunks
(e.g., previous leftover ends with "</ht" and current chunk begins "ml>")
because the code only calls findHtmlEndTagEnd on chunkString; change the logic
in the pending-tail branch to search the concatenation of the buffered
tail/leftover plus the current chunk (use the same findHtmlEndTagEnd on leftover
+ chunkString), adjust appendTail and slicing indices to account for the
combined string (so appendTail receives only the part up to htmlEndTagEnd from
the combined string, and afterClosingTags is the remainder from the chunk
portion), and ensure leftover is cleared/updated correctly so
flushPendingRouterHtml, noteBarrierMarker and subsequent writeChunk flows behave
the same as before.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 31, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7515

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7515

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7515

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7515

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7515

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7515

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7515

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7515

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7515

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7515

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7515

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7515

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7515

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7515

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7515

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7515

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7515

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7515

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7515

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7515

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7515

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7515

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7515

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7515

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7515

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7515

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7515

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7515

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7515

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7515

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7515

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7515

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7515

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7515

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7515

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7515

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7515

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7515

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7515

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7515

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7515

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7515

commit: ea7b2aa

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 31, 2026

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing fix/solid-stream-boundary-flush-main (ea7b2aa) with main (9a6c125)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant