UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

1 lines 8.3 kB
{"version":3,"file":"registryTransfer.cjs","names":[],"sources":["../../src/registryTransfer.ts"],"sourcesContent":["import * as Solid from 'solid-js'\nimport type { AnyRouteMatch, AnyRouter } from '@tanstack/router-core'\n\n/**\n * Match-state transfer over Solid's hydration registry — the bare pairing's\n * native SSR channel (no `__TSR_SSR__` script injection, no router-owned\n * stream protocol). Entries are content-addressed (`tsr:<matchId>`; match\n * ids are deterministic route-id + interpolated params, so both sides\n * derive the same key from the same URL), the identical mechanism\n * solid-query v6 ships query payloads through (`sq:<queryHash>`).\n *\n * Server half: while the render's serialization context is live, the\n * provider writes each settled match's transferable state. Client half: the\n * hydration-claiming boot — matching is synchronous, so the provider primes\n * match state from the registry and commits before rendering, without\n * running loaders or resolving route chunks up front. Route components\n * resolve at the read point under the boundaries the server actually\n * rendered (Solid `lazy` semantics), and staleness rules own any\n * post-hydration refetching.\n *\n * Both halves no-op under TanStack Start: `router.serverSsr` marks the\n * Start contract (`attachRouterServerSsrUtils` / `dehydrate` / `hydrate`),\n * which owns transfer there — and a Start-hydrated router reaches the\n * client boot with matches already committed, which skips it.\n */\nexport const MATCH_KEY_PREFIX = 'tsr:'\n\n/** The slice of a match that transfers; mirrors the single-flight match\n * shape (`createSolidStartFlightMatch`) — state, not identity: the client\n * re-derives identity by matching, then merges this in. */\ninterface TransferredMatch {\n status: AnyRouteMatch['status']\n updatedAt: number\n loaderData?: unknown\n error?: unknown\n notFound?: true\n beforeLoadContext?: Record<string, unknown>\n ssr?: AnyRouteMatch['ssr']\n}\n\ntype MatchWithBeforeLoadContext = AnyRouteMatch & {\n __beforeLoadContext?: Record<string, unknown>\n}\n\ninterface SerializationContext {\n noHydrate?: boolean\n serialize: (key: string, value: unknown) => void\n}\n\nexport function serializeMatchTransfer(router: AnyRouter): void {\n if (router.serverSsr) return\n const ctx = (\n Solid.sharedConfig as unknown as { context?: SerializationContext }\n ).context\n if (!ctx || typeof ctx.serialize !== 'function' || ctx.noHydrate) return\n\n for (const match of router.stores.matches.get()) {\n // Pending matches are skipped, not deferred: this core has no per-match\n // settle promise to hand seroval, so promise-valued entries (streaming\n // SSR, loaders landing after first flush) need a dispatch-time hook —\n // the next increment. A missing entry makes the client boot fall\n // through to today's behavior rather than half-prime.\n if (match.status === 'pending') continue\n const entry: TransferredMatch = {\n status: match.status,\n updatedAt: match.updatedAt,\n }\n if (match.loaderData !== undefined) entry.loaderData = match.loaderData\n if (match.error !== undefined) entry.error = match.error\n if (match._notFound) entry.notFound = true\n const beforeLoadContext = (match as MatchWithBeforeLoadContext)\n .__beforeLoadContext\n if (beforeLoadContext !== undefined)\n entry.beforeLoadContext = beforeLoadContext\n if (match.ssr !== undefined) entry.ssr = match.ssr\n ctx.serialize(MATCH_KEY_PREFIX + match.id, entry)\n }\n}\n\n/**\n * The hydration-claiming boot. Returns true when every synchronously\n * matched route found its registry entry and the matches were committed;\n * false falls back to the caller's existing behavior (no entries — a\n * non-registry server, `noHydrate`, or a pending match the server skipped).\n *\n * Reads the raw registry rather than sharedConfig's accessors: entries\n * arrive as inline scripts that execute at document parse, so they are\n * complete before any client code runs — and the boot must commit BEFORE\n * the hydration render (store writes inside it are owned-scope writes).\n */\nexport function primeRouterFromRegistry(router: AnyRouter): boolean {\n if (router.stores.matches.get().length > 0) return false\n const registry = (\n globalThis as unknown as { _$HY?: { r: Record<string, unknown> } }\n )._$HY?.r\n if (!registry) return false\n // A page with no match entries (SPA, or Start's own channel) skips before\n // paying for a match pass.\n let hasMatchEntries = false\n for (const key in registry) {\n if (key.startsWith(MATCH_KEY_PREFIX)) {\n hasMatchEntries = true\n break\n }\n }\n if (!hasMatchEntries) return false\n\n const matches = router.matchRoutes(router.latestLocation)\n if (matches.length === 0) return false\n\n const primed: Array<AnyRouteMatch> = []\n for (const match of matches) {\n const key = MATCH_KEY_PREFIX + match.id\n if (!(key in registry)) return false\n const raw = registry[key] as\n | TransferredMatch\n | { s: number; v?: TransferredMatch }\n | null\n delete registry[key]\n // Settled serialization refs are stamped `s`/`v`; sync-serialized\n // entries are the value itself.\n const entry =\n raw != null && typeof raw === 'object' && 's' in raw && raw.s === 1\n ? raw.v\n : (raw as TransferredMatch | null)\n if (!entry || typeof entry.status !== 'string') return false\n primed.push(applyTransferredMatch(match, entry))\n }\n\n // Commit the way the single-flight client publishes hydrated matches.\n router._committed = primed\n router.batch(() => {\n router.stores.setMatches(primed)\n router.stores.status.set('idle')\n router.stores.resolvedLocation.set(router.stores.location.get())\n })\n return true\n}\n\nfunction applyTransferredMatch(\n match: AnyRouteMatch,\n entry: TransferredMatch,\n): AnyRouteMatch {\n const next = {\n ...match,\n status: entry.status,\n updatedAt: entry.updatedAt,\n error: entry.error,\n invalid: false,\n isFetching: false,\n preload: false,\n _notFound: entry.notFound,\n } as AnyRouteMatch\n if ('loaderData' in entry) next.loaderData = entry.loaderData\n if ('beforeLoadContext' in entry) {\n ;(next as MatchWithBeforeLoadContext).__beforeLoadContext =\n entry.beforeLoadContext\n next.context = {\n ...next.context,\n ...entry.beforeLoadContext,\n }\n }\n if ('ssr' in entry) next.ssr = entry.ssr\n return next\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,mBAAmB;AAwBhC,SAAgB,uBAAuB,QAAyB;CAC9D,IAAI,OAAO,WAAW;CACtB,MAAM,MACJ,SAAM,aACN;CACF,IAAI,CAAC,OAAO,OAAO,IAAI,cAAc,cAAc,IAAI,WAAW;CAElE,KAAK,MAAM,SAAS,OAAO,OAAO,QAAQ,IAAI,GAAG;EAM/C,IAAI,MAAM,WAAW,WAAW;EAChC,MAAM,QAA0B;GAC9B,QAAQ,MAAM;GACd,WAAW,MAAM;EACnB;EACA,IAAI,MAAM,eAAe,KAAA,GAAW,MAAM,aAAa,MAAM;EAC7D,IAAI,MAAM,UAAU,KAAA,GAAW,MAAM,QAAQ,MAAM;EACnD,IAAI,MAAM,WAAW,MAAM,WAAW;EACtC,MAAM,oBAAqB,MACxB;EACH,IAAI,sBAAsB,KAAA,GACxB,MAAM,oBAAoB;EAC5B,IAAI,MAAM,QAAQ,KAAA,GAAW,MAAM,MAAM,MAAM;EAC/C,IAAI,UAAU,mBAAmB,MAAM,IAAI,KAAK;CAClD;AACF;;;;;;;;;;;;AAaA,SAAgB,wBAAwB,QAA4B;CAClE,IAAI,OAAO,OAAO,QAAQ,IAAI,EAAE,SAAS,GAAG,OAAO;CACnD,MAAM,WACJ,WACA,MAAM;CACR,IAAI,CAAC,UAAU,OAAO;CAGtB,IAAI,kBAAkB;CACtB,KAAK,MAAM,OAAO,UAChB,IAAI,IAAI,WAAA,MAA2B,GAAG;EACpC,kBAAkB;EAClB;CACF;CAEF,IAAI,CAAC,iBAAiB,OAAO;CAE7B,MAAM,UAAU,OAAO,YAAY,OAAO,cAAc;CACxD,IAAI,QAAQ,WAAW,GAAG,OAAO;CAEjC,MAAM,SAA+B,CAAC;CACtC,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,MAAM,mBAAmB,MAAM;EACrC,IAAI,EAAE,OAAO,WAAW,OAAO;EAC/B,MAAM,MAAM,SAAS;EAIrB,OAAO,SAAS;EAGhB,MAAM,QACJ,OAAO,QAAQ,OAAO,QAAQ,YAAY,OAAO,OAAO,IAAI,MAAM,IAC9D,IAAI,IACH;EACP,IAAI,CAAC,SAAS,OAAO,MAAM,WAAW,UAAU,OAAO;EACvD,OAAO,KAAK,sBAAsB,OAAO,KAAK,CAAC;CACjD;CAGA,OAAO,aAAa;CACpB,OAAO,YAAY;EACjB,OAAO,OAAO,WAAW,MAAM;EAC/B,OAAO,OAAO,OAAO,IAAI,MAAM;EAC/B,OAAO,OAAO,iBAAiB,IAAI,OAAO,OAAO,SAAS,IAAI,CAAC;CACjE,CAAC;CACD,OAAO;AACT;AAEA,SAAS,sBACP,OACA,OACe;CACf,MAAM,OAAO;EACX,GAAG;EACH,QAAQ,MAAM;EACd,WAAW,MAAM;EACjB,OAAO,MAAM;EACb,SAAS;EACT,YAAY;EACZ,SAAS;EACT,WAAW,MAAM;CACnB;CACA,IAAI,gBAAgB,OAAO,KAAK,aAAa,MAAM;CACnD,IAAI,uBAAuB,OAAO;EAC/B,KAAqC,sBACpC,MAAM;EACR,KAAK,UAAU;GACb,GAAG,KAAK;GACR,GAAG,MAAM;EACX;CACF;CACA,IAAI,SAAS,OAAO,KAAK,MAAM,MAAM;CACrC,OAAO;AACT"}