Skip to content

feat(solid): named single-flight source + loadFlightTarget, the cache-agnostic trigger - #8192

Draft
ryansolid wants to merge 2 commits into
TanStack:solid-router-v2-prefrom
ryansolid:feat/named-flight-source
Draft

feat(solid): named single-flight source + loadFlightTarget, the cache-agnostic trigger#8192
ryansolid wants to merge 2 commits into
TanStack:solid-router-v2-prefrom
ryansolid:feat/named-flight-source

Conversation

@ryansolid

@ryansolid ryansolid commented Aug 29, 2026

Copy link
Copy Markdown

Draft — pairs with the @solidjs/web multi-source single-flight protocol (solidjs/solid@653dd41e, shipping in the release after 2.0.0-rc.4). Safe to land before it ships: both halves feature-detect and fall back to current behavior.

Summary

Solid's single-flight channel is becoming multi-source: a mutation response carries 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 — so any other cache wanting mutation-response data (e.g. solid-query, whose provider subscribes under "sq" in TanStack/query#11326) displaces the router's flight data: whichever library registers last silently wins.

This PR moves the router's flight data onto its own source id ("tsr"):

  • Server: collectSolidStartFlightData registers additively via registerFlightDataSource(SOLID_START_FLIGHT_SOURCE, ...) instead of claiming the per-handler unnamed slot. A user-supplied collectFlightData option now keeps the unnamed slot to itself — its data folds alongside the router's rather than replacing it.
  • Client: the flight consumer subscribes under the same id and receives exactly the router's slice of the keyed envelope.

Net effect: a Start app using solid-query gets router loader/match refresh and query-cache hydration from one mutation round trip, with no coordination between the libraries.

loadFlightTarget: the trigger as a public primitive

The factoring behind multi-source flight is: the router owns the trigger (running the matched routes' data functions for the URL the client will show after the mutation), each cache owns its slice (what to extract from the loaded state). This PR makes the trigger public — loadFlightTarget in @tanstack/solid-router/ssr/server:

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
    },
  })
})

It derives the flight request (the outcome's pre-digested targetUrl, the mutation's cookie effects already folded in), points the router at the target through a fresh memory history, runs router.load() inside the flight request-event scope, and hands the loaded router to collect. Errors are contained per Solid Router's own collector convention — flight data is an optimization, so a failure omits the slice rather than surfacing as a mutation error (core now also contains per-source: solidjs/solid@ec523607).

Start's tsr collector consumes the same primitive, keeping only its bespoke halves: start-context acquisition, its serialized-redirect target resolution, and match-state extraction.

Known divergence from Solid Router's collector, deliberate for now: outcome.revalidateKeys scoping isn't honored — it comes from Solid's X-Revalidate convention, which TanStack mutations don't produce.

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

  • Delete the feature detection in solid-rpc-flight-client.ts (getFlightDataSourceIds probe) — subscribe directly under SOLID_START_FLIGHT_SOURCE.
  • Delete the legacy fallback in server-functions-handler.ts (the unnamed collectFlightData-slot assignment) — register via registerFlightDataSource unconditionally.
  • Bump the @solidjs/web peer floor to rc.5.

The rc.4 fallback paths defend a window nobody occupies — the multi-cache pairing this enables requires rc.5 on the consumer side anyway — so this PR waits and lands clean rather than shipping dead branches to strip later.

Compatibility

  • Both halves feature-detect the protocol on the installed @solidjs/web (via exports the protocol introduced). On 2.0.0-rc.4 and older, behavior is exactly today's: unnamed slot, user collectFlightData overrides the router's. Client and server resolve the same install, so the halves cannot disagree.
  • The wire itself degrades gracefully in the protocol (legacy true header/payload shape preserved), so no version pairing breaks.
  • The existing isSolidStartFlightData shape guard stays — required in the legacy path, redundant-but-harmless under named sources.

Verification

  • nx build @tanstack/solid-router and nx build @tanstack/solid-start green (16-task dependency graph), publint --strict + attw pass.
  • The composed collector runs end-to-end in solidjs/templates' fullstack-tanstack (solid-v2/fullstack-tanstack: multi-source single-flight solidjs/templates#287), which uses exactly the loadFlightTarget + dehydrateSettled composition above.
  • The single-flight e2e (e2e/solid-start/server-functions) exercises the legacy path unchanged; the named path activates only alongside the new @solidjs/web, whose protocol suite (request-leg negotiation, keyed envelope routing, per-source error containment, cross-version degradation) lives in that repo.

Made with Cursor

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>
@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.

…t trigger

The router's half of flight collection as a public primitive: derive the
flight request for the mutation's target, run the matched routes' data
functions, hand the loaded router to the caller's collect() — any cache
(the router's own state, a query client) composes its extraction on top.
Start's collector now consumes it; errors are contained per Solid
Router's collector convention (flight data is an optimization, never a
mutation error).

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid ryansolid changed the title feat(solid-start): named single-flight source for router flight data feat(solid): named single-flight source + loadFlightTarget, the cache-agnostic trigger Aug 29, 2026
@nx-cloud

nx-cloud Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit de72ade

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 7m 57s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 30s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-29 21:09:44 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 29, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@8192

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@8192

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@8192

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@8192

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@8192

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@8192

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@8192

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@8192

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@8192

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@8192

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@8192

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@8192

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@8192

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@8192

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@8192

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@8192

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@8192

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@8192

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@8192

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@8192

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@8192

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@8192

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@8192

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@8192

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@8192

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@8192

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@8192

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@8192

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@8192

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@8192

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@8192

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@8192

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@8192

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@8192

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@8192

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@8192

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@8192

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@8192

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@8192

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@8192

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@8192

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@8192

commit: de72ade

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud is proposing a fix for your failed CI:

We updated server-functions-handler.ts (source and pre-built dist) to access registerFlightDataSource via Object.assign({}, solidServerFunctions) rather than directly on the namespace binding. This prevents rspack's static ESM linking validation from rejecting the access against the installed @solidjs/web rc.4 exports list, where registerFlightDataSource has not yet shipped. Runtime behaviour is unchanged — the result is the function on rc.5+ or undefined on rc.4, and the existing hasNamedFlightSources guard handles both paths.

Tip

We verified this fix by re-running tanstack-solid-start-e2e-basic:build:rsbuild:ssr.

diff --git a/packages/solid-start/src/server-functions-handler.ts b/packages/solid-start/src/server-functions-handler.ts
index 502d173b3..477440c55 100644
--- a/packages/solid-start/src/server-functions-handler.ts
+++ b/packages/solid-start/src/server-functions-handler.ts
@@ -36,13 +36,19 @@ configureServerFunctionsServer({
 // older versions the collector falls back to claiming the unnamed slot
 // per-handler, exactly as before; the client half detects the same
 // installed package, so the two halves cannot disagree.
-const registerFlightDataSource = (
-  solidServerFunctions as {
+// Object.assign produces a plain object, so rspack's static ESM linking
+// validation doesn't flag the access when registerFlightDataSource hasn't
+// shipped in the installed @solidjs/web (rc.4). At runtime the own-property
+// copy still returns the function when it is present (rc.5+) or undefined
+// when it isn't — identical observable behaviour to a direct namespace read.
+const registerFlightDataSource = Object.assign(
+  {} as {
     registerFlightDataSource?: (
       source: string,
       hook: CollectFlightDataHook,
     ) => () => void
-  }
+  },
+  solidServerFunctions,
 ).registerFlightDataSource
 const hasNamedFlightSources = registerFlightDataSource !== undefined
 if (registerFlightDataSource) {

Because this branch comes from a fork, it is not possible for us to apply fixes directly, but you can apply the changes locally using the available options below.

Apply changes locally with:

npx nx-cloud apply-locally SkBW-jhDX

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

@codspeed-hq

codspeed-hq Bot commented Aug 29, 2026

Copy link
Copy Markdown

Hooray! CodSpeed harness just leveled up!

The base and head of this comparison were measured with different runner settings, so their benchmark values are not directly comparable.

What changed between base and head:

Re-run the base with the same settings to get a valid performance comparison.


Comparing ryansolid:feat/named-flight-source (de72ade) with solid-router-v2-pre (67a9040)1

Open in CodSpeed

Footnotes

  1. No successful run was found on solid-router-v2-pre (5010e89) during the generation of this report, so 67a9040 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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