Sign In

@tanstack/start-client-core

Package Overview
Dependencies
Maintainers
3
Versions
409
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.170.16
to
1.170.17
+4
-8
dist/esm/client-rpc/serverFnFetcher.js

@@ -80,5 +80,4 @@ import { TSS_CONTENT_TYPE_FRAMED, TSS_FORMDATA_CONTEXT, validateFramedProtocolVersion } from "../constants.js";

if (first.method === "POST") {
const fetchBody = await getFetchBody(first);
if (fetchBody?.contentType) headers.set("content-type", fetchBody.contentType);
body = fetchBody?.body;
body = await getFetchBody(first);
if (typeof body === "string") headers.set("content-type", "application/json");
}

@@ -113,9 +112,6 @@ return await getResponse(async () => fetchImpl(url, {

if (serializedContext !== void 0) opts.data.set(TSS_FORMDATA_CONTEXT, serializedContext);
return { body: opts.data };
return opts.data;
}
const serializedBody = await serializePayload(opts);
if (serializedBody) return {
body: serializedBody,
contentType: "application/json"
};
if (serializedBody) return serializedBody;
}

@@ -122,0 +118,0 @@ /**

@@ -1,1 +0,1 @@

{"version":3,"file":"serverFnFetcher.js","names":[],"sources":["../../../src/client-rpc/serverFnFetcher.ts"],"sourcesContent":["import {\n createRawStreamDeserializePlugin,\n encode,\n invariant,\n isNotFound,\n parseRedirect,\n} from '@tanstack/router-core'\nimport { fromCrossJSON, toJSONAsync } from 'seroval'\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 * Current async post-processing context for deserialization.\n *\n * Some deserializers need to perform async work after synchronous deserialization\n * (e.g., decoding RSC payloads, fetching remote data). This context allows them\n * to register promises that must complete before the deserialized value is used.\n *\n * This uses a synchronous execution context pattern:\n * - Each call to `fromCrossJSON` is synchronous\n * - Within that synchronous execution, all `fromSerializable` calls happen\n * - We set the context before `fromCrossJSON`, clear it after\n * - For streaming chunks, we set/clear context around each `onMessage` call\n *\n * Even with concurrent server function calls, each individual deserialization\n * is atomic (synchronous), so promises are correctly scoped to their call.\n */\nlet currentPostProcessContext: Array<Promise<unknown>> | null = null\n\n/**\n * Set the current post-processing context for async deserialization work.\n * Called before deserialization starts.\n *\n * @param ctx - Array to collect async work promises, or null to clear\n */\nexport function setPostProcessContext(\n ctx: Array<Promise<unknown>> | null,\n): void {\n currentPostProcessContext = ctx\n}\n\n/**\n * Get the current post-processing context.\n * Returns null if no deserialization is in progress.\n */\nexport function getPostProcessContext(): Array<Promise<unknown>> | null {\n return currentPostProcessContext\n}\n\n/**\n * Track an async post-processing promise in the current deserialization context.\n * Called by deserializers that need to perform async work after sync deserialization.\n *\n * If no context is active (e.g., on server), this is a no-op.\n *\n * @param promise - The async work promise to track\n */\nexport function trackPostProcessPromise(promise: Promise<unknown>): void {\n if (currentPostProcessContext) {\n currentPostProcessContext.push(promise)\n }\n}\n\n/**\n * Helper to await all post-processing promises.\n * Uses Promise.allSettled to ensure all promises complete even if some reject.\n */\nasync function awaitPostProcessPromises(\n promises: Array<Promise<unknown>>,\n): Promise<void> {\n if (promises.length > 0) {\n await Promise.allSettled(promises)\n }\n}\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\n // Use custom fetch if provided, otherwise fall back to the passed handler (global fetch)\n const fetchImpl = first.fetch ?? handler\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 fetchImpl(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 if (!contentType) {\n if (process.env.NODE_ENV !== 'production') {\n throw new Error(\n 'Invariant failed: expected content-type header to be set',\n )\n }\n\n invariant()\n }\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 JSON response, it can be simpler\n else if (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n // Track async post-processing work for this deserialization\n const postProcessPromises: Array<Promise<unknown>> = []\n setPostProcessContext(postProcessPromises)\n try {\n result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })\n } finally {\n setPostProcessContext(null)\n }\n // Await any async post-processing before returning\n await awaitPostProcessPromises(postProcessPromises)\n }\n\n if (!result) {\n if (process.env.NODE_ENV !== 'production') {\n throw new Error('Invariant failed: expected result to be resolved')\n }\n\n invariant()\n }\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\n/**\n * Processes a framed response where each JSON chunk is a complete JSON string\n * (already decoded by frame decoder).\n *\n * Uses per-chunk post-processing context to ensure async deserialization work\n * completes before the next chunk is processed. This prevents issues when\n * streaming values require async post-processing (e.g., RSC decoding).\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 for streaming refs like RawStream.\n // Keep draining until the server closes the stream.\n // Each chunk gets its own post-processing context to properly scope async work.\n let drainCancelled = false as boolean\n const drain = (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 // Set up post-processing context for this chunk\n const chunkPostProcessPromises: Array<Promise<unknown>> = []\n setPostProcessContext(chunkPostProcessPromises)\n try {\n onMessage(JSON.parse(value))\n } finally {\n setPostProcessContext(null)\n }\n // Await any async post-processing from this chunk before processing next.\n // This ensures values requiring async work are ready before their\n // containing Promise/Stream resolves/emits to consumers.\n await awaitPostProcessPromises(chunkPostProcessPromises)\n } catch (e) {\n onError?.(`Invalid JSON: ${value}`, e)\n }\n }\n }\n } catch (err) {\n if (!drainCancelled) {\n onError?.('Stream processing error:', err)\n }\n }\n })()\n\n // Process first object with its own post-processing context\n let result: any\n const initialPostProcessPromises: Array<Promise<unknown>> = []\n setPostProcessContext(initialPostProcessPromises)\n try {\n result = onMessage(firstObject)\n } catch (err) {\n setPostProcessContext(null)\n drainCancelled = true\n reader.cancel().catch(() => {})\n throw err\n }\n setPostProcessContext(null)\n\n // Await initial post-processing promises before returning result\n await awaitPostProcessPromises(initialPostProcessPromises)\n\n // If the initial decode fails async, stop draining to avoid holding\n // onto the response body and raw stream buffers unnecessarily.\n Promise.resolve(result).catch(() => {\n drainCancelled = true\n reader.cancel().catch(() => {})\n })\n\n // Detach reader once draining completes.\n drain.finally(() => {\n try {\n reader.releaseLock()\n } catch {\n // Ignore\n }\n })\n\n return result\n}\n"],"mappings":"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"}
{"version":3,"file":"serverFnFetcher.js","names":[],"sources":["../../../src/client-rpc/serverFnFetcher.ts"],"sourcesContent":["import {\n createRawStreamDeserializePlugin,\n encode,\n invariant,\n isNotFound,\n parseRedirect,\n} from '@tanstack/router-core'\nimport { fromCrossJSON, toJSONAsync } from 'seroval'\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 * Current async post-processing context for deserialization.\n *\n * Some deserializers need to perform async work after synchronous deserialization\n * (e.g., decoding RSC payloads, fetching remote data). This context allows them\n * to register promises that must complete before the deserialized value is used.\n *\n * This uses a synchronous execution context pattern:\n * - Each call to `fromCrossJSON` is synchronous\n * - Within that synchronous execution, all `fromSerializable` calls happen\n * - We set the context before `fromCrossJSON`, clear it after\n * - For streaming chunks, we set/clear context around each `onMessage` call\n *\n * Even with concurrent server function calls, each individual deserialization\n * is atomic (synchronous), so promises are correctly scoped to their call.\n */\nlet currentPostProcessContext: Array<Promise<unknown>> | null = null\n\n/**\n * Set the current post-processing context for async deserialization work.\n * Called before deserialization starts.\n *\n * @param ctx - Array to collect async work promises, or null to clear\n */\nexport function setPostProcessContext(\n ctx: Array<Promise<unknown>> | null,\n): void {\n currentPostProcessContext = ctx\n}\n\n/**\n * Get the current post-processing context.\n * Returns null if no deserialization is in progress.\n */\nexport function getPostProcessContext(): Array<Promise<unknown>> | null {\n return currentPostProcessContext\n}\n\n/**\n * Track an async post-processing promise in the current deserialization context.\n * Called by deserializers that need to perform async work after sync deserialization.\n *\n * If no context is active (e.g., on server), this is a no-op.\n *\n * @param promise - The async work promise to track\n */\nexport function trackPostProcessPromise(promise: Promise<unknown>): void {\n if (currentPostProcessContext) {\n currentPostProcessContext.push(promise)\n }\n}\n\n/**\n * Helper to await all post-processing promises.\n * Uses Promise.allSettled to ensure all promises complete even if some reject.\n */\nasync function awaitPostProcessPromises(\n promises: Array<Promise<unknown>>,\n): Promise<void> {\n if (promises.length > 0) {\n await Promise.allSettled(promises)\n }\n}\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\n // Use custom fetch if provided, otherwise fall back to the passed handler (global fetch)\n const fetchImpl = first.fetch ?? handler\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 body = await getFetchBody(first)\n if (typeof body === 'string') {\n headers.set('content-type', 'application/json')\n }\n }\n\n return await getResponse(async () =>\n fetchImpl(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<FormData | 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 opts.data\n }\n const serializedBody = await serializePayload(opts)\n if (serializedBody) {\n return serializedBody\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 if (!contentType) {\n if (process.env.NODE_ENV !== 'production') {\n throw new Error(\n 'Invariant failed: expected content-type header to be set',\n )\n }\n\n invariant()\n }\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 JSON response, it can be simpler\n else if (contentType.includes('application/json')) {\n const jsonPayload = await response.json()\n // Track async post-processing work for this deserialization\n const postProcessPromises: Array<Promise<unknown>> = []\n setPostProcessContext(postProcessPromises)\n try {\n result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })\n } finally {\n setPostProcessContext(null)\n }\n // Await any async post-processing before returning\n await awaitPostProcessPromises(postProcessPromises)\n }\n\n if (!result) {\n if (process.env.NODE_ENV !== 'production') {\n throw new Error('Invariant failed: expected result to be resolved')\n }\n\n invariant()\n }\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\n/**\n * Processes a framed response where each JSON chunk is a complete JSON string\n * (already decoded by frame decoder).\n *\n * Uses per-chunk post-processing context to ensure async deserialization work\n * completes before the next chunk is processed. This prevents issues when\n * streaming values require async post-processing (e.g., RSC decoding).\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 for streaming refs like RawStream.\n // Keep draining until the server closes the stream.\n // Each chunk gets its own post-processing context to properly scope async work.\n let drainCancelled = false as boolean\n const drain = (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 // Set up post-processing context for this chunk\n const chunkPostProcessPromises: Array<Promise<unknown>> = []\n setPostProcessContext(chunkPostProcessPromises)\n try {\n onMessage(JSON.parse(value))\n } finally {\n setPostProcessContext(null)\n }\n // Await any async post-processing from this chunk before processing next.\n // This ensures values requiring async work are ready before their\n // containing Promise/Stream resolves/emits to consumers.\n await awaitPostProcessPromises(chunkPostProcessPromises)\n } catch (e) {\n onError?.(`Invalid JSON: ${value}`, e)\n }\n }\n }\n } catch (err) {\n if (!drainCancelled) {\n onError?.('Stream processing error:', err)\n }\n }\n })()\n\n // Process first object with its own post-processing context\n let result: any\n const initialPostProcessPromises: Array<Promise<unknown>> = []\n setPostProcessContext(initialPostProcessPromises)\n try {\n result = onMessage(firstObject)\n } catch (err) {\n setPostProcessContext(null)\n drainCancelled = true\n reader.cancel().catch(() => {})\n throw err\n }\n setPostProcessContext(null)\n\n // Await initial post-processing promises before returning result\n await awaitPostProcessPromises(initialPostProcessPromises)\n\n // If the initial decode fails async, stop draining to avoid holding\n // onto the response body and raw stream buffers unnecessarily.\n Promise.resolve(result).catch(() => {\n drainCancelled = true\n reader.cancel().catch(() => {})\n })\n\n // Detach reader once draining completes.\n drain.finally(() => {\n try {\n reader.releaseLock()\n } catch {\n // Ignore\n }\n })\n\n return result\n}\n"],"mappings":"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"}
{
"name": "@tanstack/start-client-core",
"version": "1.170.16",
"version": "1.170.17",
"description": "Modern and scalable routing for React applications",

@@ -94,5 +94,5 @@ "author": "Tanner Linsley",

"seroval": "^1.6.2",
"@tanstack/router-core": "1.171.17",
"@tanstack/start-fn-stubs": "1.162.0",
"@tanstack/start-storage-context": "1.167.19"
"@tanstack/start-storage-context": "1.167.20",
"@tanstack/router-core": "1.171.18"
},

@@ -99,0 +99,0 @@ "devDependencies": {

@@ -160,7 +160,6 @@ import {

if (first.method === 'POST') {
const fetchBody = await getFetchBody(first)
if (fetchBody?.contentType) {
headers.set('content-type', fetchBody.contentType)
body = await getFetchBody(first)
if (typeof body === 'string') {
headers.set('content-type', 'application/json')
}
body = fetchBody?.body
}

@@ -208,3 +207,3 @@

opts: FunctionMiddlewareClientFnOptions<any, any, any>,
): Promise<{ body: FormData | string; contentType?: string } | undefined> {
): Promise<FormData | string | undefined> {
if (opts.data instanceof FormData) {

@@ -219,7 +218,7 @@ let serializedContext = undefined

}
return { body: opts.data }
return opts.data
}
const serializedBody = await serializePayload(opts)
if (serializedBody) {
return { body: serializedBody, contentType: 'application/json' }
return serializedBody
}

@@ -226,0 +225,0 @@ return undefined