UNPKG

@tanstack/start-server-core

Version:

Modern and scalable routing for React applications

313 lines (312 loc) 11.6 kB
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