UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

282 lines (281 loc) 8.46 kB
import { createPlugin, createStream } from "seroval"; //#region src/ssr/serializer/RawStream.ts /** * Marker class for ReadableStream<Uint8Array> that should be serialized * with base64 encoding (SSR) or binary framing (server functions). * * Wrap your binary streams with this to get efficient serialization: * ```ts * // For binary data (files, images, etc.) * return { data: new RawStream(file.stream()) } * * // For text-heavy data (RSC payloads, etc.) * return { data: new RawStream(rscStream, { hint: 'text' }) } * ``` */ var RawStream = class { constructor(stream, options) { this.stream = stream; this.hint = options?.hint ?? "binary"; } }; var BufferCtor = globalThis.Buffer; var hasNodeBuffer = !!BufferCtor && typeof BufferCtor.from === "function"; function uint8ArrayToBase64(bytes) { if (bytes.length === 0) return ""; if (hasNodeBuffer) return BufferCtor.from(bytes).toString("base64"); const CHUNK_SIZE = 32768; const chunks = []; for (let i = 0; i < bytes.length; i += CHUNK_SIZE) { const chunk = bytes.subarray(i, i + CHUNK_SIZE); chunks.push(String.fromCharCode.apply(null, chunk)); } return btoa(chunks.join("")); } function base64ToUint8Array(base64) { if (base64.length === 0) return new Uint8Array(0); if (hasNodeBuffer) { const buf = BufferCtor.from(base64, "base64"); return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); } const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes; } var RAW_STREAM_FACTORY_BINARY = Object.create(null); var RAW_STREAM_FACTORY_TEXT = Object.create(null); var RAW_STREAM_FACTORY_CONSTRUCTOR_BINARY = (stream) => new ReadableStream({ start(controller) { stream.on({ next(base64) { try { controller.enqueue(base64ToUint8Array(base64)); } catch {} }, throw(error) { controller.error(error); }, return() { try { controller.close(); } catch {} } }); } }); var textEncoderForFactory = new TextEncoder(); var RAW_STREAM_FACTORY_CONSTRUCTOR_TEXT = (stream) => { return new ReadableStream({ start(controller) { stream.on({ next(value) { try { if (typeof value === "string") controller.enqueue(textEncoderForFactory.encode(value)); else controller.enqueue(base64ToUint8Array(value.$b64)); } catch {} }, throw(error) { controller.error(error); }, return() { try { controller.close(); } catch {} } }); } }); }; var FACTORY_BINARY = `(s=>new ReadableStream({start(c){s.on({next(b){try{const d=atob(b),a=new Uint8Array(d.length);for(let i=0;i<d.length;i++)a[i]=d.charCodeAt(i);c.enqueue(a)}catch(_){}},throw(e){c.error(e)},return(){try{c.close()}catch(_){}}})}}))`; var FACTORY_TEXT = `(s=>{const e=new TextEncoder();return new ReadableStream({start(c){s.on({next(v){try{if(typeof v==='string'){c.enqueue(e.encode(v))}else{const d=atob(v.$b64),a=new Uint8Array(d.length);for(let i=0;i<d.length;i++)a[i]=d.charCodeAt(i);c.enqueue(a)}}catch(_){}},throw(x){c.error(x)},return(){try{c.close()}catch(_){}}})}})})`; function toBinaryStream(readable) { const stream = createStream(); const reader = readable.getReader(); (async () => { try { while (true) { const { done, value } = await reader.read(); if (done) { stream.return(void 0); break; } stream.next(uint8ArrayToBase64(value)); } } catch (error) { stream.throw(error); } finally { reader.releaseLock(); } })(); return stream; } function toTextStream(readable) { const stream = createStream(); const reader = readable.getReader(); const decoder = new TextDecoder("utf-8", { fatal: true }); (async () => { try { while (true) { const { done, value } = await reader.read(); if (done) { try { const remaining = decoder.decode(); if (remaining.length > 0) stream.next(remaining); } catch {} stream.return(void 0); break; } try { const text = decoder.decode(value, { stream: true }); if (text.length > 0) stream.next(text); } catch { stream.next({ $b64: uint8ArrayToBase64(value) }); } } } catch (error) { stream.throw(error); } finally { reader.releaseLock(); } })(); return stream; } /** * SSR Plugin - uses base64 or UTF-8+base64 encoding for chunks, delegates to seroval's stream mechanism. * Used during SSR when serializing to JavaScript code for HTML injection. * * Supports two modes based on RawStream hint: * - 'binary': Always base64 encode (default) * - 'text': Try UTF-8 first, fallback to base64 for invalid UTF-8 */ var RawStreamSSRPlugin = createPlugin({ tag: "tss/RawStream", extends: [createPlugin({ tag: "tss/RawStreamFactory", test(value) { return value === RAW_STREAM_FACTORY_BINARY; }, parse: { sync() {}, async() { return Promise.resolve(void 0); }, stream() {} }, serialize() { return FACTORY_BINARY; }, deserialize() { return RAW_STREAM_FACTORY_BINARY; } }), createPlugin({ tag: "tss/RawStreamFactoryText", test(value) { return value === RAW_STREAM_FACTORY_TEXT; }, parse: { sync() {}, async() { return Promise.resolve(void 0); }, stream() {} }, serialize() { return FACTORY_TEXT; }, deserialize() { return RAW_STREAM_FACTORY_TEXT; } })], test(value) { return value instanceof RawStream; }, parse: { sync(value, ctx) { const factory = value.hint === "text" ? RAW_STREAM_FACTORY_TEXT : RAW_STREAM_FACTORY_BINARY; return { hint: value.hint, factory: ctx.parse(factory), stream: ctx.parse(createStream()) }; }, async async(value, ctx) { const factory = value.hint === "text" ? RAW_STREAM_FACTORY_TEXT : RAW_STREAM_FACTORY_BINARY; const encodedStream = value.hint === "text" ? toTextStream(value.stream) : toBinaryStream(value.stream); return { hint: value.hint, factory: await ctx.parse(factory), stream: await ctx.parse(encodedStream) }; }, stream(value, ctx) { const factory = value.hint === "text" ? RAW_STREAM_FACTORY_TEXT : RAW_STREAM_FACTORY_BINARY; const encodedStream = value.hint === "text" ? toTextStream(value.stream) : toBinaryStream(value.stream); return { hint: value.hint, factory: ctx.parse(factory), stream: ctx.parse(encodedStream) }; } }, serialize(node, ctx) { return "(" + ctx.serialize(node.factory) + ")(" + ctx.serialize(node.stream) + ")"; }, deserialize(node, ctx) { const stream = ctx.deserialize(node.stream); return node.hint === "text" ? RAW_STREAM_FACTORY_CONSTRUCTOR_TEXT(stream) : RAW_STREAM_FACTORY_CONSTRUCTOR_BINARY(stream); } }); /** * Creates an RPC plugin instance that registers raw streams with a multiplexer. * Used for server function responses where we want binary framing. * Note: RPC always uses binary framing regardless of hint. * * @param onRawStream Callback invoked when a RawStream is encountered during serialization */ function createRawStreamRPCPlugin(onRawStream) { let nextStreamId = 1; return createPlugin({ tag: "tss/RawStream", test(value) { return value instanceof RawStream; }, parse: { async(value) { const streamId = nextStreamId++; onRawStream(streamId, value.stream); return Promise.resolve({ streamId }); }, stream(value) { const streamId = nextStreamId++; onRawStream(streamId, value.stream); return { streamId }; } }, serialize() { throw new Error("RawStreamRPCPlugin.serialize should not be called. RPC uses JSON serialization, not JS code generation."); }, deserialize() { throw new Error("RawStreamRPCPlugin.deserialize should not be called. Use createRawStreamDeserializePlugin on client."); } }); } /** * Creates a deserialize-only plugin for client-side stream reconstruction. * Used in serverFnFetcher to wire up streams from frame decoder. * * @param getOrCreateStream Function to get/create a stream by ID from frame decoder */ function createRawStreamDeserializePlugin(getOrCreateStream) { return createPlugin({ tag: "tss/RawStream", test: () => false, parse: {}, serialize() { throw new Error("RawStreamDeserializePlugin.serialize should not be called. Client only deserializes."); }, deserialize(node) { return getOrCreateStream(node.streamId); } }); } //#endregion export { RawStream, RawStreamSSRPlugin, createRawStreamDeserializePlugin, createRawStreamRPCPlugin }; //# sourceMappingURL=RawStream.js.map