@tanstack/start-client-core
Advanced tools
| /** | ||
| * Client-side frame decoder for multiplexed responses. | ||
| * | ||
| * Decodes binary frame protocol and reconstructs: | ||
| * - JSON stream (NDJSON lines for seroval) | ||
| * - Raw streams (binary data as ReadableStream<Uint8Array>) | ||
| */ | ||
| /** | ||
| * Result of frame decoding. | ||
| */ | ||
| export interface FrameDecoderResult { | ||
| /** Gets or creates a raw stream by ID (for use by deserialize plugin) */ | ||
| getOrCreateStream: (id: number) => ReadableStream<Uint8Array>; | ||
| /** Stream of JSON strings (NDJSON lines) */ | ||
| jsonChunks: ReadableStream<string>; | ||
| } | ||
| /** | ||
| * Creates a frame decoder that processes a multiplexed response stream. | ||
| * | ||
| * @param input The raw response body stream | ||
| * @returns Decoded JSON stream and stream getter function | ||
| */ | ||
| export declare function createFrameDecoder(input: ReadableStream<Uint8Array>): FrameDecoderResult; |
| import { FrameType, FRAME_HEADER_SIZE } from "../constants.js"; | ||
| const textDecoder = new TextDecoder(); | ||
| const EMPTY_BUFFER = new Uint8Array(0); | ||
| const MAX_FRAME_PAYLOAD_SIZE = 16 * 1024 * 1024; | ||
| const MAX_BUFFERED_BYTES = 32 * 1024 * 1024; | ||
| const MAX_STREAMS = 1024; | ||
| const MAX_FRAMES = 1e5; | ||
| function createFrameDecoder(input) { | ||
| const streamControllers = /* @__PURE__ */ new Map(); | ||
| const streams = /* @__PURE__ */ new Map(); | ||
| const cancelledStreamIds = /* @__PURE__ */ new Set(); | ||
| let cancelled = false; | ||
| let inputReader = null; | ||
| let frameCount = 0; | ||
| let jsonController; | ||
| const jsonChunks = new ReadableStream({ | ||
| start(controller) { | ||
| jsonController = controller; | ||
| }, | ||
| cancel() { | ||
| cancelled = true; | ||
| try { | ||
| inputReader?.cancel(); | ||
| } catch { | ||
| } | ||
| streamControllers.forEach((ctrl) => { | ||
| try { | ||
| ctrl.error(new Error("Framed response cancelled")); | ||
| } catch { | ||
| } | ||
| }); | ||
| streamControllers.clear(); | ||
| streams.clear(); | ||
| cancelledStreamIds.clear(); | ||
| } | ||
| }); | ||
| function getOrCreateStream(id) { | ||
| const existing = streams.get(id); | ||
| if (existing) { | ||
| return existing; | ||
| } | ||
| if (cancelledStreamIds.has(id)) { | ||
| return new ReadableStream({ | ||
| start(controller) { | ||
| controller.close(); | ||
| } | ||
| }); | ||
| } | ||
| if (streams.size >= MAX_STREAMS) { | ||
| throw new Error( | ||
| `Too many raw streams in framed response (max ${MAX_STREAMS})` | ||
| ); | ||
| } | ||
| const stream = new ReadableStream({ | ||
| start(ctrl) { | ||
| streamControllers.set(id, ctrl); | ||
| }, | ||
| cancel() { | ||
| cancelledStreamIds.add(id); | ||
| streamControllers.delete(id); | ||
| streams.delete(id); | ||
| } | ||
| }); | ||
| streams.set(id, stream); | ||
| return stream; | ||
| } | ||
| function ensureController(id) { | ||
| getOrCreateStream(id); | ||
| return streamControllers.get(id); | ||
| } | ||
| (async () => { | ||
| const reader = input.getReader(); | ||
| inputReader = reader; | ||
| const bufferList = []; | ||
| let totalLength = 0; | ||
| function readHeader() { | ||
| if (totalLength < FRAME_HEADER_SIZE) return null; | ||
| const first = bufferList[0]; | ||
| if (first.length >= FRAME_HEADER_SIZE) { | ||
| const type2 = first[0]; | ||
| const streamId2 = (first[1] << 24 | first[2] << 16 | first[3] << 8 | first[4]) >>> 0; | ||
| const length2 = (first[5] << 24 | first[6] << 16 | first[7] << 8 | first[8]) >>> 0; | ||
| return { type: type2, streamId: streamId2, length: length2 }; | ||
| } | ||
| const headerBytes = new Uint8Array(FRAME_HEADER_SIZE); | ||
| let offset = 0; | ||
| let remaining = FRAME_HEADER_SIZE; | ||
| for (let i = 0; i < bufferList.length && remaining > 0; i++) { | ||
| const chunk = bufferList[i]; | ||
| const toCopy = Math.min(chunk.length, remaining); | ||
| headerBytes.set(chunk.subarray(0, toCopy), offset); | ||
| offset += toCopy; | ||
| remaining -= toCopy; | ||
| } | ||
| const type = headerBytes[0]; | ||
| const streamId = (headerBytes[1] << 24 | headerBytes[2] << 16 | headerBytes[3] << 8 | headerBytes[4]) >>> 0; | ||
| const length = (headerBytes[5] << 24 | headerBytes[6] << 16 | headerBytes[7] << 8 | headerBytes[8]) >>> 0; | ||
| return { type, streamId, length }; | ||
| } | ||
| function extractFlattened(count) { | ||
| if (count === 0) return EMPTY_BUFFER; | ||
| const result = new Uint8Array(count); | ||
| let offset = 0; | ||
| let remaining = count; | ||
| while (remaining > 0 && bufferList.length > 0) { | ||
| const chunk = bufferList[0]; | ||
| if (!chunk) break; | ||
| const toCopy = Math.min(chunk.length, remaining); | ||
| result.set(chunk.subarray(0, toCopy), offset); | ||
| offset += toCopy; | ||
| remaining -= toCopy; | ||
| if (toCopy === chunk.length) { | ||
| bufferList.shift(); | ||
| } else { | ||
| bufferList[0] = chunk.subarray(toCopy); | ||
| } | ||
| } | ||
| totalLength -= count; | ||
| return result; | ||
| } | ||
| try { | ||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (cancelled) break; | ||
| if (done) break; | ||
| if (!value) continue; | ||
| if (totalLength + value.length > MAX_BUFFERED_BYTES) { | ||
| throw new Error( | ||
| `Framed response buffer exceeded ${MAX_BUFFERED_BYTES} bytes` | ||
| ); | ||
| } | ||
| bufferList.push(value); | ||
| totalLength += value.length; | ||
| while (true) { | ||
| const header = readHeader(); | ||
| if (!header) break; | ||
| const { type, streamId, length } = header; | ||
| if (type !== FrameType.JSON && type !== FrameType.CHUNK && type !== FrameType.END && type !== FrameType.ERROR) { | ||
| throw new Error(`Unknown frame type: ${type}`); | ||
| } | ||
| if (type === FrameType.JSON) { | ||
| if (streamId !== 0) { | ||
| throw new Error("Invalid JSON frame streamId (expected 0)"); | ||
| } | ||
| } else { | ||
| if (streamId === 0) { | ||
| throw new Error("Invalid raw frame streamId (expected non-zero)"); | ||
| } | ||
| } | ||
| if (length > MAX_FRAME_PAYLOAD_SIZE) { | ||
| throw new Error( | ||
| `Frame payload too large: ${length} bytes (max ${MAX_FRAME_PAYLOAD_SIZE})` | ||
| ); | ||
| } | ||
| const frameSize = FRAME_HEADER_SIZE + length; | ||
| if (totalLength < frameSize) break; | ||
| if (++frameCount > MAX_FRAMES) { | ||
| throw new Error( | ||
| `Too many frames in framed response (max ${MAX_FRAMES})` | ||
| ); | ||
| } | ||
| extractFlattened(FRAME_HEADER_SIZE); | ||
| const payload = extractFlattened(length); | ||
| switch (type) { | ||
| case FrameType.JSON: { | ||
| try { | ||
| jsonController.enqueue(textDecoder.decode(payload)); | ||
| } catch { | ||
| } | ||
| break; | ||
| } | ||
| case FrameType.CHUNK: { | ||
| const ctrl = ensureController(streamId); | ||
| if (ctrl) { | ||
| ctrl.enqueue(payload); | ||
| } | ||
| break; | ||
| } | ||
| case FrameType.END: { | ||
| const ctrl = ensureController(streamId); | ||
| cancelledStreamIds.add(streamId); | ||
| if (ctrl) { | ||
| try { | ||
| ctrl.close(); | ||
| } catch { | ||
| } | ||
| streamControllers.delete(streamId); | ||
| } | ||
| break; | ||
| } | ||
| case FrameType.ERROR: { | ||
| const ctrl = ensureController(streamId); | ||
| cancelledStreamIds.add(streamId); | ||
| if (ctrl) { | ||
| const message = textDecoder.decode(payload); | ||
| ctrl.error(new Error(message)); | ||
| streamControllers.delete(streamId); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (totalLength !== 0) { | ||
| throw new Error("Incomplete frame at end of framed response"); | ||
| } | ||
| try { | ||
| jsonController.close(); | ||
| } catch { | ||
| } | ||
| streamControllers.forEach((ctrl) => { | ||
| try { | ||
| ctrl.close(); | ||
| } catch { | ||
| } | ||
| }); | ||
| streamControllers.clear(); | ||
| } catch (error) { | ||
| try { | ||
| jsonController.error(error); | ||
| } catch { | ||
| } | ||
| streamControllers.forEach((ctrl) => { | ||
| try { | ||
| ctrl.error(error); | ||
| } catch { | ||
| } | ||
| }); | ||
| streamControllers.clear(); | ||
| } finally { | ||
| try { | ||
| reader.releaseLock(); | ||
| } catch { | ||
| } | ||
| inputReader = null; | ||
| } | ||
| })(); | ||
| return { getOrCreateStream, jsonChunks }; | ||
| } | ||
| export { | ||
| createFrameDecoder | ||
| }; | ||
| //# sourceMappingURL=frame-decoder.js.map |
| {"version":3,"file":"frame-decoder.js","sources":["../../../src/client-rpc/frame-decoder.ts"],"sourcesContent":["/**\n * Client-side frame decoder for multiplexed responses.\n *\n * Decodes binary frame protocol and reconstructs:\n * - JSON stream (NDJSON lines for seroval)\n * - Raw streams (binary data as ReadableStream<Uint8Array>)\n */\n\nimport { FRAME_HEADER_SIZE, FrameType } from '../constants'\n\n/** Cached TextDecoder for frame decoding */\nconst textDecoder = new TextDecoder()\n\n/** Shared empty buffer for empty buffer case - avoids allocation */\nconst EMPTY_BUFFER = new Uint8Array(0)\n\n/** Hardening limits to prevent memory/CPU DoS */\nconst MAX_FRAME_PAYLOAD_SIZE = 16 * 1024 * 1024 // 16MiB\nconst MAX_BUFFERED_BYTES = 32 * 1024 * 1024 // 32MiB\nconst MAX_STREAMS = 1024\nconst MAX_FRAMES = 100_000 // Limit total frames to prevent CPU DoS\n\n/**\n * Result of frame decoding.\n */\nexport interface FrameDecoderResult {\n /** Gets or creates a raw stream by ID (for use by deserialize plugin) */\n getOrCreateStream: (id: number) => ReadableStream<Uint8Array>\n /** Stream of JSON strings (NDJSON lines) */\n jsonChunks: ReadableStream<string>\n}\n\n/**\n * Creates a frame decoder that processes a multiplexed response stream.\n *\n * @param input The raw response body stream\n * @returns Decoded JSON stream and stream getter function\n */\nexport function createFrameDecoder(\n input: ReadableStream<Uint8Array>,\n): FrameDecoderResult {\n const streamControllers = new Map<\n number,\n ReadableStreamDefaultController<Uint8Array>\n >()\n const streams = new Map<number, ReadableStream<Uint8Array>>()\n const cancelledStreamIds = new Set<number>()\n\n let cancelled = false as boolean\n let inputReader: ReadableStreamReader<Uint8Array> | null = null\n let frameCount = 0\n\n let jsonController!: ReadableStreamDefaultController<string>\n const jsonChunks = new ReadableStream<string>({\n start(controller) {\n jsonController = controller\n },\n cancel() {\n cancelled = true\n try {\n inputReader?.cancel()\n } catch {\n // Ignore\n }\n\n streamControllers.forEach((ctrl) => {\n try {\n ctrl.error(new Error('Framed response cancelled'))\n } catch {\n // Ignore\n }\n })\n streamControllers.clear()\n streams.clear()\n cancelledStreamIds.clear()\n },\n })\n\n /**\n * Gets or creates a stream for a given stream ID.\n * Called by deserialize plugin when it encounters a RawStream reference.\n */\n function getOrCreateStream(id: number): ReadableStream<Uint8Array> {\n const existing = streams.get(id)\n if (existing) {\n return existing\n }\n\n // If we already received an END/ERROR for this streamId, returning a fresh stream\n // would hang consumers. Return an already-closed stream instead.\n if (cancelledStreamIds.has(id)) {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close()\n },\n })\n }\n\n if (streams.size >= MAX_STREAMS) {\n throw new Error(\n `Too many raw streams in framed response (max ${MAX_STREAMS})`,\n )\n }\n\n const stream = new ReadableStream<Uint8Array>({\n start(ctrl) {\n streamControllers.set(id, ctrl)\n },\n cancel() {\n cancelledStreamIds.add(id)\n streamControllers.delete(id)\n streams.delete(id)\n },\n })\n streams.set(id, stream)\n return stream\n }\n\n /**\n * Ensures stream exists and returns its controller for enqueuing data.\n * Used for CHUNK frames where we need to ensure stream is created.\n */\n function ensureController(\n id: number,\n ): ReadableStreamDefaultController<Uint8Array> | undefined {\n getOrCreateStream(id)\n return streamControllers.get(id)\n }\n\n // Process frames asynchronously\n ;(async () => {\n const reader = input.getReader()\n inputReader = reader\n\n const bufferList: Array<Uint8Array> = []\n let totalLength = 0\n\n /**\n * Reads header bytes from buffer chunks without flattening.\n * Returns header data or null if not enough bytes available.\n */\n function readHeader(): {\n type: number\n streamId: number\n length: number\n } | null {\n if (totalLength < FRAME_HEADER_SIZE) return null\n\n const first = bufferList[0]!\n\n // Fast path: header fits entirely in first chunk (common case)\n if (first.length >= FRAME_HEADER_SIZE) {\n const type = first[0]!\n const streamId =\n ((first[1]! << 24) |\n (first[2]! << 16) |\n (first[3]! << 8) |\n first[4]!) >>>\n 0\n const length =\n ((first[5]! << 24) |\n (first[6]! << 16) |\n (first[7]! << 8) |\n first[8]!) >>>\n 0\n return { type, streamId, length }\n }\n\n // Slow path: header spans multiple chunks - flatten header bytes only\n const headerBytes = new Uint8Array(FRAME_HEADER_SIZE)\n let offset = 0\n let remaining = FRAME_HEADER_SIZE\n for (let i = 0; i < bufferList.length && remaining > 0; i++) {\n const chunk = bufferList[i]!\n const toCopy = Math.min(chunk.length, remaining)\n headerBytes.set(chunk.subarray(0, toCopy), offset)\n offset += toCopy\n remaining -= toCopy\n }\n\n const type = headerBytes[0]!\n const streamId =\n ((headerBytes[1]! << 24) |\n (headerBytes[2]! << 16) |\n (headerBytes[3]! << 8) |\n headerBytes[4]!) >>>\n 0\n const length =\n ((headerBytes[5]! << 24) |\n (headerBytes[6]! << 16) |\n (headerBytes[7]! << 8) |\n headerBytes[8]!) >>>\n 0\n\n return { type, streamId, length }\n }\n\n /**\n * Flattens buffer list into single Uint8Array and removes from list.\n */\n function extractFlattened(count: number): Uint8Array {\n if (count === 0) return EMPTY_BUFFER\n\n const result = new Uint8Array(count)\n let offset = 0\n let remaining = count\n\n while (remaining > 0 && bufferList.length > 0) {\n const chunk = bufferList[0]\n if (!chunk) break\n const toCopy = Math.min(chunk.length, remaining)\n result.set(chunk.subarray(0, toCopy), offset)\n\n offset += toCopy\n remaining -= toCopy\n\n if (toCopy === chunk.length) {\n bufferList.shift()\n } else {\n bufferList[0] = chunk.subarray(toCopy)\n }\n }\n\n totalLength -= count\n return result\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const { done, value } = await reader.read()\n if (cancelled) break\n if (done) break\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!value) continue\n\n // Append incoming chunk to buffer list\n if (totalLength + value.length > MAX_BUFFERED_BYTES) {\n throw new Error(\n `Framed response buffer exceeded ${MAX_BUFFERED_BYTES} bytes`,\n )\n }\n bufferList.push(value)\n totalLength += value.length\n\n // Parse complete frames from buffer\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const header = readHeader()\n if (!header) break // Not enough bytes for header\n\n const { type, streamId, length } = header\n\n if (\n type !== FrameType.JSON &&\n type !== FrameType.CHUNK &&\n type !== FrameType.END &&\n type !== FrameType.ERROR\n ) {\n throw new Error(`Unknown frame type: ${type}`)\n }\n\n // Enforce stream id conventions: JSON uses streamId 0, raw streams use non-zero ids\n if (type === FrameType.JSON) {\n if (streamId !== 0) {\n throw new Error('Invalid JSON frame streamId (expected 0)')\n }\n } else {\n if (streamId === 0) {\n throw new Error('Invalid raw frame streamId (expected non-zero)')\n }\n }\n\n if (length > MAX_FRAME_PAYLOAD_SIZE) {\n throw new Error(\n `Frame payload too large: ${length} bytes (max ${MAX_FRAME_PAYLOAD_SIZE})`,\n )\n }\n\n const frameSize = FRAME_HEADER_SIZE + length\n if (totalLength < frameSize) break // Wait for more data\n\n if (++frameCount > MAX_FRAMES) {\n throw new Error(\n `Too many frames in framed response (max ${MAX_FRAMES})`,\n )\n }\n\n // Extract and consume header bytes\n extractFlattened(FRAME_HEADER_SIZE)\n\n // Extract payload\n const payload = extractFlattened(length)\n\n // Process frame by type\n switch (type) {\n case FrameType.JSON: {\n try {\n jsonController.enqueue(textDecoder.decode(payload))\n } catch {\n // JSON stream may be cancelled/closed\n }\n break\n }\n\n case FrameType.CHUNK: {\n const ctrl = ensureController(streamId)\n if (ctrl) {\n ctrl.enqueue(payload)\n }\n break\n }\n\n case FrameType.END: {\n const ctrl = ensureController(streamId)\n cancelledStreamIds.add(streamId)\n if (ctrl) {\n try {\n ctrl.close()\n } catch {\n // Already closed\n }\n streamControllers.delete(streamId)\n }\n break\n }\n\n case FrameType.ERROR: {\n const ctrl = ensureController(streamId)\n cancelledStreamIds.add(streamId)\n if (ctrl) {\n const message = textDecoder.decode(payload)\n ctrl.error(new Error(message))\n streamControllers.delete(streamId)\n }\n break\n }\n }\n }\n }\n\n if (totalLength !== 0) {\n throw new Error('Incomplete frame at end of framed response')\n }\n\n // Close JSON stream when done\n try {\n jsonController.close()\n } catch {\n // JSON stream may be cancelled/closed\n }\n\n // Close any remaining streams (shouldn't happen in normal operation)\n streamControllers.forEach((ctrl) => {\n try {\n ctrl.close()\n } catch {\n // Already closed\n }\n })\n streamControllers.clear()\n } catch (error) {\n // Error reading - propagate to all streams\n try {\n jsonController.error(error)\n } catch {\n // Already errored/closed\n }\n streamControllers.forEach((ctrl) => {\n try {\n ctrl.error(error)\n } catch {\n // Already errored/closed\n }\n })\n streamControllers.clear()\n } finally {\n try {\n reader.releaseLock()\n } catch {\n // Ignore\n }\n inputReader = null\n }\n })()\n\n return { getOrCreateStream, jsonChunks }\n}\n"],"names":["type","streamId","length"],"mappings":"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"} |
| /** | ||
| * Client-side frame decoder for multiplexed responses. | ||
| * | ||
| * Decodes binary frame protocol and reconstructs: | ||
| * - JSON stream (NDJSON lines for seroval) | ||
| * - Raw streams (binary data as ReadableStream<Uint8Array>) | ||
| */ | ||
| import { FRAME_HEADER_SIZE, FrameType } from '../constants' | ||
| /** Cached TextDecoder for frame decoding */ | ||
| const textDecoder = new TextDecoder() | ||
| /** Shared empty buffer for empty buffer case - avoids allocation */ | ||
| const EMPTY_BUFFER = new Uint8Array(0) | ||
| /** Hardening limits to prevent memory/CPU DoS */ | ||
| const MAX_FRAME_PAYLOAD_SIZE = 16 * 1024 * 1024 // 16MiB | ||
| const MAX_BUFFERED_BYTES = 32 * 1024 * 1024 // 32MiB | ||
| const MAX_STREAMS = 1024 | ||
| const MAX_FRAMES = 100_000 // Limit total frames to prevent CPU DoS | ||
| /** | ||
| * Result of frame decoding. | ||
| */ | ||
| export interface FrameDecoderResult { | ||
| /** Gets or creates a raw stream by ID (for use by deserialize plugin) */ | ||
| getOrCreateStream: (id: number) => ReadableStream<Uint8Array> | ||
| /** Stream of JSON strings (NDJSON lines) */ | ||
| jsonChunks: ReadableStream<string> | ||
| } | ||
| /** | ||
| * Creates a frame decoder that processes a multiplexed response stream. | ||
| * | ||
| * @param input The raw response body stream | ||
| * @returns Decoded JSON stream and stream getter function | ||
| */ | ||
| export function createFrameDecoder( | ||
| input: ReadableStream<Uint8Array>, | ||
| ): FrameDecoderResult { | ||
| const streamControllers = new Map< | ||
| number, | ||
| ReadableStreamDefaultController<Uint8Array> | ||
| >() | ||
| const streams = new Map<number, ReadableStream<Uint8Array>>() | ||
| const cancelledStreamIds = new Set<number>() | ||
| let cancelled = false as boolean | ||
| let inputReader: ReadableStreamReader<Uint8Array> | null = null | ||
| let frameCount = 0 | ||
| let jsonController!: ReadableStreamDefaultController<string> | ||
| const jsonChunks = new ReadableStream<string>({ | ||
| start(controller) { | ||
| jsonController = controller | ||
| }, | ||
| cancel() { | ||
| cancelled = true | ||
| try { | ||
| inputReader?.cancel() | ||
| } catch { | ||
| // Ignore | ||
| } | ||
| streamControllers.forEach((ctrl) => { | ||
| try { | ||
| ctrl.error(new Error('Framed response cancelled')) | ||
| } catch { | ||
| // Ignore | ||
| } | ||
| }) | ||
| streamControllers.clear() | ||
| streams.clear() | ||
| cancelledStreamIds.clear() | ||
| }, | ||
| }) | ||
| /** | ||
| * Gets or creates a stream for a given stream ID. | ||
| * Called by deserialize plugin when it encounters a RawStream reference. | ||
| */ | ||
| function getOrCreateStream(id: number): ReadableStream<Uint8Array> { | ||
| const existing = streams.get(id) | ||
| if (existing) { | ||
| return existing | ||
| } | ||
| // If we already received an END/ERROR for this streamId, returning a fresh stream | ||
| // would hang consumers. Return an already-closed stream instead. | ||
| if (cancelledStreamIds.has(id)) { | ||
| return new ReadableStream<Uint8Array>({ | ||
| start(controller) { | ||
| controller.close() | ||
| }, | ||
| }) | ||
| } | ||
| if (streams.size >= MAX_STREAMS) { | ||
| throw new Error( | ||
| `Too many raw streams in framed response (max ${MAX_STREAMS})`, | ||
| ) | ||
| } | ||
| const stream = new ReadableStream<Uint8Array>({ | ||
| start(ctrl) { | ||
| streamControllers.set(id, ctrl) | ||
| }, | ||
| cancel() { | ||
| cancelledStreamIds.add(id) | ||
| streamControllers.delete(id) | ||
| streams.delete(id) | ||
| }, | ||
| }) | ||
| streams.set(id, stream) | ||
| return stream | ||
| } | ||
| /** | ||
| * Ensures stream exists and returns its controller for enqueuing data. | ||
| * Used for CHUNK frames where we need to ensure stream is created. | ||
| */ | ||
| function ensureController( | ||
| id: number, | ||
| ): ReadableStreamDefaultController<Uint8Array> | undefined { | ||
| getOrCreateStream(id) | ||
| return streamControllers.get(id) | ||
| } | ||
| // Process frames asynchronously | ||
| ;(async () => { | ||
| const reader = input.getReader() | ||
| inputReader = reader | ||
| const bufferList: Array<Uint8Array> = [] | ||
| let totalLength = 0 | ||
| /** | ||
| * Reads header bytes from buffer chunks without flattening. | ||
| * Returns header data or null if not enough bytes available. | ||
| */ | ||
| function readHeader(): { | ||
| type: number | ||
| streamId: number | ||
| length: number | ||
| } | null { | ||
| if (totalLength < FRAME_HEADER_SIZE) return null | ||
| const first = bufferList[0]! | ||
| // Fast path: header fits entirely in first chunk (common case) | ||
| if (first.length >= FRAME_HEADER_SIZE) { | ||
| const type = first[0]! | ||
| const streamId = | ||
| ((first[1]! << 24) | | ||
| (first[2]! << 16) | | ||
| (first[3]! << 8) | | ||
| first[4]!) >>> | ||
| 0 | ||
| const length = | ||
| ((first[5]! << 24) | | ||
| (first[6]! << 16) | | ||
| (first[7]! << 8) | | ||
| first[8]!) >>> | ||
| 0 | ||
| return { type, streamId, length } | ||
| } | ||
| // Slow path: header spans multiple chunks - flatten header bytes only | ||
| const headerBytes = new Uint8Array(FRAME_HEADER_SIZE) | ||
| let offset = 0 | ||
| let remaining = FRAME_HEADER_SIZE | ||
| for (let i = 0; i < bufferList.length && remaining > 0; i++) { | ||
| const chunk = bufferList[i]! | ||
| const toCopy = Math.min(chunk.length, remaining) | ||
| headerBytes.set(chunk.subarray(0, toCopy), offset) | ||
| offset += toCopy | ||
| remaining -= toCopy | ||
| } | ||
| const type = headerBytes[0]! | ||
| const streamId = | ||
| ((headerBytes[1]! << 24) | | ||
| (headerBytes[2]! << 16) | | ||
| (headerBytes[3]! << 8) | | ||
| headerBytes[4]!) >>> | ||
| 0 | ||
| const length = | ||
| ((headerBytes[5]! << 24) | | ||
| (headerBytes[6]! << 16) | | ||
| (headerBytes[7]! << 8) | | ||
| headerBytes[8]!) >>> | ||
| 0 | ||
| return { type, streamId, length } | ||
| } | ||
| /** | ||
| * Flattens buffer list into single Uint8Array and removes from list. | ||
| */ | ||
| function extractFlattened(count: number): Uint8Array { | ||
| if (count === 0) return EMPTY_BUFFER | ||
| const result = new Uint8Array(count) | ||
| let offset = 0 | ||
| let remaining = count | ||
| while (remaining > 0 && bufferList.length > 0) { | ||
| const chunk = bufferList[0] | ||
| if (!chunk) break | ||
| const toCopy = Math.min(chunk.length, remaining) | ||
| result.set(chunk.subarray(0, toCopy), offset) | ||
| offset += toCopy | ||
| remaining -= toCopy | ||
| if (toCopy === chunk.length) { | ||
| bufferList.shift() | ||
| } else { | ||
| bufferList[0] = chunk.subarray(toCopy) | ||
| } | ||
| } | ||
| totalLength -= count | ||
| return result | ||
| } | ||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
| while (true) { | ||
| const { done, value } = await reader.read() | ||
| if (cancelled) break | ||
| if (done) break | ||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
| if (!value) continue | ||
| // Append incoming chunk to buffer list | ||
| if (totalLength + value.length > MAX_BUFFERED_BYTES) { | ||
| throw new Error( | ||
| `Framed response buffer exceeded ${MAX_BUFFERED_BYTES} bytes`, | ||
| ) | ||
| } | ||
| bufferList.push(value) | ||
| totalLength += value.length | ||
| // Parse complete frames from buffer | ||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
| while (true) { | ||
| const header = readHeader() | ||
| if (!header) break // Not enough bytes for header | ||
| const { type, streamId, length } = header | ||
| if ( | ||
| type !== FrameType.JSON && | ||
| type !== FrameType.CHUNK && | ||
| type !== FrameType.END && | ||
| type !== FrameType.ERROR | ||
| ) { | ||
| throw new Error(`Unknown frame type: ${type}`) | ||
| } | ||
| // Enforce stream id conventions: JSON uses streamId 0, raw streams use non-zero ids | ||
| if (type === FrameType.JSON) { | ||
| if (streamId !== 0) { | ||
| throw new Error('Invalid JSON frame streamId (expected 0)') | ||
| } | ||
| } else { | ||
| if (streamId === 0) { | ||
| throw new Error('Invalid raw frame streamId (expected non-zero)') | ||
| } | ||
| } | ||
| if (length > MAX_FRAME_PAYLOAD_SIZE) { | ||
| throw new Error( | ||
| `Frame payload too large: ${length} bytes (max ${MAX_FRAME_PAYLOAD_SIZE})`, | ||
| ) | ||
| } | ||
| const frameSize = FRAME_HEADER_SIZE + length | ||
| if (totalLength < frameSize) break // Wait for more data | ||
| if (++frameCount > MAX_FRAMES) { | ||
| throw new Error( | ||
| `Too many frames in framed response (max ${MAX_FRAMES})`, | ||
| ) | ||
| } | ||
| // Extract and consume header bytes | ||
| extractFlattened(FRAME_HEADER_SIZE) | ||
| // Extract payload | ||
| const payload = extractFlattened(length) | ||
| // Process frame by type | ||
| switch (type) { | ||
| case FrameType.JSON: { | ||
| try { | ||
| jsonController.enqueue(textDecoder.decode(payload)) | ||
| } catch { | ||
| // JSON stream may be cancelled/closed | ||
| } | ||
| break | ||
| } | ||
| case FrameType.CHUNK: { | ||
| const ctrl = ensureController(streamId) | ||
| if (ctrl) { | ||
| ctrl.enqueue(payload) | ||
| } | ||
| break | ||
| } | ||
| case FrameType.END: { | ||
| const ctrl = ensureController(streamId) | ||
| cancelledStreamIds.add(streamId) | ||
| if (ctrl) { | ||
| try { | ||
| ctrl.close() | ||
| } catch { | ||
| // Already closed | ||
| } | ||
| streamControllers.delete(streamId) | ||
| } | ||
| break | ||
| } | ||
| case FrameType.ERROR: { | ||
| const ctrl = ensureController(streamId) | ||
| cancelledStreamIds.add(streamId) | ||
| if (ctrl) { | ||
| const message = textDecoder.decode(payload) | ||
| ctrl.error(new Error(message)) | ||
| streamControllers.delete(streamId) | ||
| } | ||
| break | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (totalLength !== 0) { | ||
| throw new Error('Incomplete frame at end of framed response') | ||
| } | ||
| // Close JSON stream when done | ||
| try { | ||
| jsonController.close() | ||
| } catch { | ||
| // JSON stream may be cancelled/closed | ||
| } | ||
| // Close any remaining streams (shouldn't happen in normal operation) | ||
| streamControllers.forEach((ctrl) => { | ||
| try { | ||
| ctrl.close() | ||
| } catch { | ||
| // Already closed | ||
| } | ||
| }) | ||
| streamControllers.clear() | ||
| } catch (error) { | ||
| // Error reading - propagate to all streams | ||
| try { | ||
| jsonController.error(error) | ||
| } catch { | ||
| // Already errored/closed | ||
| } | ||
| streamControllers.forEach((ctrl) => { | ||
| try { | ||
| ctrl.error(error) | ||
| } catch { | ||
| // Already errored/closed | ||
| } | ||
| }) | ||
| streamControllers.clear() | ||
| } finally { | ||
| try { | ||
| reader.releaseLock() | ||
| } catch { | ||
| // Ignore | ||
| } | ||
| inputReader = null | ||
| } | ||
| })() | ||
| return { getOrCreateStream, jsonChunks } | ||
| } |
@@ -1,6 +0,7 @@ | ||
| import { encode, parseRedirect, isNotFound } from "@tanstack/router-core"; | ||
| import { encode, createRawStreamDeserializePlugin, parseRedirect, isNotFound } from "@tanstack/router-core"; | ||
| import { fromCrossJSON, toJSONAsync } from "seroval"; | ||
| import invariant from "tiny-invariant"; | ||
| import { getDefaultSerovalPlugins } from "../getDefaultSerovalPlugins.js"; | ||
| import { TSS_FORMDATA_CONTEXT, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED } from "../constants.js"; | ||
| import { TSS_CONTENT_TYPE_FRAMED, TSS_FORMDATA_CONTEXT, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED, validateFramedProtocolVersion } from "../constants.js"; | ||
| import { createFrameDecoder } from "./frame-decoder.js"; | ||
| let serovalPlugins = null; | ||
@@ -26,3 +27,6 @@ const hop = Object.prototype.hasOwnProperty; | ||
| if (type === "payload") { | ||
| headers.set("accept", "application/x-ndjson, application/json"); | ||
| headers.set( | ||
| "accept", | ||
| `${TSS_CONTENT_TYPE_FRAMED}, application/x-ndjson, application/json` | ||
| ); | ||
| } | ||
@@ -120,4 +124,22 @@ if (first.method === "GET") { | ||
| let result; | ||
| if (contentType.includes("application/x-ndjson")) { | ||
| if (contentType.includes(TSS_CONTENT_TYPE_FRAMED)) { | ||
| validateFramedProtocolVersion(contentType); | ||
| if (!response.body) { | ||
| throw new Error("No response body for framed response"); | ||
| } | ||
| const { getOrCreateStream, jsonChunks } = createFrameDecoder( | ||
| response.body | ||
| ); | ||
| const rawStreamPlugin = createRawStreamDeserializePlugin(getOrCreateStream); | ||
| const plugins = [rawStreamPlugin, ...serovalPlugins || []]; | ||
| const refs = /* @__PURE__ */ new Map(); | ||
| result = await processFramedResponse({ | ||
| jsonStream: jsonChunks, | ||
| onMessage: (msg) => fromCrossJSON(msg, { refs, plugins }), | ||
| onError(msg, error) { | ||
| console.error(msg, error); | ||
| } | ||
| }); | ||
| } else if (contentType.includes("application/x-ndjson")) { | ||
| const refs = /* @__PURE__ */ new Map(); | ||
| result = await processServerFnResponse({ | ||
@@ -130,4 +152,3 @@ response, | ||
| }); | ||
| } | ||
| if (contentType.includes("application/json")) { | ||
| } else if (contentType.includes("application/json")) { | ||
| const jsonPayload = await response.json(); | ||
@@ -223,2 +244,32 @@ result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins }); | ||
| } | ||
| async function processFramedResponse({ | ||
| jsonStream, | ||
| onMessage, | ||
| onError | ||
| }) { | ||
| const reader = jsonStream.getReader(); | ||
| const { value: firstValue, done: firstDone } = await reader.read(); | ||
| if (firstDone || !firstValue) { | ||
| throw new Error("Stream ended before first object"); | ||
| } | ||
| const firstObject = JSON.parse(firstValue); | ||
| (async () => { | ||
| try { | ||
| while (true) { | ||
| const { value, done } = await reader.read(); | ||
| if (done) break; | ||
| if (value) { | ||
| try { | ||
| onMessage(JSON.parse(value)); | ||
| } catch (e) { | ||
| onError?.(`Invalid JSON: ${value}`, e); | ||
| } | ||
| } | ||
| } | ||
| } catch (err) { | ||
| onError?.("Stream processing error:", err); | ||
| } | ||
| })(); | ||
| return onMessage(firstObject); | ||
| } | ||
| export { | ||
@@ -225,0 +276,0 @@ serverFnFetcher |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"serverFnFetcher.js","sources":["../../../src/client-rpc/serverFnFetcher.ts"],"sourcesContent":["import { encode, isNotFound, parseRedirect } from '@tanstack/router-core'\nimport { fromCrossJSON, toJSONAsync } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { getDefaultSerovalPlugins } from '../getDefaultSerovalPlugins'\nimport {\n TSS_FORMDATA_CONTEXT,\n X_TSS_RAW_RESPONSE,\n X_TSS_SERIALIZED,\n} from '../constants'\nimport type { FunctionMiddlewareClientFnOptions } from '../createMiddleware'\nimport type { Plugin as SerovalPlugin } from 'seroval'\n\nlet serovalPlugins: Array<SerovalPlugin<any, any>> | null = null\n\n/**\n * Checks if an object has at least one own enumerable property.\n * More efficient than Object.keys(obj).length > 0 as it short-circuits on first property.\n */\nconst hop = Object.prototype.hasOwnProperty\nfunction hasOwnProperties(obj: object): boolean {\n for (const _ in obj) {\n if (hop.call(obj, _)) {\n return true\n }\n }\n return false\n}\n// caller =>\n// serverFnFetcher =>\n// client =>\n// server =>\n// fn =>\n// seroval =>\n// client middleware =>\n// serverFnFetcher =>\n// caller\n\nexport async function serverFnFetcher(\n url: string,\n args: Array<any>,\n handler: (url: string, requestInit: RequestInit) => Promise<Response>,\n) {\n if (!serovalPlugins) {\n serovalPlugins = getDefaultSerovalPlugins()\n }\n const _first = args[0]\n\n const first = _first as FunctionMiddlewareClientFnOptions<any, any, any> & {\n headers?: HeadersInit\n }\n const type = first.data instanceof FormData ? 'formData' : 'payload'\n\n // Arrange the headers\n const headers = first.headers ? new Headers(first.headers) : new Headers()\n headers.set('x-tsr-serverFn', 'true')\n\n if (type === 'payload') {\n headers.set('accept', 'application/x-ndjson, application/json')\n }\n\n // If the method is GET, we need to move the payload to the query string\n if (first.method === 'GET') {\n if (type === 'formData') {\n throw new Error('FormData is not supported with GET requests')\n }\n const serializedPayload = await serializePayload(first)\n if (serializedPayload !== undefined) {\n const encodedPayload = encode({\n payload: serializedPayload,\n })\n if (url.includes('?')) {\n url += `&${encodedPayload}`\n } else {\n url += `?${encodedPayload}`\n }\n }\n }\n\n let body = undefined\n if (first.method === 'POST') {\n const fetchBody = await getFetchBody(first)\n if (fetchBody?.contentType) {\n headers.set('content-type', fetchBody.contentType)\n }\n body = fetchBody?.body\n }\n\n return await getResponse(async () =>\n handler(url, {\n method: first.method,\n headers,\n signal: first.signal,\n body,\n }),\n )\n}\n\nasync function serializePayload(\n opts: FunctionMiddlewareClientFnOptions<any, any, any>,\n): Promise<string | undefined> {\n let payloadAvailable = false\n const payloadToSerialize: any = {}\n if (opts.data !== undefined) {\n payloadAvailable = true\n payloadToSerialize['data'] = opts.data\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (opts.context && hasOwnProperties(opts.context)) {\n payloadAvailable = true\n payloadToSerialize['context'] = opts.context\n }\n\n if (payloadAvailable) {\n return serialize(payloadToSerialize)\n }\n return undefined\n}\n\nasync function serialize(data: any) {\n return JSON.stringify(\n await Promise.resolve(toJSONAsync(data, { plugins: serovalPlugins! })),\n )\n}\n\nasync function getFetchBody(\n opts: FunctionMiddlewareClientFnOptions<any, any, any>,\n): Promise<{ body: FormData | string; contentType?: string } | undefined> {\n if (opts.data instanceof FormData) {\n let serializedContext = undefined\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (opts.context && hasOwnProperties(opts.context)) {\n serializedContext = await serialize(opts.context)\n }\n if (serializedContext !== undefined) {\n opts.data.set(TSS_FORMDATA_CONTEXT, serializedContext)\n }\n return { body: opts.data }\n }\n const serializedBody = await serializePayload(opts)\n if (serializedBody) {\n return { body: serializedBody, contentType: 'application/json' }\n }\n return undefined\n}\n\n/**\n * Retrieves a response from a given function and manages potential errors\n * and special response types including redirects and not found errors.\n *\n * @param fn - The function to execute for obtaining the response.\n * @returns The processed response from the function.\n * @throws If the response is invalid or an error occurs during processing.\n */\nasync function getResponse(fn: () => Promise<Response>) {\n let response: Response\n try {\n response = await fn() // client => server => fn => server => client\n } catch (error) {\n if (error instanceof Response) {\n response = error\n } else {\n console.log(error)\n throw error\n }\n }\n\n if (response.headers.get(X_TSS_RAW_RESPONSE) === 'true') {\n return response\n }\n\n const contentType = response.headers.get('content-type')\n invariant(contentType, 'expected content-type header to be set')\n const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED)\n\n // If the response is serialized by the start server, we need to process it\n // differently than a normal response.\n if (serializedByStart) {\n let result\n // If it's a stream from the start serializer, process it as such\n if (contentType.includes('application/x-ndjson')) {\n const refs = new Map()\n result = await processServerFnResponse({\n response,\n onMessage: (msg) =>\n fromCrossJSON(msg, { refs, plugins: serovalPlugins! }),\n onError(msg, error) {\n // TODO how could we notify consumer that an error occurred?\n console.error(msg, error)\n },\n })\n }\n // If it's a JSON response, it can be simpler\n if (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })\n }\n\n invariant(result, 'expected result to be resolved')\n if (result instanceof Error) {\n throw result\n }\n\n return result\n }\n\n // If it wasn't processed by the start serializer, check\n // if it's JSON\n if (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n const redirect = parseRedirect(jsonPayload)\n if (redirect) {\n throw redirect\n }\n if (isNotFound(jsonPayload)) {\n throw jsonPayload\n }\n return jsonPayload\n }\n\n // Otherwise, if it's not OK, throw the content\n if (!response.ok) {\n throw new Error(await response.text())\n }\n\n // Or return the response itself\n return response\n}\n\nasync function processServerFnResponse({\n response,\n onMessage,\n onError,\n}: {\n response: Response\n onMessage: (msg: any) => any\n onError?: (msg: string, error?: any) => void\n}) {\n if (!response.body) {\n throw new Error('No response body')\n }\n\n const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()\n\n let buffer = ''\n let firstRead = false\n let firstObject\n\n while (!firstRead) {\n const { value, done } = await reader.read()\n if (value) buffer += value\n\n if (buffer.length === 0 && done) {\n throw new Error('Stream ended before first object')\n }\n\n // common case: buffer ends with newline\n if (buffer.endsWith('\\n')) {\n const lines = buffer.split('\\n').filter(Boolean)\n const firstLine = lines[0]\n if (!firstLine) throw new Error('No JSON line in the first chunk')\n firstObject = JSON.parse(firstLine)\n firstRead = true\n buffer = lines.slice(1).join('\\n')\n } else {\n // fallback: wait for a newline to parse first object safely\n const newlineIndex = buffer.indexOf('\\n')\n if (newlineIndex >= 0) {\n const line = buffer.slice(0, newlineIndex).trim()\n buffer = buffer.slice(newlineIndex + 1)\n if (line.length > 0) {\n firstObject = JSON.parse(line)\n firstRead = true\n }\n }\n }\n }\n\n // process rest of the stream asynchronously\n ;(async () => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const { value, done } = await reader.read()\n if (value) buffer += value\n\n const lastNewline = buffer.lastIndexOf('\\n')\n if (lastNewline >= 0) {\n const chunk = buffer.slice(0, lastNewline)\n buffer = buffer.slice(lastNewline + 1)\n const lines = chunk.split('\\n').filter(Boolean)\n\n for (const line of lines) {\n try {\n onMessage(JSON.parse(line))\n } catch (e) {\n onError?.(`Invalid JSON line: ${line}`, e)\n }\n }\n }\n\n if (done) {\n break\n }\n }\n } catch (err) {\n onError?.('Stream processing error:', err)\n }\n })()\n\n return onMessage(firstObject)\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"serverFnFetcher.js","sources":["../../../src/client-rpc/serverFnFetcher.ts"],"sourcesContent":["import {\n createRawStreamDeserializePlugin,\n encode,\n isNotFound,\n parseRedirect,\n} from '@tanstack/router-core'\nimport { fromCrossJSON, toJSONAsync } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { getDefaultSerovalPlugins } from '../getDefaultSerovalPlugins'\nimport {\n TSS_CONTENT_TYPE_FRAMED,\n TSS_FORMDATA_CONTEXT,\n X_TSS_RAW_RESPONSE,\n X_TSS_SERIALIZED,\n validateFramedProtocolVersion,\n} from '../constants'\nimport { createFrameDecoder } from './frame-decoder'\nimport type { FunctionMiddlewareClientFnOptions } from '../createMiddleware'\nimport type { Plugin as SerovalPlugin } from 'seroval'\n\nlet serovalPlugins: Array<SerovalPlugin<any, any>> | null = null\n\n/**\n * Checks if an object has at least one own enumerable property.\n * More efficient than Object.keys(obj).length > 0 as it short-circuits on first property.\n */\nconst hop = Object.prototype.hasOwnProperty\nfunction hasOwnProperties(obj: object): boolean {\n for (const _ in obj) {\n if (hop.call(obj, _)) {\n return true\n }\n }\n return false\n}\n// caller =>\n// serverFnFetcher =>\n// client =>\n// server =>\n// fn =>\n// seroval =>\n// client middleware =>\n// serverFnFetcher =>\n// caller\n\nexport async function serverFnFetcher(\n url: string,\n args: Array<any>,\n handler: (url: string, requestInit: RequestInit) => Promise<Response>,\n) {\n if (!serovalPlugins) {\n serovalPlugins = getDefaultSerovalPlugins()\n }\n const _first = args[0]\n\n const first = _first as FunctionMiddlewareClientFnOptions<any, any, any> & {\n headers?: HeadersInit\n }\n const type = first.data instanceof FormData ? 'formData' : 'payload'\n\n // Arrange the headers\n const headers = first.headers ? new Headers(first.headers) : new Headers()\n headers.set('x-tsr-serverFn', 'true')\n\n if (type === 'payload') {\n headers.set(\n 'accept',\n `${TSS_CONTENT_TYPE_FRAMED}, application/x-ndjson, application/json`,\n )\n }\n\n // If the method is GET, we need to move the payload to the query string\n if (first.method === 'GET') {\n if (type === 'formData') {\n throw new Error('FormData is not supported with GET requests')\n }\n const serializedPayload = await serializePayload(first)\n if (serializedPayload !== undefined) {\n const encodedPayload = encode({\n payload: serializedPayload,\n })\n if (url.includes('?')) {\n url += `&${encodedPayload}`\n } else {\n url += `?${encodedPayload}`\n }\n }\n }\n\n let body = undefined\n if (first.method === 'POST') {\n const fetchBody = await getFetchBody(first)\n if (fetchBody?.contentType) {\n headers.set('content-type', fetchBody.contentType)\n }\n body = fetchBody?.body\n }\n\n return await getResponse(async () =>\n handler(url, {\n method: first.method,\n headers,\n signal: first.signal,\n body,\n }),\n )\n}\n\nasync function serializePayload(\n opts: FunctionMiddlewareClientFnOptions<any, any, any>,\n): Promise<string | undefined> {\n let payloadAvailable = false\n const payloadToSerialize: any = {}\n if (opts.data !== undefined) {\n payloadAvailable = true\n payloadToSerialize['data'] = opts.data\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (opts.context && hasOwnProperties(opts.context)) {\n payloadAvailable = true\n payloadToSerialize['context'] = opts.context\n }\n\n if (payloadAvailable) {\n return serialize(payloadToSerialize)\n }\n return undefined\n}\n\nasync function serialize(data: any) {\n return JSON.stringify(\n await Promise.resolve(toJSONAsync(data, { plugins: serovalPlugins! })),\n )\n}\n\nasync function getFetchBody(\n opts: FunctionMiddlewareClientFnOptions<any, any, any>,\n): Promise<{ body: FormData | string; contentType?: string } | undefined> {\n if (opts.data instanceof FormData) {\n let serializedContext = undefined\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (opts.context && hasOwnProperties(opts.context)) {\n serializedContext = await serialize(opts.context)\n }\n if (serializedContext !== undefined) {\n opts.data.set(TSS_FORMDATA_CONTEXT, serializedContext)\n }\n return { body: opts.data }\n }\n const serializedBody = await serializePayload(opts)\n if (serializedBody) {\n return { body: serializedBody, contentType: 'application/json' }\n }\n return undefined\n}\n\n/**\n * Retrieves a response from a given function and manages potential errors\n * and special response types including redirects and not found errors.\n *\n * @param fn - The function to execute for obtaining the response.\n * @returns The processed response from the function.\n * @throws If the response is invalid or an error occurs during processing.\n */\nasync function getResponse(fn: () => Promise<Response>) {\n let response: Response\n try {\n response = await fn() // client => server => fn => server => client\n } catch (error) {\n if (error instanceof Response) {\n response = error\n } else {\n console.log(error)\n throw error\n }\n }\n\n if (response.headers.get(X_TSS_RAW_RESPONSE) === 'true') {\n return response\n }\n\n const contentType = response.headers.get('content-type')\n invariant(contentType, 'expected content-type header to be set')\n const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED)\n\n // If the response is serialized by the start server, we need to process it\n // differently than a normal response.\n if (serializedByStart) {\n let result\n\n // If it's a framed response (contains RawStream), use frame decoder\n if (contentType.includes(TSS_CONTENT_TYPE_FRAMED)) {\n // Validate protocol version compatibility\n validateFramedProtocolVersion(contentType)\n\n if (!response.body) {\n throw new Error('No response body for framed response')\n }\n\n const { getOrCreateStream, jsonChunks } = createFrameDecoder(\n response.body,\n )\n\n // Create deserialize plugin that wires up the raw streams\n const rawStreamPlugin =\n createRawStreamDeserializePlugin(getOrCreateStream)\n const plugins = [rawStreamPlugin, ...(serovalPlugins || [])]\n\n const refs = new Map()\n result = await processFramedResponse({\n jsonStream: jsonChunks,\n onMessage: (msg: any) => fromCrossJSON(msg, { refs, plugins }),\n onError(msg, error) {\n console.error(msg, error)\n },\n })\n }\n // If it's a stream from the start serializer, process it as such\n else if (contentType.includes('application/x-ndjson')) {\n const refs = new Map()\n result = await processServerFnResponse({\n response,\n onMessage: (msg) =>\n fromCrossJSON(msg, { refs, plugins: serovalPlugins! }),\n onError(msg, error) {\n // TODO how could we notify consumer that an error occurred?\n console.error(msg, error)\n },\n })\n }\n // If it's a JSON response, it can be simpler\n else if (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })\n }\n\n invariant(result, 'expected result to be resolved')\n if (result instanceof Error) {\n throw result\n }\n\n return result\n }\n\n // If it wasn't processed by the start serializer, check\n // if it's JSON\n if (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n const redirect = parseRedirect(jsonPayload)\n if (redirect) {\n throw redirect\n }\n if (isNotFound(jsonPayload)) {\n throw jsonPayload\n }\n return jsonPayload\n }\n\n // Otherwise, if it's not OK, throw the content\n if (!response.ok) {\n throw new Error(await response.text())\n }\n\n // Or return the response itself\n return response\n}\n\nasync function processServerFnResponse({\n response,\n onMessage,\n onError,\n}: {\n response: Response\n onMessage: (msg: any) => any\n onError?: (msg: string, error?: any) => void\n}) {\n if (!response.body) {\n throw new Error('No response body')\n }\n\n const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()\n\n let buffer = ''\n let firstRead = false\n let firstObject\n\n while (!firstRead) {\n const { value, done } = await reader.read()\n if (value) buffer += value\n\n if (buffer.length === 0 && done) {\n throw new Error('Stream ended before first object')\n }\n\n // common case: buffer ends with newline\n if (buffer.endsWith('\\n')) {\n const lines = buffer.split('\\n').filter(Boolean)\n const firstLine = lines[0]\n if (!firstLine) throw new Error('No JSON line in the first chunk')\n firstObject = JSON.parse(firstLine)\n firstRead = true\n buffer = lines.slice(1).join('\\n')\n } else {\n // fallback: wait for a newline to parse first object safely\n const newlineIndex = buffer.indexOf('\\n')\n if (newlineIndex >= 0) {\n const line = buffer.slice(0, newlineIndex).trim()\n buffer = buffer.slice(newlineIndex + 1)\n if (line.length > 0) {\n firstObject = JSON.parse(line)\n firstRead = true\n }\n }\n }\n }\n\n // process rest of the stream asynchronously\n ;(async () => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const { value, done } = await reader.read()\n if (value) buffer += value\n\n const lastNewline = buffer.lastIndexOf('\\n')\n if (lastNewline >= 0) {\n const chunk = buffer.slice(0, lastNewline)\n buffer = buffer.slice(lastNewline + 1)\n const lines = chunk.split('\\n').filter(Boolean)\n\n for (const line of lines) {\n try {\n onMessage(JSON.parse(line))\n } catch (e) {\n onError?.(`Invalid JSON line: ${line}`, e)\n }\n }\n }\n\n if (done) {\n break\n }\n }\n } catch (err) {\n onError?.('Stream processing error:', err)\n }\n })()\n\n return onMessage(firstObject)\n}\n\n/**\n * Processes a framed response where each JSON chunk is a complete JSON string\n * (already decoded by frame decoder).\n */\nasync function processFramedResponse({\n jsonStream,\n onMessage,\n onError,\n}: {\n jsonStream: ReadableStream<string>\n onMessage: (msg: any) => any\n onError?: (msg: string, error?: any) => void\n}) {\n const reader = jsonStream.getReader()\n\n // Read first JSON frame - this is the main result\n const { value: firstValue, done: firstDone } = await reader.read()\n if (firstDone || !firstValue) {\n throw new Error('Stream ended before first object')\n }\n\n // Each frame is a complete JSON string\n const firstObject = JSON.parse(firstValue)\n\n // Process remaining frames asynchronously (for streaming refs like RawStream)\n ;(async () => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n if (value) {\n try {\n onMessage(JSON.parse(value))\n } catch (e) {\n onError?.(`Invalid JSON: ${value}`, e)\n }\n }\n }\n } catch (err) {\n onError?.('Stream processing error:', err)\n }\n })()\n\n return onMessage(firstObject)\n}\n"],"names":[],"mappings":"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"} |
@@ -7,2 +7,30 @@ export declare const TSS_FORMDATA_CONTEXT = "__TSS_CONTEXT"; | ||
| export declare const X_TSS_CONTEXT = "x-tss-context"; | ||
| /** Content-Type for multiplexed framed responses (RawStream support) */ | ||
| export declare const TSS_CONTENT_TYPE_FRAMED = "application/x-tss-framed"; | ||
| /** | ||
| * Frame types for binary multiplexing protocol. | ||
| */ | ||
| export declare const FrameType: { | ||
| /** Seroval JSON chunk (NDJSON line) */ | ||
| readonly JSON: 0; | ||
| /** Raw stream data chunk */ | ||
| readonly CHUNK: 1; | ||
| /** Raw stream end (EOF) */ | ||
| readonly END: 2; | ||
| /** Raw stream error */ | ||
| readonly ERROR: 3; | ||
| }; | ||
| export type FrameType = (typeof FrameType)[keyof typeof FrameType]; | ||
| /** Header size in bytes: type(1) + streamId(4) + length(4) */ | ||
| export declare const FRAME_HEADER_SIZE = 9; | ||
| /** Current protocol version for framed responses */ | ||
| export declare const TSS_FRAMED_PROTOCOL_VERSION = 1; | ||
| /** Full Content-Type header value with version parameter */ | ||
| export declare const TSS_CONTENT_TYPE_FRAMED_VERSIONED = "application/x-tss-framed; v=1"; | ||
| export declare function parseFramedProtocolVersion(contentType: string): number | undefined; | ||
| /** | ||
| * Validates that the server's protocol version is compatible with this client. | ||
| * Throws an error if versions are incompatible. | ||
| */ | ||
| export declare function validateFramedProtocolVersion(contentType: string): void; | ||
| export {}; |
@@ -9,4 +9,39 @@ const TSS_FORMDATA_CONTEXT = "__TSS_CONTEXT"; | ||
| const X_TSS_CONTEXT = "x-tss-context"; | ||
| const TSS_CONTENT_TYPE_FRAMED = "application/x-tss-framed"; | ||
| const FrameType = { | ||
| /** Seroval JSON chunk (NDJSON line) */ | ||
| JSON: 0, | ||
| /** Raw stream data chunk */ | ||
| CHUNK: 1, | ||
| /** Raw stream end (EOF) */ | ||
| END: 2, | ||
| /** Raw stream error */ | ||
| ERROR: 3 | ||
| }; | ||
| const FRAME_HEADER_SIZE = 9; | ||
| const TSS_FRAMED_PROTOCOL_VERSION = 1; | ||
| const TSS_CONTENT_TYPE_FRAMED_VERSIONED = `${TSS_CONTENT_TYPE_FRAMED}; v=${TSS_FRAMED_PROTOCOL_VERSION}`; | ||
| const FRAMED_VERSION_REGEX = /;\s*v=(\d+)/; | ||
| function parseFramedProtocolVersion(contentType) { | ||
| const match = contentType.match(FRAMED_VERSION_REGEX); | ||
| return match ? parseInt(match[1], 10) : void 0; | ||
| } | ||
| function validateFramedProtocolVersion(contentType) { | ||
| const serverVersion = parseFramedProtocolVersion(contentType); | ||
| if (serverVersion === void 0) { | ||
| return; | ||
| } | ||
| if (serverVersion !== TSS_FRAMED_PROTOCOL_VERSION) { | ||
| throw new Error( | ||
| `Incompatible framed protocol version: server=${serverVersion}, client=${TSS_FRAMED_PROTOCOL_VERSION}. Please ensure client and server are using compatible versions.` | ||
| ); | ||
| } | ||
| } | ||
| export { | ||
| FRAME_HEADER_SIZE, | ||
| FrameType, | ||
| TSS_CONTENT_TYPE_FRAMED, | ||
| TSS_CONTENT_TYPE_FRAMED_VERSIONED, | ||
| TSS_FORMDATA_CONTEXT, | ||
| TSS_FRAMED_PROTOCOL_VERSION, | ||
| TSS_SERVER_FUNCTION, | ||
@@ -16,4 +51,6 @@ TSS_SERVER_FUNCTION_FACTORY, | ||
| X_TSS_RAW_RESPONSE, | ||
| X_TSS_SERIALIZED | ||
| X_TSS_SERIALIZED, | ||
| parseFramedProtocolVersion, | ||
| validateFramedProtocolVersion | ||
| }; | ||
| //# sourceMappingURL=constants.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"constants.js","sources":["../../src/constants.ts"],"sourcesContent":["export const TSS_FORMDATA_CONTEXT = '__TSS_CONTEXT'\nexport const TSS_SERVER_FUNCTION = Symbol.for('TSS_SERVER_FUNCTION')\nexport const TSS_SERVER_FUNCTION_FACTORY = Symbol.for(\n 'TSS_SERVER_FUNCTION_FACTORY',\n)\n\nexport const X_TSS_SERIALIZED = 'x-tss-serialized'\nexport const X_TSS_RAW_RESPONSE = 'x-tss-raw'\nexport const X_TSS_CONTEXT = 'x-tss-context'\nexport {}\n"],"names":[],"mappings":"AAAO,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB,OAAO,IAAI,qBAAqB;AAC5D,MAAM,8BAA8B,OAAO;AAAA,EAChD;AACF;AAEO,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;"} | ||
| {"version":3,"file":"constants.js","sources":["../../src/constants.ts"],"sourcesContent":["export const TSS_FORMDATA_CONTEXT = '__TSS_CONTEXT'\nexport const TSS_SERVER_FUNCTION = Symbol.for('TSS_SERVER_FUNCTION')\nexport const TSS_SERVER_FUNCTION_FACTORY = Symbol.for(\n 'TSS_SERVER_FUNCTION_FACTORY',\n)\n\nexport const X_TSS_SERIALIZED = 'x-tss-serialized'\nexport const X_TSS_RAW_RESPONSE = 'x-tss-raw'\nexport const X_TSS_CONTEXT = 'x-tss-context'\n\n/** Content-Type for multiplexed framed responses (RawStream support) */\nexport const TSS_CONTENT_TYPE_FRAMED = 'application/x-tss-framed'\n\n/**\n * Frame types for binary multiplexing protocol.\n */\nexport const FrameType = {\n /** Seroval JSON chunk (NDJSON line) */\n JSON: 0,\n /** Raw stream data chunk */\n CHUNK: 1,\n /** Raw stream end (EOF) */\n END: 2,\n /** Raw stream error */\n ERROR: 3,\n} as const\n\nexport type FrameType = (typeof FrameType)[keyof typeof FrameType]\n\n/** Header size in bytes: type(1) + streamId(4) + length(4) */\nexport const FRAME_HEADER_SIZE = 9\n\n/** Current protocol version for framed responses */\nexport const TSS_FRAMED_PROTOCOL_VERSION = 1\n\n/** Full Content-Type header value with version parameter */\nexport const TSS_CONTENT_TYPE_FRAMED_VERSIONED = `${TSS_CONTENT_TYPE_FRAMED}; v=${TSS_FRAMED_PROTOCOL_VERSION}`\n\n/**\n * Parses the version parameter from a framed Content-Type header.\n * Returns undefined if no version parameter is present.\n */\nconst FRAMED_VERSION_REGEX = /;\\s*v=(\\d+)/\nexport function parseFramedProtocolVersion(\n contentType: string,\n): number | undefined {\n // Match \"v=<number>\" in the content-type parameters\n const match = contentType.match(FRAMED_VERSION_REGEX)\n return match ? parseInt(match[1]!, 10) : undefined\n}\n\n/**\n * Validates that the server's protocol version is compatible with this client.\n * Throws an error if versions are incompatible.\n */\nexport function validateFramedProtocolVersion(contentType: string): void {\n const serverVersion = parseFramedProtocolVersion(contentType)\n if (serverVersion === undefined) {\n // No version specified - assume compatible (backwards compat)\n return\n }\n if (serverVersion !== TSS_FRAMED_PROTOCOL_VERSION) {\n throw new Error(\n `Incompatible framed protocol version: server=${serverVersion}, client=${TSS_FRAMED_PROTOCOL_VERSION}. ` +\n `Please ensure client and server are using compatible versions.`,\n )\n }\n}\nexport {}\n"],"names":[],"mappings":"AAAO,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB,OAAO,IAAI,qBAAqB;AAC5D,MAAM,8BAA8B,OAAO;AAAA,EAChD;AACF;AAEO,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;AAGtB,MAAM,0BAA0B;AAKhC,MAAM,YAAY;AAAA;AAAA,EAEvB,MAAM;AAAA;AAAA,EAEN,OAAO;AAAA;AAAA,EAEP,KAAK;AAAA;AAAA,EAEL,OAAO;AACT;AAKO,MAAM,oBAAoB;AAG1B,MAAM,8BAA8B;AAGpC,MAAM,oCAAoC,GAAG,uBAAuB,OAAO,2BAA2B;AAM7G,MAAM,uBAAuB;AACtB,SAAS,2BACd,aACoB;AAEpB,QAAM,QAAQ,YAAY,MAAM,oBAAoB;AACpD,SAAO,QAAQ,SAAS,MAAM,CAAC,GAAI,EAAE,IAAI;AAC3C;AAMO,SAAS,8BAA8B,aAA2B;AACvE,QAAM,gBAAgB,2BAA2B,WAAW;AAC5D,MAAI,kBAAkB,QAAW;AAE/B;AAAA,EACF;AACA,MAAI,kBAAkB,6BAA6B;AACjD,UAAM,IAAI;AAAA,MACR,gDAAgD,aAAa,YAAY,2BAA2B;AAAA,IAAA;AAAA,EAGxG;AACF;"} |
@@ -1,1 +0,1 @@ | ||
| export declare function getDefaultSerovalPlugins(): (import('seroval').Plugin<any, import('seroval').SerovalNode> | import('seroval').Plugin<Error, any> | import('seroval').Plugin<ReadableStream<any>, any>)[]; | ||
| export declare function getDefaultSerovalPlugins(): (import('seroval').Plugin<any, any> | import('seroval').Plugin<Error, any> | import('seroval').Plugin<ReadableStream<any>, any>)[]; |
| export type { JsonResponse } from '@tanstack/router-core/ssr/client'; | ||
| export { hydrate, json, mergeHeaders } from '@tanstack/router-core/ssr/client'; | ||
| export { RawStream } from '@tanstack/router-core'; | ||
| export type { OnRawStreamCallback } from '@tanstack/router-core'; | ||
| export { createIsomorphicFn, createServerOnlyFn, createClientOnlyFn, type IsomorphicFn, type ServerOnlyFn, type ClientOnlyFn, type IsomorphicFnBase, } from '@tanstack/start-fn-stubs'; | ||
@@ -8,3 +10,4 @@ export { createServerFn } from './createServerFn.js'; | ||
| export { execValidator, flattenMiddlewares, executeMiddleware, } from './createServerFn.js'; | ||
| export { TSS_FORMDATA_CONTEXT, TSS_SERVER_FUNCTION, X_TSS_SERIALIZED, X_TSS_RAW_RESPONSE, X_TSS_CONTEXT, } from './constants.js'; | ||
| export { TSS_FORMDATA_CONTEXT, TSS_SERVER_FUNCTION, TSS_CONTENT_TYPE_FRAMED, TSS_CONTENT_TYPE_FRAMED_VERSIONED, TSS_FRAMED_PROTOCOL_VERSION, FrameType, FRAME_HEADER_SIZE, X_TSS_SERIALIZED, X_TSS_RAW_RESPONSE, X_TSS_CONTEXT, validateFramedProtocolVersion, } from './constants.js'; | ||
| export type { FrameType as FrameTypeValue } from './constants.js'; | ||
| export type * from './serverRoute.js'; | ||
@@ -11,0 +14,0 @@ export type * from './startEntry.js'; |
+10
-2
| import { hydrate, json, mergeHeaders } from "@tanstack/router-core/ssr/client"; | ||
| import { RawStream } from "@tanstack/router-core"; | ||
| import { createClientOnlyFn, createIsomorphicFn, createServerOnlyFn } from "@tanstack/start-fn-stubs"; | ||
| import { createServerFn, execValidator, executeMiddleware, flattenMiddlewares } from "./createServerFn.js"; | ||
| import { createMiddleware } from "./createMiddleware.js"; | ||
| import { TSS_FORMDATA_CONTEXT, TSS_SERVER_FUNCTION, X_TSS_CONTEXT, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED } from "./constants.js"; | ||
| import { FRAME_HEADER_SIZE, FrameType, TSS_CONTENT_TYPE_FRAMED, TSS_CONTENT_TYPE_FRAMED_VERSIONED, TSS_FORMDATA_CONTEXT, TSS_FRAMED_PROTOCOL_VERSION, TSS_SERVER_FUNCTION, X_TSS_CONTEXT, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED, validateFramedProtocolVersion } from "./constants.js"; | ||
| import { createStart } from "./createStart.js"; | ||
@@ -12,3 +13,9 @@ import { getRouterInstance } from "./getRouterInstance.js"; | ||
| export { | ||
| FRAME_HEADER_SIZE, | ||
| FrameType, | ||
| RawStream, | ||
| TSS_CONTENT_TYPE_FRAMED, | ||
| TSS_CONTENT_TYPE_FRAMED_VERSIONED, | ||
| TSS_FORMDATA_CONTEXT, | ||
| TSS_FRAMED_PROTOCOL_VERSION, | ||
| TSS_SERVER_FUNCTION, | ||
@@ -34,4 +41,5 @@ X_TSS_CONTEXT, | ||
| mergeHeaders, | ||
| safeObjectMerge | ||
| safeObjectMerge, | ||
| validateFramedProtocolVersion | ||
| }; | ||
| //# sourceMappingURL=index.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"} | ||
| {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"} |
+4
-4
| { | ||
| "name": "@tanstack/start-client-core", | ||
| "version": "1.143.12", | ||
| "version": "1.144.0", | ||
| "description": "Modern and scalable routing for React applications", | ||
@@ -69,5 +69,5 @@ "author": "Tanner Linsley", | ||
| "tiny-warning": "^1.0.3", | ||
| "@tanstack/start-fn-stubs": "1.143.8", | ||
| "@tanstack/router-core": "1.143.6", | ||
| "@tanstack/start-storage-context": "1.143.12" | ||
| "@tanstack/router-core": "1.144.0", | ||
| "@tanstack/start-storage-context": "1.144.0", | ||
| "@tanstack/start-fn-stubs": "1.143.8" | ||
| }, | ||
@@ -74,0 +74,0 @@ "scripts": { |
@@ -1,2 +0,7 @@ | ||
| import { encode, isNotFound, parseRedirect } from '@tanstack/router-core' | ||
| import { | ||
| createRawStreamDeserializePlugin, | ||
| encode, | ||
| isNotFound, | ||
| parseRedirect, | ||
| } from '@tanstack/router-core' | ||
| import { fromCrossJSON, toJSONAsync } from 'seroval' | ||
@@ -6,6 +11,9 @@ import invariant from 'tiny-invariant' | ||
| import { | ||
| TSS_CONTENT_TYPE_FRAMED, | ||
| TSS_FORMDATA_CONTEXT, | ||
| X_TSS_RAW_RESPONSE, | ||
| X_TSS_SERIALIZED, | ||
| validateFramedProtocolVersion, | ||
| } from '../constants' | ||
| import { createFrameDecoder } from './frame-decoder' | ||
| import type { FunctionMiddlewareClientFnOptions } from '../createMiddleware' | ||
@@ -59,3 +67,6 @@ import type { Plugin as SerovalPlugin } from 'seroval' | ||
| if (type === 'payload') { | ||
| headers.set('accept', 'application/x-ndjson, application/json') | ||
| headers.set( | ||
| 'accept', | ||
| `${TSS_CONTENT_TYPE_FRAMED}, application/x-ndjson, application/json`, | ||
| ) | ||
| } | ||
@@ -182,4 +193,32 @@ | ||
| let result | ||
| // If it's a framed response (contains RawStream), use frame decoder | ||
| if (contentType.includes(TSS_CONTENT_TYPE_FRAMED)) { | ||
| // Validate protocol version compatibility | ||
| validateFramedProtocolVersion(contentType) | ||
| if (!response.body) { | ||
| throw new Error('No response body for framed response') | ||
| } | ||
| const { getOrCreateStream, jsonChunks } = createFrameDecoder( | ||
| response.body, | ||
| ) | ||
| // Create deserialize plugin that wires up the raw streams | ||
| const rawStreamPlugin = | ||
| createRawStreamDeserializePlugin(getOrCreateStream) | ||
| const plugins = [rawStreamPlugin, ...(serovalPlugins || [])] | ||
| const refs = new Map() | ||
| result = await processFramedResponse({ | ||
| jsonStream: jsonChunks, | ||
| onMessage: (msg: any) => fromCrossJSON(msg, { refs, plugins }), | ||
| onError(msg, error) { | ||
| console.error(msg, error) | ||
| }, | ||
| }) | ||
| } | ||
| // If it's a stream from the start serializer, process it as such | ||
| if (contentType.includes('application/x-ndjson')) { | ||
| else if (contentType.includes('application/x-ndjson')) { | ||
| const refs = new Map() | ||
@@ -197,3 +236,3 @@ result = await processServerFnResponse({ | ||
| // If it's a JSON response, it can be simpler | ||
| if (contentType.includes('application/json')) { | ||
| else if (contentType.includes('application/json')) { | ||
| const jsonPayload = await response.json() | ||
@@ -317,1 +356,48 @@ result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! }) | ||
| } | ||
| /** | ||
| * Processes a framed response where each JSON chunk is a complete JSON string | ||
| * (already decoded by frame decoder). | ||
| */ | ||
| async function processFramedResponse({ | ||
| jsonStream, | ||
| onMessage, | ||
| onError, | ||
| }: { | ||
| jsonStream: ReadableStream<string> | ||
| onMessage: (msg: any) => any | ||
| onError?: (msg: string, error?: any) => void | ||
| }) { | ||
| const reader = jsonStream.getReader() | ||
| // Read first JSON frame - this is the main result | ||
| const { value: firstValue, done: firstDone } = await reader.read() | ||
| if (firstDone || !firstValue) { | ||
| throw new Error('Stream ended before first object') | ||
| } | ||
| // Each frame is a complete JSON string | ||
| const firstObject = JSON.parse(firstValue) | ||
| // Process remaining frames asynchronously (for streaming refs like RawStream) | ||
| ;(async () => { | ||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
| while (true) { | ||
| const { value, done } = await reader.read() | ||
| if (done) break | ||
| if (value) { | ||
| try { | ||
| onMessage(JSON.parse(value)) | ||
| } catch (e) { | ||
| onError?.(`Invalid JSON: ${value}`, e) | ||
| } | ||
| } | ||
| } | ||
| } catch (err) { | ||
| onError?.('Stream processing error:', err) | ||
| } | ||
| })() | ||
| return onMessage(firstObject) | ||
| } |
+59
-0
@@ -10,2 +10,61 @@ export const TSS_FORMDATA_CONTEXT = '__TSS_CONTEXT' | ||
| export const X_TSS_CONTEXT = 'x-tss-context' | ||
| /** Content-Type for multiplexed framed responses (RawStream support) */ | ||
| export const TSS_CONTENT_TYPE_FRAMED = 'application/x-tss-framed' | ||
| /** | ||
| * Frame types for binary multiplexing protocol. | ||
| */ | ||
| export const FrameType = { | ||
| /** Seroval JSON chunk (NDJSON line) */ | ||
| JSON: 0, | ||
| /** Raw stream data chunk */ | ||
| CHUNK: 1, | ||
| /** Raw stream end (EOF) */ | ||
| END: 2, | ||
| /** Raw stream error */ | ||
| ERROR: 3, | ||
| } as const | ||
| export type FrameType = (typeof FrameType)[keyof typeof FrameType] | ||
| /** Header size in bytes: type(1) + streamId(4) + length(4) */ | ||
| export const FRAME_HEADER_SIZE = 9 | ||
| /** Current protocol version for framed responses */ | ||
| export const TSS_FRAMED_PROTOCOL_VERSION = 1 | ||
| /** Full Content-Type header value with version parameter */ | ||
| export const TSS_CONTENT_TYPE_FRAMED_VERSIONED = `${TSS_CONTENT_TYPE_FRAMED}; v=${TSS_FRAMED_PROTOCOL_VERSION}` | ||
| /** | ||
| * Parses the version parameter from a framed Content-Type header. | ||
| * Returns undefined if no version parameter is present. | ||
| */ | ||
| const FRAMED_VERSION_REGEX = /;\s*v=(\d+)/ | ||
| export function parseFramedProtocolVersion( | ||
| contentType: string, | ||
| ): number | undefined { | ||
| // Match "v=<number>" in the content-type parameters | ||
| const match = contentType.match(FRAMED_VERSION_REGEX) | ||
| return match ? parseInt(match[1]!, 10) : undefined | ||
| } | ||
| /** | ||
| * Validates that the server's protocol version is compatible with this client. | ||
| * Throws an error if versions are incompatible. | ||
| */ | ||
| export function validateFramedProtocolVersion(contentType: string): void { | ||
| const serverVersion = parseFramedProtocolVersion(contentType) | ||
| if (serverVersion === undefined) { | ||
| // No version specified - assume compatible (backwards compat) | ||
| return | ||
| } | ||
| if (serverVersion !== TSS_FRAMED_PROTOCOL_VERSION) { | ||
| throw new Error( | ||
| `Incompatible framed protocol version: server=${serverVersion}, client=${TSS_FRAMED_PROTOCOL_VERSION}. ` + | ||
| `Please ensure client and server are using compatible versions.`, | ||
| ) | ||
| } | ||
| } | ||
| export {} |
+10
-0
@@ -5,2 +5,5 @@ export type { JsonResponse } from '@tanstack/router-core/ssr/client' | ||
| export { RawStream } from '@tanstack/router-core' | ||
| export type { OnRawStreamCallback } from '@tanstack/router-core' | ||
| export { | ||
@@ -84,6 +87,13 @@ createIsomorphicFn, | ||
| TSS_SERVER_FUNCTION, | ||
| TSS_CONTENT_TYPE_FRAMED, | ||
| TSS_CONTENT_TYPE_FRAMED_VERSIONED, | ||
| TSS_FRAMED_PROTOCOL_VERSION, | ||
| FrameType, | ||
| FRAME_HEADER_SIZE, | ||
| X_TSS_SERIALIZED, | ||
| X_TSS_RAW_RESPONSE, | ||
| X_TSS_CONTEXT, | ||
| validateFramedProtocolVersion, | ||
| } from './constants' | ||
| export type { FrameType as FrameTypeValue } from './constants' | ||
@@ -90,0 +100,0 @@ export type * from './serverRoute' |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
317953
19.27%90
4.65%6028
16.91%+ Added
+ Added
- Removed
- Removed