+ This component checks that the client middleware can send a reference to
+ a server function in the context, which can then be invoked in the
+ server function handler.
+
+
+ It should return{' '}
+
+
+ {JSON.stringify('bar')}
+
+
+
+
+ serverFn when invoked in the loader returns:
+
+
+ {JSON.stringify(serverFnClientResult)}
+
+
+
+ serverFn when invoked on the client returns:
+
+
+ {JSON.stringify(serverFnLoaderResult)}
+
+
+
+
+ )
+}
diff --git a/packages/start-client-core/src/serializer.ts b/packages/start-client-core/src/serializer.ts
index 1fb11eead5..bae53d7ce0 100644
--- a/packages/start-client-core/src/serializer.ts
+++ b/packages/start-client-core/src/serializer.ts
@@ -203,4 +203,17 @@ const serializers = [
// From
(v) => BigInt(v),
),
+ createSerializer(
+ // Key
+ 'server-function',
+ // Check
+ (v): v is { functionId: string } =>
+ typeof v === 'function' &&
+ 'functionId' in v &&
+ typeof v.functionId === 'string',
+ // To
+ ({ functionId }) => ({ functionId, __serverFn: true }),
+ // From, dummy impl. the actual server function lookup is done on the server in packages/start-server-core/src/server-functions-handler.ts
+ (v) => v,
+ ),
] as const
diff --git a/packages/start-server-core/src/server-functions-handler.ts b/packages/start-server-core/src/server-functions-handler.ts
index af12972a9a..31dfd599b8 100644
--- a/packages/start-server-core/src/server-functions-handler.ts
+++ b/packages/start-server-core/src/server-functions-handler.ts
@@ -15,35 +15,36 @@ function sanitizeBase(base: string | undefined) {
return base.replace(/^\/|\/$/g, '')
}
-export const handleServerAction = async ({ request }: { request: Request }) => {
- const controller = new AbortController()
- const signal = controller.signal
- const abort = () => controller.abort()
- request.signal.addEventListener('abort', abort)
+async function revive(root: any, reviver?: (key: string, value: any) => any) {
+ async function reviveNode(holder: any, key: string) {
+ const value = holder[key]
- const method = request.method
- const url = new URL(request.url, 'http://localhost:3000')
- // extract the serverFnId from the url as host/_serverFn/:serverFnId
- // Define a regex to match the path and extract the :thing part
- const regex = new RegExp(
- `${sanitizeBase(process.env.TSS_SERVER_FN_BASE)}/([^/?#]+)`,
- )
+ if (value && typeof value === 'object') {
+ await Promise.all(Object.keys(value).map((k) => reviveNode(value, k)))
+ }
- // Execute the regex
- const match = url.pathname.match(regex)
- const serverFnId = match ? match[1] : null
- const search = Object.fromEntries(url.searchParams.entries()) as {
- payload?: any
- createServerFn?: boolean
+ if (reviver) {
+ holder[key] = await reviver(key, holder[key])
+ }
}
- const isCreateServerFn = 'createServerFn' in search
- const isRaw = 'raw' in search
+ const holder = { '': root }
+ await reviveNode(holder, '')
+ return holder['']
+}
- if (typeof serverFnId !== 'string') {
- throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
+async function reviveServerFns(key: string, value: any) {
+ if (value && value.__serverFn === true && value.functionId) {
+ const serverFn = await getServerFnById(value.functionId)
+ return async (opts: any, signal: any): Promise => {
+ const result = await serverFn(opts ?? {}, signal)
+ return result.result
+ }
}
+ return value
+}
+async function getServerFnById(serverFnId: string) {
const { default: serverFnManifest } = await loadVirtualModule(
VIRTUAL_MODULES.serverFnManifest,
)
@@ -71,6 +72,45 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
`Server function module export not resolved for serverFn ID: ${serverFnId}`,
)
}
+ return action
+}
+
+async function parsePayload(payload: any) {
+ const parsedPayload = startSerializer.parse(payload)
+ await revive(parsedPayload, reviveServerFns)
+ return parsedPayload
+}
+
+export const handleServerAction = async ({ request }: { request: Request }) => {
+ const controller = new AbortController()
+ const signal = controller.signal
+ const abort = () => controller.abort()
+ request.signal.addEventListener('abort', abort)
+
+ const method = request.method
+ const url = new URL(request.url, 'http://localhost:3000')
+ // extract the serverFnId from the url as host/_serverFn/:serverFnId
+ // Define a regex to match the path and extract the :thing part
+ const regex = new RegExp(
+ `${sanitizeBase(process.env.TSS_SERVER_FN_BASE)}/([^/?#]+)`,
+ )
+
+ // Execute the regex
+ const match = url.pathname.match(regex)
+ const serverFnId = match ? match[1] : null
+ const search = Object.fromEntries(url.searchParams.entries()) as {
+ payload?: any
+ createServerFn?: boolean
+ }
+
+ const isCreateServerFn = 'createServerFn' in search
+ const isRaw = 'raw' in search
+
+ if (typeof serverFnId !== 'string') {
+ throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
+ }
+
+ const action = await getServerFnById(serverFnId)
// Known FormData 'Content-Type' header values
const formDataContentTypes = [
@@ -109,7 +149,7 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
}
// If there's a payload, we should try to parse it
- payload = payload ? startSerializer.parse(payload) : payload
+ payload = payload ? await parsePayload(payload) : payload
// Send it through!
return await action(payload, signal)
@@ -120,7 +160,7 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
// We should probably try to deserialize the payload
// as JSON, but we'll just pass it through for now.
- const payload = startSerializer.parse(jsonPayloadAsString)
+ const payload = await parsePayload(jsonPayloadAsString)
// If this POST request was created by createServerFn,
// it's payload will be the only argument