Skip to content

Commit 2aab8d0

Browse files
fix: server functions reference serialization
1 parent fe43f1b commit 2aab8d0

File tree

5 files changed

+182
-24
lines changed

5 files changed

+182
-24
lines changed

e2e/react-start/server-functions/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { Route as IndexRouteImport } from './routes/index'
2525
import { Route as MiddlewareIndexRouteImport } from './routes/middleware/index'
2626
import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index'
2727
import { Route as CookiesIndexRouteImport } from './routes/cookies/index'
28+
import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn'
2829
import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router'
2930
import { Route as CookiesSetRouteImport } from './routes/cookies/set'
3031
import { Route as FormdataRedirectTargetNameRouteImport } from './routes/formdata-redirect/target.$name'
@@ -109,6 +110,11 @@ const CookiesIndexRoute = CookiesIndexRouteImport.update({
109110
path: '/cookies/',
110111
getParentRoute: () => rootRouteImport,
111112
} as any)
113+
const MiddlewareSendServerFnRoute = MiddlewareSendServerFnRouteImport.update({
114+
id: '/middleware/send-serverFn',
115+
path: '/middleware/send-serverFn',
116+
getParentRoute: () => rootRouteImport,
117+
} as any)
112118
const MiddlewareClientMiddlewareRouterRoute =
113119
MiddlewareClientMiddlewareRouterRouteImport.update({
114120
id: '/middleware/client-middleware-router',
@@ -143,6 +149,7 @@ export interface FileRoutesByFullPath {
143149
'/submit-post-formdata': typeof SubmitPostFormdataRoute
144150
'/cookies/set': typeof CookiesSetRoute
145151
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
152+
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
146153
'/cookies': typeof CookiesIndexRoute
147154
'/formdata-redirect': typeof FormdataRedirectIndexRoute
148155
'/middleware': typeof MiddlewareIndexRoute
@@ -164,6 +171,7 @@ export interface FileRoutesByTo {
164171
'/submit-post-formdata': typeof SubmitPostFormdataRoute
165172
'/cookies/set': typeof CookiesSetRoute
166173
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
174+
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
167175
'/cookies': typeof CookiesIndexRoute
168176
'/formdata-redirect': typeof FormdataRedirectIndexRoute
169177
'/middleware': typeof MiddlewareIndexRoute
@@ -186,6 +194,7 @@ export interface FileRoutesById {
186194
'/submit-post-formdata': typeof SubmitPostFormdataRoute
187195
'/cookies/set': typeof CookiesSetRoute
188196
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
197+
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
189198
'/cookies/': typeof CookiesIndexRoute
190199
'/formdata-redirect/': typeof FormdataRedirectIndexRoute
191200
'/middleware/': typeof MiddlewareIndexRoute
@@ -209,6 +218,7 @@ export interface FileRouteTypes {
209218
| '/submit-post-formdata'
210219
| '/cookies/set'
211220
| '/middleware/client-middleware-router'
221+
| '/middleware/send-serverFn'
212222
| '/cookies'
213223
| '/formdata-redirect'
214224
| '/middleware'
@@ -230,6 +240,7 @@ export interface FileRouteTypes {
230240
| '/submit-post-formdata'
231241
| '/cookies/set'
232242
| '/middleware/client-middleware-router'
243+
| '/middleware/send-serverFn'
233244
| '/cookies'
234245
| '/formdata-redirect'
235246
| '/middleware'
@@ -251,6 +262,7 @@ export interface FileRouteTypes {
251262
| '/submit-post-formdata'
252263
| '/cookies/set'
253264
| '/middleware/client-middleware-router'
265+
| '/middleware/send-serverFn'
254266
| '/cookies/'
255267
| '/formdata-redirect/'
256268
| '/middleware/'
@@ -273,6 +285,7 @@ export interface RootRouteChildren {
273285
SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute
274286
CookiesSetRoute: typeof CookiesSetRoute
275287
MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute
288+
MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute
276289
CookiesIndexRoute: typeof CookiesIndexRoute
277290
FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute
278291
MiddlewareIndexRoute: typeof MiddlewareIndexRoute
@@ -393,6 +406,13 @@ declare module '@tanstack/react-router' {
393406
preLoaderRoute: typeof CookiesIndexRouteImport
394407
parentRoute: typeof rootRouteImport
395408
}
409+
'/middleware/send-serverFn': {
410+
id: '/middleware/send-serverFn'
411+
path: '/middleware/send-serverFn'
412+
fullPath: '/middleware/send-serverFn'
413+
preLoaderRoute: typeof MiddlewareSendServerFnRouteImport
414+
parentRoute: typeof rootRouteImport
415+
}
396416
'/middleware/client-middleware-router': {
397417
id: '/middleware/client-middleware-router'
398418
path: '/middleware/client-middleware-router'
@@ -433,6 +453,7 @@ const rootRouteChildren: RootRouteChildren = {
433453
SubmitPostFormdataRoute: SubmitPostFormdataRoute,
434454
CookiesSetRoute: CookiesSetRoute,
435455
MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
456+
MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute,
436457
CookiesIndexRoute: CookiesIndexRoute,
437458
FormdataRedirectIndexRoute: FormdataRedirectIndexRoute,
438459
MiddlewareIndexRoute: MiddlewareIndexRoute,

e2e/react-start/server-functions/src/routes/middleware/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ function RouteComponent() {
1919
Client Middleware has access to router instance
2020
</Route.Link>
2121
</li>
22+
<li>
23+
<Route.Link to="./send-serverFn" data-testid="send-serverFn-link">
24+
Client Middleware can send server function reference in context
25+
</Route.Link>
26+
</li>
2227
</ul>
2328
</div>
2429
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { createMiddleware, createServerFn } from '@tanstack/react-start'
3+
import React from 'react'
4+
5+
const middleware = createMiddleware({ type: 'function' }).client(
6+
async ({ next }) => {
7+
return next({
8+
sendContext: {
9+
serverFn: barFn as any,
10+
},
11+
})
12+
},
13+
)
14+
15+
const fooFn = createServerFn()
16+
.middleware([middleware])
17+
.handler(({ context }) => {
18+
return context.serverFn()
19+
})
20+
const barFn = createServerFn().handler(() => {
21+
return 'bar'
22+
})
23+
24+
export const Route = createFileRoute('/middleware/send-serverFn')({
25+
component: RouteComponent,
26+
loader: async () => ({ serverFnLoaderResult: await fooFn() }),
27+
})
28+
29+
function RouteComponent() {
30+
const [serverFnClientResult, setServerFnClientResult] = React.useState({})
31+
const { serverFnLoaderResult } = Route.useLoaderData()
32+
33+
return (
34+
<div
35+
className="p-2 m-2 grid gap-2"
36+
data-testid="client-middleware-router-route-component"
37+
>
38+
<h3>Send server function in context</h3>
39+
<p>
40+
This component checks that the client middleware can send a reference to
41+
a server function in the context, which can then be invoked in the
42+
server function handler.
43+
</p>
44+
<div>
45+
It should return{' '}
46+
<code>
47+
<pre data-testid="expected-server-fn-result">
48+
{JSON.stringify('bar')}
49+
</pre>
50+
</code>
51+
</div>
52+
<p>
53+
serverFn when invoked in the loader returns:
54+
<br />
55+
<span data-testid="serverFn-loader-result">
56+
{JSON.stringify(serverFnClientResult)}
57+
</span>
58+
</p>
59+
<p>
60+
serverFn when invoked on the client returns:
61+
<br />
62+
<span data-testid="serverFn-client-result">
63+
{JSON.stringify(serverFnLoaderResult)}
64+
</span>
65+
</p>
66+
<button
67+
data-testid="btn-serverFn"
68+
type="button"
69+
className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
70+
onClick={() => {
71+
fooFn().then(setServerFnClientResult)
72+
}}
73+
>
74+
Invoke Server Function
75+
</button>
76+
</div>
77+
)
78+
}

packages/start-client-core/src/serializer.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,17 @@ const serializers = [
203203
// From
204204
(v) => BigInt(v),
205205
),
206+
createSerializer(
207+
// Key
208+
'server-function',
209+
// Check
210+
(v): v is { functionId: string } =>
211+
typeof v === 'function' &&
212+
'functionId' in v &&
213+
typeof v.functionId === 'string',
214+
// To
215+
({ functionId }) => ({ functionId, __serverFn: true }),
216+
// From, dummy impl. the actual server function lookup is done on the server in packages/start-server-core/src/server-functions-handler.ts
217+
(v) => v,
218+
),
206219
] as const

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

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,36 @@ function sanitizeBase(base: string | undefined) {
1515
return base.replace(/^\/|\/$/g, '')
1616
}
1717

18-
export const handleServerAction = async ({ request }: { request: Request }) => {
19-
const controller = new AbortController()
20-
const signal = controller.signal
21-
const abort = () => controller.abort()
22-
request.signal.addEventListener('abort', abort)
18+
async function revive(root: any, reviver?: (key: string, value: any) => any) {
19+
async function reviveNode(holder: any, key: string) {
20+
const value = holder[key]
2321

24-
const method = request.method
25-
const url = new URL(request.url, 'http://localhost:3000')
26-
// extract the serverFnId from the url as host/_serverFn/:serverFnId
27-
// Define a regex to match the path and extract the :thing part
28-
const regex = new RegExp(
29-
`${sanitizeBase(process.env.TSS_SERVER_FN_BASE)}/([^/?#]+)`,
30-
)
22+
if (value && typeof value === 'object') {
23+
await Promise.all(Object.keys(value).map((k) => reviveNode(value, k)))
24+
}
3125

32-
// Execute the regex
33-
const match = url.pathname.match(regex)
34-
const serverFnId = match ? match[1] : null
35-
const search = Object.fromEntries(url.searchParams.entries()) as {
36-
payload?: any
37-
createServerFn?: boolean
26+
if (reviver) {
27+
holder[key] = await reviver(key, holder[key])
28+
}
3829
}
3930

40-
const isCreateServerFn = 'createServerFn' in search
41-
const isRaw = 'raw' in search
31+
const holder = { '': root }
32+
await reviveNode(holder, '')
33+
return holder['']
34+
}
4235

43-
if (typeof serverFnId !== 'string') {
44-
throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
36+
async function reviveServerFns(key: string, value: any) {
37+
if (value && value.__serverFn === true && value.functionId) {
38+
const serverFn = await getServerFnById(value.functionId)
39+
return async (opts: any, signal: any): Promise<any> => {
40+
const result = await serverFn(opts ?? {}, signal)
41+
return result.result
42+
}
4543
}
44+
return value
45+
}
4646

47+
async function getServerFnById(serverFnId: string) {
4748
const { default: serverFnManifest } = await loadVirtualModule(
4849
VIRTUAL_MODULES.serverFnManifest,
4950
)
@@ -62,6 +63,7 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
6263
throw new Error('Server function module not resolved for ' + serverFnId)
6364
}
6465

66+
console.log(fnModule)
6567
const action = fnModule[serverFnInfo.functionName]
6668

6769
if (!action) {
@@ -71,6 +73,45 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
7173
`Server function module export not resolved for serverFn ID: ${serverFnId}`,
7274
)
7375
}
76+
return action
77+
}
78+
79+
async function parsePayload(payload: any) {
80+
const parsedPayload = startSerializer.parse(payload)
81+
await revive(parsedPayload, reviveServerFns)
82+
return parsedPayload
83+
}
84+
85+
export const handleServerAction = async ({ request }: { request: Request }) => {
86+
const controller = new AbortController()
87+
const signal = controller.signal
88+
const abort = () => controller.abort()
89+
request.signal.addEventListener('abort', abort)
90+
91+
const method = request.method
92+
const url = new URL(request.url, 'http://localhost:3000')
93+
// extract the serverFnId from the url as host/_serverFn/:serverFnId
94+
// Define a regex to match the path and extract the :thing part
95+
const regex = new RegExp(
96+
`${sanitizeBase(process.env.TSS_SERVER_FN_BASE)}/([^/?#]+)`,
97+
)
98+
99+
// Execute the regex
100+
const match = url.pathname.match(regex)
101+
const serverFnId = match ? match[1] : null
102+
const search = Object.fromEntries(url.searchParams.entries()) as {
103+
payload?: any
104+
createServerFn?: boolean
105+
}
106+
107+
const isCreateServerFn = 'createServerFn' in search
108+
const isRaw = 'raw' in search
109+
110+
if (typeof serverFnId !== 'string') {
111+
throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
112+
}
113+
114+
const action = await getServerFnById(serverFnId)
74115

75116
// Known FormData 'Content-Type' header values
76117
const formDataContentTypes = [
@@ -109,7 +150,7 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
109150
}
110151

111152
// If there's a payload, we should try to parse it
112-
payload = payload ? startSerializer.parse(payload) : payload
153+
payload = payload ? await parsePayload(payload) : payload
113154

114155
// Send it through!
115156
return await action(payload, signal)
@@ -120,7 +161,7 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
120161

121162
// We should probably try to deserialize the payload
122163
// as JSON, but we'll just pass it through for now.
123-
const payload = startSerializer.parse(jsonPayloadAsString)
164+
const payload = await parsePayload(jsonPayloadAsString)
124165

125166
// If this POST request was created by createServerFn,
126167
// it's payload will be the only argument

0 commit comments

Comments
 (0)