feat(solid): Solid-owned SSR transport and payload channel - #8214
feat(solid): Solid-owned SSR transport and payload channel#8214ryansolid wants to merge 3 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
bfed342 to
aab6864
Compare
|
View your CI Pipeline Execution ↗ for commit 86203b8
☁️ Nx Cloud last updated this comment at |
aab6864 to
81a9cc6
Compare
Hooray! CodSpeed harness just leveled up!The base and head of this comparison were measured with different runner settings, so their benchmark values are not directly comparable. What changed between base and head:
Re-run the base with the same settings to get a valid performance comparison. Comparing Footnotes |
|
Yeah we should see if we can do this without modifying core packages. We don't have good versioning for changes to those right now until we are on the main branch |
7958328 to
86203b8
Compare
…(Phase 2a+2b, zero core diff) Phase 2a — the Solid path drops transformStreamWithRouter: router scripts ride the response writer directly in renderRouterToStream (Solid-owned sink, script-barrier scanning until the <Scripts /> chunk is written, doctype handling, 60s serialization timeout, abort/cleanup semantics). Phase 2b — the DehydratedRouter rides Solid's eval-free JSON codec (__TSR_P record queue) instead of the $_TSR script channel, implemented entirely as Solid-side overrides of the framework-only serverSsr members (installed via the onServerSsrAttach lifecycle): - serverSsr.dehydrate is replaced with a Solid implementation mirroring core's (rendered matches, isShell slicing, options.dehydrate() data, manifest derived from the public router.ssr.manifest getter) that serializes through createJSONSerializer into a Solid-owned script buffer; takeBufferedScripts/liftScriptBarrier serve it with core's shell-inline tag shape, so <Scripts /> and the 2a sink are unchanged. - isDehydrated/isSerializationFinished/onSerializationFinished answer from Solid-side state; setRenderFinished/cleanup wrap the originals. - Core's seeded $R scope header and $_TSR bootstrap are drained and discarded at attach — the SSR HTML carries no executable payload scripts and no parse-time eval. - The client installs a synthetic window.$_TSR whose lazy router getter decodes the record queue (createJSONDataTable, push-hooked for streamed late records), so core hydrate() is byte-for-byte unchanged. The shim defers to a real $_TSR bootstrap (older-server documents). - The installer is resolved through solidSsrTransferSlot, filled by the ssr/server entry (sideEffects allowlist keeps the fill alive): the encode half of Solid's codec must never enter the client module graph. Zero diff to router-core, start-client-core, react-router, vue-router, and their start packages. A follow-up core-hooks PR against main will collapse the overrides into supported seams; the unexported core pieces mirrored here (dehydrateMatch, ScriptBuffer, barrier id, inline-css placeholder) are marked as such in routerPayloadServer. Co-authored-by: Cursor <cursoragent@cursor.com>
…h zero core diff Records the shipped 2a/2b architecture: serverSsr member overrides via onServerSsrAttach, the slot-armed installer, the manifest derivation from the public getter, the synthetic $_TSR client shim, and the follow-up core-hooks PR against main. Marks the resolved open questions (deferred loaderData streaming, script-channel consumer inventory) and the nine-suite regression gate. Co-authored-by: Cursor <cursoragent@cursor.com>
The SSR scenarios asserted the $_TSR bootstrap to locate the dehydration payload in rendered HTML; Solid's channel now emits __TSR_P records instead. findDehydrationMarkerIndex accepts either marker so the benchmarks stay meaningful across both channels. Co-authored-by: Cursor <cursoragent@cursor.com>
86203b8 to
2ff67a1
Compare
|
A plain-language summary of what this solves, since the description above is mechanism-heavy. Some context on how this builds on #8213: before it, the router's SSR story was effectively Start-shaped. The This PR completes the same story inside Start, where three pieces of shared-path overhead remain on the Solid adapter:
What it costs core: verifiably nothing — the diff is confined to |
Summary
Phase 2 of the Solid-native SSR plan (RFC updated in the diff). Moves the Solid adapter's SSR stream transport and dehydration payload onto Solid's own channels — with zero diff outside the Solid packages.
router-core,start-client-core,react-router,vue-router, and their start packages are byte-for-byte untouched:renderRouterToStreamno longer pipes throughtransformReadableStreamWithRouter(the per-chunk decode +</body>scan + splice transform). Router scripts write directly to the response sink as the serializer emits them; the script barrier lifts once the chunk carrying<Scripts />has been written (chunks are scanned only until that marker is seen); the response closes when both the render completed and serialization finished, preserving the transform's 60s timeout and abort/cleanup semantics. Zero per-chunk decode/scan/splice on the hot path.DehydratedRouterrides Solid's eval-free JSON record codec (the samecreateJSONSerializer/createJSONDataTableinfrastructure Start's server functions use) as__TSR_Pqueue pushes, instead of the$_TSRscript channel. The rendered HTML carries no$_TSRbootstrap, no$Rcross-reference header, and no parse-time eval — every router script is an inert JSON data push. This also removes the parse-time wall that blocked registry-addressed transfer: adapter-typed values decode at runtime throughmakeSerovalPlugin-wrapped adapters rather than evaluating$_TSR.t.get(key)(...)calls before implementations exist.How it stays out of core
Everything rides Solid-side overrides of the framework-only
router.serverSsrmembers, installed through the existingonServerSsrAttachlifecycle:Routerconstructor registers anonServerSsrAttachlistener that resolves an installer through a slot (solidSsrTransferSlot) filled by thessr/serverentry module. The encode half of Solid's codec is server-only and must never enter the client module graph; the packagesideEffectsallowlist keeps bundlers from dropping the slot fill. Unfilled slot ⇒ core's script channel runs completely unchanged.installSolidSsrTransfer) — replacesserverSsr.dehydratewith a Solid implementation that builds theDehydratedRouterthe way core does (rendered matches,isShellslicing,options.dehydrate()data, manifest derived from the publicrouter.ssr.manifestgetter) and serializes it through the JSON codec into a Solid-owned script buffer.takeBufferedScripts/liftScriptBarrierare overridden to serve that buffer with core's exact shell-inline tag shape, so<Scripts />inlining and barrier deferral work unchanged.isDehydrated/isSerializationFinished/onSerializationFinishedanswer from Solid-side state;setRenderFinished/cleanupwrap the originals. Core's seeded$Rheader and$_TSRbootstrap are drained and discarded at attach.$_TSR) — corehydrate()is unchanged; it still readswindow.$_TSR. Solid installs a synthetic$_TSRwhose lazyroutergetter decodes the__TSR_Pqueue (push-hooked so late records settle streamedloaderDatapromises), applied byRouterClientand Solid'shydrateStart. The decode module is loaded through a dynamic import so it shares the chunk Solid's web runtime already lazy-loads (a static import merges it into the client entry chunk, which then registers as a dynamic-import target and loses itsisEntryflag to the Solid vite plugin's lazy-entry normalization — breaking Start's manifest capture for apps on the default client entry); the shim's install returns a promise the callers await before core hydrate. The synthetic'sh()deletes the global once hydration completes — it has no post-hydration role since late records ride the queue's hookedpush, never$_TSR.p— sotypeof window.$_TSR === 'undefined'stays a valid hydration-finished probe on both channels (thebasic-authe2e suite relies on it). The shim defers to a real$_TSRbootstrap, so documents rendered by a script-channel server still hydrate.The unexported core pieces mirrored on the Solid side (
dehydrateMatch,ScriptBuffer+ barrier tag shape, the inline-CSS placeholder) are marked as mirrors inrouterPayloadServer.ts. A follow-up PR againstmainwill add the small core hooks (framework dehydrated-router hand-off, script enqueue, serialization-finished signal) that let these overrides and mirrors collapse into supported seams — this PR deliberately does not depend on it, since core releases on a different cadence than this pre-release branch.Verification
All on this branch after the rebase onto
solid-router-v2-pre(post #8192/#8213 merge):@tanstack/solid-routerunit tests: 865 passed | 2 skipped across 59 files, zero type errors (jsdom + server-mode + typecheck).packages/solid-router/repro-external-ssr, real server/client bundles, protocol-less SSR → hydration in jsdom): PASS — clean hydration, zero console errors/warnings, no pending flash, post-hydration nav works, streamedloaderDatachunk-order assertions hold.basic(80),server-functions(29),deferred-hydration(15),selective-ssr(11),scroll-restoration(10),basic-auth(7),basic-solid-query(6),serialization-adapters(5),server-routes(2),spa-mode(2) — 167 passed covering streaming order, selective-SSR lanes, adapter decode through the shim, scroll scripts, shell mode, and auth/redirect flows with the hydration-probe semantics.examples/solid/start-large,examples/solid/start-basic-authjs, and@benchmarks/bundle-sizeall build clean.__TSR_Precords only — zero$_TSRoccurrences.Notes for reviewers
serverSsroverrides install atattachRouterServerSsrUtilstime viaonServerSsrAttach, i.e. before any dehydrate/injection call site can run — but the ordering is implicit. The core-hooks PR will make it explicit.isDehydrated/serialization flags never advance in this mode (core'sdehydratebody never runs); anything inside core that consulted them directly rather than through theserverSsrsurface would misread. Nothing does today — flagging it as the kind of coupling the core hooks should eliminate.$_TSRor__TSR_P), so CodSpeed stays meaningful across frameworks.@solidjs/web/serialization/decodeinto a chunk Solid's runtime also dynamically imports will strip the client entry'sisEntryunder the current vite-plugin normalization. The core-hooks PR won't change this — it's a bundler-graph property — but it's now documented in the RFC.