UNPKG

@tanstack/start-server-core

Version:

Modern and scalable routing for React applications

216 lines (215 loc) 7.7 kB
import { getResponse } from "./request-response.js"; import { getServerFnById } from "./getServerFnById.js"; import { TSS_CONTENT_TYPE_FRAMED_VERSIONED, createMultiplexedStream } from "./frame-protocol.js"; import { TSS_FORMDATA_CONTEXT, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED, getDefaultSerovalPlugins, safeObjectMerge } from "@tanstack/start-client-core"; import { createRawStreamRPCPlugin, invariant, isNotFound, isRedirect } from "@tanstack/router-core"; 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 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"; if (!serovalPlugins) serovalPlugins = getDefaultSerovalPlugins(); const contentType = request.headers.get("Content-Type"); function parsePayload(payload) { return fromJSON(payload, { plugins: serovalPlugins }); } return await (async () => { try { let res = await (async () => { 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); } return await action(params); } 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 ? parsePayload(JSON.parse(payloadParam)) : {}; payload.context = safeObjectMerge(payload.context, context); payload.method = methodUpper; return await action(payload); } let jsonPayload; if (contentType?.includes("application/json")) jsonPayload = await request.json(); const payload = jsonPayload ? parsePayload(jsonPayload) : {}; payload.context = safeObjectMerge(payload.context, context); payload.method = methodUpper; return await action(payload); })(); const unwrapped = 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); function serializeResult(res) { let nonStreamingBody = void 0; const alsResponse = getResponse(); if (res !== void 0) { const rawStreams = /* @__PURE__ */ new Map(); let initialPhase = true; let lateStreamWriter; let lateStreamReadable = void 0; const pendingLateStreams = []; const plugins = [createRawStreamRPCPlugin((id, stream) => { if (initialPhase) { rawStreams.set(id, stream); return; } if (lateStreamWriter) { lateStreamWriter.write({ id, stream }).catch(() => {}); return; } pendingLateStreams.push({ id, stream }); }), ...serovalPlugins || []]; let done = false; const callbacks = { onParse: (value) => { nonStreamingBody = value; }, onDone: () => { done = true; }, onError: (error) => { throw error; } }; toCrossJSONStream(res, { refs: /* @__PURE__ */ new Map(), plugins, onParse(value) { callbacks.onParse(value); }, onDone() { callbacks.onDone(); }, onError: (error) => { callbacks.onError(error); } }); initialPhase = false; if (done && rawStreams.size === 0) return new Response(nonStreamingBody ? JSON.stringify(nonStreamingBody) : void 0, { status: alsResponse.status, statusText: alsResponse.statusText, headers: { "Content-Type": "application/json", [X_TSS_SERIALIZED]: "true" } }); const { readable, writable } = new TransformStream(); lateStreamReadable = readable; lateStreamWriter = writable.getWriter(); for (const registration of pendingLateStreams) lateStreamWriter.write(registration).catch(() => {}); pendingLateStreams.length = 0; const multiplexedStream = createMultiplexedStream(new ReadableStream({ start(controller) { callbacks.onParse = (value) => { controller.enqueue(JSON.stringify(value) + "\n"); }; callbacks.onDone = () => { try { controller.close(); } catch {} lateStreamWriter?.close().catch(() => {}).finally(() => { lateStreamWriter = void 0; }); }; callbacks.onError = (error) => { controller.error(error); lateStreamWriter?.abort(error).catch(() => {}).finally(() => { lateStreamWriter = void 0; }); }; if (nonStreamingBody !== void 0) callbacks.onParse(nonStreamingBody); if (done) callbacks.onDone(); }, cancel() { lateStreamWriter?.abort().catch(() => {}); lateStreamWriter = void 0; } }), rawStreams, lateStreamReadable); return new Response(multiplexedStream, { status: alsResponse.status, statusText: alsResponse.statusText, headers: { "Content-Type": TSS_CONTENT_TYPE_FRAMED_VERSIONED, [X_TSS_SERIALIZED]: "true" } }); } return new Response(void 0, { status: alsResponse.status, statusText: alsResponse.statusText }); } } catch (error) { if (error instanceof Response) return error; if (isNotFound(error)) return isNotFoundResponse(error); console.info(); console.info("Server Fn Error!"); console.info(); console.error(error); console.info(); const serializedError = JSON.stringify(await Promise.resolve(toCrossJSONAsync(error, { refs: /* @__PURE__ */ new Map(), plugins: serovalPlugins }))); const response = getResponse(); return new Response(serializedError, { status: response.status ?? 500, statusText: response.statusText, headers: { "Content-Type": "application/json", [X_TSS_SERIALIZED]: "true" } }); } })(); }; 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