@tanstack/start-client-core
Version:
Modern and scalable routing for React applications
226 lines (225 loc) • 7.95 kB
JavaScript
import { TSS_CONTENT_TYPE_FRAMED, TSS_FORMDATA_CONTEXT, validateFramedProtocolVersion } from "../constants.js";
import { getDefaultSerovalPlugins } from "../getDefaultSerovalPlugins.js";
import { createFrameDecoder } from "./frame-decoder.js";
import { createRawStreamDeserializePlugin, encode, isNotFound, parseRedirect } from "@tanstack/router-core";
import { fromCrossJSON, toJSONAsync } from "seroval";
import invariant from "tiny-invariant";
//#region src/client-rpc/serverFnFetcher.ts
var serovalPlugins = null;
/**
* Checks if an object has at least one own enumerable property.
* More efficient than Object.keys(obj).length > 0 as it short-circuits on first property.
*/
var hop = Object.prototype.hasOwnProperty;
function hasOwnProperties(obj) {
for (const _ in obj) if (hop.call(obj, _)) return true;
return false;
}
async function serverFnFetcher(url, args, handler) {
if (!serovalPlugins) serovalPlugins = getDefaultSerovalPlugins();
const first = args[0];
const fetchImpl = first.fetch ?? handler;
const type = first.data instanceof FormData ? "formData" : "payload";
const headers = first.headers ? new Headers(first.headers) : new Headers();
headers.set("x-tsr-serverFn", "true");
if (type === "payload") headers.set("accept", `${TSS_CONTENT_TYPE_FRAMED}, application/x-ndjson, application/json`);
if (first.method === "GET") {
if (type === "formData") throw new Error("FormData is not supported with GET requests");
const serializedPayload = await serializePayload(first);
if (serializedPayload !== void 0) {
const encodedPayload = encode({ payload: serializedPayload });
if (url.includes("?")) url += `&${encodedPayload}`;
else url += `?${encodedPayload}`;
}
}
let body = void 0;
if (first.method === "POST") {
const fetchBody = await getFetchBody(first);
if (fetchBody?.contentType) headers.set("content-type", fetchBody.contentType);
body = fetchBody?.body;
}
return await getResponse(async () => fetchImpl(url, {
method: first.method,
headers,
signal: first.signal,
body
}));
}
async function serializePayload(opts) {
let payloadAvailable = false;
const payloadToSerialize = {};
if (opts.data !== void 0) {
payloadAvailable = true;
payloadToSerialize["data"] = opts.data;
}
if (opts.context && hasOwnProperties(opts.context)) {
payloadAvailable = true;
payloadToSerialize["context"] = opts.context;
}
if (payloadAvailable) return serialize(payloadToSerialize);
}
async function serialize(data) {
return JSON.stringify(await Promise.resolve(toJSONAsync(data, { plugins: serovalPlugins })));
}
async function getFetchBody(opts) {
if (opts.data instanceof FormData) {
let serializedContext = void 0;
if (opts.context && hasOwnProperties(opts.context)) serializedContext = await serialize(opts.context);
if (serializedContext !== void 0) opts.data.set(TSS_FORMDATA_CONTEXT, serializedContext);
return { body: opts.data };
}
const serializedBody = await serializePayload(opts);
if (serializedBody) return {
body: serializedBody,
contentType: "application/json"
};
}
/**
* Retrieves a response from a given function and manages potential errors
* and special response types including redirects and not found errors.
*
* @param fn - The function to execute for obtaining the response.
* @returns The processed response from the function.
* @throws If the response is invalid or an error occurs during processing.
*/
async function getResponse(fn) {
let response;
try {
response = await fn();
} catch (error) {
if (error instanceof Response) response = error;
else {
console.log(error);
throw error;
}
}
if (response.headers.get("x-tss-raw") === "true") return response;
const contentType = response.headers.get("content-type");
invariant(contentType, "expected content-type header to be set");
if (!!response.headers.get("x-tss-serialized")) {
let result;
if (contentType.includes("application/x-tss-framed")) {
validateFramedProtocolVersion(contentType);
if (!response.body) throw new Error("No response body for framed response");
const { getOrCreateStream, jsonChunks } = createFrameDecoder(response.body);
const plugins = [createRawStreamDeserializePlugin(getOrCreateStream), ...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({
response,
onMessage: (msg) => fromCrossJSON(msg, {
refs,
plugins: serovalPlugins
}),
onError(msg, error) {
console.error(msg, error);
}
});
} else if (contentType.includes("application/json")) result = fromCrossJSON(await response.json(), { plugins: serovalPlugins });
invariant(result, "expected result to be resolved");
if (result instanceof Error) throw result;
return result;
}
if (contentType.includes("application/json")) {
const jsonPayload = await response.json();
const redirect = parseRedirect(jsonPayload);
if (redirect) throw redirect;
if (isNotFound(jsonPayload)) throw jsonPayload;
return jsonPayload;
}
if (!response.ok) throw new Error(await response.text());
return response;
}
async function processServerFnResponse({ response, onMessage, onError }) {
if (!response.body) throw new Error("No response body");
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
let firstRead = false;
let firstObject;
while (!firstRead) {
const { value, done } = await reader.read();
if (value) buffer += value;
if (buffer.length === 0 && done) throw new Error("Stream ended before first object");
if (buffer.endsWith("\n")) {
const lines = buffer.split("\n").filter(Boolean);
const firstLine = lines[0];
if (!firstLine) throw new Error("No JSON line in the first chunk");
firstObject = JSON.parse(firstLine);
firstRead = true;
buffer = lines.slice(1).join("\n");
} else {
const newlineIndex = buffer.indexOf("\n");
if (newlineIndex >= 0) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (line.length > 0) {
firstObject = JSON.parse(line);
firstRead = true;
}
}
}
}
(async () => {
try {
while (true) {
const { value, done } = await reader.read();
if (value) buffer += value;
const lastNewline = buffer.lastIndexOf("\n");
if (lastNewline >= 0) {
const chunk = buffer.slice(0, lastNewline);
buffer = buffer.slice(lastNewline + 1);
const lines = chunk.split("\n").filter(Boolean);
for (const line of lines) try {
onMessage(JSON.parse(line));
} catch (e) {
onError?.(`Invalid JSON line: ${line}`, e);
}
}
if (done) break;
}
} catch (err) {
onError?.("Stream processing error:", err);
}
})();
return onMessage(firstObject);
}
/**
* Processes a framed response where each JSON chunk is a complete JSON string
* (already decoded by frame decoder).
*/
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);
}
//#endregion
export { serverFnFetcher };
//# sourceMappingURL=serverFnFetcher.js.map