-
-
Notifications
You must be signed in to change notification settings - Fork 242
Expand file tree
/
Copy pathrpc.ts
More file actions
124 lines (115 loc) · 2.95 KB
/
rpc.ts
File metadata and controls
124 lines (115 loc) · 2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import {
decode,
encode,
type DecodeOptions,
type DecodePlugin,
type EncodeOptions,
type EncodePlugin,
} from 'turbo-stream'
type RequestPayload = {
method: string
args: any[]
}
type ResponsePayload = {
ok: boolean
data: any
}
export function createRpcServer<T extends object>(handlers: T) {
return async (request: Request): Promise<Response> => {
if (!request.body) {
throw new Error(`loadModuleDevProxy error: missing request body`)
}
const reqPayload = await decode<RequestPayload>(
request.body.pipeThrough(new TextDecoderStream()),
decodeOptions,
)
const handler = (handlers as any)[reqPayload.method]
if (!handler) {
throw new Error(
`loadModuleDevProxy error: unknown method ${reqPayload.method}`,
)
}
const resPayload: ResponsePayload = { ok: true, data: undefined }
try {
resPayload.data = await handler(...reqPayload.args)
} catch (e) {
resPayload.ok = false
resPayload.data = e
}
return new Response(encode(resPayload, encodeOptions))
}
}
export function createRpcClient<T>(options: { endpoint: string }): T {
async function callRpc(method: string, args: any[]) {
const reqPayload: RequestPayload = {
method,
args,
}
const body = encode(reqPayload, encodeOptions).pipeThrough(
new TextEncoderStream(),
)
const res = await fetch(options.endpoint, {
method: 'POST',
body,
// @ts-ignore undici compat
duplex: 'half',
})
if (!res.ok || !res.body) {
throw new Error(
`loadModuleDevProxy error: ${res.status} ${res.statusText}`,
)
}
const resPayload = await decode<ResponsePayload>(
res.body.pipeThrough(new TextDecoderStream()),
decodeOptions,
)
if (!resPayload.ok) {
throw resPayload.data
}
return resPayload.data
}
return new Proxy(
{},
{
get(_target, p, _receiver) {
if (typeof p !== 'string' || p === 'then') {
return
}
return (...args: any[]) => callRpc(p, args)
},
},
) as any
}
const encodePlugin: EncodePlugin = (value) => {
if (value instanceof Response) {
const data: ConstructorParameters<typeof Response> = [
value.body,
{
status: value.status,
statusText: value.statusText,
headers: value.headers,
},
]
return ['vite-rsc/response', ...data]
}
if (value instanceof Headers) {
const data: ConstructorParameters<typeof Headers> = [[...value]]
return ['vite-rsc/headers', ...data]
}
}
const decodePlugin: DecodePlugin = (type, ...data) => {
if (type === 'vite-rsc/response') {
const value = new Response(...(data as any))
return { value }
}
if (type === 'vite-rsc/headers') {
const value = new Headers(...(data as any))
return { value }
}
}
const encodeOptions: EncodeOptions = {
plugins: [encodePlugin],
}
const decodeOptions: DecodeOptions = {
plugins: [decodePlugin],
}