Skip to content

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

Draft
ryansolid wants to merge 2 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source
Draft

feat(solid-query): built-in single-flight consumer via FLIGHT_DATA_SOURCE#11326
ryansolid wants to merge 2 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/flight-data-source

Conversation

@ryansolid

@ryansolid ryansolid 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) return undefined
  const queryClient = createQueryClient()
  return loadFlightTarget({
    router: createAppRouter(queryClient),
    event,
    outcome,
    collect: async () => {
      const state = await dehydrateSettled(queryClient)
      return state.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

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant