@tanstack/start-client-core 1.143.9 → 1.144.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Client-side frame decoder for multiplexed responses.
3
+ *
4
+ * Decodes binary frame protocol and reconstructs:
5
+ * - JSON stream (NDJSON lines for seroval)
6
+ * - Raw streams (binary data as ReadableStream<Uint8Array>)
7
+ */
8
+ /**
9
+ * Result of frame decoding.
10
+ */
11
+ export interface FrameDecoderResult {
12
+ /** Gets or creates a raw stream by ID (for use by deserialize plugin) */
13
+ getOrCreateStream: (id: number) => ReadableStream<Uint8Array>;
14
+ /** Stream of JSON strings (NDJSON lines) */
15
+ jsonChunks: ReadableStream<string>;
16
+ }
17
+ /**
18
+ * Creates a frame decoder that processes a multiplexed response stream.
19
+ *
20
+ * @param input The raw response body stream
21
+ * @returns Decoded JSON stream and stream getter function
22
+ */
23
+ export declare function createFrameDecoder(input: ReadableStream<Uint8Array>): FrameDecoderResult;
@@ -0,0 +1,243 @@
1
+ import { FrameType, FRAME_HEADER_SIZE } from "../constants.js";
2
+ const textDecoder = new TextDecoder();
3
+ const EMPTY_BUFFER = new Uint8Array(0);
4
+ const MAX_FRAME_PAYLOAD_SIZE = 16 * 1024 * 1024;
5
+ const MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
6
+ const MAX_STREAMS = 1024;
7
+ const MAX_FRAMES = 1e5;
8
+ function createFrameDecoder(input) {
9
+ const streamControllers = /* @__PURE__ */ new Map();
10
+ const streams = /* @__PURE__ */ new Map();
11
+ const cancelledStreamIds = /* @__PURE__ */ new Set();
12
+ let cancelled = false;
13
+ let inputReader = null;
14
+ let frameCount = 0;
15
+ let jsonController;
16
+ const jsonChunks = new ReadableStream({
17
+ start(controller) {
18
+ jsonController = controller;
19
+ },
20
+ cancel() {
21
+ cancelled = true;
22
+ try {
23
+ inputReader?.cancel();
24
+ } catch {
25
+ }
26
+ streamControllers.forEach((ctrl) => {
27
+ try {
28
+ ctrl.error(new Error("Framed response cancelled"));
29
+ } catch {
30
+ }
31
+ });
32
+ streamControllers.clear();
33
+ streams.clear();
34
+ cancelledStreamIds.clear();
35
+ }
36
+ });
37
+ function getOrCreateStream(id) {
38
+ const existing = streams.get(id);
39
+ if (existing) {
40
+ return existing;
41
+ }
42
+ if (cancelledStreamIds.has(id)) {
43
+ return new ReadableStream({
44
+ start(controller) {
45
+ controller.close();
46
+ }
47
+ });
48
+ }
49
+ if (streams.size >= MAX_STREAMS) {
50
+ throw new Error(
51
+ `Too many raw streams in framed response (max ${MAX_STREAMS})`
52
+ );
53
+ }
54
+ const stream = new ReadableStream({
55
+ start(ctrl) {
56
+ streamControllers.set(id, ctrl);
57
+ },
58
+ cancel() {
59
+ cancelledStreamIds.add(id);
60
+ streamControllers.delete(id);
61
+ streams.delete(id);
62
+ }
63
+ });
64
+ streams.set(id, stream);
65
+ return stream;
66
+ }
67
+ function ensureController(id) {
68
+ getOrCreateStream(id);
69
+ return streamControllers.get(id);
70
+ }
71
+ (async () => {
72
+ const reader = input.getReader();
73
+ inputReader = reader;
74
+ const bufferList = [];
75
+ let totalLength = 0;
76
+ function readHeader() {
77
+ if (totalLength < FRAME_HEADER_SIZE) return null;
78
+ const first = bufferList[0];
79
+ if (first.length >= FRAME_HEADER_SIZE) {
80
+ const type2 = first[0];
81
+ const streamId2 = (first[1] << 24 | first[2] << 16 | first[3] << 8 | first[4]) >>> 0;
82
+ const length2 = (first[5] << 24 | first[6] << 16 | first[7] << 8 | first[8]) >>> 0;
83
+ return { type: type2, streamId: streamId2, length: length2 };
84
+ }
85
+ const headerBytes = new Uint8Array(FRAME_HEADER_SIZE);
86
+ let offset = 0;
87
+ let remaining = FRAME_HEADER_SIZE;
88
+ for (let i = 0; i < bufferList.length && remaining > 0; i++) {
89
+ const chunk = bufferList[i];
90
+ const toCopy = Math.min(chunk.length, remaining);
91
+ headerBytes.set(chunk.subarray(0, toCopy), offset);
92
+ offset += toCopy;
93
+ remaining -= toCopy;
94
+ }
95
+ const type = headerBytes[0];
96
+ const streamId = (headerBytes[1] << 24 | headerBytes[2] << 16 | headerBytes[3] << 8 | headerBytes[4]) >>> 0;
97
+ const length = (headerBytes[5] << 24 | headerBytes[6] << 16 | headerBytes[7] << 8 | headerBytes[8]) >>> 0;
98
+ return { type, streamId, length };
99
+ }
100
+ function extractFlattened(count) {
101
+ if (count === 0) return EMPTY_BUFFER;
102
+ const result = new Uint8Array(count);
103
+ let offset = 0;
104
+ let remaining = count;
105
+ while (remaining > 0 && bufferList.length > 0) {
106
+ const chunk = bufferList[0];
107
+ if (!chunk) break;
108
+ const toCopy = Math.min(chunk.length, remaining);
109
+ result.set(chunk.subarray(0, toCopy), offset);
110
+ offset += toCopy;
111
+ remaining -= toCopy;
112
+ if (toCopy === chunk.length) {
113
+ bufferList.shift();
114
+ } else {
115
+ bufferList[0] = chunk.subarray(toCopy);
116
+ }
117
+ }
118
+ totalLength -= count;
119
+ return result;
120
+ }
121
+ try {
122
+ while (true) {
123
+ const { done, value } = await reader.read();
124
+ if (cancelled) break;
125
+ if (done) break;
126
+ if (!value) continue;
127
+ if (totalLength + value.length > MAX_BUFFERED_BYTES) {
128
+ throw new Error(
129
+ `Framed response buffer exceeded ${MAX_BUFFERED_BYTES} bytes`
130
+ );
131
+ }
132
+ bufferList.push(value);
133
+ totalLength += value.length;
134
+ while (true) {
135
+ const header = readHeader();
136
+ if (!header) break;
137
+ const { type, streamId, length } = header;
138
+ if (type !== FrameType.JSON && type !== FrameType.CHUNK && type !== FrameType.END && type !== FrameType.ERROR) {
139
+ throw new Error(`Unknown frame type: ${type}`);
140
+ }
141
+ if (type === FrameType.JSON) {
142
+ if (streamId !== 0) {
143
+ throw new Error("Invalid JSON frame streamId (expected 0)");
144
+ }
145
+ } else {
146
+ if (streamId === 0) {
147
+ throw new Error("Invalid raw frame streamId (expected non-zero)");
148
+ }
149
+ }
150
+ if (length > MAX_FRAME_PAYLOAD_SIZE) {
151
+ throw new Error(
152
+ `Frame payload too large: ${length} bytes (max ${MAX_FRAME_PAYLOAD_SIZE})`
153
+ );
154
+ }
155
+ const frameSize = FRAME_HEADER_SIZE + length;
156
+ if (totalLength < frameSize) break;
157
+ if (++frameCount > MAX_FRAMES) {
158
+ throw new Error(
159
+ `Too many frames in framed response (max ${MAX_FRAMES})`
160
+ );
161
+ }
162
+ extractFlattened(FRAME_HEADER_SIZE);
163
+ const payload = extractFlattened(length);
164
+ switch (type) {
165
+ case FrameType.JSON: {
166
+ try {
167
+ jsonController.enqueue(textDecoder.decode(payload));
168
+ } catch {
169
+ }
170
+ break;
171
+ }
172
+ case FrameType.CHUNK: {
173
+ const ctrl = ensureController(streamId);
174
+ if (ctrl) {
175
+ ctrl.enqueue(payload);
176
+ }
177
+ break;
178
+ }
179
+ case FrameType.END: {
180
+ const ctrl = ensureController(streamId);
181
+ cancelledStreamIds.add(streamId);
182
+ if (ctrl) {
183
+ try {
184
+ ctrl.close();
185
+ } catch {
186
+ }
187
+ streamControllers.delete(streamId);
188
+ }
189
+ break;
190
+ }
191
+ case FrameType.ERROR: {
192
+ const ctrl = ensureController(streamId);
193
+ cancelledStreamIds.add(streamId);
194
+ if (ctrl) {
195
+ const message = textDecoder.decode(payload);
196
+ ctrl.error(new Error(message));
197
+ streamControllers.delete(streamId);
198
+ }
199
+ break;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ if (totalLength !== 0) {
205
+ throw new Error("Incomplete frame at end of framed response");
206
+ }
207
+ try {
208
+ jsonController.close();
209
+ } catch {
210
+ }
211
+ streamControllers.forEach((ctrl) => {
212
+ try {
213
+ ctrl.close();
214
+ } catch {
215
+ }
216
+ });
217
+ streamControllers.clear();
218
+ } catch (error) {
219
+ try {
220
+ jsonController.error(error);
221
+ } catch {
222
+ }
223
+ streamControllers.forEach((ctrl) => {
224
+ try {
225
+ ctrl.error(error);
226
+ } catch {
227
+ }
228
+ });
229
+ streamControllers.clear();
230
+ } finally {
231
+ try {
232
+ reader.releaseLock();
233
+ } catch {
234
+ }
235
+ inputReader = null;
236
+ }
237
+ })();
238
+ return { getOrCreateStream, jsonChunks };
239
+ }
240
+ export {
241
+ createFrameDecoder
242
+ };
243
+ //# sourceMappingURL=frame-decoder.js.map
@@ -0,0 +1 @@
1
+ {"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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"}
@@ -1,8 +1,9 @@
1
- import { encode, parseRedirect, isNotFound } from "@tanstack/router-core";
1
+ import { encode, createRawStreamDeserializePlugin, parseRedirect, isNotFound } from "@tanstack/router-core";
2
2
  import { fromCrossJSON, toJSONAsync } from "seroval";
3
3
  import invariant from "tiny-invariant";
4
4
  import { getDefaultSerovalPlugins } from "../getDefaultSerovalPlugins.js";
5
- import { TSS_FORMDATA_CONTEXT, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED } from "../constants.js";
5
+ import { TSS_CONTENT_TYPE_FRAMED, TSS_FORMDATA_CONTEXT, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED, validateFramedProtocolVersion } from "../constants.js";
6
+ import { createFrameDecoder } from "./frame-decoder.js";
6
7
  let serovalPlugins = null;
7
8
  const hop = Object.prototype.hasOwnProperty;
8
9
  function hasOwnProperties(obj) {
@@ -21,9 +22,12 @@ async function serverFnFetcher(url, args, handler) {
21
22
  const first = _first;
22
23
  const type = first.data instanceof FormData ? "formData" : "payload";
23
24
  const headers = first.headers ? new Headers(first.headers) : new Headers();
24
- headers.set("x-tsr-redirect", "manual");
25
+ headers.set("x-tsr-serverFn", "true");
25
26
  if (type === "payload") {
26
- headers.set("accept", "application/x-ndjson, application/json");
27
+ headers.set(
28
+ "accept",
29
+ `${TSS_CONTENT_TYPE_FRAMED}, application/x-ndjson, application/json`
30
+ );
27
31
  }
28
32
  if (first.method === "GET") {
29
33
  if (type === "formData") {
@@ -114,17 +118,27 @@ async function getResponse(fn) {
114
118
  const contentType = response.headers.get("content-type");
115
119
  invariant(contentType, "expected content-type header to be set");
116
120
  const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED);
117
- if (!response.ok) {
118
- if (serializedByStart && contentType.includes("application/json")) {
119
- const jsonPayload = await response.json();
120
- const result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins });
121
- throw result;
122
- }
123
- throw new Error(await response.text());
124
- }
125
121
  if (serializedByStart) {
126
122
  let result;
127
- if (contentType.includes("application/x-ndjson")) {
123
+ if (contentType.includes(TSS_CONTENT_TYPE_FRAMED)) {
124
+ validateFramedProtocolVersion(contentType);
125
+ if (!response.body) {
126
+ throw new Error("No response body for framed response");
127
+ }
128
+ const { getOrCreateStream, jsonChunks } = createFrameDecoder(
129
+ response.body
130
+ );
131
+ const rawStreamPlugin = createRawStreamDeserializePlugin(getOrCreateStream);
132
+ const plugins = [rawStreamPlugin, ...serovalPlugins || []];
133
+ const refs = /* @__PURE__ */ new Map();
134
+ result = await processFramedResponse({
135
+ jsonStream: jsonChunks,
136
+ onMessage: (msg) => fromCrossJSON(msg, { refs, plugins }),
137
+ onError(msg, error) {
138
+ console.error(msg, error);
139
+ }
140
+ });
141
+ } else if (contentType.includes("application/x-ndjson")) {
128
142
  const refs = /* @__PURE__ */ new Map();
129
143
  result = await processServerFnResponse({
130
144
  response,
@@ -133,8 +147,7 @@ async function getResponse(fn) {
133
147
  console.error(msg, error);
134
148
  }
135
149
  });
136
- }
137
- if (contentType.includes("application/json")) {
150
+ } else if (contentType.includes("application/json")) {
138
151
  const jsonPayload = await response.json();
139
152
  result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins });
140
153
  }
@@ -155,6 +168,9 @@ async function getResponse(fn) {
155
168
  }
156
169
  return jsonPayload;
157
170
  }
171
+ if (!response.ok) {
172
+ throw new Error(await response.text());
173
+ }
158
174
  return response;
159
175
  }
160
176
  async function processServerFnResponse({
@@ -222,6 +238,36 @@ async function processServerFnResponse({
222
238
  })();
223
239
  return onMessage(firstObject);
224
240
  }
241
+ async function processFramedResponse({
242
+ jsonStream,
243
+ onMessage,
244
+ onError
245
+ }) {
246
+ const reader = jsonStream.getReader();
247
+ const { value: firstValue, done: firstDone } = await reader.read();
248
+ if (firstDone || !firstValue) {
249
+ throw new Error("Stream ended before first object");
250
+ }
251
+ const firstObject = JSON.parse(firstValue);
252
+ (async () => {
253
+ try {
254
+ while (true) {
255
+ const { value, done } = await reader.read();
256
+ if (done) break;
257
+ if (value) {
258
+ try {
259
+ onMessage(JSON.parse(value));
260
+ } catch (e) {
261
+ onError?.(`Invalid JSON: ${value}`, e);
262
+ }
263
+ }
264
+ }
265
+ } catch (err) {
266
+ onError?.("Stream processing error:", err);
267
+ }
268
+ })();
269
+ return onMessage(firstObject);
270
+ }
225
271
  export {
226
272
  serverFnFetcher
227
273
  };
@@ -1 +1 @@
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\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-redirect', 'manual')\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()\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 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 // If the response is not ok, throw an error\n if (!response.ok) {\n if (serializedByStart && contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n const result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })\n throw result\n }\n\n throw new Error(await response.text())\n }\n\n if (serializedByStart) {\n let result\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 (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })\n }\n invariant(result, 'expected result to be resolved')\n if (result instanceof Error) {\n throw result\n }\n return result\n }\n\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 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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"}
1
+ {"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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"}
@@ -3,4 +3,33 @@ export declare const TSS_SERVER_FUNCTION: unique symbol;
3
3
  export declare const TSS_SERVER_FUNCTION_FACTORY: unique symbol;
4
4
  export declare const X_TSS_SERIALIZED = "x-tss-serialized";
5
5
  export declare const X_TSS_RAW_RESPONSE = "x-tss-raw";
6
+ export declare const X_TSS_CONTEXT = "x-tss-context";
7
+ /** Content-Type for multiplexed framed responses (RawStream support) */
8
+ export declare const TSS_CONTENT_TYPE_FRAMED = "application/x-tss-framed";
9
+ /**
10
+ * Frame types for binary multiplexing protocol.
11
+ */
12
+ export declare const FrameType: {
13
+ /** Seroval JSON chunk (NDJSON line) */
14
+ readonly JSON: 0;
15
+ /** Raw stream data chunk */
16
+ readonly CHUNK: 1;
17
+ /** Raw stream end (EOF) */
18
+ readonly END: 2;
19
+ /** Raw stream error */
20
+ readonly ERROR: 3;
21
+ };
22
+ export type FrameType = (typeof FrameType)[keyof typeof FrameType];
23
+ /** Header size in bytes: type(1) + streamId(4) + length(4) */
24
+ export declare const FRAME_HEADER_SIZE = 9;
25
+ /** Current protocol version for framed responses */
26
+ export declare const TSS_FRAMED_PROTOCOL_VERSION = 1;
27
+ /** Full Content-Type header value with version parameter */
28
+ export declare const TSS_CONTENT_TYPE_FRAMED_VERSIONED = "application/x-tss-framed; v=1";
29
+ export declare function parseFramedProtocolVersion(contentType: string): number | undefined;
30
+ /**
31
+ * Validates that the server's protocol version is compatible with this client.
32
+ * Throws an error if versions are incompatible.
33
+ */
34
+ export declare function validateFramedProtocolVersion(contentType: string): void;
6
35
  export {};
@@ -5,11 +5,50 @@ const TSS_SERVER_FUNCTION_FACTORY = Symbol.for(
5
5
  );
6
6
  const X_TSS_SERIALIZED = "x-tss-serialized";
7
7
  const X_TSS_RAW_RESPONSE = "x-tss-raw";
8
+ const X_TSS_CONTEXT = "x-tss-context";
9
+ const TSS_CONTENT_TYPE_FRAMED = "application/x-tss-framed";
10
+ const FrameType = {
11
+ /** Seroval JSON chunk (NDJSON line) */
12
+ JSON: 0,
13
+ /** Raw stream data chunk */
14
+ CHUNK: 1,
15
+ /** Raw stream end (EOF) */
16
+ END: 2,
17
+ /** Raw stream error */
18
+ ERROR: 3
19
+ };
20
+ const FRAME_HEADER_SIZE = 9;
21
+ const TSS_FRAMED_PROTOCOL_VERSION = 1;
22
+ const TSS_CONTENT_TYPE_FRAMED_VERSIONED = `${TSS_CONTENT_TYPE_FRAMED}; v=${TSS_FRAMED_PROTOCOL_VERSION}`;
23
+ const FRAMED_VERSION_REGEX = /;\s*v=(\d+)/;
24
+ function parseFramedProtocolVersion(contentType) {
25
+ const match = contentType.match(FRAMED_VERSION_REGEX);
26
+ return match ? parseInt(match[1], 10) : void 0;
27
+ }
28
+ function validateFramedProtocolVersion(contentType) {
29
+ const serverVersion = parseFramedProtocolVersion(contentType);
30
+ if (serverVersion === void 0) {
31
+ return;
32
+ }
33
+ if (serverVersion !== TSS_FRAMED_PROTOCOL_VERSION) {
34
+ throw new Error(
35
+ `Incompatible framed protocol version: server=${serverVersion}, client=${TSS_FRAMED_PROTOCOL_VERSION}. Please ensure client and server are using compatible versions.`
36
+ );
37
+ }
38
+ }
8
39
  export {
40
+ FRAME_HEADER_SIZE,
41
+ FrameType,
42
+ TSS_CONTENT_TYPE_FRAMED,
43
+ TSS_CONTENT_TYPE_FRAMED_VERSIONED,
9
44
  TSS_FORMDATA_CONTEXT,
45
+ TSS_FRAMED_PROTOCOL_VERSION,
10
46
  TSS_SERVER_FUNCTION,
11
47
  TSS_SERVER_FUNCTION_FACTORY,
48
+ X_TSS_CONTEXT,
12
49
  X_TSS_RAW_RESPONSE,
13
- X_TSS_SERIALIZED
50
+ X_TSS_SERIALIZED,
51
+ parseFramedProtocolVersion,
52
+ validateFramedProtocolVersion
14
53
  };
15
54
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
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 {}\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;"}
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'\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;"}
@@ -105,7 +105,7 @@ export interface ServerFnTypes<in out TRegister, in out TMethod extends Method,
105
105
  allInput: IntersectAllValidatorInputs<TMiddlewares, TInputValidator>;
106
106
  allOutput: IntersectAllValidatorOutputs<TMiddlewares, TInputValidator>;
107
107
  }
108
- export declare function flattenMiddlewares(middlewares: Array<AnyFunctionMiddleware | AnyRequestMiddleware>): Array<AnyFunctionMiddleware | AnyRequestMiddleware>;
108
+ export declare function flattenMiddlewares<T extends AnyFunctionMiddleware | AnyRequestMiddleware>(middlewares: Array<T>, maxDepth?: number): Array<T>;
109
109
  export type ServerFnMiddlewareOptions = {
110
110
  method: Method;
111
111
  data: any;
@@ -123,5 +123,4 @@ export type NextFn = (ctx: ServerFnMiddlewareResult) => Promise<ServerFnMiddlewa
123
123
  export type MiddlewareFn = (ctx: ServerFnMiddlewareOptions & {
124
124
  next: NextFn;
125
125
  }) => Promise<ServerFnMiddlewareResult>;
126
- export declare const applyMiddleware: (middlewareFn: MiddlewareFn, ctx: ServerFnMiddlewareOptions, nextFn: NextFn) => Promise<ServerFnMiddlewareResult>;
127
126
  export declare function execValidator(validator: AnyValidator, input: unknown): unknown;