Skip to content

Commit 7f71e15

Browse files
jacob-ebeyhi-ogawa
andauthored
feat(rsc): support serialization of Request and Response with loadModuleDevProxy (#1004)
Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com>
1 parent cc77583 commit 7f71e15

File tree

4 files changed

+87
-15
lines changed

4 files changed

+87
-15
lines changed

packages/plugin-rsc/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ The plugin provides an additional helper for multi environment interaction.
228228

229229
This allows importing `ssr` environment module specified by `environments.ssr.build.rollupOptions.input[entryName]` inside `rsc` environment and vice versa.
230230

231-
During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process. When enabling `rsc({ loadModuleDevProxy: true })` plugin option, the loaded module is implemented as a proxy with `fetch`-based RPC to call in node environment on the main Vite process, which for example, allows `rsc` environment inside cloudflare workers to access `ssr` environment on the main Vite process.
231+
During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process. When enabling `rsc({ loadModuleDevProxy: true })` plugin option, the loaded module is implemented as a proxy with `fetch`-based RPC to call in node environment on the main Vite process, which for example, allows `rsc` environment inside cloudflare workers to access `ssr` environment on the main Vite process. This proxy mechanism uses [turbo-stream](https://github.com/jacob-ebey/turbo-stream) for serializing data types beyond JSON, with custom encoders/decoders to additionally support `Request` and `Response` instances.
232232

233233
During production build, this API will be rewritten into a static import of the specified entry of other environment build and the modules are executed inside the same runtime.
234234

packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.rsc.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type RscPayload = {
1919
async function handler(request: Request): Promise<Response> {
2020
// differentiate RSC, SSR, action, etc.
2121
const renderRequest = parseRenderRequest(request)
22+
request = renderRequest.request
2223

2324
// handle server function request
2425
let returnValue: RscPayload['returnValue'] | undefined
@@ -82,19 +83,12 @@ async function handler(request: Request): Promise<Response> {
8283
const { renderHTML } = await import.meta.viteRsc.loadModule<
8384
typeof import('./entry.ssr.tsx')
8485
>('ssr', 'index')
85-
const ssrResult = await renderHTML(rscStream, {
86+
return await renderHTML(rscStream, {
87+
request,
8688
formState,
8789
// allow quick simulation of javascript disabled browser
8890
debugNojs: renderRequest.url.searchParams.has('__nojs'),
8991
})
90-
91-
// respond html
92-
return new Response(ssrResult.stream, {
93-
status: ssrResult.status,
94-
headers: {
95-
'Content-type': 'text/html',
96-
},
97-
})
9892
}
9993

10094
export default {

packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.ssr.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ export type RenderHTML = typeof renderHTML
1010
export async function renderHTML(
1111
rscStream: ReadableStream<Uint8Array>,
1212
options?: {
13+
request: Request
1314
formState?: ReactFormState
1415
nonce?: string
1516
debugNojs?: boolean
1617
},
17-
): Promise<{ stream: ReadableStream<Uint8Array>; status?: number }> {
18+
): Promise<Response> {
1819
// duplicate one RSC stream into two.
1920
// - one for SSR (ReactClient.createFromReadableStream below)
2021
// - another for browser hydration payload by injecting <script>...FLIGHT_DATA...</script>.
@@ -71,5 +72,5 @@ export async function renderHTML(
7172
)
7273
}
7374

74-
return { stream: responseStream, status }
75+
return new Response(responseStream, { status })
7576
}

packages/plugin-rsc/src/utils/rpc.ts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { decode, encode } from 'turbo-stream'
1+
import {
2+
decode,
3+
encode,
4+
type DecodePlugin,
5+
type EncodePlugin,
6+
} from 'turbo-stream'
27

38
type RequestPayload = {
49
method: string
@@ -10,13 +15,74 @@ type ResponsePayload = {
1015
data: any
1116
}
1217

18+
const decodePlugins: DecodePlugin[] = [
19+
(type, ...rest) => {
20+
switch (type) {
21+
case 'Request': {
22+
const [method, url, headers, body] = rest as [
23+
string,
24+
string,
25+
[string, string][],
26+
null | ReadableStream<Uint8Array>,
27+
]
28+
return {
29+
value: new Request(url, {
30+
body,
31+
headers,
32+
method,
33+
// @ts-ignore undici compat
34+
duplex: body ? 'half' : undefined,
35+
}),
36+
}
37+
}
38+
case 'Response': {
39+
const [status, statusText, headers, body] = rest as [
40+
number,
41+
string,
42+
[string, string][],
43+
null | ReadableStream<Uint8Array>,
44+
]
45+
return {
46+
value: new Response(body, {
47+
headers,
48+
status,
49+
statusText,
50+
}),
51+
}
52+
}
53+
}
54+
return false
55+
},
56+
]
57+
58+
const encodePlugins: EncodePlugin[] = [
59+
(obj) => {
60+
if (obj instanceof Request) {
61+
return ['Request', obj.method, obj.url, Array.from(obj.headers), obj.body]
62+
}
63+
if (obj instanceof Response) {
64+
return [
65+
'Response',
66+
obj.status,
67+
obj.statusText,
68+
Array.from(obj.headers),
69+
obj.body,
70+
]
71+
}
72+
return false
73+
},
74+
]
75+
1376
export function createRpcServer<T extends object>(handlers: T) {
1477
return async (request: Request): Promise<Response> => {
1578
if (!request.body) {
1679
throw new Error(`loadModuleDevProxy error: missing request body`)
1780
}
1881
const reqPayload = await decode<RequestPayload>(
1982
request.body.pipeThrough(new TextDecoderStream()),
83+
{
84+
plugins: decodePlugins,
85+
},
2086
)
2187
const handler = (handlers as any)[reqPayload.method]
2288
if (!handler) {
@@ -31,7 +97,12 @@ export function createRpcServer<T extends object>(handlers: T) {
3197
resPayload.ok = false
3298
resPayload.data = e
3399
}
34-
return new Response(encode(resPayload))
100+
return new Response(
101+
encode(resPayload, {
102+
plugins: encodePlugins,
103+
redactErrors: false,
104+
}),
105+
)
35106
}
36107
}
37108

@@ -41,7 +112,10 @@ export function createRpcClient<T>(options: { endpoint: string }): T {
41112
method,
42113
args,
43114
}
44-
const body = encode(reqPayload).pipeThrough(new TextEncoderStream())
115+
const body = encode(reqPayload, {
116+
plugins: encodePlugins,
117+
redactErrors: false,
118+
}).pipeThrough(new TextEncoderStream())
45119
const res = await fetch(options.endpoint, {
46120
method: 'POST',
47121
body,
@@ -55,6 +129,9 @@ export function createRpcClient<T>(options: { endpoint: string }): T {
55129
}
56130
const resPayload = await decode<ResponsePayload>(
57131
res.body.pipeThrough(new TextDecoderStream()),
132+
{
133+
plugins: decodePlugins,
134+
},
58135
)
59136
if (!resPayload.ok) {
60137
throw resPayload.data

0 commit comments

Comments
 (0)