Skip to content

Adapter 0.9.0: pages stream, as on a Worker - #62

Closed
nicu-chiciuc wants to merge 5 commits into
mainfrom
nicu-adapter-streaming
Closed

nicu-chiciuc wants to merge 5 commits into
mainfrom
nicu-adapter-streaming

Conversation

@nicu-chiciuc

@nicu-chiciuc nicu-chiciuc commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Closes issue 49. Stacked on PR 61, whose /stream page is the proof.

What changes

The adapter's server entry is now TanStack's own stream handler, createStartHandler(defaultStreamHandler), plus the URLSearchParams.size getter Convex lacks. The whole-render wait, the renderSsrHtmlResponse detour, the doctype trimming, and the progressiveChunkSize override are gone.

The one obstacle was bundling, not the runtime. TanStack's stream renderer and router-core's stream transformer import Node's stream classes at the top of their files, for a branch that runs only where React has no Web Stream renderer. Convex has that renderer, so the branch never runs, and Convex's bundler had no node:stream to resolve. convexSsr() now resolves node:stream to a stub module shipped with the adapter, whose classes throw if anything calls them. node:async_hooks already bundled, Convex provides AsyncLocalStorage. The example's server bundle after the change imports node:async_hooks and nothing else from Node.

Measured

/stream from PR 61, fetched with a browser user agent:

Host First byte Last byte
Convex, before (gallant-llama preview of PR 61) 2.4 s 2.4 s
Convex, after (the example's dev deployment) 0.5 s 2.5 s
Workers twin 0.1 s 2.1 s

With curl's own user agent, TanStack takes the client for a bot and sends the whole document at 2.2 s, on both hosts. /, /about/, /todos (307 to /signin), /signin, and a 404 answer as before.

Research behind it

Every host TanStack documents has Node's stream module: Node, Bun, Deno, Netlify's functions, Vercel through Nitro, and Cloudflare Workers through nodejs_compat, on by default since compatibility date 2026-08-04. Convex's runtime is the one without it, so nobody upstream has needed the split; the one related PR, TanStack/router#8094 for Solid, has waited since August without review and does not touch the React file. The stub is independent of upstream.

Adapter 0.9.0, breaking nothing in apps: src/server.ts keeps re-exporting the entry.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

nicu-chiciuc and others added 3 commits October 16, 2026 20:59
…wait

The same page in both examples. Its loader returns a promise that resolves two seconds later,
and the page renders it with Await behind a fallback. On a host that streams, the document goes
out at once with the fallback and the value follows in a later chunk, with React's swap script;
on a host that sends the document whole, everything arrives after two seconds. The Workers twin
under wrangler dev, fetched with a browser user agent: 4.2 kB at 0 ms, the rest 2.0 s later. With
curl's own user agent TanStack treats the client as a bot and waits for the whole document first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ate 2026-08-04

Cloudflare turns nodejs_compat on by default for Workers whose compatibility date is 2026-08-04
or later (changelog 2026-08-04-nodejs-compat-default); the twin's date is 2026-10-01. Checked by
building and running the twin without the flag under wrangler dev: the server bundle still imports
node:stream and node:async_hooks, and the Worker serves the pages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codex: a navigation inside the app runs the page's loader in the browser, so the timer ran there
on both hosts and the demo never reached the server. The header link now reloads the document.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Preview of examples/tanstack-spa: https://steady-hare-438.convex.site

@github-actions

Copy link
Copy Markdown

Preview of examples/tanstack-ssr: https://next-rook-172.convex.site

@pkg-pr-new

pkg-pr-new Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/@samebase/convex-hosting@62
pnpm add https://pkg.pr.new/@samebase/convex-hosting-tanstack-start@62

commit: 9d41bb0

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://nicu-adapter-streaming-convex-hosting-tanstack-ssr-workers.pfp.workers.dev (commit 9d41bb0)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4450efd3-convex-hosting-tanstack-ssr-workers.pfp.workers.dev 9d41bb0 2026-10-16T19:19:32.157Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://6caaa145-convex-hosting-tanstack-ssr-workers.pfp.workers.dev 2e06776 2026-10-16T19:05:39.385Z Visit the dashboard ↗

nicu-chiciuc and others added 2 commits October 16, 2026 22:18
The header read the user with useQuery, which does not run on the server, so a signed-out visitor
got an empty header slot and the "Sign in" link appeared only once the browser had hydrated,
connected, and asked; the same on both hosts, because the code is the same. The server knows the
session from its token: SignedOut and SignedIn from the convex-auth entry render it, so a rendered
page carries the right header in its first bytes. The name still comes from the cache the root
loader filled for a page rendered as the user. A prerendered page has no token and shows its
header once the browser knows, as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server entry is TanStack's own stream handler: React's Web Stream renderer, the document sent
as it renders, each Suspense boundary and deferred loader value in a later chunk, status and
redirects decided before the first byte, the whole document for a client TanStack takes for a bot.
The whole-render wait and the progressiveChunkSize override of 0.6.0 are gone with it.

The one obstacle was bundling: TanStack's stream renderer and router-core's stream transformer
import Node's stream classes at the top of their files for a branch that runs only where React has
no Web Stream renderer. Convex has that renderer, so the branch never runs, and Convex's bundler
had no node:stream to resolve. convexSsr() now resolves node:stream to a stub whose classes throw
if anything calls them. node:async_hooks already bundled, Convex provides AsyncLocalStorage.

Measured on the SSR example's dev deployment, /stream with a browser user agent: first byte at
0.5 s with the fallback, last byte at 2.5 s with the value; with curl's user agent, the whole
document at 2.2 s. The Workers twin: 0.1 s and 2.1 s. Closes #49.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@nicu-chiciuc

Copy link
Copy Markdown
Contributor Author

Folded into #61: one pull request for the streaming fix and the page that proves it.

@nicu-chiciuc
nicu-chiciuc deleted the nicu-adapter-streaming branch October 16, 2026 19:24
@nicu-chiciuc
nicu-chiciuc restored the nicu-adapter-streaming branch October 16, 2026 19:28
@nicu-chiciuc nicu-chiciuc reopened this Oct 16, 2026
@nicu-chiciuc

Copy link
Copy Markdown
Contributor Author

Reopened: kept as its own pull request after all, stacked on #61.

@nicu-chiciuc

Copy link
Copy Markdown
Contributor Author

Folded into #61, which is now the streaming pull request; the header fix is #63.

@nicu-chiciuc
nicu-chiciuc deleted the nicu-adapter-streaming branch October 16, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stream server-rendered pages, as the Worker does

1 participant