feat(solid-router): native SSR match transfer, hydration-claiming boot, provider-owned dispatch - #8213
Conversation
…els carry the Router + Query pairing solid-query v6's QueryClientProvider serializes the request's cache into Solid's hydration registry during SSR and primes the client cache from it, so running the ssr-query transport alongside it ships every query payload twice. The package's two runtime conveniences are each a few lines of userland composition on public APIs: the provider wrap via the router's Wrap option, and cache-driven redirect() errors handed to router.navigate from the caches' config.onError. Converts the three Solid Start e2e apps to the composition (all suites green, including the redirect-from-query tests) and marks the package deprecated for the v2 line. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
just a small FYI: we try to use This helps people who instantiate a router in their unit tests |
af88d3d to
d30f736
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
Solid's single-flight channel is becoming multi-source (solidjs/solid 653dd41e): mutation responses carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Today Start claims the single unnamed slot on both halves, which means any other cache wanting mutation-response data (e.g. solid-query, whose provider subscribes under "sq" in TanStack/query#11326) displaces the router's — whichever registers last wins, silently. The router's flight data now rides its own source id ("tsr"): the server collector registers additively with registerFlightDataSource and the client subscribes its consumer under the same id, so router loader/match state and other caches' slices coexist on one round trip. A user-supplied collectFlightData hook keeps the unnamed slot to itself, adding data alongside the router's instead of displacing it. Both halves feature-detect the protocol on the installed @solidjs/web (it ships in the release after 2.0.0-rc.4) and fall back to the exact previous unnamed-slot behavior on older versions; since client and server resolve the same install, the halves cannot disagree. Co-authored-by: Cursor <cursoragent@cursor.com>
…t trigger The router's half of flight collection as a public primitive: derive the flight request for the mutation's target, run the matched routes' data functions, hand the loaded router to the caller's collect() — any cache (the router's own state, a query client) composes its extraction on top. Start's collector now consumes it; errors are contained per Solid Router's collector convention (flight data is an optimization, never a mutation error). Co-authored-by: Cursor <cursoragent@cursor.com>
…5 fallback @solidjs/web 2.0.0-rc.5 ships the multi-source single-flight protocol, so the feature detection and cast shims bridging unreleased types come out: the client subscribes directly under SOLID_START_FLIGHT_SOURCE, the server registers its collector via registerFlightDataSource unconditionally (the unnamed collectFlightData slot now always belongs to the user), and the @solidjs/web peer floor moves to rc.5. Co-authored-by: Cursor <cursoragent@cursor.com>
With the pre-rc.5 fallback stripped, the client advertises its named source and the server echoes what it folded: X-Single-Flight is "tsr" on both sides, not the legacy "true". Co-authored-by: Cursor <cursoragent@cursor.com>
d8fed87 to
51f9c95
Compare
|
View your CI Pipeline Execution ↗ for commit 78b9028
☁️ Nx Cloud last updated this comment at |
…rc.6 Repo-wide (packages, examples, e2e apps, benchmarks) — a scoped bump leaves the workspace mixed, and examples/benchmarks then build workspace solid-start dists (which import registerFlightDataSource, rc.5+) against their own @solidjs/web rc.4 resolution. rc.6 ships the named flight-data source API this branch requires plus the settle-walk fix that unblocks the Solid Query pairing. @tanstack/solid-start's peer floor moves to rc.6. The SSR bench helpers move onto rc.6's wire shape: scripted callers use the data address (`<endpoint>/data/<id>`) — the bare address now answers document traffic with the no-JS convention. Co-authored-by: Cursor <cursoragent@cursor.com>
…aiming boot Proves the RFC's Phase 1 claims on the external-SSR harness, against published core (web 2.0.0-rc.5): - Server render serializes each match's loaderData/status into Solid's hydration registry, content-addressed (`tsr:<matchId>`), the identical mechanism solid-query v6 ships queries through — no `__TSR_SSR__` script channel. - The client boot matches synchronously, primes match state from the registry (populated at document parse), and commits — no `router.load()` before hydrate, loaders do not re-run (0 client runs), hydration claims the server DOM identically, and post-hydration navigation with an unresolved chunk still shows pending UI and settles under its boundary. Spike-level notes: the commit must happen before hydrate() (store writes inside the hydration render are owned-scope writes), the transfer covers settled matches (promise-valued entries for pending loaders are the same serialize call, next step), and the serialization-context guard keys on `ctx.serialize` presence (`ctx.async` is not set under this web version's renderToString). Co-authored-by: Cursor <cursoragent@cursor.com>
…ot (Phase 1) Grows the spike into the adapter. RouterProvider serializes each settled match's state (loaderData, status, error, notFound, beforeLoadContext, ssr) into Solid's hydration registry during server render — content-addressed (`tsr:<matchId>`), the same channel solid-query v6 ships queries through, no `__TSR_SSR__` script injection. The Router constructor owns the client half: when the registry holds entries for the synchronously matched routes, it primes and commits match state at creation — always outside a render, after the document (and therefore the entries) parsed, before hydrate(). No load pass before hydration, no loader re-runs; route chunks resolve at the read point under the boundaries the server rendered. Both halves are inert outside the bare pairing: the server skips when `router.serverSsr` marks the Start contract, and the boot falls through on the first missing entry (SPA pages, Start's own channel). Placement is load-bearing: committing inside the hydration render — even with writes moved off the owner — leaves the claiming walk's registry bookkeeping desynced (nodes reuse correctly but audit as unclaimed). Router creation is the client's natural pre-render moment, and the harness A/B proved it clean. Validated: external-SSR harness green end to end (registry primed, zero client loader runs, identical DOM claimed, pending UI on post-hydration nav), solid-router unit suite 865 tests green, and all 37 Solid Start e2e tests green against the rc.6-candidate core (workspace tarballs) with the named-source strips in place. The harness now contains zero transfer code. Pending matches are skipped, not deferred — promise-valued entries (streaming SSR) are the next increment. Co-authored-by: Cursor <cursoragent@cursor.com>
…er, boundary bullet corrected Co-authored-by: Cursor <cursoragent@cursor.com>
RouterProvider now owns router.load() on the server, parking the render on it through an async memo — no more explicit await router.load() in entries; blocking semantics ride Solid's async SSR. The bare-pairing harness moves to renderToStream and proves deferred loaderData promises stream natively (fallback in the shell chunk, value in a later chunk, settled through hydration without <Await>). Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
51f9c95 to
fab8acb
Compare
Hooray! CodSpeed harness just leveled up!The base and head of this comparison were measured with different runner settings, so their benchmark values are not directly comparable. What changed between base and head:
Re-run the base with the same settings to get a valid performance comparison. Comparing Footnotes |
There was a problem hiding this comment.
Important
At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.
Nx Cloud is proposing a fix for your failed CI:
We fixed two classes of CI failure introduced by this PR: the new repro-external-ssr/ directory was added to packages/solid-router/tsconfig.json's include array so ESLint's TypeScript parser can resolve those files, and all createEffect(fn) single-argument calls across the affected example projects were migrated to createTrackedEffect(fn) — the solid-js 2.0 drop-in replacement for the old unified-callback form. These changes align the examples with the solid-js 2.0.0-rc.6 API that the PR bumped to.
Warning
- ❌ We could not verify this fix.
- The suggested diff is too large to display here, but you can view it on Nx Cloud ↗
Because this branch comes from a fork, it is not possible for us to apply fixes directly, but you can apply the changes locally using the available options below.
Apply changes locally with:
npx nx-cloud apply-locally MFnt-PGzv
Apply fix locally with your editor ↗ View interactive diff ↗
🎓 Learn more about Self-Healing CI on nx.dev
Two things were failing the Test job: - @tanstack/solid-router test:eslint: the repro-external-ssr harness is not part of the package tsconfig, so the typed parser rejected its .tsx files. Ignore the harness in the package eslint config; it is a standalone vite script, not shipped code. - Four example builds (basic-solid-query, basic-solid-query-file-based, kitchen-sink-file-based, kitchen-sink-solid-query) run tsc, and the rc.6 types no longer accept the one-argument createEffect form. Convert the eleven call sites to the two-argument createEffect(source, effect) form already used by the sibling examples and e2e apps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Phase 1 of the Solid-native SSR plan (RFC included in the diff:
RFC-solid-native-ssr.md). Replaces the React-inherited SSR bootstrapping in the Solid adapter with Solid's own hydration machinery:loaderData, status,__beforeLoadContext) into Solid's hydration registry undertsr:keys — the same channelsolid-queryuses — instead of relying on the$_TSRscript for match data. DeferredloaderDatafields stream natively through seroval promise serialization.Routerconstructor, beforehydrate()runs. Matches are committed outside the hydration render's owned scope, so downstream apps no longer need boot-timerouter.load()workarounds (e.g. thebootLoadpattern) or prefetch pausing.RouterProviderkicks offrouter.load()on the server and parks the render on it via an async memo, preserving blocking loader semantics through Solid's async SSR (renderToStream) with no explicitawait router.load()in entry files.Includes a protocol-less external-SSR repro harness (
packages/solid-router/repro-external-ssr/) that round-trips server render → hydration in jsdom and asserts loader single-run, node claiming, deferred streaming order, and interactivity.Stacked on #8192 (named single-flight sources) — review the last 4 commits.
Test plan
@tanstack/solid-routerunit + server-mode suitesMade with Cursor