feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE - #11326

Merged
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Sep 2, 2026
Merged

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolidryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — depends on the unreleased @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e). Ready to land once that ships and the peer range bumps.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response can carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Independent caches — Solid Router's route data and the TanStack Query cache — refresh from one round trip without competing for the single legacy consumer slot (which previously meant whichever library subscribed last silently displaced the other).

This PR internalizes the query cache's half:

  • QueryClientProvider subscribes a consumer under the new exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry namespace). A mutation response carrying that slice — a DehydratedState — hydrates the provider's client before the mutation's promise resolves: mounted queries on those keys update in the same beat, with no follow-up refetches.
  • The server half stays app/framework territory (producing the data requires the app's router/loaders), registered additively under the same id. With TanStack Router the collector is a two-primitive composition — the router's trigger, the cache's extraction:
import{registerFlightDataSource}from'@solidjs/web/server-functions/server'import{loadFlightTarget}from'@tanstack/solid-router/ssr/server'import{FLIGHT_DATA_SOURCE,dehydrateSettled}from'@tanstack/solid-query'registerFlightDataSource(FLIGHT_DATA_SOURCE,(event,outcome)=>{if(!outcome.targetUrl)returnundefinedconstqueryClient=createQueryClient()returnloadFlightTarget({router: createAppRouter(queryClient),
event,
outcome,collect: async()=>{conststate=awaitdehydrateSettled(queryClient)returnstate.queries.length>0 ? state : undefined},})})

Apps delete their hand-rolled subscribeFlightData(...) + hydrate(...) client wiring entirely. Subscribing is inert when no server collector exists (the server folds nothing for the source), client-only (the server-side consumer registry is module state shared across requests), and torn down with the provider.

New: dehydrateSettled, SSR teardown, dehydrate filtering

Three additions that complete the native SSR/flight story (covering everything router-ssr-query-core's transport provided, on the query side):

  • dehydrateSettled(client, options?) — the extraction half of a flight collector. Route loaders commonly fire prefetches without awaiting them; plain dehydrate() would snapshot those mid-fetch and ship nothing. Waits for every in-flight fetch, chased to quiescence (a settled batch can dispatch dependent fetches), then dehydrates.
  • SSR teardown — the provider cancels and clears the per-request cache when the server render disposes. Query-core defaults gcTime to Infinity on the server, but any app setting a finite gcTime in defaultOptions would otherwise pin the per-request client (and everything its queries closed over) until the timers fire.
  • Dehydrate filtering — the registry serializer now respects defaultOptions.dehydrate.shouldDehydrateQuery, so apps keep sensitive or oversized queries out of the HTML payload with the same knob they'd pass any other transport. Consulted per cache event until it passes, so a filter rejecting pending queries still admits the settled value if it lands while the request's serialization context is live.

Before landing (once @solidjs/web 2.0.0-rc.5 ships)

  • Delete the subscribeFlightSource typed shim in QueryClientProvider.tsx — call the named-source subscribeFlightData(FLIGHT_DATA_SOURCE, ...) overload directly.
  • Bump the @solidjs/web peer floor to rc.5.

This PR deliberately waits for the release rather than landing with the shim: the whole stack (this, TanStack/router#8192, solidjs/templates#287) ships in lockstep, so landing early buys nothing and leaves cleanup to forget.

Notes

  • A typed shim bridges the installed @solidjs/web declarations until the named-source overload ships and the peer range bumps; it should be removed at that point.
  • Tests drive the registered consumer directly (registration lifecycle, hydration of mounted queries without refetch, seeding never-mounted entries); the wire protocol itself — request-leg source negotiation, keyed envelope, slice routing, per-source error containment, legacy degradation — is tested in @solidjs/web's suite.
  • Pairs with the SSR story already in v6: initial-load transfer is content-addressed through the hydration registry (sq:<queryHash>), post-mutation transfer is source-addressed through the flight envelope (sq) — the same recognition model at two moments.

Verification

  • flightData.test.tsx (consumer) and dehydrateSettled.test.tsx (settling, quiescence-chasing, failure settling + option forwarding) passing; the SSR fixture suite gained teardown (cacheEmptyAfterDispose on both string and streaming renders) and filter assertions (the filtered query's registry entry stays off the wire while others — including a never-rendered prefetch — still ship).
  • Full solid-query suite: 28 files, 352 passed / 1 intentionally skipped, type check clean, against a locally built @solidjs/web carrying the protocol.

Made with Cursor

…URCE
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, so
independent caches (Solid Router's route data, the query cache) refresh
from one round trip without competing for the single legacy slot.
QueryClientProvider now subscribes the query cache's consumer under the
exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry
namespace): a mutation response carrying that slice — a DehydratedState
produced by a server collector registered with
registerFlightDataSource(FLIGHT_DATA_SOURCE, hook) — hydrates the
provider's client before the mutation's promise resolves. Mounted
queries on those keys update with no follow-up refetches, and apps
delete the hand-rolled subscribeFlightData/hydrate wiring entirely.
Subscribing is inert when no server collector exists (the server folds
nothing), client-only (the server registry is cross-request module
state), and torn down with the provider.
Requires the @solidjs/web release following 2.0.0-rc.4 for the
named-source protocol; a typed shim bridges the installed declarations
until the peer range bumps.
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitaiBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a353b061-5117-4d1c-8694-3d7314b5b1d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ering
Fills the gaps the router-ssr-query transport used to cover, natively:
- dehydrateSettled(client): the extraction half of a single-flight
collector — waits for every in-flight fetch (chased to quiescence) so
loaders' fire-and-forget prefetches land before dehydrating.
- SSR teardown: the provider cancels and clears the per-request cache on
render disposal, so user-configured finite gcTime timers cannot pin the
client after the response.
- The registry serializer now respects defaultOptions.dehydrate
.shouldDehydrateQuery, the same knob apps use on any other transport.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to solidjs/solid that referenced this pull request Sep 1, 2026
…ainst workspace-built core
Core-side suites structurally cannot represent the adapter's composed
shapes — rc.5 shipped a settle-walk regression (#3181's fix waking parked
readers into uninitialized projections) that only the adapter's own suite
could see: premature wakes are self-healing for ordinary async nodes and
corrupt only through an empty-seed projection over a stable chained
promise with boundary-parked readers.
The gate packs signals/solid/web from the tree, downloads the adapter
repo, forces resolution through pnpm-workspace.yaml overrides (pnpm 11
ignores package.json pnpm.overrides), asserts the tarballs actually
resolved via file+ store realpaths (a version compare cannot tell tarball
from registry), builds the TS reference graph, and runs the full suite.
Wired into scripts/release.mjs before publish so a candidate that breaks
the flagship adapter fails on the runner, not on npm. Tracks the PR head
carrying the Solid 2.0 pairing until TanStack/query#11326 merges to main.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
… bridge
rc.6 publishes the named flight-data source API with real types, so the
subscribeFlightData cast goes away; the peer floor moves to rc.6 because
rc.5's settle-walk regression breaks query hydration. Also adds the
missing changeset for dehydrateSettled and the SSR teardown work.
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Updated@​solidjs/​web@​2.0.0-rc.3 ⏵ 2.0.0-rc.6100+110083+197+1100
Updatedsolid-js@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+110095+196+1100
Updated@​solidjs/​signals@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+1100100+198100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f2f3fe

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 21:59:13 UTC

@pkg-pr-new

pkg-pr-newBot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11326

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11326

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11326

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11326

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11326

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11326

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11326

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11326

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11326

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11326

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11326

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11326

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11326

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11326

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11326

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11326

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11326

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11326

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11326

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11326

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11326

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11326

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11326

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11326

commit: 5f2f3fe

@brenelz
brenelz merged commit 72f8185 into TanStack:solid-query-v6-preSep 2, 2026
11 of 12 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
brenelz pushed a commit to TanStack/router that referenced this pull request Sep 2, 2026
…-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
brenelz added a commit to TanStack/router that referenced this pull request Sep 2, 2026
…t, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming 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>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (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>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
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>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@ryansolid@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE - #11326

Merged
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Sep 2, 2026
Merged

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolidryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — depends on the unreleased @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e). Ready to land once that ships and the peer range bumps.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response can carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Independent caches — Solid Router's route data and the TanStack Query cache — refresh from one round trip without competing for the single legacy consumer slot (which previously meant whichever library subscribed last silently displaced the other).

This PR internalizes the query cache's half:

  • QueryClientProvider subscribes a consumer under the new exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry namespace). A mutation response carrying that slice — a DehydratedState — hydrates the provider's client before the mutation's promise resolves: mounted queries on those keys update in the same beat, with no follow-up refetches.
  • The server half stays app/framework territory (producing the data requires the app's router/loaders), registered additively under the same id. With TanStack Router the collector is a two-primitive composition — the router's trigger, the cache's extraction:
import{registerFlightDataSource}from'@solidjs/web/server-functions/server'import{loadFlightTarget}from'@tanstack/solid-router/ssr/server'import{FLIGHT_DATA_SOURCE,dehydrateSettled}from'@tanstack/solid-query'registerFlightDataSource(FLIGHT_DATA_SOURCE,(event,outcome)=>{if(!outcome.targetUrl)returnundefinedconstqueryClient=createQueryClient()returnloadFlightTarget({router: createAppRouter(queryClient),
event,
outcome,collect: async()=>{conststate=awaitdehydrateSettled(queryClient)returnstate.queries.length>0 ? state : undefined},})})

Apps delete their hand-rolled subscribeFlightData(...) + hydrate(...) client wiring entirely. Subscribing is inert when no server collector exists (the server folds nothing for the source), client-only (the server-side consumer registry is module state shared across requests), and torn down with the provider.

New: dehydrateSettled, SSR teardown, dehydrate filtering

Three additions that complete the native SSR/flight story (covering everything router-ssr-query-core's transport provided, on the query side):

  • dehydrateSettled(client, options?) — the extraction half of a flight collector. Route loaders commonly fire prefetches without awaiting them; plain dehydrate() would snapshot those mid-fetch and ship nothing. Waits for every in-flight fetch, chased to quiescence (a settled batch can dispatch dependent fetches), then dehydrates.
  • SSR teardown — the provider cancels and clears the per-request cache when the server render disposes. Query-core defaults gcTime to Infinity on the server, but any app setting a finite gcTime in defaultOptions would otherwise pin the per-request client (and everything its queries closed over) until the timers fire.
  • Dehydrate filtering — the registry serializer now respects defaultOptions.dehydrate.shouldDehydrateQuery, so apps keep sensitive or oversized queries out of the HTML payload with the same knob they'd pass any other transport. Consulted per cache event until it passes, so a filter rejecting pending queries still admits the settled value if it lands while the request's serialization context is live.

Before landing (once @solidjs/web 2.0.0-rc.5 ships)

  • Delete the subscribeFlightSource typed shim in QueryClientProvider.tsx — call the named-source subscribeFlightData(FLIGHT_DATA_SOURCE, ...) overload directly.
  • Bump the @solidjs/web peer floor to rc.5.

This PR deliberately waits for the release rather than landing with the shim: the whole stack (this, TanStack/router#8192, solidjs/templates#287) ships in lockstep, so landing early buys nothing and leaves cleanup to forget.

Notes

  • A typed shim bridges the installed @solidjs/web declarations until the named-source overload ships and the peer range bumps; it should be removed at that point.
  • Tests drive the registered consumer directly (registration lifecycle, hydration of mounted queries without refetch, seeding never-mounted entries); the wire protocol itself — request-leg source negotiation, keyed envelope, slice routing, per-source error containment, legacy degradation — is tested in @solidjs/web's suite.
  • Pairs with the SSR story already in v6: initial-load transfer is content-addressed through the hydration registry (sq:<queryHash>), post-mutation transfer is source-addressed through the flight envelope (sq) — the same recognition model at two moments.

Verification

  • flightData.test.tsx (consumer) and dehydrateSettled.test.tsx (settling, quiescence-chasing, failure settling + option forwarding) passing; the SSR fixture suite gained teardown (cacheEmptyAfterDispose on both string and streaming renders) and filter assertions (the filtered query's registry entry stays off the wire while others — including a never-rendered prefetch — still ship).
  • Full solid-query suite: 28 files, 352 passed / 1 intentionally skipped, type check clean, against a locally built @solidjs/web carrying the protocol.

Made with Cursor

…URCE
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, so
independent caches (Solid Router's route data, the query cache) refresh
from one round trip without competing for the single legacy slot.
QueryClientProvider now subscribes the query cache's consumer under the
exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry
namespace): a mutation response carrying that slice — a DehydratedState
produced by a server collector registered with
registerFlightDataSource(FLIGHT_DATA_SOURCE, hook) — hydrates the
provider's client before the mutation's promise resolves. Mounted
queries on those keys update with no follow-up refetches, and apps
delete the hand-rolled subscribeFlightData/hydrate wiring entirely.
Subscribing is inert when no server collector exists (the server folds
nothing), client-only (the server registry is cross-request module
state), and torn down with the provider.
Requires the @solidjs/web release following 2.0.0-rc.4 for the
named-source protocol; a typed shim bridges the installed declarations
until the peer range bumps.
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitaiBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a353b061-5117-4d1c-8694-3d7314b5b1d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ering
Fills the gaps the router-ssr-query transport used to cover, natively:
- dehydrateSettled(client): the extraction half of a single-flight
collector — waits for every in-flight fetch (chased to quiescence) so
loaders' fire-and-forget prefetches land before dehydrating.
- SSR teardown: the provider cancels and clears the per-request cache on
render disposal, so user-configured finite gcTime timers cannot pin the
client after the response.
- The registry serializer now respects defaultOptions.dehydrate
.shouldDehydrateQuery, the same knob apps use on any other transport.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to solidjs/solid that referenced this pull request Sep 1, 2026
…ainst workspace-built core
Core-side suites structurally cannot represent the adapter's composed
shapes — rc.5 shipped a settle-walk regression (#3181's fix waking parked
readers into uninitialized projections) that only the adapter's own suite
could see: premature wakes are self-healing for ordinary async nodes and
corrupt only through an empty-seed projection over a stable chained
promise with boundary-parked readers.
The gate packs signals/solid/web from the tree, downloads the adapter
repo, forces resolution through pnpm-workspace.yaml overrides (pnpm 11
ignores package.json pnpm.overrides), asserts the tarballs actually
resolved via file+ store realpaths (a version compare cannot tell tarball
from registry), builds the TS reference graph, and runs the full suite.
Wired into scripts/release.mjs before publish so a candidate that breaks
the flagship adapter fails on the runner, not on npm. Tracks the PR head
carrying the Solid 2.0 pairing until TanStack/query#11326 merges to main.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
… bridge
rc.6 publishes the named flight-data source API with real types, so the
subscribeFlightData cast goes away; the peer floor moves to rc.6 because
rc.5's settle-walk regression breaks query hydration. Also adds the
missing changeset for dehydrateSettled and the SSR teardown work.
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Updated@​solidjs/​web@​2.0.0-rc.3 ⏵ 2.0.0-rc.6100+110083+197+1100
Updatedsolid-js@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+110095+196+1100
Updated@​solidjs/​signals@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+1100100+198100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f2f3fe

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 21:59:13 UTC

@pkg-pr-new

pkg-pr-newBot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11326

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11326

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11326

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11326

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11326

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11326

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11326

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11326

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11326

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11326

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11326

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11326

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11326

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11326

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11326

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11326

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11326

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11326

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11326

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11326

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11326

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11326

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11326

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11326

commit: 5f2f3fe

@brenelz
brenelz merged commit 72f8185 into TanStack:solid-query-v6-preSep 2, 2026
11 of 12 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
brenelz pushed a commit to TanStack/router that referenced this pull request Sep 2, 2026
…-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
brenelz added a commit to TanStack/router that referenced this pull request Sep 2, 2026
…t, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming 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>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (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>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
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>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@ryansolid@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE - #11326

Merged
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Sep 2, 2026
Merged

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolidryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — depends on the unreleased @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e). Ready to land once that ships and the peer range bumps.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response can carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Independent caches — Solid Router's route data and the TanStack Query cache — refresh from one round trip without competing for the single legacy consumer slot (which previously meant whichever library subscribed last silently displaced the other).

This PR internalizes the query cache's half:

  • QueryClientProvider subscribes a consumer under the new exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry namespace). A mutation response carrying that slice — a DehydratedState — hydrates the provider's client before the mutation's promise resolves: mounted queries on those keys update in the same beat, with no follow-up refetches.
  • The server half stays app/framework territory (producing the data requires the app's router/loaders), registered additively under the same id. With TanStack Router the collector is a two-primitive composition — the router's trigger, the cache's extraction:
import{registerFlightDataSource}from'@solidjs/web/server-functions/server'import{loadFlightTarget}from'@tanstack/solid-router/ssr/server'import{FLIGHT_DATA_SOURCE,dehydrateSettled}from'@tanstack/solid-query'registerFlightDataSource(FLIGHT_DATA_SOURCE,(event,outcome)=>{if(!outcome.targetUrl)returnundefinedconstqueryClient=createQueryClient()returnloadFlightTarget({router: createAppRouter(queryClient),
event,
outcome,collect: async()=>{conststate=awaitdehydrateSettled(queryClient)returnstate.queries.length>0 ? state : undefined},})})

Apps delete their hand-rolled subscribeFlightData(...) + hydrate(...) client wiring entirely. Subscribing is inert when no server collector exists (the server folds nothing for the source), client-only (the server-side consumer registry is module state shared across requests), and torn down with the provider.

New: dehydrateSettled, SSR teardown, dehydrate filtering

Three additions that complete the native SSR/flight story (covering everything router-ssr-query-core's transport provided, on the query side):

  • dehydrateSettled(client, options?) — the extraction half of a flight collector. Route loaders commonly fire prefetches without awaiting them; plain dehydrate() would snapshot those mid-fetch and ship nothing. Waits for every in-flight fetch, chased to quiescence (a settled batch can dispatch dependent fetches), then dehydrates.
  • SSR teardown — the provider cancels and clears the per-request cache when the server render disposes. Query-core defaults gcTime to Infinity on the server, but any app setting a finite gcTime in defaultOptions would otherwise pin the per-request client (and everything its queries closed over) until the timers fire.
  • Dehydrate filtering — the registry serializer now respects defaultOptions.dehydrate.shouldDehydrateQuery, so apps keep sensitive or oversized queries out of the HTML payload with the same knob they'd pass any other transport. Consulted per cache event until it passes, so a filter rejecting pending queries still admits the settled value if it lands while the request's serialization context is live.

Before landing (once @solidjs/web 2.0.0-rc.5 ships)

  • Delete the subscribeFlightSource typed shim in QueryClientProvider.tsx — call the named-source subscribeFlightData(FLIGHT_DATA_SOURCE, ...) overload directly.
  • Bump the @solidjs/web peer floor to rc.5.

This PR deliberately waits for the release rather than landing with the shim: the whole stack (this, TanStack/router#8192, solidjs/templates#287) ships in lockstep, so landing early buys nothing and leaves cleanup to forget.

Notes

  • A typed shim bridges the installed @solidjs/web declarations until the named-source overload ships and the peer range bumps; it should be removed at that point.
  • Tests drive the registered consumer directly (registration lifecycle, hydration of mounted queries without refetch, seeding never-mounted entries); the wire protocol itself — request-leg source negotiation, keyed envelope, slice routing, per-source error containment, legacy degradation — is tested in @solidjs/web's suite.
  • Pairs with the SSR story already in v6: initial-load transfer is content-addressed through the hydration registry (sq:<queryHash>), post-mutation transfer is source-addressed through the flight envelope (sq) — the same recognition model at two moments.

Verification

  • flightData.test.tsx (consumer) and dehydrateSettled.test.tsx (settling, quiescence-chasing, failure settling + option forwarding) passing; the SSR fixture suite gained teardown (cacheEmptyAfterDispose on both string and streaming renders) and filter assertions (the filtered query's registry entry stays off the wire while others — including a never-rendered prefetch — still ship).
  • Full solid-query suite: 28 files, 352 passed / 1 intentionally skipped, type check clean, against a locally built @solidjs/web carrying the protocol.

Made with Cursor

…URCE
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, so
independent caches (Solid Router's route data, the query cache) refresh
from one round trip without competing for the single legacy slot.
QueryClientProvider now subscribes the query cache's consumer under the
exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry
namespace): a mutation response carrying that slice — a DehydratedState
produced by a server collector registered with
registerFlightDataSource(FLIGHT_DATA_SOURCE, hook) — hydrates the
provider's client before the mutation's promise resolves. Mounted
queries on those keys update with no follow-up refetches, and apps
delete the hand-rolled subscribeFlightData/hydrate wiring entirely.
Subscribing is inert when no server collector exists (the server folds
nothing), client-only (the server registry is cross-request module
state), and torn down with the provider.
Requires the @solidjs/web release following 2.0.0-rc.4 for the
named-source protocol; a typed shim bridges the installed declarations
until the peer range bumps.
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitaiBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a353b061-5117-4d1c-8694-3d7314b5b1d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ering
Fills the gaps the router-ssr-query transport used to cover, natively:
- dehydrateSettled(client): the extraction half of a single-flight
collector — waits for every in-flight fetch (chased to quiescence) so
loaders' fire-and-forget prefetches land before dehydrating.
- SSR teardown: the provider cancels and clears the per-request cache on
render disposal, so user-configured finite gcTime timers cannot pin the
client after the response.
- The registry serializer now respects defaultOptions.dehydrate
.shouldDehydrateQuery, the same knob apps use on any other transport.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to solidjs/solid that referenced this pull request Sep 1, 2026
…ainst workspace-built core
Core-side suites structurally cannot represent the adapter's composed
shapes — rc.5 shipped a settle-walk regression (#3181's fix waking parked
readers into uninitialized projections) that only the adapter's own suite
could see: premature wakes are self-healing for ordinary async nodes and
corrupt only through an empty-seed projection over a stable chained
promise with boundary-parked readers.
The gate packs signals/solid/web from the tree, downloads the adapter
repo, forces resolution through pnpm-workspace.yaml overrides (pnpm 11
ignores package.json pnpm.overrides), asserts the tarballs actually
resolved via file+ store realpaths (a version compare cannot tell tarball
from registry), builds the TS reference graph, and runs the full suite.
Wired into scripts/release.mjs before publish so a candidate that breaks
the flagship adapter fails on the runner, not on npm. Tracks the PR head
carrying the Solid 2.0 pairing until TanStack/query#11326 merges to main.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
… bridge
rc.6 publishes the named flight-data source API with real types, so the
subscribeFlightData cast goes away; the peer floor moves to rc.6 because
rc.5's settle-walk regression breaks query hydration. Also adds the
missing changeset for dehydrateSettled and the SSR teardown work.
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Updated@​solidjs/​web@​2.0.0-rc.3 ⏵ 2.0.0-rc.6100+110083+197+1100
Updatedsolid-js@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+110095+196+1100
Updated@​solidjs/​signals@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+1100100+198100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f2f3fe

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 21:59:13 UTC

@pkg-pr-new

pkg-pr-newBot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11326

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11326

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11326

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11326

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11326

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11326

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11326

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11326

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11326

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11326

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11326

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11326

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11326

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11326

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11326

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11326

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11326

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11326

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11326

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11326

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11326

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11326

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11326

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11326

commit: 5f2f3fe

@brenelz
brenelz merged commit 72f8185 into TanStack:solid-query-v6-preSep 2, 2026
11 of 12 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
brenelz pushed a commit to TanStack/router that referenced this pull request Sep 2, 2026
…-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
brenelz added a commit to TanStack/router that referenced this pull request Sep 2, 2026
…t, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming 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>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (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>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
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>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@ryansolid@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE - #11326

Merged
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Sep 2, 2026
Merged

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolidryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — depends on the unreleased @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e). Ready to land once that ships and the peer range bumps.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response can carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Independent caches — Solid Router's route data and the TanStack Query cache — refresh from one round trip without competing for the single legacy consumer slot (which previously meant whichever library subscribed last silently displaced the other).

This PR internalizes the query cache's half:

  • QueryClientProvider subscribes a consumer under the new exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry namespace). A mutation response carrying that slice — a DehydratedState — hydrates the provider's client before the mutation's promise resolves: mounted queries on those keys update in the same beat, with no follow-up refetches.
  • The server half stays app/framework territory (producing the data requires the app's router/loaders), registered additively under the same id. With TanStack Router the collector is a two-primitive composition — the router's trigger, the cache's extraction:
import{registerFlightDataSource}from'@solidjs/web/server-functions/server'import{loadFlightTarget}from'@tanstack/solid-router/ssr/server'import{FLIGHT_DATA_SOURCE,dehydrateSettled}from'@tanstack/solid-query'registerFlightDataSource(FLIGHT_DATA_SOURCE,(event,outcome)=>{if(!outcome.targetUrl)returnundefinedconstqueryClient=createQueryClient()returnloadFlightTarget({router: createAppRouter(queryClient),
event,
outcome,collect: async()=>{conststate=awaitdehydrateSettled(queryClient)returnstate.queries.length>0 ? state : undefined},})})

Apps delete their hand-rolled subscribeFlightData(...) + hydrate(...) client wiring entirely. Subscribing is inert when no server collector exists (the server folds nothing for the source), client-only (the server-side consumer registry is module state shared across requests), and torn down with the provider.

New: dehydrateSettled, SSR teardown, dehydrate filtering

Three additions that complete the native SSR/flight story (covering everything router-ssr-query-core's transport provided, on the query side):

  • dehydrateSettled(client, options?) — the extraction half of a flight collector. Route loaders commonly fire prefetches without awaiting them; plain dehydrate() would snapshot those mid-fetch and ship nothing. Waits for every in-flight fetch, chased to quiescence (a settled batch can dispatch dependent fetches), then dehydrates.
  • SSR teardown — the provider cancels and clears the per-request cache when the server render disposes. Query-core defaults gcTime to Infinity on the server, but any app setting a finite gcTime in defaultOptions would otherwise pin the per-request client (and everything its queries closed over) until the timers fire.
  • Dehydrate filtering — the registry serializer now respects defaultOptions.dehydrate.shouldDehydrateQuery, so apps keep sensitive or oversized queries out of the HTML payload with the same knob they'd pass any other transport. Consulted per cache event until it passes, so a filter rejecting pending queries still admits the settled value if it lands while the request's serialization context is live.

Before landing (once @solidjs/web 2.0.0-rc.5 ships)

  • Delete the subscribeFlightSource typed shim in QueryClientProvider.tsx — call the named-source subscribeFlightData(FLIGHT_DATA_SOURCE, ...) overload directly.
  • Bump the @solidjs/web peer floor to rc.5.

This PR deliberately waits for the release rather than landing with the shim: the whole stack (this, TanStack/router#8192, solidjs/templates#287) ships in lockstep, so landing early buys nothing and leaves cleanup to forget.

Notes

  • A typed shim bridges the installed @solidjs/web declarations until the named-source overload ships and the peer range bumps; it should be removed at that point.
  • Tests drive the registered consumer directly (registration lifecycle, hydration of mounted queries without refetch, seeding never-mounted entries); the wire protocol itself — request-leg source negotiation, keyed envelope, slice routing, per-source error containment, legacy degradation — is tested in @solidjs/web's suite.
  • Pairs with the SSR story already in v6: initial-load transfer is content-addressed through the hydration registry (sq:<queryHash>), post-mutation transfer is source-addressed through the flight envelope (sq) — the same recognition model at two moments.

Verification

  • flightData.test.tsx (consumer) and dehydrateSettled.test.tsx (settling, quiescence-chasing, failure settling + option forwarding) passing; the SSR fixture suite gained teardown (cacheEmptyAfterDispose on both string and streaming renders) and filter assertions (the filtered query's registry entry stays off the wire while others — including a never-rendered prefetch — still ship).
  • Full solid-query suite: 28 files, 352 passed / 1 intentionally skipped, type check clean, against a locally built @solidjs/web carrying the protocol.

Made with Cursor

…URCE
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, so
independent caches (Solid Router's route data, the query cache) refresh
from one round trip without competing for the single legacy slot.
QueryClientProvider now subscribes the query cache's consumer under the
exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry
namespace): a mutation response carrying that slice — a DehydratedState
produced by a server collector registered with
registerFlightDataSource(FLIGHT_DATA_SOURCE, hook) — hydrates the
provider's client before the mutation's promise resolves. Mounted
queries on those keys update with no follow-up refetches, and apps
delete the hand-rolled subscribeFlightData/hydrate wiring entirely.
Subscribing is inert when no server collector exists (the server folds
nothing), client-only (the server registry is cross-request module
state), and torn down with the provider.
Requires the @solidjs/web release following 2.0.0-rc.4 for the
named-source protocol; a typed shim bridges the installed declarations
until the peer range bumps.
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitaiBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a353b061-5117-4d1c-8694-3d7314b5b1d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ering
Fills the gaps the router-ssr-query transport used to cover, natively:
- dehydrateSettled(client): the extraction half of a single-flight
collector — waits for every in-flight fetch (chased to quiescence) so
loaders' fire-and-forget prefetches land before dehydrating.
- SSR teardown: the provider cancels and clears the per-request cache on
render disposal, so user-configured finite gcTime timers cannot pin the
client after the response.
- The registry serializer now respects defaultOptions.dehydrate
.shouldDehydrateQuery, the same knob apps use on any other transport.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to solidjs/solid that referenced this pull request Sep 1, 2026
…ainst workspace-built core
Core-side suites structurally cannot represent the adapter's composed
shapes — rc.5 shipped a settle-walk regression (#3181's fix waking parked
readers into uninitialized projections) that only the adapter's own suite
could see: premature wakes are self-healing for ordinary async nodes and
corrupt only through an empty-seed projection over a stable chained
promise with boundary-parked readers.
The gate packs signals/solid/web from the tree, downloads the adapter
repo, forces resolution through pnpm-workspace.yaml overrides (pnpm 11
ignores package.json pnpm.overrides), asserts the tarballs actually
resolved via file+ store realpaths (a version compare cannot tell tarball
from registry), builds the TS reference graph, and runs the full suite.
Wired into scripts/release.mjs before publish so a candidate that breaks
the flagship adapter fails on the runner, not on npm. Tracks the PR head
carrying the Solid 2.0 pairing until TanStack/query#11326 merges to main.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
… bridge
rc.6 publishes the named flight-data source API with real types, so the
subscribeFlightData cast goes away; the peer floor moves to rc.6 because
rc.5's settle-walk regression breaks query hydration. Also adds the
missing changeset for dehydrateSettled and the SSR teardown work.
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Updated@​solidjs/​web@​2.0.0-rc.3 ⏵ 2.0.0-rc.6100+110083+197+1100
Updatedsolid-js@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+110095+196+1100
Updated@​solidjs/​signals@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+1100100+198100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f2f3fe

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 21:59:13 UTC

@pkg-pr-new

pkg-pr-newBot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11326

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11326

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11326

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11326

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11326

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11326

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11326

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11326

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11326

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11326

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11326

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11326

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11326

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11326

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11326

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11326

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11326

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11326

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11326

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11326

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11326

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11326

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11326

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11326

commit: 5f2f3fe

@brenelz
brenelz merged commit 72f8185 into TanStack:solid-query-v6-preSep 2, 2026
11 of 12 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
brenelz pushed a commit to TanStack/router that referenced this pull request Sep 2, 2026
…-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
brenelz added a commit to TanStack/router that referenced this pull request Sep 2, 2026
…t, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming 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>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (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>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
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>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@ryansolid@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE - #11326

Merged
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Sep 2, 2026
Merged

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolidryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — depends on the unreleased @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e). Ready to land once that ships and the peer range bumps.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response can carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Independent caches — Solid Router's route data and the TanStack Query cache — refresh from one round trip without competing for the single legacy consumer slot (which previously meant whichever library subscribed last silently displaced the other).

This PR internalizes the query cache's half:

  • QueryClientProvider subscribes a consumer under the new exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry namespace). A mutation response carrying that slice — a DehydratedState — hydrates the provider's client before the mutation's promise resolves: mounted queries on those keys update in the same beat, with no follow-up refetches.
  • The server half stays app/framework territory (producing the data requires the app's router/loaders), registered additively under the same id. With TanStack Router the collector is a two-primitive composition — the router's trigger, the cache's extraction:
import{registerFlightDataSource}from'@solidjs/web/server-functions/server'import{loadFlightTarget}from'@tanstack/solid-router/ssr/server'import{FLIGHT_DATA_SOURCE,dehydrateSettled}from'@tanstack/solid-query'registerFlightDataSource(FLIGHT_DATA_SOURCE,(event,outcome)=>{if(!outcome.targetUrl)returnundefinedconstqueryClient=createQueryClient()returnloadFlightTarget({router: createAppRouter(queryClient),
event,
outcome,collect: async()=>{conststate=awaitdehydrateSettled(queryClient)returnstate.queries.length>0 ? state : undefined},})})

Apps delete their hand-rolled subscribeFlightData(...) + hydrate(...) client wiring entirely. Subscribing is inert when no server collector exists (the server folds nothing for the source), client-only (the server-side consumer registry is module state shared across requests), and torn down with the provider.

New: dehydrateSettled, SSR teardown, dehydrate filtering

Three additions that complete the native SSR/flight story (covering everything router-ssr-query-core's transport provided, on the query side):

  • dehydrateSettled(client, options?) — the extraction half of a flight collector. Route loaders commonly fire prefetches without awaiting them; plain dehydrate() would snapshot those mid-fetch and ship nothing. Waits for every in-flight fetch, chased to quiescence (a settled batch can dispatch dependent fetches), then dehydrates.
  • SSR teardown — the provider cancels and clears the per-request cache when the server render disposes. Query-core defaults gcTime to Infinity on the server, but any app setting a finite gcTime in defaultOptions would otherwise pin the per-request client (and everything its queries closed over) until the timers fire.
  • Dehydrate filtering — the registry serializer now respects defaultOptions.dehydrate.shouldDehydrateQuery, so apps keep sensitive or oversized queries out of the HTML payload with the same knob they'd pass any other transport. Consulted per cache event until it passes, so a filter rejecting pending queries still admits the settled value if it lands while the request's serialization context is live.

Before landing (once @solidjs/web 2.0.0-rc.5 ships)

  • Delete the subscribeFlightSource typed shim in QueryClientProvider.tsx — call the named-source subscribeFlightData(FLIGHT_DATA_SOURCE, ...) overload directly.
  • Bump the @solidjs/web peer floor to rc.5.

This PR deliberately waits for the release rather than landing with the shim: the whole stack (this, TanStack/router#8192, solidjs/templates#287) ships in lockstep, so landing early buys nothing and leaves cleanup to forget.

Notes

  • A typed shim bridges the installed @solidjs/web declarations until the named-source overload ships and the peer range bumps; it should be removed at that point.
  • Tests drive the registered consumer directly (registration lifecycle, hydration of mounted queries without refetch, seeding never-mounted entries); the wire protocol itself — request-leg source negotiation, keyed envelope, slice routing, per-source error containment, legacy degradation — is tested in @solidjs/web's suite.
  • Pairs with the SSR story already in v6: initial-load transfer is content-addressed through the hydration registry (sq:<queryHash>), post-mutation transfer is source-addressed through the flight envelope (sq) — the same recognition model at two moments.

Verification

  • flightData.test.tsx (consumer) and dehydrateSettled.test.tsx (settling, quiescence-chasing, failure settling + option forwarding) passing; the SSR fixture suite gained teardown (cacheEmptyAfterDispose on both string and streaming renders) and filter assertions (the filtered query's registry entry stays off the wire while others — including a never-rendered prefetch — still ship).
  • Full solid-query suite: 28 files, 352 passed / 1 intentionally skipped, type check clean, against a locally built @solidjs/web carrying the protocol.

Made with Cursor

…URCE
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, so
independent caches (Solid Router's route data, the query cache) refresh
from one round trip without competing for the single legacy slot.
QueryClientProvider now subscribes the query cache's consumer under the
exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry
namespace): a mutation response carrying that slice — a DehydratedState
produced by a server collector registered with
registerFlightDataSource(FLIGHT_DATA_SOURCE, hook) — hydrates the
provider's client before the mutation's promise resolves. Mounted
queries on those keys update with no follow-up refetches, and apps
delete the hand-rolled subscribeFlightData/hydrate wiring entirely.
Subscribing is inert when no server collector exists (the server folds
nothing), client-only (the server registry is cross-request module
state), and torn down with the provider.
Requires the @solidjs/web release following 2.0.0-rc.4 for the
named-source protocol; a typed shim bridges the installed declarations
until the peer range bumps.
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitaiBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a353b061-5117-4d1c-8694-3d7314b5b1d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ering
Fills the gaps the router-ssr-query transport used to cover, natively:
- dehydrateSettled(client): the extraction half of a single-flight
collector — waits for every in-flight fetch (chased to quiescence) so
loaders' fire-and-forget prefetches land before dehydrating.
- SSR teardown: the provider cancels and clears the per-request cache on
render disposal, so user-configured finite gcTime timers cannot pin the
client after the response.
- The registry serializer now respects defaultOptions.dehydrate
.shouldDehydrateQuery, the same knob apps use on any other transport.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to solidjs/solid that referenced this pull request Sep 1, 2026
…ainst workspace-built core
Core-side suites structurally cannot represent the adapter's composed
shapes — rc.5 shipped a settle-walk regression (#3181's fix waking parked
readers into uninitialized projections) that only the adapter's own suite
could see: premature wakes are self-healing for ordinary async nodes and
corrupt only through an empty-seed projection over a stable chained
promise with boundary-parked readers.
The gate packs signals/solid/web from the tree, downloads the adapter
repo, forces resolution through pnpm-workspace.yaml overrides (pnpm 11
ignores package.json pnpm.overrides), asserts the tarballs actually
resolved via file+ store realpaths (a version compare cannot tell tarball
from registry), builds the TS reference graph, and runs the full suite.
Wired into scripts/release.mjs before publish so a candidate that breaks
the flagship adapter fails on the runner, not on npm. Tracks the PR head
carrying the Solid 2.0 pairing until TanStack/query#11326 merges to main.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
… bridge
rc.6 publishes the named flight-data source API with real types, so the
subscribeFlightData cast goes away; the peer floor moves to rc.6 because
rc.5's settle-walk regression breaks query hydration. Also adds the
missing changeset for dehydrateSettled and the SSR teardown work.
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Updated@​solidjs/​web@​2.0.0-rc.3 ⏵ 2.0.0-rc.6100+110083+197+1100
Updatedsolid-js@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+110095+196+1100
Updated@​solidjs/​signals@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+1100100+198100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f2f3fe

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 21:59:13 UTC

@pkg-pr-new

pkg-pr-newBot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11326

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11326

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11326

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11326

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11326

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11326

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11326

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11326

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11326

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11326

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11326

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11326

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11326

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11326

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11326

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11326

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11326

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11326

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11326

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11326

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11326

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11326

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11326

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11326

commit: 5f2f3fe

@brenelz
brenelz merged commit 72f8185 into TanStack:solid-query-v6-preSep 2, 2026
11 of 12 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
brenelz pushed a commit to TanStack/router that referenced this pull request Sep 2, 2026
…-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
brenelz added a commit to TanStack/router that referenced this pull request Sep 2, 2026
…t, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming 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>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (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>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
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>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@ryansolid@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE - #11326

Merged
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Sep 2, 2026
Merged

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolidryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — depends on the unreleased @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e). Ready to land once that ships and the peer range bumps.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response can carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Independent caches — Solid Router's route data and the TanStack Query cache — refresh from one round trip without competing for the single legacy consumer slot (which previously meant whichever library subscribed last silently displaced the other).

This PR internalizes the query cache's half:

  • QueryClientProvider subscribes a consumer under the new exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry namespace). A mutation response carrying that slice — a DehydratedState — hydrates the provider's client before the mutation's promise resolves: mounted queries on those keys update in the same beat, with no follow-up refetches.
  • The server half stays app/framework territory (producing the data requires the app's router/loaders), registered additively under the same id. With TanStack Router the collector is a two-primitive composition — the router's trigger, the cache's extraction:
import{registerFlightDataSource}from'@solidjs/web/server-functions/server'import{loadFlightTarget}from'@tanstack/solid-router/ssr/server'import{FLIGHT_DATA_SOURCE,dehydrateSettled}from'@tanstack/solid-query'registerFlightDataSource(FLIGHT_DATA_SOURCE,(event,outcome)=>{if(!outcome.targetUrl)returnundefinedconstqueryClient=createQueryClient()returnloadFlightTarget({router: createAppRouter(queryClient),
event,
outcome,collect: async()=>{conststate=awaitdehydrateSettled(queryClient)returnstate.queries.length>0 ? state : undefined},})})

Apps delete their hand-rolled subscribeFlightData(...) + hydrate(...) client wiring entirely. Subscribing is inert when no server collector exists (the server folds nothing for the source), client-only (the server-side consumer registry is module state shared across requests), and torn down with the provider.

New: dehydrateSettled, SSR teardown, dehydrate filtering

Three additions that complete the native SSR/flight story (covering everything router-ssr-query-core's transport provided, on the query side):

  • dehydrateSettled(client, options?) — the extraction half of a flight collector. Route loaders commonly fire prefetches without awaiting them; plain dehydrate() would snapshot those mid-fetch and ship nothing. Waits for every in-flight fetch, chased to quiescence (a settled batch can dispatch dependent fetches), then dehydrates.
  • SSR teardown — the provider cancels and clears the per-request cache when the server render disposes. Query-core defaults gcTime to Infinity on the server, but any app setting a finite gcTime in defaultOptions would otherwise pin the per-request client (and everything its queries closed over) until the timers fire.
  • Dehydrate filtering — the registry serializer now respects defaultOptions.dehydrate.shouldDehydrateQuery, so apps keep sensitive or oversized queries out of the HTML payload with the same knob they'd pass any other transport. Consulted per cache event until it passes, so a filter rejecting pending queries still admits the settled value if it lands while the request's serialization context is live.

Before landing (once @solidjs/web 2.0.0-rc.5 ships)

  • Delete the subscribeFlightSource typed shim in QueryClientProvider.tsx — call the named-source subscribeFlightData(FLIGHT_DATA_SOURCE, ...) overload directly.
  • Bump the @solidjs/web peer floor to rc.5.

This PR deliberately waits for the release rather than landing with the shim: the whole stack (this, TanStack/router#8192, solidjs/templates#287) ships in lockstep, so landing early buys nothing and leaves cleanup to forget.

Notes

  • A typed shim bridges the installed @solidjs/web declarations until the named-source overload ships and the peer range bumps; it should be removed at that point.
  • Tests drive the registered consumer directly (registration lifecycle, hydration of mounted queries without refetch, seeding never-mounted entries); the wire protocol itself — request-leg source negotiation, keyed envelope, slice routing, per-source error containment, legacy degradation — is tested in @solidjs/web's suite.
  • Pairs with the SSR story already in v6: initial-load transfer is content-addressed through the hydration registry (sq:<queryHash>), post-mutation transfer is source-addressed through the flight envelope (sq) — the same recognition model at two moments.

Verification

  • flightData.test.tsx (consumer) and dehydrateSettled.test.tsx (settling, quiescence-chasing, failure settling + option forwarding) passing; the SSR fixture suite gained teardown (cacheEmptyAfterDispose on both string and streaming renders) and filter assertions (the filtered query's registry entry stays off the wire while others — including a never-rendered prefetch — still ship).
  • Full solid-query suite: 28 files, 352 passed / 1 intentionally skipped, type check clean, against a locally built @solidjs/web carrying the protocol.

Made with Cursor

…URCE
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, so
independent caches (Solid Router's route data, the query cache) refresh
from one round trip without competing for the single legacy slot.
QueryClientProvider now subscribes the query cache's consumer under the
exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry
namespace): a mutation response carrying that slice — a DehydratedState
produced by a server collector registered with
registerFlightDataSource(FLIGHT_DATA_SOURCE, hook) — hydrates the
provider's client before the mutation's promise resolves. Mounted
queries on those keys update with no follow-up refetches, and apps
delete the hand-rolled subscribeFlightData/hydrate wiring entirely.
Subscribing is inert when no server collector exists (the server folds
nothing), client-only (the server registry is cross-request module
state), and torn down with the provider.
Requires the @solidjs/web release following 2.0.0-rc.4 for the
named-source protocol; a typed shim bridges the installed declarations
until the peer range bumps.
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitaiBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a353b061-5117-4d1c-8694-3d7314b5b1d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ering
Fills the gaps the router-ssr-query transport used to cover, natively:
- dehydrateSettled(client): the extraction half of a single-flight
collector — waits for every in-flight fetch (chased to quiescence) so
loaders' fire-and-forget prefetches land before dehydrating.
- SSR teardown: the provider cancels and clears the per-request cache on
render disposal, so user-configured finite gcTime timers cannot pin the
client after the response.
- The registry serializer now respects defaultOptions.dehydrate
.shouldDehydrateQuery, the same knob apps use on any other transport.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to solidjs/solid that referenced this pull request Sep 1, 2026
…ainst workspace-built core
Core-side suites structurally cannot represent the adapter's composed
shapes — rc.5 shipped a settle-walk regression (#3181's fix waking parked
readers into uninitialized projections) that only the adapter's own suite
could see: premature wakes are self-healing for ordinary async nodes and
corrupt only through an empty-seed projection over a stable chained
promise with boundary-parked readers.
The gate packs signals/solid/web from the tree, downloads the adapter
repo, forces resolution through pnpm-workspace.yaml overrides (pnpm 11
ignores package.json pnpm.overrides), asserts the tarballs actually
resolved via file+ store realpaths (a version compare cannot tell tarball
from registry), builds the TS reference graph, and runs the full suite.
Wired into scripts/release.mjs before publish so a candidate that breaks
the flagship adapter fails on the runner, not on npm. Tracks the PR head
carrying the Solid 2.0 pairing until TanStack/query#11326 merges to main.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
… bridge
rc.6 publishes the named flight-data source API with real types, so the
subscribeFlightData cast goes away; the peer floor moves to rc.6 because
rc.5's settle-walk regression breaks query hydration. Also adds the
missing changeset for dehydrateSettled and the SSR teardown work.
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Updated@​solidjs/​web@​2.0.0-rc.3 ⏵ 2.0.0-rc.6100+110083+197+1100
Updatedsolid-js@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+110095+196+1100
Updated@​solidjs/​signals@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+1100100+198100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f2f3fe

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 21:59:13 UTC

@pkg-pr-new

pkg-pr-newBot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11326

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11326

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11326

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11326

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11326

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11326

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11326

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11326

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11326

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11326

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11326

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11326

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11326

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11326

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11326

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11326

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11326

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11326

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11326

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11326

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11326

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11326

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11326

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11326

commit: 5f2f3fe

@brenelz
brenelz merged commit 72f8185 into TanStack:solid-query-v6-preSep 2, 2026
11 of 12 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
brenelz pushed a commit to TanStack/router that referenced this pull request Sep 2, 2026
…-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
brenelz added a commit to TanStack/router that referenced this pull request Sep 2, 2026
…t, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming 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>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (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>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
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>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@ryansolid@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE - #11326

Merged
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Sep 2, 2026
Merged

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolidryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — depends on the unreleased @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e). Ready to land once that ships and the peer range bumps.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response can carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Independent caches — Solid Router's route data and the TanStack Query cache — refresh from one round trip without competing for the single legacy consumer slot (which previously meant whichever library subscribed last silently displaced the other).

This PR internalizes the query cache's half:

  • QueryClientProvider subscribes a consumer under the new exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry namespace). A mutation response carrying that slice — a DehydratedState — hydrates the provider's client before the mutation's promise resolves: mounted queries on those keys update in the same beat, with no follow-up refetches.
  • The server half stays app/framework territory (producing the data requires the app's router/loaders), registered additively under the same id. With TanStack Router the collector is a two-primitive composition — the router's trigger, the cache's extraction:
import{registerFlightDataSource}from'@solidjs/web/server-functions/server'import{loadFlightTarget}from'@tanstack/solid-router/ssr/server'import{FLIGHT_DATA_SOURCE,dehydrateSettled}from'@tanstack/solid-query'registerFlightDataSource(FLIGHT_DATA_SOURCE,(event,outcome)=>{if(!outcome.targetUrl)returnundefinedconstqueryClient=createQueryClient()returnloadFlightTarget({router: createAppRouter(queryClient),
event,
outcome,collect: async()=>{conststate=awaitdehydrateSettled(queryClient)returnstate.queries.length>0 ? state : undefined},})})

Apps delete their hand-rolled subscribeFlightData(...) + hydrate(...) client wiring entirely. Subscribing is inert when no server collector exists (the server folds nothing for the source), client-only (the server-side consumer registry is module state shared across requests), and torn down with the provider.

New: dehydrateSettled, SSR teardown, dehydrate filtering

Three additions that complete the native SSR/flight story (covering everything router-ssr-query-core's transport provided, on the query side):

  • dehydrateSettled(client, options?) — the extraction half of a flight collector. Route loaders commonly fire prefetches without awaiting them; plain dehydrate() would snapshot those mid-fetch and ship nothing. Waits for every in-flight fetch, chased to quiescence (a settled batch can dispatch dependent fetches), then dehydrates.
  • SSR teardown — the provider cancels and clears the per-request cache when the server render disposes. Query-core defaults gcTime to Infinity on the server, but any app setting a finite gcTime in defaultOptions would otherwise pin the per-request client (and everything its queries closed over) until the timers fire.
  • Dehydrate filtering — the registry serializer now respects defaultOptions.dehydrate.shouldDehydrateQuery, so apps keep sensitive or oversized queries out of the HTML payload with the same knob they'd pass any other transport. Consulted per cache event until it passes, so a filter rejecting pending queries still admits the settled value if it lands while the request's serialization context is live.

Before landing (once @solidjs/web 2.0.0-rc.5 ships)

  • Delete the subscribeFlightSource typed shim in QueryClientProvider.tsx — call the named-source subscribeFlightData(FLIGHT_DATA_SOURCE, ...) overload directly.
  • Bump the @solidjs/web peer floor to rc.5.

This PR deliberately waits for the release rather than landing with the shim: the whole stack (this, TanStack/router#8192, solidjs/templates#287) ships in lockstep, so landing early buys nothing and leaves cleanup to forget.

Notes

  • A typed shim bridges the installed @solidjs/web declarations until the named-source overload ships and the peer range bumps; it should be removed at that point.
  • Tests drive the registered consumer directly (registration lifecycle, hydration of mounted queries without refetch, seeding never-mounted entries); the wire protocol itself — request-leg source negotiation, keyed envelope, slice routing, per-source error containment, legacy degradation — is tested in @solidjs/web's suite.
  • Pairs with the SSR story already in v6: initial-load transfer is content-addressed through the hydration registry (sq:<queryHash>), post-mutation transfer is source-addressed through the flight envelope (sq) — the same recognition model at two moments.

Verification

  • flightData.test.tsx (consumer) and dehydrateSettled.test.tsx (settling, quiescence-chasing, failure settling + option forwarding) passing; the SSR fixture suite gained teardown (cacheEmptyAfterDispose on both string and streaming renders) and filter assertions (the filtered query's registry entry stays off the wire while others — including a never-rendered prefetch — still ship).
  • Full solid-query suite: 28 files, 352 passed / 1 intentionally skipped, type check clean, against a locally built @solidjs/web carrying the protocol.

Made with Cursor

…URCE
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, so
independent caches (Solid Router's route data, the query cache) refresh
from one round trip without competing for the single legacy slot.
QueryClientProvider now subscribes the query cache's consumer under the
exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry
namespace): a mutation response carrying that slice — a DehydratedState
produced by a server collector registered with
registerFlightDataSource(FLIGHT_DATA_SOURCE, hook) — hydrates the
provider's client before the mutation's promise resolves. Mounted
queries on those keys update with no follow-up refetches, and apps
delete the hand-rolled subscribeFlightData/hydrate wiring entirely.
Subscribing is inert when no server collector exists (the server folds
nothing), client-only (the server registry is cross-request module
state), and torn down with the provider.
Requires the @solidjs/web release following 2.0.0-rc.4 for the
named-source protocol; a typed shim bridges the installed declarations
until the peer range bumps.
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitaiBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a353b061-5117-4d1c-8694-3d7314b5b1d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ering
Fills the gaps the router-ssr-query transport used to cover, natively:
- dehydrateSettled(client): the extraction half of a single-flight
collector — waits for every in-flight fetch (chased to quiescence) so
loaders' fire-and-forget prefetches land before dehydrating.
- SSR teardown: the provider cancels and clears the per-request cache on
render disposal, so user-configured finite gcTime timers cannot pin the
client after the response.
- The registry serializer now respects defaultOptions.dehydrate
.shouldDehydrateQuery, the same knob apps use on any other transport.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to solidjs/solid that referenced this pull request Sep 1, 2026
…ainst workspace-built core
Core-side suites structurally cannot represent the adapter's composed
shapes — rc.5 shipped a settle-walk regression (#3181's fix waking parked
readers into uninitialized projections) that only the adapter's own suite
could see: premature wakes are self-healing for ordinary async nodes and
corrupt only through an empty-seed projection over a stable chained
promise with boundary-parked readers.
The gate packs signals/solid/web from the tree, downloads the adapter
repo, forces resolution through pnpm-workspace.yaml overrides (pnpm 11
ignores package.json pnpm.overrides), asserts the tarballs actually
resolved via file+ store realpaths (a version compare cannot tell tarball
from registry), builds the TS reference graph, and runs the full suite.
Wired into scripts/release.mjs before publish so a candidate that breaks
the flagship adapter fails on the runner, not on npm. Tracks the PR head
carrying the Solid 2.0 pairing until TanStack/query#11326 merges to main.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
… bridge
rc.6 publishes the named flight-data source API with real types, so the
subscribeFlightData cast goes away; the peer floor moves to rc.6 because
rc.5's settle-walk regression breaks query hydration. Also adds the
missing changeset for dehydrateSettled and the SSR teardown work.
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Updated@​solidjs/​web@​2.0.0-rc.3 ⏵ 2.0.0-rc.6100+110083+197+1100
Updatedsolid-js@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+110095+196+1100
Updated@​solidjs/​signals@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+1100100+198100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f2f3fe

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 21:59:13 UTC

@pkg-pr-new

pkg-pr-newBot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11326

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11326

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11326

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11326

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11326

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11326

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11326

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11326

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11326

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11326

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11326

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11326

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11326

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11326

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11326

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11326

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11326

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11326

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11326

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11326

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11326

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11326

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11326

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11326

commit: 5f2f3fe

@brenelz
brenelz merged commit 72f8185 into TanStack:solid-query-v6-preSep 2, 2026
11 of 12 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
brenelz pushed a commit to TanStack/router that referenced this pull request Sep 2, 2026
…-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
brenelz added a commit to TanStack/router that referenced this pull request Sep 2, 2026
…t, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming 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>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (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>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
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>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@ryansolid@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE - #11326

Merged
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Sep 2, 2026
Merged

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
brenelz merged 3 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolidryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — depends on the unreleased @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e). Ready to land once that ships and the peer range bumps.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response can carry a keyed envelope of per-cache slices, each routed to the consumer subscribed under its source id. Independent caches — Solid Router's route data and the TanStack Query cache — refresh from one round trip without competing for the single legacy consumer slot (which previously meant whichever library subscribed last silently displaced the other).

This PR internalizes the query cache's half:

  • QueryClientProvider subscribes a consumer under the new exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry namespace). A mutation response carrying that slice — a DehydratedState — hydrates the provider's client before the mutation's promise resolves: mounted queries on those keys update in the same beat, with no follow-up refetches.
  • The server half stays app/framework territory (producing the data requires the app's router/loaders), registered additively under the same id. With TanStack Router the collector is a two-primitive composition — the router's trigger, the cache's extraction:
import{registerFlightDataSource}from'@solidjs/web/server-functions/server'import{loadFlightTarget}from'@tanstack/solid-router/ssr/server'import{FLIGHT_DATA_SOURCE,dehydrateSettled}from'@tanstack/solid-query'registerFlightDataSource(FLIGHT_DATA_SOURCE,(event,outcome)=>{if(!outcome.targetUrl)returnundefinedconstqueryClient=createQueryClient()returnloadFlightTarget({router: createAppRouter(queryClient),
event,
outcome,collect: async()=>{conststate=awaitdehydrateSettled(queryClient)returnstate.queries.length>0 ? state : undefined},})})

Apps delete their hand-rolled subscribeFlightData(...) + hydrate(...) client wiring entirely. Subscribing is inert when no server collector exists (the server folds nothing for the source), client-only (the server-side consumer registry is module state shared across requests), and torn down with the provider.

New: dehydrateSettled, SSR teardown, dehydrate filtering

Three additions that complete the native SSR/flight story (covering everything router-ssr-query-core's transport provided, on the query side):

  • dehydrateSettled(client, options?) — the extraction half of a flight collector. Route loaders commonly fire prefetches without awaiting them; plain dehydrate() would snapshot those mid-fetch and ship nothing. Waits for every in-flight fetch, chased to quiescence (a settled batch can dispatch dependent fetches), then dehydrates.
  • SSR teardown — the provider cancels and clears the per-request cache when the server render disposes. Query-core defaults gcTime to Infinity on the server, but any app setting a finite gcTime in defaultOptions would otherwise pin the per-request client (and everything its queries closed over) until the timers fire.
  • Dehydrate filtering — the registry serializer now respects defaultOptions.dehydrate.shouldDehydrateQuery, so apps keep sensitive or oversized queries out of the HTML payload with the same knob they'd pass any other transport. Consulted per cache event until it passes, so a filter rejecting pending queries still admits the settled value if it lands while the request's serialization context is live.

Before landing (once @solidjs/web 2.0.0-rc.5 ships)

  • Delete the subscribeFlightSource typed shim in QueryClientProvider.tsx — call the named-source subscribeFlightData(FLIGHT_DATA_SOURCE, ...) overload directly.
  • Bump the @solidjs/web peer floor to rc.5.

This PR deliberately waits for the release rather than landing with the shim: the whole stack (this, TanStack/router#8192, solidjs/templates#287) ships in lockstep, so landing early buys nothing and leaves cleanup to forget.

Notes

  • A typed shim bridges the installed @solidjs/web declarations until the named-source overload ships and the peer range bumps; it should be removed at that point.
  • Tests drive the registered consumer directly (registration lifecycle, hydration of mounted queries without refetch, seeding never-mounted entries); the wire protocol itself — request-leg source negotiation, keyed envelope, slice routing, per-source error containment, legacy degradation — is tested in @solidjs/web's suite.
  • Pairs with the SSR story already in v6: initial-load transfer is content-addressed through the hydration registry (sq:<queryHash>), post-mutation transfer is source-addressed through the flight envelope (sq) — the same recognition model at two moments.

Verification

  • flightData.test.tsx (consumer) and dehydrateSettled.test.tsx (settling, quiescence-chasing, failure settling + option forwarding) passing; the SSR fixture suite gained teardown (cacheEmptyAfterDispose on both string and streaming renders) and filter assertions (the filtered query's registry entry stays off the wire while others — including a never-rendered prefetch — still ship).
  • Full solid-query suite: 28 files, 352 passed / 1 intentionally skipped, type check clean, against a locally built @solidjs/web carrying the protocol.

Made with Cursor

…URCE
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, so
independent caches (Solid Router's route data, the query cache) refresh
from one round trip without competing for the single legacy slot.
QueryClientProvider now subscribes the query cache's consumer under the
exported FLIGHT_DATA_SOURCE id ("sq", matching the sq: hydration-registry
namespace): a mutation response carrying that slice — a DehydratedState
produced by a server collector registered with
registerFlightDataSource(FLIGHT_DATA_SOURCE, hook) — hydrates the
provider's client before the mutation's promise resolves. Mounted
queries on those keys update with no follow-up refetches, and apps
delete the hand-rolled subscribeFlightData/hydrate wiring entirely.
Subscribing is inert when no server collector exists (the server folds
nothing), client-only (the server registry is cross-request module
state), and torn down with the provider.
Requires the @solidjs/web release following 2.0.0-rc.4 for the
named-source protocol; a typed shim bridges the installed declarations
until the peer range bumps.
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitaiBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: a353b061-5117-4d1c-8694-3d7314b5b1d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ering
Fills the gaps the router-ssr-query transport used to cover, natively:
- dehydrateSettled(client): the extraction half of a single-flight
collector — waits for every in-flight fetch (chased to quiescence) so
loaders' fire-and-forget prefetches land before dehydrating.
- SSR teardown: the provider cancels and clears the per-request cache on
render disposal, so user-configured finite gcTime timers cannot pin the
client after the response.
- The registry serializer now respects defaultOptions.dehydrate
.shouldDehydrateQuery, the same knob apps use on any other transport.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to solidjs/solid that referenced this pull request Sep 1, 2026
…ainst workspace-built core
Core-side suites structurally cannot represent the adapter's composed
shapes — rc.5 shipped a settle-walk regression (#3181's fix waking parked
readers into uninitialized projections) that only the adapter's own suite
could see: premature wakes are self-healing for ordinary async nodes and
corrupt only through an empty-seed projection over a stable chained
promise with boundary-parked readers.
The gate packs signals/solid/web from the tree, downloads the adapter
repo, forces resolution through pnpm-workspace.yaml overrides (pnpm 11
ignores package.json pnpm.overrides), asserts the tarballs actually
resolved via file+ store realpaths (a version compare cannot tell tarball
from registry), builds the TS reference graph, and runs the full suite.
Wired into scripts/release.mjs before publish so a candidate that breaks
the flagship adapter fails on the runner, not on npm. Tracks the PR head
carrying the Solid 2.0 pairing until TanStack/query#11326 merges to main.
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
ryansolid added a commit to ryansolid/tanstack-router that referenced this pull request Sep 2, 2026
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>
… bridge
rc.6 publishes the named flight-data source API with real types, so the
subscribeFlightData cast goes away; the peer floor moves to rc.6 because
rc.5's settle-walk regression breaks query hydration. Also adds the
missing changeset for dehydrateSettled and the SSR teardown work.
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Updated@​solidjs/​web@​2.0.0-rc.3 ⏵ 2.0.0-rc.6100+110083+197+1100
Updatedsolid-js@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+110095+196+1100
Updated@​solidjs/​signals@​2.0.0-rc.4 ⏵ 2.0.0-rc.6100+1100100+198100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f2f3fe

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 21:59:13 UTC

@pkg-pr-new

pkg-pr-newBot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11326

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11326

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11326

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11326

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11326

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11326

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11326

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11326

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11326

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11326

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11326

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11326

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11326

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11326

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11326

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11326

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11326

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11326

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11326

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11326

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11326

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11326

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11326

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11326

commit: 5f2f3fe

@brenelz
brenelz merged commit 72f8185 into TanStack:solid-query-v6-preSep 2, 2026
11 of 12 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
brenelz pushed a commit to TanStack/router that referenced this pull request Sep 2, 2026
…-agnostic trigger (#8192)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* ci: apply automated fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
brenelz added a commit to TanStack/router that referenced this pull request Sep 2, 2026
…t, provider-owned dispatch (#8213)
* refactor(solid): retire solid-router-ssr-query — Solid's native channels 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>
* chore: changeset for the solid-router-ssr-query deprecation notice
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-start): named single-flight source for router flight data
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>
* feat(solid-router): loadFlightTarget, the cache-agnostic single-flight 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>
* refactor(solid-start): require named flight sources, drop the pre-rc.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>
* test(e2e): expect the named single-flight source header
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>
* chore(solid): bump solid-js, @solidjs/web, @solidjs/signals to 2.0.0-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>
* spike(solid-router): Phase 1 — registry match transfer + hydration-claiming 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>
* feat(solid-router): native SSR match transfer + hydration-claiming boot (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>
* docs(rfc): record Phase 1 landed state — transfer + boot in the adapter, boundary bullet corrected
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(solid-router): provider-owned server dispatch + streamed loaderData
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>
* chore: changeset for Phase 1 native SSR transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: apply automated fixes
* fix(solid): unbreak CI after the solid-js rc.6 bump
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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@ryansolid@brenelz