Skip to content

Commit e918deb

Browse files
committed
Add bidirectional encoding
1 parent 9515519 commit e918deb

File tree

5 files changed

+153
-122
lines changed

5 files changed

+153
-122
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createEffect, createSignal } from "solid-js";
2+
3+
async function ping(value: Blob) {
4+
"use server";
5+
return value;
6+
}
7+
8+
const blobURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNjYgMTU1LjMnPjxwYXRoIGQ9J00xNjMgMzVTMTEwLTQgNjkgNWwtMyAxYy02IDItMTEgNS0xNCA5bC0yIDMtMTUgMjYgMjYgNWMxMSA3IDI1IDEwIDM4IDdsNDYgOSAxOC0zMHonIGZpbGw9JyM3NmIzZTEnLz48bGluZWFyR3JhZGllbnQgaWQ9J2EnIGdyYWRpZW50VW5pdHM9J3VzZXJTcGFjZU9uVXNlJyB4MT0nMjcuNScgeTE9JzMnIHgyPScxNTInIHkyPSc2My41Jz48c3RvcCBvZmZzZXQ9Jy4xJyBzdG9wLWNvbG9yPScjNzZiM2UxJy8+PHN0b3Agb2Zmc2V0PScuMycgc3RvcC1jb2xvcj0nI2RjZjJmZCcvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nIzc2YjNlMScvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0nTTE2MyAzNVMxMTAtNCA2OSA1bC0zIDFjLTYgMi0xMSA1LTE0IDlsLTIgMy0xNSAyNiAyNiA1YzExIDcgMjUgMTAgMzggN2w0NiA5IDE4LTMweicgb3BhY2l0eT0nLjMnIGZpbGw9J3VybCgjYSknLz48cGF0aCBkPSdNNTIgMzVsLTQgMWMtMTcgNS0yMiAyMS0xMyAzNSAxMCAxMyAzMSAyMCA0OCAxNWw2Mi0yMVM5MiAyNiA1MiAzNXonIGZpbGw9JyM1MThhYzgnLz48bGluZWFyR3JhZGllbnQgaWQ9J2InIGdyYWRpZW50VW5pdHM9J3VzZXJTcGFjZU9uVXNlJyB4MT0nOTUuOCcgeTE9JzMyLjYnIHgyPSc3NCcgeTI9JzEwNS4yJz48c3RvcCBvZmZzZXQ9JzAnIHN0b3AtY29sb3I9JyM3NmIzZTEnLz48c3RvcCBvZmZzZXQ9Jy41JyBzdG9wLWNvbG9yPScjNDM3N2JiJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjMWYzYjc3Jy8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSdNNTIgMzVsLTQgMWMtMTcgNS0yMiAyMS0xMyAzNSAxMCAxMyAzMSAyMCA0OCAxNWw2Mi0yMVM5MiAyNiA1MiAzNXonIG9wYWNpdHk9Jy4zJyBmaWxsPSd1cmwoI2IpJy8+PGxpbmVhckdyYWRpZW50IGlkPSdjJyBncmFkaWVudFVuaXRzPSd1c2VyU3BhY2VPblVzZScgeDE9JzE4LjQnIHkxPSc2NC4yJyB4Mj0nMTQ0LjMnIHkyPScxNDkuOCc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjMzE1YWE5Jy8+PHN0b3Agb2Zmc2V0PScuNScgc3RvcC1jb2xvcj0nIzUxOGFjOCcvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nIzMxNWFhOScvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0nTTEzNCA4MGE0NSA0NSAwIDAwLTQ4LTE1TDI0IDg1IDQgMTIwbDExMiAxOSAyMC0zNmM0LTcgMy0xNS0yLTIzeicgZmlsbD0ndXJsKCNjKScvPjxsaW5lYXJHcmFkaWVudCBpZD0nZCcgZ3JhZGllbnRVbml0cz0ndXNlclNwYWNlT25Vc2UnIHgxPSc3NS4yJyB5MT0nNzQuNScgeDI9JzI0LjQnIHkyPScyNjAuOCc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjNDM3N2JiJy8+PHN0b3Agb2Zmc2V0PScuNScgc3RvcC1jb2xvcj0nIzFhMzM2YicvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nIzFhMzM2YicvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0nTTExNCAxMTVhNDUgNDUgMCAwMC00OC0xNUw0IDEyMHM1MyA0MCA5NCAzMGwzLTFjMTctNSAyMy0yMSAxMy0zNHonIGZpbGw9J3VybCgjZCknLz48L3N2Zz4=';
9+
10+
export default function App() {
11+
const [output, setOutput] = createSignal<{ result?: boolean }>({});
12+
13+
createEffect(async () => {
14+
const request = await fetch(blobURI);
15+
const blob = await request.blob();
16+
const result = await ping(blob);
17+
const value = await blob.text();
18+
const test = await result.text();
19+
20+
setOutput(prev => ({ ...prev, result: value === test }));
21+
});
22+
23+
return (
24+
<main>
25+
<span id="server-fn-test">{JSON.stringify(output())}</span>
26+
</main>
27+
);
28+
}

packages/start/src/server/serialization.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export async function deserializeJSONStream(response: Response | Request) {
229229
return undefined;
230230
}
231231

232-
export async function deserializeJSStream(id: string, response: Response) {
232+
export async function deserializeJSStream(id: string, response: Request | Response) {
233233
if (!response.body) {
234234
throw new Error("missing body");
235235
}

packages/start/src/server/server-functions-handler.ts

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ import { getFetchEvent, mergeResponseHeaders } from "./fetchEvent.ts";
99
import { createPageEvent } from "./handler.ts";
1010
import {
1111
deserializeFromJSONString,
12-
deserializeJSONStream,
1312
serializeToJSONStream,
1413
serializeToJSStream,
1514
} from "./serialization.ts";
16-
import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts";
15+
import { BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./server-functions-shared.ts";
1716
import type { FetchEvent, PageEvent } from "./types.ts";
1817
import { getExpectedRedirectStatus } from "./util.ts";
1918

@@ -54,40 +53,7 @@ export async function handleServerFunction(h3Event: H3Event) {
5453
}
5554
}
5655
if (request.method === "POST") {
57-
const contentType = request.headers.get("content-type");
58-
const startType = request.headers.get(BODY_FORMAT_KEY);
59-
const clone = request.clone();
60-
61-
switch (true) {
62-
case startType === BodyFormat.Seroval:
63-
parsed = (await deserializeJSONStream(clone)) as any[];
64-
break;
65-
case startType === BodyFormat.String:
66-
parsed.push(await clone.text());
67-
break;
68-
case startType === BodyFormat.File: {
69-
const formData = await clone.formData();
70-
parsed.push(formData.get(BODY_FORMAL_FILE));
71-
break;
72-
}
73-
case startType === BodyFormat.FormData:
74-
case contentType?.startsWith("multipart/form-data"):
75-
parsed.push(await clone.formData());
76-
break;
77-
case startType === BodyFormat.URLSearchParams:
78-
case contentType?.startsWith("application/x-www-form-urlencoded"):
79-
parsed.push(new URLSearchParams(await clone.text()));
80-
break;
81-
case startType === BodyFormat.Blob:
82-
parsed.push(await clone.blob());
83-
break;
84-
case startType === BodyFormat.ArrayBuffer:
85-
parsed.push(await clone.arrayBuffer());
86-
break;
87-
case startType === BodyFormat.Uint8Array:
88-
parsed.push(await clone.bytes());
89-
break;
90-
}
56+
parsed.push(await extractBody('', false, request.clone()));
9157
}
9258
try {
9359
let result = await provideRequestEvent(event, async () => {
@@ -121,12 +87,18 @@ export async function handleServerFunction(h3Event: H3Event) {
12187
// handle no JS success case
12288
if (!instance) return handleNoJS(result, request, parsed);
12389

124-
h3Event.res.headers.set(BODY_FORMAT_KEY, "true");
125-
if (import.meta.env.SEROVAL_MODE === "js") {
126-
h3Event.res.headers.set("content-type", "text/javascript");
127-
return serializeToJSStream(instance, result);
128-
}
129-
return serializeToJSONStream(result);
90+
const body = getHeadersAndBody(result);
91+
if (body) {
92+
return new Response(body.body, {
93+
headers: body.headers,
94+
});
95+
}
96+
h3Event.res.headers.set(BODY_FORMAT_KEY, BodyFormat.Seroval);
97+
if (import.meta.env.SEROVAL_MODE === "js") {
98+
h3Event.res.headers.set("content-type", "text/javascript");
99+
return serializeToJSStream(instance, result);
100+
}
101+
return serializeToJSONStream(result);
130102
} catch (x) {
131103
if (x instanceof Response) {
132104
if (singleFlight && instance) {
@@ -153,7 +125,13 @@ export async function handleServerFunction(h3Event: H3Event) {
153125
x = handleNoJS(x, request, parsed, true);
154126
}
155127
if (instance) {
156-
h3Event.res.headers.set(BODY_FORMAT_KEY, "true");
128+
const body = getHeadersAndBody(x);
129+
if (body) {
130+
return new Response(body.body, {
131+
headers: body.headers,
132+
});
133+
}
134+
h3Event.res.headers.set(BODY_FORMAT_KEY, BodyFormat.Seroval);
157135
if (import.meta.env.SEROVAL_MODE === "js") {
158136
h3Event.res.headers.set("content-type", "text/javascript");
159137
return serializeToJSStream(instance, x);

packages/start/src/server/server-functions-shared.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { deserializeJSONStream, deserializeJSStream } from "./serialization";
12

23
export const BODY_FORMAT_KEY = "X-Start-Type";
34

@@ -13,3 +14,103 @@ export const enum BodyFormat {
1314
ArrayBuffer = "6",
1415
Uint8Array = "7",
1516
}
17+
18+
export function getHeadersAndBody(body: any):
19+
| {
20+
headers?: HeadersInit;
21+
body: BodyInit;
22+
}
23+
| undefined {
24+
switch (true) {
25+
case typeof body === "string":
26+
return {
27+
headers: {
28+
"Content-Type": "text/plain",
29+
[BODY_FORMAT_KEY]: BodyFormat.String,
30+
},
31+
body,
32+
};
33+
case body instanceof FormData:
34+
return {
35+
headers: {
36+
[BODY_FORMAT_KEY]: BodyFormat.FormData,
37+
},
38+
body,
39+
};
40+
case body instanceof URLSearchParams:
41+
return {
42+
headers: {
43+
"Content-Type": "application/x-www-form-urlencoded",
44+
[BODY_FORMAT_KEY]: BodyFormat.URLSearchParams,
45+
},
46+
body,
47+
};
48+
case body instanceof File: {
49+
const formData = new FormData();
50+
formData.append(BODY_FORMAL_FILE, body, body.name);
51+
return {
52+
headers: {
53+
[BODY_FORMAT_KEY]: BodyFormat.File,
54+
},
55+
body: formData,
56+
};
57+
}
58+
case body instanceof Blob:
59+
return {
60+
headers: {
61+
[BODY_FORMAT_KEY]: BodyFormat.Blob,
62+
},
63+
body,
64+
};
65+
case body instanceof ArrayBuffer:
66+
return {
67+
headers: {
68+
[BODY_FORMAT_KEY]: BodyFormat.ArrayBuffer,
69+
},
70+
body,
71+
};
72+
case body instanceof Uint8Array:
73+
return {
74+
headers: {
75+
[BODY_FORMAT_KEY]: BodyFormat.Uint8Array,
76+
},
77+
body: new Uint8Array(body),
78+
};
79+
default:
80+
return undefined;
81+
}
82+
}
83+
84+
export async function extractBody(instance: string, client: boolean, source: Request | Response) {
85+
const contentType = source.headers.get("content-type");
86+
const startType = source.headers.get(BODY_FORMAT_KEY);
87+
const clone = source.clone();
88+
89+
switch (true) {
90+
case startType === BodyFormat.Seroval:
91+
if (client && import.meta.env.SEROVAL_MODE === "js") {
92+
return await deserializeJSStream(instance, clone);
93+
}
94+
return await deserializeJSONStream(clone);
95+
case startType === BodyFormat.String:
96+
return await clone.text();
97+
case startType === BodyFormat.File: {
98+
const formData = await clone.formData();
99+
return formData.get(BODY_FORMAL_FILE);
100+
}
101+
case startType === BodyFormat.FormData:
102+
case contentType?.startsWith("multipart/form-data"):
103+
return await clone.formData();
104+
case startType === BodyFormat.URLSearchParams:
105+
case contentType?.startsWith("application/x-www-form-urlencoded"):
106+
return new URLSearchParams(await clone.text());
107+
case startType === BodyFormat.Blob:
108+
return await clone.blob();
109+
case startType === BodyFormat.ArrayBuffer:
110+
return await clone.arrayBuffer();
111+
case startType === BodyFormat.Uint8Array:
112+
return await clone.bytes();
113+
}
114+
115+
throw new Error("Unknown body format");
116+
}

packages/start/src/server/server-runtime.ts

Lines changed: 2 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
// serializeToJSONStream,
66
serializeToJSONString,
77
} from "./serialization.ts";
8-
import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts";
8+
import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./server-functions-shared.ts";
99

1010
let INSTANCE = 0;
1111

@@ -26,70 +26,6 @@ function createRequest(
2626
});
2727
}
2828

29-
function getHeadersAndBody(body: any): {
30-
headers?: HeadersInit;
31-
body: BodyInit;
32-
} | undefined {
33-
switch (true) {
34-
case typeof body === "string":
35-
return {
36-
headers: {
37-
"Content-Type": "text/plain",
38-
[BODY_FORMAT_KEY]: BodyFormat.String,
39-
},
40-
body,
41-
};
42-
case body instanceof FormData:
43-
return {
44-
headers: {
45-
[BODY_FORMAT_KEY]: BodyFormat.FormData,
46-
},
47-
body,
48-
};
49-
case body instanceof URLSearchParams:
50-
return {
51-
headers: {
52-
"Content-Type": "application/x-www-form-urlencoded",
53-
[BODY_FORMAT_KEY]: BodyFormat.URLSearchParams,
54-
},
55-
body,
56-
};
57-
case body instanceof File: {
58-
const formData = new FormData();
59-
formData.append(BODY_FORMAL_FILE, body, body.name);
60-
return {
61-
headers: {
62-
[BODY_FORMAT_KEY]: BodyFormat.File,
63-
},
64-
body: formData,
65-
};
66-
}
67-
case body instanceof Blob:
68-
return {
69-
headers: {
70-
[BODY_FORMAT_KEY]: BodyFormat.Blob,
71-
},
72-
body,
73-
};
74-
case body instanceof ArrayBuffer:
75-
return {
76-
headers: {
77-
[BODY_FORMAT_KEY]: BodyFormat.ArrayBuffer,
78-
},
79-
body,
80-
};
81-
case body instanceof Uint8Array:
82-
return {
83-
headers: {
84-
[BODY_FORMAT_KEY]: BodyFormat.Uint8Array,
85-
},
86-
body: new Uint8Array(body),
87-
};
88-
default:
89-
return undefined;
90-
}
91-
}
92-
9329
async function initializeResponse(
9430
base: string,
9531
id: string,
@@ -158,20 +94,8 @@ async function fetchServerFunction(
15894
return response;
15995
}
16096

161-
const contentType = response.headers.get("Content-Type");
16297
const clone = response.clone();
163-
let result;
164-
if (contentType?.startsWith("text/plain")) {
165-
result = await clone.text();
166-
} else if (contentType?.startsWith("application/json")) {
167-
result = await clone.json();
168-
} else if (response.headers.get(BODY_FORMAT_KEY)) {
169-
if (import.meta.env.SEROVAL_MODE === "js") {
170-
result = await deserializeJSStream(instance, clone);
171-
} else {
172-
result = await deserializeJSONStream(clone);
173-
}
174-
}
98+
const result = await extractBody(instance, true, clone);
17599
if (response.headers.has("X-Error")) {
176100
throw result;
177101
}

0 commit comments

Comments
 (0)