-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix: preserve solid deferred stream order #7515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -124,6 +124,24 @@ function findHtmlBoundary(str: string): number { | |
| return lastClosingTagEnd | ||
| } | ||
|
|
||
| function findHtmlEndTagEnd(str: string, searchFrom: number): number { | ||
| for (let i = searchFrom; i <= str.length - 7; i++) { | ||
| if ( | ||
| str.charCodeAt(i) === 60 && | ||
| str.charCodeAt(i + 1) === 47 && | ||
| (str.charCodeAt(i + 2) | 32) === 104 && | ||
| (str.charCodeAt(i + 3) | 32) === 116 && | ||
| (str.charCodeAt(i + 4) | 32) === 109 && | ||
| (str.charCodeAt(i + 5) | 32) === 108 && | ||
| str.charCodeAt(i + 6) === 62 | ||
| ) { | ||
| return i + 7 | ||
| } | ||
| } | ||
|
|
||
| return -1 | ||
| } | ||
|
|
||
| /** | ||
| * Releasing the lock can throw if a pending read is still settling or if the | ||
| * lock was already released. | ||
|
|
@@ -522,6 +540,7 @@ function makeMainStream( | |
| clearPendingRouterHtml() | ||
| leftover = '' | ||
| pendingTail = '' | ||
| pendingTailComplete = false | ||
| clearPending() | ||
|
|
||
| if (cancelReader) { | ||
|
|
@@ -551,8 +570,11 @@ function makeMainStream( | |
| // between-chunk text buffer; keep bounded to avoid unbounded memory | ||
| let leftover = '' | ||
|
|
||
| // captured bytes from </body> onward; must stay behind router scripts. | ||
| // Captured closing tags that must stay after router-injected scripts. | ||
| // Some renderers, like Solid, continue streaming boundary chunks after | ||
| // </html>; those chunks should still pass through before these tags. | ||
| let pendingTail = '' | ||
| let pendingTailComplete = false | ||
|
|
||
| let streamBarrierLifted = false | ||
| let streamBarrierMarkerSeen = false | ||
|
|
@@ -785,26 +807,75 @@ function makeMainStream( | |
|
|
||
| const chunkString = leftover ? leftover + text : text | ||
|
|
||
| // If we already saw </body>, everything else is tail. Keep it bounded | ||
| // and held until router scripts are ready so injection remains before </body>. | ||
| // If we've captured the closing tags, keep streaming subsequent app | ||
| // chunks before those tags instead of buffering them until render end. | ||
| if (state >= MergeState.HoldingTail) { | ||
| appendTail(chunkString) | ||
| 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 | ||
|
Comment on lines
+813
to
+836
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle Once 💡 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 |
||
| } | ||
|
|
||
| flushPendingRouterHtml() | ||
| writeChunk(chunkString) | ||
| if (cleanedUp || isDone()) return | ||
| noteBarrierMarker(chunkString) | ||
| liftBarrierAfterBoundary() | ||
| if (cleanedUp || isDone()) return | ||
| flushPendingRouterHtml() | ||
| leftover = '' | ||
| continue | ||
| } | ||
|
|
||
| const boundary = findHtmlBoundary(chunkString) | ||
| if (boundary < -1) { | ||
| const bodyEndIndex = -boundary - 2 | ||
| const htmlEndTagEnd = findHtmlEndTagEnd(chunkString, bodyEndIndex) | ||
| state = MergeState.HoldingTail | ||
| appendTail(chunkString.slice(bodyEndIndex)) | ||
| if (htmlEndTagEnd === -1) { | ||
| appendTail(chunkString.slice(bodyEndIndex)) | ||
| } else { | ||
| appendTail(chunkString.slice(bodyEndIndex, htmlEndTagEnd)) | ||
| pendingTailComplete = true | ||
| } | ||
| const bodyChunk = chunkString.slice(0, bodyEndIndex) | ||
| writeChunk(bodyChunk) | ||
| if (cleanedUp || isDone()) return | ||
| noteBarrierMarker(bodyChunk) | ||
| liftBarrierAfterBoundary() | ||
| if (cleanedUp || isDone()) return | ||
| flushPendingRouterHtml() | ||
| if (htmlEndTagEnd !== -1) { | ||
| const afterClosingTags = chunkString.slice(htmlEndTagEnd) | ||
| if (afterClosingTags) { | ||
| writeChunk(afterClosingTags) | ||
| if (cleanedUp || isDone()) return | ||
| noteBarrierMarker(afterClosingTags) | ||
| liftBarrierAfterBoundary() | ||
| if (cleanedUp || isDone()) return | ||
| flushPendingRouterHtml() | ||
| } | ||
| } | ||
| leftover = '' | ||
| continue | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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