UNPKG

@tanstack/start-server-core

Version:

Modern and scalable routing for React applications

166 lines (165 loc) 4.98 kB
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