Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
291 lines
8.8 KiB
Plaintext
291 lines
8.8 KiB
Plaintext
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";
|
|
}
|
|
};
|
|
const BufferCtor = globalThis.Buffer;
|
|
const 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;
|
|
}
|
|
const RAW_STREAM_FACTORY_BINARY = Object.create(null);
|
|
const RAW_STREAM_FACTORY_TEXT = Object.create(null);
|
|
const 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 {}
|
|
}
|
|
});
|
|
} });
|
|
const textEncoderForFactory = new TextEncoder();
|
|
const 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 {}
|
|
}
|
|
});
|
|
} });
|
|
};
|
|
const 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(_){}}})}}))`;
|
|
const 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
|
|
*/
|
|
const RawStreamSSRPlugin = /* @__PURE__ */ createPlugin({
|
|
tag: "tss/RawStream",
|
|
extends: [/* @__PURE__ */ createPlugin({
|
|
tag: "tss/RawStreamFactory",
|
|
test(value) {
|
|
return value === RAW_STREAM_FACTORY_BINARY;
|
|
},
|
|
parse: {
|
|
sync(_value, _ctx, _data) {
|
|
return {};
|
|
},
|
|
async async(_value, _ctx, _data) {
|
|
return {};
|
|
},
|
|
stream(_value, _ctx, _data) {
|
|
return {};
|
|
}
|
|
},
|
|
serialize(_node, _ctx, _data) {
|
|
return FACTORY_BINARY;
|
|
},
|
|
deserialize(_node, _ctx, _data) {
|
|
return RAW_STREAM_FACTORY_BINARY;
|
|
}
|
|
}), /* @__PURE__ */ createPlugin({
|
|
tag: "tss/RawStreamFactoryText",
|
|
test(value) {
|
|
return value === RAW_STREAM_FACTORY_TEXT;
|
|
},
|
|
parse: {
|
|
sync(_value, _ctx, _data) {
|
|
return {};
|
|
},
|
|
async async(_value, _ctx, _data) {
|
|
return {};
|
|
},
|
|
stream(_value, _ctx, _data) {
|
|
return {};
|
|
}
|
|
},
|
|
serialize(_node, _ctx, _data) {
|
|
return FACTORY_TEXT;
|
|
},
|
|
deserialize(_node, _ctx, _data) {
|
|
return RAW_STREAM_FACTORY_TEXT;
|
|
}
|
|
})],
|
|
test(value) {
|
|
return value instanceof RawStream;
|
|
},
|
|
parse: {
|
|
sync(value, ctx, _data) {
|
|
const factory = value.hint === "text" ? RAW_STREAM_FACTORY_TEXT : RAW_STREAM_FACTORY_BINARY;
|
|
return {
|
|
hint: ctx.parse(value.hint),
|
|
factory: ctx.parse(factory),
|
|
stream: ctx.parse(createStream())
|
|
};
|
|
},
|
|
async async(value, ctx, _data) {
|
|
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: await ctx.parse(value.hint),
|
|
factory: await ctx.parse(factory),
|
|
stream: await ctx.parse(encodedStream)
|
|
};
|
|
},
|
|
stream(value, ctx, _data) {
|
|
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: ctx.parse(value.hint),
|
|
factory: ctx.parse(factory),
|
|
stream: ctx.parse(encodedStream)
|
|
};
|
|
}
|
|
},
|
|
serialize(node, ctx, _data) {
|
|
return "(" + ctx.serialize(node.factory) + ")(" + ctx.serialize(node.stream) + ")";
|
|
},
|
|
deserialize(node, ctx, _data) {
|
|
const stream = ctx.deserialize(node.stream);
|
|
return ctx.deserialize(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
|
|
*/
|
|
/* @__NO_SIDE_EFFECTS__ */
|
|
function createRawStreamRPCPlugin(onRawStream) {
|
|
let nextStreamId = 1;
|
|
return /* @__PURE__ */ createPlugin({
|
|
tag: "tss/RawStream",
|
|
test(value) {
|
|
return value instanceof RawStream;
|
|
},
|
|
parse: {
|
|
async async(value, ctx, _data) {
|
|
const streamId = nextStreamId++;
|
|
onRawStream(streamId, value.stream);
|
|
return { streamId: await ctx.parse(streamId) };
|
|
},
|
|
stream(value, ctx, _data) {
|
|
const streamId = nextStreamId++;
|
|
onRawStream(streamId, value.stream);
|
|
return { streamId: ctx.parse(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 /* @__PURE__ */ createPlugin({
|
|
tag: "tss/RawStream",
|
|
test: () => false,
|
|
parse: {},
|
|
serialize() {
|
|
throw new Error("RawStreamDeserializePlugin.serialize should not be called. Client only deserializes.");
|
|
},
|
|
deserialize(node, ctx, _data) {
|
|
return getOrCreateStream(typeof ctx?.deserialize === "function" ? ctx.deserialize(node.streamId) : node.streamId);
|
|
}
|
|
});
|
|
}
|
|
//#endregion
|
|
export { RawStream, RawStreamSSRPlugin, createRawStreamDeserializePlugin, createRawStreamRPCPlugin };
|
|
|
|
//# sourceMappingURL=RawStream.js.map |