UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

124 lines • 5.13 kB
import { isHydratable } from 'solid-js'; import { getHydrationWriter, takeHydrationValue } from '@solidjs/web'; /** * Match-state transfer over Solid's hydration registry — the bare pairing's * native SSR channel (no `__TSR_SSR__` script injection, no router-owned * stream protocol). Entries are content-addressed (`tsr:<matchId>`; match * ids are deterministic route-id + interpolated params, so both sides * derive the same key from the same URL), the identical mechanism * solid-query v6 ships query payloads through (`sq:<queryHash>`). * * Server half: while the render takes values (`getHydrationWriter`), the * provider writes each settled match's transferable state. Client half: the * hydration-claiming boot — matching is synchronous, so the provider primes * match state from the registry and commits before rendering, without * running loaders or resolving route chunks up front. Route components * resolve at the read point under the boundaries the server actually * rendered (Solid `lazy` semantics), and staleness rules own any * post-hydration refetching. * * Both halves no-op under TanStack Start: `router.serverSsr` marks the * Start contract (`attachRouterServerSsrUtils` / `dehydrate` / `hydrate`), * which owns transfer there — and a Start-hydrated router reaches the * client boot with matches already committed, which skips it. */ export const MATCH_KEY_PREFIX = 'tsr:'; export function serializeMatchTransfer(router) { if (router.serverSsr) return; const writer = getHydrationWriter(); if (!writer || !isHydratable()) return; for (const match of router.stores.matches.get()) { // Pending matches are skipped, not deferred: this core has no per-match // settle promise to hand seroval, so promise-valued entries (streaming // SSR, loaders landing after first flush) need a dispatch-time hook — // the next increment. A missing entry makes the client boot fall // through to today's behavior rather than half-prime. if (match.status === 'pending') continue; const entry = { status: match.status, updatedAt: match.updatedAt, }; if (match.loaderData !== undefined) entry.loaderData = match.loaderData; if (match.error !== undefined) entry.error = match.error; if (match._notFound) entry.notFound = true; const beforeLoadContext = match .__beforeLoadContext; if (beforeLoadContext !== undefined) entry.beforeLoadContext = beforeLoadContext; if (match.ssr !== undefined) entry.ssr = match.ssr; writer.write(MATCH_KEY_PREFIX + match.id, entry); } } /** * The hydration-claiming boot. Returns true when every synchronously * matched route found its registry entry and the matches were committed; * false falls back to the caller's existing behavior (no entries — a * non-registry server, `<NoHydration>`, or a pending match the server * skipped). * * Entries arrive as inline scripts that execute at document parse, so they * are complete before any client code runs and `takeHydrationValue` reads * them before `hydrate()` — the boot must commit BEFORE the hydration * render (store writes inside it are owned-scope writes). Each matched * route takes its own key: a page without entries (SPA, or Start's own * channel) falls through at the first match. */ export function primeRouterFromRegistry(router) { if (router.stores.matches.get().length > 0) return false; const matches = router.matchRoutes(router.latestLocation); if (matches.length === 0) return false; const primed = []; for (const match of matches) { const taken = takeHydrationValue(MATCH_KEY_PREFIX + match.id); if (taken?.status !== 'resolved') return false; const entry = taken.value; if (!entry || typeof entry.status !== 'string') return false; primed.push(applyTransferredMatch(match, entry)); } // Commit the way the single-flight client publishes hydrated matches. router._committed = primed; router.batch(() => { router.stores.setMatches(primed); router.stores.status.set('idle'); router.stores.resolvedLocation.set(router.stores.location.get()); }); return true; } function applyTransferredMatch(match, entry) { const next = { ...match, status: entry.status, updatedAt: entry.updatedAt, error: entry.error, invalid: false, isFetching: false, preload: false, _notFound: entry.notFound, }; if ('loaderData' in entry) next.loaderData = entry.loaderData; if ('beforeLoadContext' in entry) { ; next.__beforeLoadContext = entry.beforeLoadContext; next.context = { ...next.context, ...entry.beforeLoadContext, }; } if ('ssr' in entry) next.ssr = entry.ssr; return next; } //# sourceMappingURL=registryTransfer.js.map