@tanstack/start-server-core
Version:
Modern and scalable routing for React applications
313 lines (312 loc) • 11.6 kB
JavaScript
import { getResponse } from "./request-response.js";
import { getServerFnById } from "./getServerFnById.js";
import { createMultiplexedStream } from "./frame-protocol.js";
import { TSS_CONTENT_TYPE_FRAMED_VERSIONED, TSS_FORMDATA_CONTEXT, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED, getSerovalPlugins, safeObjectMerge } from "@tanstack/start-client-core";
import { invariant, isNotFound, isRedirect } from "@tanstack/router-core";
import { createRawStreamRPCPlugin, defaultSerovalDeserializerPlugins } from "@tanstack/router-core/ssr/server";
import { MAX_FRAMED_STREAMS, MAX_FRAME_PAYLOAD_SIZE } from "@tanstack/start-client-core/client-rpc";
import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from "seroval";
//#region src/server-functions-handler.ts
var serovalPlugins = void 0;
var FORM_DATA_CONTENT_TYPES = ["multipart/form-data", "application/x-www-form-urlencoded"];
var MAX_PAYLOAD_SIZE = 1e6;
var MAX_PENDING_SERIALIZATION_RECORDS = 1024;
var MAX_PENDING_SERIALIZATION_BYTES = 32 * 1024 * 1024;
var textEncoder = new TextEncoder();
function encodeSerializationRecord(value) {
return textEncoder.encode(JSON.stringify(value));
}
function exceedsPendingSerializationLimit(record, recordCount, pendingBytes) {
return recordCount >= MAX_PENDING_SERIALIZATION_RECORDS || pendingBytes + record.byteLength > MAX_PENDING_SERIALIZATION_BYTES;
}
function runSerializationCleanup(dispose) {
try {
dispose();
} catch {}
}
function cancelRawStream(stream, reason) {
stream.cancel(reason).catch(() => {});
}
var handleServerAction = async ({ request, context, serverFnId }) => {
const methodUpper = request.method.toUpperCase();
const url = new URL(request.url);
const action = await getServerFnById(serverFnId, { origin: "client" });
if (action.method && methodUpper !== action.method) return new Response(`expected ${action.method} method. Got ${methodUpper}`, {
status: 405,
headers: { Allow: action.method }
});
const isServerFn = request.headers.get("x-tsr-serverFn") === "true";
serovalPlugins ??= getSerovalPlugins(defaultSerovalDeserializerPlugins);
const contentType = request.headers.get("Content-Type");
try {
let res;
if (FORM_DATA_CONTENT_TYPES.some((type) => contentType && contentType.includes(type))) {
if (methodUpper === "GET") {
if (process.env.NODE_ENV !== "production") throw new Error("Invariant failed: GET requests with FormData payloads are not supported");
invariant();
}
const formData = await request.formData();
const serializedContext = formData.get(TSS_FORMDATA_CONTEXT);
formData.delete(TSS_FORMDATA_CONTEXT);
const params = {
context,
data: formData,
method: methodUpper
};
if (typeof serializedContext === "string") try {
const deserializedContext = fromJSON(JSON.parse(serializedContext), { plugins: serovalPlugins });
if (typeof deserializedContext === "object" && deserializedContext) params.context = safeObjectMerge(deserializedContext, context);
} catch (e) {
if (process.env.NODE_ENV === "development") console.warn("Failed to parse FormData context:", e);
}
res = await action(params);
} else if (methodUpper === "GET") {
const payloadParam = url.searchParams.get("payload");
if (payloadParam && payloadParam.length > MAX_PAYLOAD_SIZE) throw new Error("Payload too large");
const payload = payloadParam ? fromJSON(JSON.parse(payloadParam), { plugins: serovalPlugins }) : {};
payload.context = safeObjectMerge(payload.context, context);
payload.method = methodUpper;
res = await action(payload);
} else {
const payload = contentType?.includes("application/json") ? fromJSON(await request.json(), { plugins: serovalPlugins }) : {};
payload.context = safeObjectMerge(payload.context, context);
payload.method = methodUpper;
res = await action(payload);
}
const unwrapped = res.result !== void 0 ? res.result : res.error;
if (isNotFound(res)) res = isNotFoundResponse(res);
if (!isServerFn) return unwrapped;
if (unwrapped instanceof Response) {
if (isRedirect(unwrapped)) return unwrapped;
unwrapped.headers.set(X_TSS_RAW_RESPONSE, "true");
return unwrapped;
}
return serializeResult(res, request.signal, serovalPlugins);
} catch (error) {
if (error instanceof Response) return error;
if (isNotFound(error)) return isNotFoundResponse(error);
console.error("Server Fn Error!", error);
const serializedError = JSON.stringify(await toCrossJSONAsync(error, {
refs: /* @__PURE__ */ new Map(),
plugins: serovalPlugins
}));
const response = getResponse();
const headers = {
"Content-Type": "application/json",
[X_TSS_SERIALIZED]: "true"
};
try {
return new Response(serializedError, {
status: response.status ?? 500,
statusText: response.statusText,
headers
});
} catch {
return new Response(serializedError, {
status: 500,
statusText: "",
headers
});
}
}
};
/**
* Serializes a server-function result. A result that Seroval completes
* synchronously without RawStreams becomes plain JSON; everything else is a
* framed response whose records and raw streams are multiplexed in order.
*/
function serializeResult(res, signal, plugins) {
const alsResponse = getResponse();
const initialRecords = [];
let initialBytes = 0;
const pendingRawStreams = [];
let done = false;
let initialParsed = false;
let serializationFailure;
let disposeSerialization;
let onParse = (value, initial) => {
if (serializationFailure) return;
initialParsed ||= initial;
const record = encodeSerializationRecord(value);
if (exceedsPendingSerializationLimit(record, initialRecords.length, initialBytes)) {
serializationFailure = [/* @__PURE__ */ new Error("Server function serialization exceeded its pending output limit")];
return;
}
initialRecords.push(record);
initialBytes += record.byteLength;
};
let onDone = () => {
if (initialParsed) done = true;
};
let onError = (error) => {
serializationFailure ??= [error];
};
const rawStreamPlugin = createRawStreamRPCPlugin((id, stream) => {
if (serializationFailure) {
cancelRawStream(stream, serializationFailure[0]);
return;
}
if (id > MAX_FRAMED_STREAMS) {
const error = /* @__PURE__ */ new Error(`Too many raw streams in framed response (max ${MAX_FRAMED_STREAMS})`);
cancelRawStream(stream, error);
onError(error);
return;
}
pendingRawStreams.push({
id,
stream
});
});
const dispose = toCrossJSONStream(res, {
refs: /* @__PURE__ */ new Map(),
plugins: [rawStreamPlugin, ...plugins],
onParse(value, initial) {
onParse(value, initial);
},
onDone() {
onDone();
},
onError: (error) => {
onError(error);
}
});
if (serializationFailure) {
runSerializationCleanup(dispose);
for (const registration of pendingRawStreams) cancelRawStream(registration.stream, serializationFailure[0]);
throw serializationFailure[0];
}
if (!done) disposeSerialization = dispose;
if (done && pendingRawStreams.length === 0 && initialRecords.length === 1) return new Response(initialRecords[0], {
status: alsResponse.status,
statusText: alsResponse.statusText,
headers: {
"Content-Type": "application/json",
[X_TSS_SERIALIZED]: "true"
}
});
if (done && initialRecords.length === 1) {
const json = initialRecords[0];
if (json.byteLength > MAX_FRAME_PAYLOAD_SIZE) {
const error = /* @__PURE__ */ new Error("Server function serialization exceeded its pending output limit");
for (const registration of pendingRawStreams) cancelRawStream(registration.stream, error);
throw error;
}
const rawStreams = pendingRawStreams.splice(0);
initialRecords.length = 0;
return createFramedResponse(new ReadableStream({
start(controller) {
controller.enqueue({
json,
rawStreams
});
controller.close();
},
cancel(reason) {
for (const registration of rawStreams) cancelRawStream(registration.stream, reason);
}
}), { signal });
}
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const recordAbortController = new AbortController();
let pendingBytes = 0;
const pendingRecords = /* @__PURE__ */ new Set();
const abortRecordStream = (error) => {
if (serializationFailure) return;
serializationFailure = [error];
const disposeCurrentSerialization = disposeSerialization;
disposeSerialization = void 0;
for (const registration of pendingRawStreams.splice(0)) cancelRawStream(registration.stream, error);
for (const record of pendingRecords) for (const registration of record.rawStreams) cancelRawStream(registration.stream, error);
pendingRecords.clear();
recordAbortController.abort(error);
writer.abort(error).catch(() => {});
if (disposeCurrentSerialization) runSerializationCleanup(disposeCurrentSerialization);
};
const writeRecord = (json, rawStreams) => {
if (serializationFailure) {
for (const registration of rawStreams) cancelRawStream(registration.stream, serializationFailure[0]);
return false;
}
if (json.byteLength > MAX_FRAME_PAYLOAD_SIZE || exceedsPendingSerializationLimit(json, pendingRecords.size, pendingBytes)) {
const error = /* @__PURE__ */ new Error("Server function serialization exceeded its pending output limit");
for (const registration of rawStreams) cancelRawStream(registration.stream, error);
onError(error);
return false;
}
pendingBytes += json.byteLength;
const record = {
json,
rawStreams
};
pendingRecords.add(record);
writer.write(record).then(() => {
pendingRecords.delete(record);
pendingBytes -= json.byteLength;
}, (error) => {
const stillOwned = pendingRecords.delete(record);
pendingBytes -= json.byteLength;
if (stillOwned) for (const registration of rawStreams) cancelRawStream(registration.stream, error);
});
return true;
};
onParse = (value) => {
if (serializationFailure) return;
writeRecord(encodeSerializationRecord(value), pendingRawStreams.splice(0));
};
onDone = () => {
if (serializationFailure) return;
disposeSerialization = void 0;
writer.close().catch(() => {});
};
onError = (error) => {
abortRecordStream(error);
};
const initialRawStreams = pendingRawStreams.splice(0);
for (let index = 0; index < initialRecords.length; index++) {
const isLast = index === initialRecords.length - 1;
if (!writeRecord(initialRecords[index], isLast ? initialRawStreams : [])) {
if (!isLast) for (const registration of initialRawStreams) cancelRawStream(registration.stream, serializationFailure[0]);
initialRecords.length = 0;
throw serializationFailure[0];
}
}
initialRecords.length = 0;
if (done) onDone();
writer.closed.catch((error) => {
abortRecordStream(error);
});
return createFramedResponse(readable, {
signal: AbortSignal.any([recordAbortController.signal, signal]),
onCancel: abortRecordStream
});
function createFramedResponse(records, options) {
const multiplexedStream = createMultiplexedStream(records, options);
try {
return new Response(multiplexedStream, {
status: alsResponse.status,
statusText: alsResponse.statusText,
headers: {
"Content-Type": TSS_CONTENT_TYPE_FRAMED_VERSIONED,
[X_TSS_SERIALIZED]: "true"
}
});
} catch (error) {
cancelRawStream(multiplexedStream, error);
throw error;
}
}
}
function isNotFoundResponse(error) {
const { headers, ...rest } = error;
return new Response(JSON.stringify(rest), {
status: 404,
headers: {
"Content-Type": "application/json",
...headers || {}
}
});
}
//#endregion
export { handleServerAction };
//# sourceMappingURL=server-functions-handler.js.map