diff --git a/.changeset/plenty-geese-enter.md b/.changeset/plenty-geese-enter.md
new file mode 100644
index 000000000..27c7c5457
--- /dev/null
+++ b/.changeset/plenty-geese-enter.md
@@ -0,0 +1,5 @@
+---
+"@solidjs/start": minor
+---
+
+seroval json mode
diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts
index 7a8131be9..edba86fcc 100644
--- a/apps/tests/src/e2e/server-function.test.ts
+++ b/apps/tests/src/e2e/server-function.test.ts
@@ -67,4 +67,9 @@ test.describe("server-function", () => {
await page.goto("http://localhost:3000/generator-server-function");
await expect(page.locator("#server-fn-test")).toContainText("¡Hola, Mundo!");
});
+
+ test("should build with a server function ping", async ({ page }) => {
+ await page.goto("http://localhost:3000/server-function-ping");
+ await expect(page.locator("#server-fn-test")).toContainText('{"result":true}');
+ });
});
diff --git a/apps/tests/src/routes/server-function-ping.tsx b/apps/tests/src/routes/server-function-ping.tsx
new file mode 100644
index 000000000..70f9d2617
--- /dev/null
+++ b/apps/tests/src/routes/server-function-ping.tsx
@@ -0,0 +1,23 @@
+import { createEffect, createSignal } from "solid-js";
+
+async function ping(value: string) {
+ "use server";
+
+ return await Promise.resolve(value);
+}
+
+export default function App() {
+ const [output, setOutput] = createSignal<{ result?: boolean }>({});
+
+ createEffect(async () => {
+ const value = `${Math.random() * 1000}`;
+ const result = await ping(value);
+ setOutput(prev => ({ ...prev, result: value === result }));
+ });
+
+ return (
+
+ {JSON.stringify(output())}
+
+ );
+}
diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts
index 2523f2b6d..ea602a5e6 100644
--- a/packages/start/src/config/index.ts
+++ b/packages/start/src/config/index.ts
@@ -21,6 +21,10 @@ export interface SolidStartOptions {
routeDir?: string;
extensions?: string[];
middleware?: string;
+ serialization?: {
+ // This only matters for server function responses
+ mode?: 'js' | 'json';
+ };
}
const absolute = (path: string, root: string) =>
@@ -129,6 +133,7 @@ export function solidStart(options?: SolidStartOptions): Array {
"import.meta.env.START_APP_ENTRY": `"${appEntryPath}"`,
"import.meta.env.START_CLIENT_ENTRY": `"${handlers.client}"`,
"import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay),
+ "import.meta.env.SEROVAL_MODE": JSON.stringify(start.serialization?.mode || 'json'),
},
builder: {
sharedPlugins: true,
diff --git a/packages/start/src/server/serialization.ts b/packages/start/src/server/serialization.ts
new file mode 100644
index 000000000..c157d157e
--- /dev/null
+++ b/packages/start/src/server/serialization.ts
@@ -0,0 +1,253 @@
+import {
+ crossSerializeStream,
+ deserialize,
+ Feature,
+ fromCrossJSON,
+ getCrossReferenceHeader,
+ type SerovalNode,
+ toCrossJSONStream,
+} from "seroval";
+import {
+ AbortSignalPlugin,
+ CustomEventPlugin,
+ DOMExceptionPlugin,
+ EventPlugin,
+ FormDataPlugin,
+ HeadersPlugin,
+ ReadableStreamPlugin,
+ RequestPlugin,
+ ResponsePlugin,
+ URLPlugin,
+ URLSearchParamsPlugin,
+} from "seroval-plugins/web";
+
+// TODO(Alexis): if we can, allow providing an option to extend these.
+const DEFAULT_PLUGINS = [
+ AbortSignalPlugin,
+ CustomEventPlugin,
+ DOMExceptionPlugin,
+ EventPlugin,
+ FormDataPlugin,
+ HeadersPlugin,
+ ReadableStreamPlugin,
+ RequestPlugin,
+ ResponsePlugin,
+ URLSearchParamsPlugin,
+ URLPlugin,
+];
+const MAX_SERIALIZATION_DEPTH_LIMIT = 64;
+const DISABLED_FEATURES = Feature.RegExp;
+
+/**
+ * Alexis:
+ *
+ * A "chunk" is a piece of data emitted by the streaming serializer.
+ * Each chunk is represented by a 32-bit value (encoded in hexadecimal),
+ * followed by the encoded string (8-bit representation). This format
+ * is important so we know how much of the chunk being streamed we
+ * are expecting before parsing the entire string data.
+ *
+ * This is sort of a bootleg "multipart/form-data" except it's bad at
+ * handling File/Blob LOL
+ *
+ * The format is as follows:
+ * ;0xFFFFFFFF;
+ */
+function createChunk(data: string): Uint8Array {
+ const encodeData = new TextEncoder().encode(data);
+ const bytes = encodeData.length;
+ const baseHex = bytes.toString(16);
+ const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit
+ const head = new TextEncoder().encode(`;0x${totalHex};`);
+
+ const chunk = new Uint8Array(12 + bytes);
+ chunk.set(head);
+ chunk.set(encodeData, 12);
+ return chunk;
+}
+
+export function serializeToJSStream(id: string, value: any) {
+ return new ReadableStream({
+ start(controller) {
+ crossSerializeStream(value, {
+ scopeId: id,
+ plugins: DEFAULT_PLUGINS,
+ onSerialize(data: string, initial: boolean) {
+ controller.enqueue(
+ createChunk(
+ initial ? `(${getCrossReferenceHeader(id)},${data})` : data,
+ ),
+ );
+ },
+ onDone() {
+ controller.close();
+ },
+ onError(error: any) {
+ controller.error(error);
+ },
+ });
+ },
+ });
+}
+
+export function serializeToJSONStream(value: any) {
+ return new ReadableStream({
+ start(controller) {
+ toCrossJSONStream(value, {
+ disabledFeatures: DISABLED_FEATURES,
+ depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT,
+ plugins: DEFAULT_PLUGINS,
+ onParse(node) {
+ controller.enqueue(createChunk(JSON.stringify(node)));
+ },
+ onDone() {
+ controller.close();
+ },
+ onError(error) {
+ controller.error(error);
+ },
+ });
+ },
+ });
+}
+
+class SerovalChunkReader {
+ reader: ReadableStreamDefaultReader;
+ buffer: Uint8Array;
+ done: boolean;
+ constructor(stream: ReadableStream) {
+ this.reader = stream.getReader();
+ this.buffer = new Uint8Array(0);
+ this.done = false;
+ }
+
+ async readChunk() {
+ // if there's no chunk, read again
+ const chunk = await this.reader.read();
+ if (!chunk.done) {
+ // repopulate the buffer
+ const newBuffer = new Uint8Array(this.buffer.length + chunk.value.length);
+ newBuffer.set(this.buffer);
+ newBuffer.set(chunk.value, this.buffer.length);
+ this.buffer = newBuffer;
+ } else {
+ this.done = true;
+ }
+ }
+
+ async next(): Promise<
+ { done: true; value: undefined } | { done: false; value: string }
+ > {
+ // Check if the buffer is empty
+ if (this.buffer.length === 0) {
+ // if we are already done...
+ if (this.done) {
+ return {
+ done: true,
+ value: undefined,
+ };
+ }
+ // Otherwise, read a new chunk
+ await this.readChunk();
+ return await this.next();
+ }
+ // Read the "byte header"
+ // The byte header tells us how big the expected data is
+ // so we know how much data we should wait before we
+ // deserialize the data
+ const head = new TextDecoder().decode(this.buffer.subarray(1, 11));
+ const bytes = Number.parseInt(head, 16); // ;0x00000000;
+ // Check if the buffer has enough bytes to be parsed
+ while (bytes > this.buffer.length - 12) {
+ // If it's not enough, and the reader is done
+ // then the chunk is invalid.
+ if (this.done) {
+ throw new Error("Malformed server function stream.");
+ }
+ // Otherwise, we read more chunks
+ await this.readChunk();
+ }
+ // Extract the exact chunk as defined by the byte header
+ const partial = new TextDecoder().decode(
+ this.buffer.subarray(12, 12 + bytes),
+ );
+ // The rest goes to the buffer
+ this.buffer = this.buffer.subarray(12 + bytes);
+
+ // Deserialize the chunk
+ return {
+ done: false,
+ value: partial,
+ };
+ }
+
+ async drain(interpret: (chunk: string) => void) {
+ while (true) {
+ const result = await this.next();
+ if (result.done) {
+ break;
+ } else {
+ interpret(result.value);
+ }
+ }
+ }
+}
+
+export async function serializeToJSONString(value: any) {
+ const response = new Response(serializeToJSONStream(value));
+ return await response.text();
+}
+
+export async function deserializeFromJSONString(json: string) {
+ const blob = new Response(json);
+ return await deserializeJSONStream(blob);
+}
+
+export async function deserializeJSONStream(response: Response | Request) {
+ if (!response.body) {
+ throw new Error("missing body");
+ }
+ const reader = new SerovalChunkReader(response.body);
+ const result = await reader.next();
+ if (!result.done) {
+ const refs = new Map();
+
+ function interpretChunk(chunk: string): unknown {
+ const value = fromCrossJSON(JSON.parse(chunk) as SerovalNode, {
+ refs,
+ disabledFeatures: DISABLED_FEATURES,
+ depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT,
+ plugins: DEFAULT_PLUGINS,
+ });
+ return value;
+ }
+
+ void reader.drain(interpretChunk);
+
+ return interpretChunk(result.value);
+ }
+ return undefined;
+}
+
+export async function deserializeJSStream(id: string, response: Response) {
+ if (!response.body) {
+ throw new Error("missing body");
+ }
+ const reader = new SerovalChunkReader(response.body);
+
+ const result = await reader.next();
+
+ if (!result.done) {
+ reader.drain(deserialize).then(
+ () => {
+ // @ts-ignore
+ delete $R[id];
+ },
+ () => {
+ // no-op
+ },
+ );
+ return deserialize(result.value);
+ }
+ return undefined;
+}
diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts
index 160672684..62aa27f69 100644
--- a/packages/start/src/server/server-functions-handler.ts
+++ b/packages/start/src/server/server-functions-handler.ts
@@ -1,74 +1,21 @@
-import { getServerFnById } from "solidstart:server-fn-manifest";
import { parseSetCookie } from "cookie-es";
import { type H3Event, parseCookies } from "h3";
-import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval";
-import {
- CustomEventPlugin,
- DOMExceptionPlugin,
- EventPlugin,
- FormDataPlugin,
- HeadersPlugin,
- ReadableStreamPlugin,
- RequestPlugin,
- ResponsePlugin,
- URLPlugin,
- URLSearchParamsPlugin,
-} from "seroval-plugins/web";
import { sharedConfig } from "solid-js";
import { renderToString } from "solid-js/web";
import { provideRequestEvent } from "solid-js/web/storage";
+import { getServerFnById } from "solidstart:server-fn-manifest";
import { getFetchEvent, mergeResponseHeaders } from "./fetchEvent.ts";
import { createPageEvent } from "./handler.ts";
+import {
+ deserializeFromJSONString,
+ deserializeJSONStream,
+ serializeToJSONStream,
+ serializeToJSStream,
+} from "./serialization.ts";
import type { FetchEvent, PageEvent } from "./types.ts";
import { getExpectedRedirectStatus } from "./util.ts";
-function createChunk(data: string) {
- const encodeData = new TextEncoder().encode(data);
- const bytes = encodeData.length;
- const baseHex = bytes.toString(16);
- const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit
- const head = new TextEncoder().encode(`;0x${totalHex};`);
-
- const chunk = new Uint8Array(12 + bytes);
- chunk.set(head);
- chunk.set(encodeData, 12);
- return chunk;
-}
-
-function serializeToStream(id: string, value: any) {
- return new ReadableStream({
- start(controller) {
- crossSerializeStream(value, {
- scopeId: id,
- plugins: [
- CustomEventPlugin,
- DOMExceptionPlugin,
- EventPlugin,
- FormDataPlugin,
- HeadersPlugin,
- ReadableStreamPlugin,
- RequestPlugin,
- ResponsePlugin,
- URLSearchParamsPlugin,
- URLPlugin,
- ],
- onSerialize(data: string, initial: boolean) {
- controller.enqueue(
- createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data),
- );
- },
- onDone() {
- controller.close();
- },
- onError(error: any) {
- controller.error(error);
- },
- });
- },
- });
-}
-
export async function handleServerFunction(h3Event: H3Event) {
const event = getFetchEvent(h3Event);
const request = event.request;
@@ -99,51 +46,25 @@ export async function handleServerFunction(h3Event: H3Event) {
if (!instance || h3Event.method === "GET") {
const args = url.searchParams.get("args");
if (args) {
- const json = JSON.parse(args);
- (json.t
- ? (fromJSON(json, {
- plugins: [
- CustomEventPlugin,
- DOMExceptionPlugin,
- EventPlugin,
- FormDataPlugin,
- HeadersPlugin,
- ReadableStreamPlugin,
- RequestPlugin,
- ResponsePlugin,
- URLSearchParamsPlugin,
- URLPlugin,
- ],
- }) as any)
- : json
- ).forEach((arg: any) => {
+ const result = (await deserializeFromJSONString(args)) as any[];
+ for (const arg of result) {
parsed.push(arg);
- });
+ }
}
}
- if (h3Event.method === "POST") {
+ if (request.method === "POST") {
const contentType = request.headers.get("content-type");
+ const clone = request.clone();
if (
contentType?.startsWith("multipart/form-data") ||
contentType?.startsWith("application/x-www-form-urlencoded")
) {
- parsed.push(await event.request.formData());
- } else if (contentType?.startsWith("application/json")) {
- parsed = fromJSON(await event.request.json(), {
- plugins: [
- CustomEventPlugin,
- DOMExceptionPlugin,
- EventPlugin,
- FormDataPlugin,
- HeadersPlugin,
- ReadableStreamPlugin,
- RequestPlugin,
- ResponsePlugin,
- URLSearchParamsPlugin,
- URLPlugin,
- ],
- });
+ parsed.push(await clone.formData());
+ } else if (contentType?.startsWith('application/json')) {
+ parsed = await clone.json() as any[];
+ } else if (request.headers.has('x-serialized')) {
+ parsed = (await deserializeJSONStream(clone)) as any[];
}
}
try {
@@ -178,9 +99,12 @@ export async function handleServerFunction(h3Event: H3Event) {
// handle no JS success case
if (!instance) return handleNoJS(result, request, parsed);
- h3Event.res.headers.set("content-type", "text/javascript");
-
- return serializeToStream(instance, result);
+ h3Event.res.headers.set("x-serialized", "true");
+ if (import.meta.env.SEROVAL_MODE === "js") {
+ h3Event.res.headers.set("content-type", "text/javascript");
+ return serializeToJSStream(instance, result);
+ }
+ return serializeToJSONStream(result);
} catch (x) {
if (x instanceof Response) {
if (singleFlight && instance) {
@@ -189,28 +113,41 @@ export async function handleServerFunction(h3Event: H3Event) {
// forward headers
if ((x as any).headers) mergeResponseHeaders(h3Event, (x as any).headers);
// forward non-redirect statuses
- if ((x as any).status && (!instance || (x as any).status < 300 || (x as any).status >= 400))
+ if (
+ (x as any).status &&
+ (!instance || (x as any).status < 300 || (x as any).status >= 400)
+ )
h3Event.res.status = (x as any).status;
if ((x as any).customBody) {
x = (x as any).customBody();
} else if ((x as any).body === undefined) x = null;
h3Event.res.headers.set("X-Error", "true");
} else if (instance) {
- const error = x instanceof Error ? x.message : typeof x === "string" ? x : "true";
+ const error =
+ x instanceof Error ? x.message : typeof x === "string" ? x : "true";
h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, ""));
} else {
x = handleNoJS(x, request, parsed, true);
}
if (instance) {
- h3Event.res.headers.set("content-type", "text/javascript");
- return serializeToStream(instance, x);
+ h3Event.res.headers.set("x-serialized", "true");
+ if (import.meta.env.SEROVAL_MODE === "js") {
+ h3Event.res.headers.set("content-type", "text/javascript");
+ return serializeToJSStream(instance, x);
+ }
+ return serializeToJSONStream(x);
}
return x;
}
}
-function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boolean) {
+function handleNoJS(
+ result: any,
+ request: Request,
+ parsed: any[],
+ thrown?: boolean,
+) {
const url = new URL(request.url);
const isError = result instanceof Error;
let statusCode = 302;
@@ -220,7 +157,10 @@ function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boole
if (result.headers.has("Location")) {
headers.set(
`Location`,
- new URL(result.headers.get("Location")!, url.origin + import.meta.env.BASE_URL).toString(),
+ new URL(
+ result.headers.get("Location")!,
+ url.origin + import.meta.env.BASE_URL,
+ ).toString(),
);
statusCode = getExpectedRedirectStatus(result);
}
@@ -237,7 +177,10 @@ function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boole
result: isError ? result.message : result,
thrown: thrown,
error: isError,
- input: [...parsed.slice(0, -1), [...parsed[parsed.length - 1].entries()]],
+ input: [
+ ...parsed.slice(0, -1),
+ [...parsed[parsed.length - 1].entries()],
+ ],
}),
)}; Secure; HttpOnly;`,
);
@@ -263,7 +206,7 @@ function createSingleFlightHeaders(sourceEvent: FetchEvent) {
// useH3Internals = true;
// sourceEvent.nativeEvent.node.req.headers.cookie = "";
// }
- SetCookies.forEach(cookie => {
+ SetCookies.forEach((cookie) => {
if (!cookie) return;
const { maxAge, expires, name, value } = parseSetCookie(cookie);
if (maxAge != null && maxAge <= 0) {
@@ -284,7 +227,10 @@ function createSingleFlightHeaders(sourceEvent: FetchEvent) {
return headers;
}
-async function handleSingleFlight(sourceEvent: FetchEvent, result: any): Promise {
+async function handleSingleFlight(
+ sourceEvent: FetchEvent,
+ result: any,
+): Promise {
let revalidate: string[];
let url = new URL(sourceEvent.request.headers.get("referer")!).toString();
if (result instanceof Response) {
diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts
index 8bbabe40b..3a31cc2f7 100644
--- a/packages/start/src/server/server-runtime.ts
+++ b/packages/start/src/server/server-runtime.ts
@@ -1,122 +1,20 @@
-// @ts-ignore - seroval exports issue with NodeNext
-import { join } from "pathe";
-import { deserialize, toJSONAsync } from "seroval";
-import {
- CustomEventPlugin,
- DOMExceptionPlugin,
- EventPlugin,
- FormDataPlugin,
- HeadersPlugin,
- ReadableStreamPlugin,
- RequestPlugin,
- ResponsePlugin,
- URLPlugin,
- URLSearchParamsPlugin,
-} from "seroval-plugins/web";
-import { type Component } from "solid-js";
-
-class SerovalChunkReader {
- reader: ReadableStreamDefaultReader;
- buffer: Uint8Array;
- done: boolean;
- constructor(stream: ReadableStream) {
- this.reader = stream.getReader();
- this.buffer = new Uint8Array(0);
- this.done = false;
- }
-
- async readChunk() {
- // if there's no chunk, read again
- const chunk = await this.reader.read();
- if (!chunk.done) {
- // repopulate the buffer
- let newBuffer = new Uint8Array(this.buffer.length + chunk.value.length);
- newBuffer.set(this.buffer);
- newBuffer.set(chunk.value, this.buffer.length);
- this.buffer = newBuffer;
- } else {
- this.done = true;
- }
- }
-
- async next(): Promise {
- // Check if the buffer is empty
- if (this.buffer.length === 0) {
- // if we are already done...
- if (this.done) {
- return {
- done: true,
- value: undefined,
- };
- }
- // Otherwise, read a new chunk
- await this.readChunk();
- return await this.next();
- }
- // Read the "byte header"
- // The byte header tells us how big the expected data is
- // so we know how much data we should wait before we
- // deserialize the data
- const head = new TextDecoder().decode(this.buffer.subarray(1, 11));
- const bytes = Number.parseInt(head, 16); // ;0x00000000;
- // Check if the buffer has enough bytes to be parsed
- while (bytes > this.buffer.length - 12) {
- // If it's not enough, and the reader is done
- // then the chunk is invalid.
- if (this.done) {
- throw new Error("Malformed server function stream.");
- }
- // Otherwise, we read more chunks
- await this.readChunk();
- }
- // Extract the exact chunk as defined by the byte header
- const partial = new TextDecoder().decode(this.buffer.subarray(12, 12 + bytes));
- // The rest goes to the buffer
- this.buffer = this.buffer.subarray(12 + bytes);
-
- // Deserialize the chunk
- return {
- done: false,
- value: deserialize(partial),
- };
- }
-
- async drain() {
- while (true) {
- const result = await this.next();
- if (result.done) {
- break;
- }
- }
- }
-}
-
-async function deserializeStream(id: string, response: Response) {
- if (!response.body) {
- throw new Error("missing body");
- }
- const reader = new SerovalChunkReader(response.body);
- const result = await reader.next();
-
- if (!result.done) {
- reader.drain().then(
- () => {
- // @ts-ignore
- delete $R[id];
- },
- () => {
- // no-op
- },
- );
- }
-
- return result.value;
-}
+import { type Component } from "solid-js";
+import {
+ deserializeJSONStream,
+ deserializeJSStream,
+ // serializeToJSONStream,
+ serializeToJSONString,
+} from "./serialization";
let INSTANCE = 0;
-function createRequest(base: string, id: string, instance: string, options: RequestInit) {
+function createRequest(
+ base: string,
+ id: string,
+ instance: string,
+ options: RequestInit,
+) {
return fetch(base, {
method: "POST",
...options,
@@ -127,20 +25,6 @@ function createRequest(base: string, id: string, instance: string, options: Requ
},
});
}
-
-const plugins = [
- CustomEventPlugin,
- DOMExceptionPlugin,
- EventPlugin,
- FormDataPlugin,
- HeadersPlugin,
- ReadableStreamPlugin,
- RequestPlugin,
- ResponsePlugin,
- URLSearchParamsPlugin,
- URLPlugin,
-];
-
async function fetchServerFunction(
base: string,
id: string,
@@ -154,15 +38,25 @@ async function fetchServerFunction(
? createRequest(base, id, instance, { ...options, body: args[0] })
: args.length === 1 && args[0] instanceof URLSearchParams
? createRequest(base, id, instance, {
- ...options,
- body: args[0],
- headers: { ...options.headers, "Content-Type": "application/x-www-form-urlencoded" },
- })
+ ...options,
+ body: args[0],
+ headers: {
+ ...options.headers,
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ })
: createRequest(base, id, instance, {
- ...options,
- body: JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))),
- headers: { ...options.headers, "Content-Type": "application/json" },
- }));
+ ...options,
+ // TODO(Alexis): move to serializeToJSONStream
+ body: await serializeToJSONString(args),
+ // duplex: 'half',
+ // body: serializeToJSONStream(args),
+ headers: {
+ ...options.headers,
+ "x-serialized": "true",
+ "Content-Type": "text/plain"
+ },
+ }));
if (
response.headers.has("Location") ||
@@ -172,20 +66,28 @@ async function fetchServerFunction(
if (response.body) {
/* @ts-ignore-next-line */
response.customBody = () => {
- return deserializeStream(instance, response);
+ if (import.meta.env.SEROVAL_MODE === "js") {
+ return deserializeJSStream(instance, response.clone());
+ }
+ return deserializeJSONStream(response.clone());
};
}
return response;
}
const contentType = response.headers.get("Content-Type");
+ const clone = response.clone();
let result;
- if (contentType && contentType.startsWith("text/plain")) {
- result = await response.text();
- } else if (contentType && contentType.startsWith("application/json")) {
- result = await response.json();
- } else {
- result = await deserializeStream(instance, response);
+ if (contentType?.startsWith("text/plain")) {
+ result = await clone.text();
+ } else if (contentType?.startsWith("application/json")) {
+ result = await clone.json();
+ } else if (response.headers.get('x-serialized')) {
+ if (import.meta.env.SEROVAL_MODE === "js") {
+ result = await deserializeJSStream(instance, clone);
+ } else {
+ result = await deserializeJSONStream(clone);
+ }
}
if (response.headers.has("X-Error")) {
throw result;
@@ -197,7 +99,8 @@ export function createServerReference(id: string) {
let baseURL = import.meta.env.BASE_URL ?? "/";
if (!baseURL.endsWith("/")) baseURL += "/";
- const fn = (...args: any[]) => fetchServerFunction(`${baseURL}_server`, id, {}, args);
+ const fn = (...args: any[]) =>
+ fetchServerFunction(`${baseURL}_server`, id, {}, args);
return new Proxy(fn, {
get(target, prop, receiver) {
@@ -211,15 +114,16 @@ export function createServerReference(id: string) {
const url = `${baseURL}_server?id=${encodeURIComponent(id)}`;
return (options: RequestInit) => {
const fn = async (...args: any[]) => {
- const encodeArgs = options.method && options.method.toUpperCase() === "GET";
+ const encodeArgs =
+ options.method && options.method.toUpperCase() === "GET";
return fetchServerFunction(
encodeArgs
? url +
- (args.length
- ? `&args=${encodeURIComponent(
- JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))),
- )}`
- : "")
+ (args.length
+ ? `&args=${encodeURIComponent(
+ await serializeToJSONString(args),
+ )}`
+ : "")
: `${baseURL}_server`,
id,
options,