feat(web): @solidjs/web/data — router-agnostic query()/action() and a single-flight query channel - #2960
feat(web): @solidjs/web/data — router-agnostic query()/action() and a single-flight query channel#2960brenelz wants to merge 1 commit into
Conversation
…nd a single-flight query channel
Ports @solidjs/router's query()/action() (next branch) onto the
server-function transport with no router dependency, plus the single-flight
channel that ships post-mutation query values on the mutation response.
- query(fn, name): request-deduped async cache. The same key (name + stable
argument hash) in a route loader/preload and a component memo returns the
SAME promise; Solid 2 memos unwrap it into the nearest <Loading>. Cache is
per-request in event.locals on the server, module-level with
observed/timed freshness on the client. Server functions are auto-declared
GET via transport metadata (keeps reads cacheable and off the single-flight
path). revalidate, query.get/set/delete/clear, and collectQueries/
seedQueries (explicit SSR seeding for hosts that warm the cache outside
the render tree) included.
- action(fn): form-bindable mutations. toString() renders the server
function's stable .url into <form action={...}> — no-JS posts work
natively; a document-level submit listener intercepts matching forms and
calls the RPC stub.
- flight: installQueryFlightConsumer() (client) applies post-mutation query
values via query.set, guarded against stale locations;
createQueryFlightCollector(warmQueries) on /server produces the
collectFlightData hook — the host supplies only warmQueries(href, outcome).
The /server split keeps @solidjs/web/storage (node:async_hooks) out of
client bundles.
Extracted from two app experiments where these files ran byte-identical on
TanStack Solid Router (workerd) and @solidjs/router (nitro/node) hosts; only
warmQueries differed. Verified in both apps against this build: dedupe (one
server call per page render), form interception, and single-flight (one POST,
no follow-up read, DOM updates from the response payload).
Not yet ported from solid-router: submission state (useSubmission),
redirect/Response handling, preload intent semantics.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: fb6f046 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will not alter performance
Comparing Footnotes
|
|
I think this is interesting and it is super tempting from a turn-key point of view but I think it is too strong of a position to take core. We should consider if this is main library worthy but core keeps it clean that our contract is around communication protocol, not application logic. I think caching falls into application logic as it needs to make a decision. The benefit of being able to streamline the protocol is definitely there but we need to think carefully where this belongs. I'm just pretty confident it is not here. |
What
A new
@solidjs/web/datasubpath: router-agnosticquery()andaction()(ported from @solidjs/router's next branch), plus a single-flight channel that ships post-mutation query values on the mutation response.The transport already carries most of an actions/data story — stable
.urls on server-function references (documented for form actions), no-JS form-post handling, and the single-flight wire protocol (SINGLE_FLIGHT_HEADER,collectFlightData,subscribeFlightData). This fills in the data layer that rides those rails, with the same split the flight protocol itself uses: the package standardizes the mechanism, the router/host decides what data loading means.API
@solidjs/web/data(isomorphic)query(fn, name)— request-deduped async cache. The same key (name+ stable argument hash) in a route loader/preload and a component memo returns the same promise; Solid 2 memos unwrap it into the nearest<Loading>, so there's no resource machinery. Cache is per-request inevent.localson the server, module-level on the client with solid-router's freshness semantics (fresh while observed, short window otherwise, GC'd). Server functions are auto-declaredGETvia transport metadata — reads stay cacheable and off the single-flight path. Plusrevalidate(prefix match),query.get/set/delete/clear,keyFor/key.action(fn)— form-bindable mutations:toString()renders the reference's real.urlinto<form action={...}>(no-JS posts hit the endpoint natively); a single document-level submit listener intercepts matching forms (includingformactionoverrides) and calls the RPC stub.installQueryFlightConsumer()— registers the flight-data consumer that applies single-flight payloads viaquery.set(version-signal bump → reading memos re-run, no refetch), guarded against stale locations (navigated/redirected mid-flight →revalidate()).collectQueries()/seedQueries()— explicit SSR seeding channel for hosts that warm the cache outside the render tree (e.g. TanStack Router'sdehydrate/hydrate; its serializer streams the un-awaited promises). Not needed when queries are only read inside components — Solid serializes the reading memo itself.@solidjs/web/data/servercreateQueryFlightCollector(warmQueries)— produces thecollectFlightDatahook: re-establishes the request-event scope, derives the client's post-mutation location (redirectLocation, elseReferer), calls the host'swarmQueries(href, outcome), then resolves this request's query cache into the payload. Separate subpath because it pulls in@solidjs/web/storage(node:async_hooks).Provenance & testing
Extracted from two app experiments where these exact files ran byte-identical across hosts — TanStack Solid Router on Cloudflare workerd, and @solidjs/router 2.0.0-next on nitro/node — with only
warmQueriesdiffering (~6 lines each: TanStack builds a router at the location andload()s it; @solidjs/router delegates to itscreateFlightDataCollector, whose flight event shareslocalswith the request).Verified in both apps against this build (linked into their
node_modules):pnpm build(tsc + vite) green in both; nitro prod verified end-to-end on the built serverIn-repo:
data/test/query.spec.tscovers dedupe by key, argument hashing,revalidateretriggering tracking scopes, andquery.setseeding without a refetch. Full solid-web default suite: 334 passed / 0 failed.Not yet ported from solid-router
useSubmission/useSubmissions) and.with()argument bindingResponsehandling inquery(cookie forwarding,Locationfollowing)preload/navigate/native)Naming, placement (
/datavs folding into/server-functions), and whether the freshness constants should be options — all judgment calls I'm happy to revisit.🤖 Generated with Claude Code