/** * Segment Codec * * RSC serialization/deserialization for cached segments. * Handles the Flight protocol stream <-> string conversion * and the segment-level encode/decode lifecycle. */ /// import type { ResolvedSegment } from "../types.js"; import type { SerializedSegmentData } from "./types.js"; import { renderToReadableStream, createTemporaryReferenceSet, } from "@vitejs/plugin-rsc/rsc"; import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc"; // ============================================================================ // Stream Utilities (internal) // ============================================================================ /** * Convert a ReadableStream to a string. */ export async function streamToString( stream: ReadableStream, ): Promise { const reader = stream.getReader(); const decoder = new TextDecoder(); let result = ""; while (true) { const { done, value } = await reader.read(); if (done) break; result += decoder.decode(value, { stream: true }); } result += decoder.decode(); // flush return result; } /** * Convert a string to a ReadableStream. */ export function stringToStream(str: string): ReadableStream { const encoder = new TextEncoder(); const uint8 = encoder.encode(str); return new ReadableStream({ start(controller) { controller.enqueue(uint8); controller.close(); }, }); } // ============================================================================ // RSC Serialization Primitives (internal) // ============================================================================ /** * RSC-serialize a value using React Server Components stream. * Used for serializing loaderData, layout, loading components etc. * * Returns undefined for null/undefined inputs (component fields that are absent). * For contexts where null is a valid result (loader caching, "use cache"), * use serializeResult() instead which preserves null through RSC Flight. */ export async function rscSerialize( value: unknown, ): Promise { if (value === undefined || value === null) return undefined; const temporaryReferences = createTemporaryReferenceSet(); const stream = renderToReadableStream(value, { temporaryReferences }); return streamToString(stream); } /** * RSC-deserialize a value from a stored string. */ export async function rscDeserialize( encoded: string | undefined, ): Promise { if (!encoded) return undefined; const temporaryReferences = createTemporaryReferenceSet(); const stream = stringToStream(encoded); return createFromReadableStream(stream, { temporaryReferences }); } // ============================================================================ // Null-Preserving RSC Serialization (for caching) // ============================================================================ /** * RSC-serialize any value including null. * Unlike rscSerialize(), this does NOT skip null — it serializes it through * RSC Flight so that a loader returning null produces a valid cached entry * rather than a permanent cache miss. * * Returns null only on serialization failure. */ export async function serializeResult(value: unknown): Promise { try { const temporaryReferences = createTemporaryReferenceSet(); const stream = renderToReadableStream(value, { temporaryReferences }); return await streamToString(stream); } catch { return null; } } /** * RSC-deserialize a cached result string. * Counterpart to serializeResult() — always receives a non-empty string. */ export async function deserializeResult(encoded: string): Promise { const temporaryReferences = createTemporaryReferenceSet(); const stream = stringToStream(encoded); return createFromReadableStream(stream, { temporaryReferences }); } // ============================================================================ // Public API // ============================================================================ /** * RSC-deserialize a single encoded component string back to a React element. * Used by the static handler runtime to revive pre-rendered components. * Identical to deserializeResult. */ export const deserializeComponent: (encoded: string) => Promise = deserializeResult; /** * Serialize segments for storage. * Each segment's component, layout, loading, and loaderData are RSC-serialized. * Metadata is preserved as-is. */ export async function serializeSegments( segments: ResolvedSegment[], ): Promise { return Promise.all( segments.map(async (segment): Promise => { const temporaryReferences = createTemporaryReferenceSet(); // Await component if it's a Promise (intercepts with loading keep component as Promise) const componentResolved = segment.component instanceof Promise ? await segment.component : segment.component; // Serialize the component to RSC stream const stream = renderToReadableStream(componentResolved, { temporaryReferences, }); // RSC-serialize loading: "null" string distinguishes explicit null from undefined const encodedLoading = segment.loading !== undefined ? segment.loading === null ? "null" : await rscSerialize(segment.loading) : undefined; // Await loaderData / loaderDataPromise if they're Promises const loaderDataResolved = segment.loaderData instanceof Promise ? await segment.loaderData : segment.loaderData; const loaderDataPromiseResolved = segment.loaderDataPromise instanceof Promise ? await segment.loaderDataPromise : segment.loaderDataPromise; // Parallelize stream-to-string and RSC serialization of sub-fields const [ encoded, encodedLayout, encodedLoaderData, encodedLoaderDataPromise, ] = await Promise.all([ streamToString(stream), segment.layout ? rscSerialize(segment.layout) : undefined, rscSerialize(loaderDataResolved), rscSerialize(loaderDataPromiseResolved), ]); return { encoded, encodedLayout, encodedLoading, encodedLoaderData, encodedLoaderDataPromise, metadata: { id: segment.id, type: segment.type, namespace: segment.namespace, index: segment.index, params: segment.params, slot: segment.slot, belongsToRoute: segment.belongsToRoute, layoutName: segment.layoutName, parallelName: segment.parallelName, loaderId: segment.loaderId, loaderIds: segment.loaderIds, transition: segment.transition, mountPath: segment.mountPath, }, }; }), ); } /** * Deserialize segments from storage. * Reconstructs ResolvedSegment objects from RSC-serialized data. */ export async function deserializeSegments( data: SerializedSegmentData[], ): Promise { return Promise.all( data.map(async (item): Promise => { const temporaryReferences = createTemporaryReferenceSet(); // Handle the "null" sentinel for loading before RSC deserialization. // During serialization, loading: null is stored as the string "null" to // distinguish it from undefined. const loadingIsNullSentinel = item.encodedLoading === "null"; const [component, layout, loaderData, loaderDataPromise, loadingData] = await Promise.all([ createFromReadableStream(stringToStream(item.encoded), { temporaryReferences, }), rscDeserialize(item.encodedLayout), rscDeserialize(item.encodedLoaderData), rscDeserialize(item.encodedLoaderDataPromise), loadingIsNullSentinel ? (null as any) : rscDeserialize(item.encodedLoading), ]); return { ...item.metadata, component, layout, loading: loadingData, loaderData, loaderDataPromise, } as ResolvedSegment; }), ); }