@tanstack/router-core
Version:
Modern and scalable routing for React applications
282 lines (281 loc) • 8.46 kB
JavaScript
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