From ad070dce60505bd195bc055ba719813efd858533 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Sat, 30 May 2026 09:25:22 -0500 Subject: [PATCH 1/3] test: cover solid deferred streaming order --- e2e/react-start/basic/tests/streaming.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/e2e/react-start/basic/tests/streaming.spec.ts b/e2e/react-start/basic/tests/streaming.spec.ts index 15f60b7deb..0457d94123 100644 --- a/e2e/react-start/basic/tests/streaming.spec.ts +++ b/e2e/react-start/basic/tests/streaming.spec.ts @@ -27,6 +27,22 @@ test('Directly visiting the deferred route', async ({ page }) => { ) }) +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!', + ) +}) + test('streaming loader data', async ({ page }) => { await page.goto('/stream') From 49552018885263ae35f4e5e44b00964e4550dd1b Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Sat, 30 May 2026 18:55:54 -0500 Subject: [PATCH 2/3] fix: preserve solid deferred stream order --- e2e/solid-start/basic/tests/streaming.spec.ts | 16 ++++ .../src/ssr/transformStreamWithRouter.ts | 81 +++++++++++++++++-- packages/solid-router/src/awaited.tsx | 8 +- 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/e2e/solid-start/basic/tests/streaming.spec.ts b/e2e/solid-start/basic/tests/streaming.spec.ts index 5fc81f1fed..c202f12260 100644 --- a/e2e/solid-start/basic/tests/streaming.spec.ts +++ b/e2e/solid-start/basic/tests/streaming.spec.ts @@ -28,6 +28,22 @@ test('Directly visiting the deferred route', async ({ page }) => { ) }) +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!', + ) +}) + test('streaming loader data', async ({ page }) => { await page.goto('/stream') diff --git a/packages/router-core/src/ssr/transformStreamWithRouter.ts b/packages/router-core/src/ssr/transformStreamWithRouter.ts index 4393f955b3..3d7b840a2f 100644 --- a/packages/router-core/src/ssr/transformStreamWithRouter.ts +++ b/packages/router-core/src/ssr/transformStreamWithRouter.ts @@ -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 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 + // ; those chunks should still pass through before these tags. let pendingTail = '' + let pendingTailComplete = false let streamBarrierLifted = false let streamBarrierMarkerSeen = false @@ -785,10 +807,42 @@ function makeMainStream( const chunkString = leftover ? leftover + text : text - // If we already saw , everything else is tail. Keep it bounded - // and held until router scripts are ready so injection remains before . + // 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 + } + + flushPendingRouterHtml() + writeChunk(chunkString) + if (cleanedUp || isDone()) return + noteBarrierMarker(chunkString) + liftBarrierAfterBoundary() + if (cleanedUp || isDone()) return + flushPendingRouterHtml() leftover = '' continue } @@ -796,8 +850,14 @@ function makeMainStream( 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 @@ -805,6 +865,17 @@ function makeMainStream( 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 } diff --git a/packages/solid-router/src/awaited.tsx b/packages/solid-router/src/awaited.tsx index fb1edb14e0..641a9b6a07 100644 --- a/packages/solid-router/src/awaited.tsx +++ b/packages/solid-router/src/awaited.tsx @@ -31,13 +31,7 @@ export function Await( }, ) { if (!('fallback' in props)) { - const [resource] = Solid.createResource( - () => defer(props.promise), - (p) => p, - { - deferStream: true, - }, - ) + const [resource] = Solid.createResource(() => defer(props.promise), (p) => p) return ( From ea7b2aacd1ce95d0ad94ee69750160184017cb6a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 30 May 2026 23:58:14 +0000 Subject: [PATCH 3/3] ci: apply automated fixes --- packages/solid-router/src/awaited.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/solid-router/src/awaited.tsx b/packages/solid-router/src/awaited.tsx index 641a9b6a07..a5df61c6f5 100644 --- a/packages/solid-router/src/awaited.tsx +++ b/packages/solid-router/src/awaited.tsx @@ -31,7 +31,10 @@ export function Await( }, ) { if (!('fallback' in props)) { - const [resource] = Solid.createResource(() => defer(props.promise), (p) => p) + const [resource] = Solid.createResource( + () => defer(props.promise), + (p) => p, + ) return (