From 371073430d82ee63e5e7ea45099d8982d4b0ce62 Mon Sep 17 00:00:00 2001 From: Amariah Kamau Date: Mon, 13 Jul 2026 12:34:14 +0300 Subject: [PATCH] fix(ssr): flush stream buffer before awaiting component-body promise Move the flush call inside the async function so it executes before awaiting the component-body promise, mirroring the isAsyncGenerator branch pattern. Without this, the buffer stays stuck behind the pending promise and nothing streams to the client until it resolves. Add an e2e test verifying that static prefix content streams to the client before a slow async component resolves. --- .changeset/flush-before-component-promise.md | 6 +++++ .../components/streaming/streaming-flush.tsx | 21 ++++++++++++++++++ e2e/qwik-e2e/apps/e2e/src/root.tsx | 2 ++ e2e/qwik-e2e/tests/streaming.e2e.ts | 22 +++++++++++++++++++ packages/qwik/src/core/ssr/ssr-render-jsx.ts | 1 + 5 files changed, 52 insertions(+) create mode 100644 .changeset/flush-before-component-promise.md create mode 100644 e2e/qwik-e2e/apps/e2e/src/components/streaming/streaming-flush.tsx diff --git a/.changeset/flush-before-component-promise.md b/.changeset/flush-before-component-promise.md new file mode 100644 index 00000000000..6c571e33480 --- /dev/null +++ b/.changeset/flush-before-component-promise.md @@ -0,0 +1,6 @@ +--- +'@qwik.dev/core': patch +--- + +Fix SSR in-order streaming not flushing before awaiting a component-body promise. + diff --git a/e2e/qwik-e2e/apps/e2e/src/components/streaming/streaming-flush.tsx b/e2e/qwik-e2e/apps/e2e/src/components/streaming/streaming-flush.tsx new file mode 100644 index 00000000000..34958e3d811 --- /dev/null +++ b/e2e/qwik-e2e/apps/e2e/src/components/streaming/streaming-flush.tsx @@ -0,0 +1,21 @@ +import { component$ } from '@qwik.dev/core'; + +export function delay(time: number) { + return new Promise((resolve) => { + setTimeout(() => resolve(), time); + }); +} + +export const AsyncCmp = component$(async () => { + await delay(5000); + return Async done; +}); + +export const StreamingFlush = component$(() => { + return ( +
+

Prefix content

+ +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/e2e/src/root.tsx b/e2e/qwik-e2e/apps/e2e/src/root.tsx index 9d5a8b62c3b..7c1b0bfe5a2 100644 --- a/e2e/qwik-e2e/apps/e2e/src/root.tsx +++ b/e2e/qwik-e2e/apps/e2e/src/root.tsx @@ -26,6 +26,7 @@ import NakedObjectStoreReactivityIssue5001 from './components/signals/NakedObjec import { Signals } from './components/signals/signals'; import { SlotParent } from './components/slot/slot'; import { StreamingRoot } from './components/streaming/streaming'; +import { StreamingFlush } from './components/streaming/streaming-flush'; import { Styles } from './components/styles/styles'; import { SyncQRL } from './components/sync-qrl/sync-qrl'; import { Toggle } from './components/toggle/toggle'; @@ -66,6 +67,7 @@ const tests: Record = { '/e2e/resource-fn': () => , '/e2e/treeshaking': () => , '/e2e/streaming': () => , + '/e2e/streaming-flush': () => , '/e2e/mount': () => , '/e2e/ref': () => , '/e2e/signals': () => , diff --git a/e2e/qwik-e2e/tests/streaming.e2e.ts b/e2e/qwik-e2e/tests/streaming.e2e.ts index a1a468af06f..09a2e6782c8 100644 --- a/e2e/qwik-e2e/tests/streaming.e2e.ts +++ b/e2e/qwik-e2e/tests/streaming.e2e.ts @@ -66,3 +66,25 @@ test.describe('streaming', () => { await expect(count).toHaveText('Rerender: 2'); }); }); + +test.describe('streaming flush', () => { + test('should flush prefix before awaiting slow async component', async ({ page }) => { + await page.goto('/e2e/streaming-flush', { + waitUntil: 'commit', + }); + + // Prefix content must be visible quickly — the flush sends it + // before blocking on the 5s async component. + await expect(page.locator('#prefix')).toHaveText('Prefix content', { + timeout: 3000, + }); + + // Async content must NOT exist yet (still blocked on the delay). + await expect(page.locator('#async-result')).toHaveCount(0); + + // After the async component resolves, the content appears. + await expect(page.locator('#async-result')).toHaveText('Async done', { + timeout: 8000, + }); + }); +}); diff --git a/packages/qwik/src/core/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/ssr/ssr-render-jsx.ts index c650f665e82..330d090cdeb 100644 --- a/packages/qwik/src/core/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/ssr/ssr-render-jsx.ts @@ -314,6 +314,7 @@ function processJSXNode( if (isPromise(jsxOutput)) { // Defer reading QScopedStyle until after the promise resolves enqueue(async () => { + await ssr.streamHandler.flush(); const resolvedOutput = await jsxOutput; const compStyleComponentId = addComponentStylePrefix(host.getProp(QScopedStyle));