@tanstack/start-server-core
Version:
Modern and scalable routing for React applications
166 lines (165 loc) • 4.98 kB
JavaScript
import { FRAME_HEADER_SIZE, FrameType, TSS_CONTENT_TYPE_FRAMED_VERSIONED } from "@tanstack/start-client-core";
//#region src/frame-protocol.ts
/**
* Binary frame protocol for multiplexing JSON and raw streams over HTTP.
*
* Frame format: [type:1][streamId:4][length:4][payload:length]
* - type: 1 byte - frame type (JSON, CHUNK, END, ERROR)
* - streamId: 4 bytes big-endian uint32 - stream identifier
* - length: 4 bytes big-endian uint32 - payload length
* - payload: variable length bytes
*/
/** Cached TextEncoder for frame encoding */
var textEncoder = new TextEncoder();
/** Shared empty payload for END frames - avoids allocation per call */
var EMPTY_PAYLOAD = new Uint8Array(0);
/**
* Encodes a single frame with header and payload.
*/
function encodeFrame(type, streamId, payload) {
const frame = new Uint8Array(FRAME_HEADER_SIZE + payload.length);
frame[0] = type;
frame[1] = streamId >>> 24 & 255;
frame[2] = streamId >>> 16 & 255;
frame[3] = streamId >>> 8 & 255;
frame[4] = streamId & 255;
frame[5] = payload.length >>> 24 & 255;
frame[6] = payload.length >>> 16 & 255;
frame[7] = payload.length >>> 8 & 255;
frame[8] = payload.length & 255;
frame.set(payload, FRAME_HEADER_SIZE);
return frame;
}
/**
* Encodes a JSON frame (type 0, streamId 0).
*/
function encodeJSONFrame(json) {
return encodeFrame(FrameType.JSON, 0, textEncoder.encode(json));
}
/**
* Encodes a raw stream chunk frame.
*/
function encodeChunkFrame(streamId, chunk) {
return encodeFrame(FrameType.CHUNK, streamId, chunk);
}
/**
* Encodes a raw stream end frame.
*/
function encodeEndFrame(streamId) {
return encodeFrame(FrameType.END, streamId, EMPTY_PAYLOAD);
}
/**
* Encodes a raw stream error frame.
*/
function encodeErrorFrame(streamId, error) {
const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
return encodeFrame(FrameType.ERROR, streamId, textEncoder.encode(message));
}
/**
* Creates a multiplexed ReadableStream from JSON stream and raw streams.
*
* The JSON stream emits NDJSON lines (from seroval's toCrossJSONStream).
* Raw streams are pumped concurrently, interleaved with JSON frames.
*
* Supports late stream registration for RawStreams discovered after initial
* serialization (e.g., from resolved Promises).
*
* @param jsonStream Stream of JSON strings (each string is one NDJSON line)
* @param rawStreams Map of stream IDs to raw binary streams (known at start)
* @param lateStreamSource Optional stream of late registrations for streams discovered later
*/
function createMultiplexedStream(jsonStream, rawStreams, lateStreamSource) {
let controller;
let cancelled = false;
const readers = [];
const enqueue = (frame) => {
if (cancelled) return false;
try {
controller.enqueue(frame);
return true;
} catch {
return false;
}
};
const errorOutput = (error) => {
if (cancelled) return;
cancelled = true;
try {
controller.error(error);
} catch {}
for (const reader of readers) reader.cancel().catch(() => {});
};
async function pumpRawStream(streamId, stream) {
const reader = stream.getReader();
readers.push(reader);
try {
while (!cancelled) {
const { done, value } = await reader.read();
if (done) {
enqueue(encodeEndFrame(streamId));
return;
}
if (!enqueue(encodeChunkFrame(streamId, value))) return;
}
} catch (error) {
enqueue(encodeErrorFrame(streamId, error));
} finally {
reader.releaseLock();
}
}
async function pumpJSON() {
const reader = jsonStream.getReader();
readers.push(reader);
try {
while (!cancelled) {
const { done, value } = await reader.read();
if (done) return;
if (!enqueue(encodeJSONFrame(value))) return;
}
} catch (error) {
errorOutput(error);
throw error;
} finally {
reader.releaseLock();
}
}
async function pumpLateStreams() {
if (!lateStreamSource) return [];
const lateStreamPumps = [];
const reader = lateStreamSource.getReader();
readers.push(reader);
try {
while (!cancelled) {
const { done, value } = await reader.read();
if (done) break;
lateStreamPumps.push(pumpRawStream(value.id, value.stream));
}
} finally {
reader.releaseLock();
}
return lateStreamPumps;
}
return new ReadableStream({
async start(ctrl) {
controller = ctrl;
const pumps = [pumpJSON()];
for (const [streamId, stream] of rawStreams) pumps.push(pumpRawStream(streamId, stream));
if (lateStreamSource) pumps.push(pumpLateStreams());
try {
const latePumps = (await Promise.all(pumps)).find(Array.isArray);
if (latePumps && latePumps.length > 0) await Promise.all(latePumps);
if (!cancelled) try {
controller.close();
} catch {}
} catch {}
},
cancel() {
cancelled = true;
for (const reader of readers) reader.cancel().catch(() => {});
readers.length = 0;
}
});
}
//#endregion
export { TSS_CONTENT_TYPE_FRAMED_VERSIONED, createMultiplexedStream };
//# sourceMappingURL=frame-protocol.js.map