You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@tanstack/start-client-core

Package Overview
Dependencies
Maintainers
4
Versions
350
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tanstack/start-client-core - npm Package Compare versions

Comparing version
1.143.12
to
1.144.0
+23
dist/esm/client-rpc/frame-decoder.d.ts
/**
* 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 }
}
+57
-6

@@ -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

@@ -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';

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":";;;;;;;;;;;"}
{
"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)
}

@@ -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 {}

@@ -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'