Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(rsc): Return flight from RSAs #11403

Merged
merged 4 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions packages/router/src/rsc/ClientRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ReactNode } from 'react'
import React, { useMemo } from 'react'

import { analyzeRoutes } from '../analyzeRoutes.js'
Expand All @@ -8,7 +7,7 @@ import { namedRoutes } from '../namedRoutes.js'
import { RouterContextProvider } from '../router-context.js'
import type { RouterProps } from '../router.js'

import { rscFetch } from './rscFetchForClientRouter.js'
import { RscFetcher } from './RscFetcher.js'

export const Router = ({ useAuth, paramTypes, children }: RouterProps) => {
return (
Expand Down Expand Up @@ -49,9 +48,8 @@ const LocationAwareRouter = ({
// 'No route found for the current URL. Make sure you have a route ' +
// 'defined for the root of your React app.',
// )
return rscFetch('__rwjs__Routes', {
location: { pathname, search },
}) as unknown as ReactNode
const rscProps = { location: { pathname, search } }
return <RscFetcher rscId="__rwjs__Routes" rscProps={rscProps} />
}

const requestedRoute = pathRouteMap[activeRoutePath]
Expand All @@ -72,6 +70,8 @@ const LocationAwareRouter = ({
)
}

const rscProps = { location: { pathname, search } }

return (
<RouterContextProvider
useAuth={useAuth}
Expand All @@ -80,16 +80,19 @@ const LocationAwareRouter = ({
activeRouteName={requestedRoute.name}
>
<AuthenticatedRoute unauthenticated={unauthenticated}>
{rscFetch('__rwjs__Routes', { location: { pathname, search } })}
<RscFetcher rscId="__rwjs__Routes" rscProps={rscProps} />
</AuthenticatedRoute>
</RouterContextProvider>
)
}

// TODO (RSC): Our types dont fully handle async components
return rscFetch('__rwjs__Routes', {
location: { pathname, search },
}) as unknown as ReactNode
const rscProps = { location: { pathname, search } }
return <RscFetcher rscId="__rwjs__Routes" rscProps={rscProps} />
}

export type { RscFetchProps } from './rscFetchForClientRouter.js'
export interface RscFetchProps extends Record<string, unknown> {
location: {
pathname: string
search: string
}
}
144 changes: 144 additions & 0 deletions packages/router/src/rsc/RscFetcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type React from 'react'
import { useState, useEffect } from 'react'

import type { Options } from 'react-server-dom-webpack/client'
import { createFromFetch, encodeReply } from 'react-server-dom-webpack/client'

import { RscCache } from './RscCache.js'

const BASE_PATH = '/rw-rsc/'

const rscCache = new RscCache()

export interface RscProps extends Record<string, unknown> {
location: {
pathname: string
search: string
}
}

export function rscFetch(
rscId: string,
serializedProps: string,
// setComponent?: (component: Thenable<React.ReactElement>) => void,
) {
console.log('rscFetch :: rscId', rscId)
console.log('rscFetch :: props', serializedProps)

// TODO (RSC): The cache key should be rscId + serializedProps
const cached = rscCache.get(serializedProps)
if (cached) {
console.log('rscFetch :: cache hit for', serializedProps)
return cached
}

const searchParams = new URLSearchParams()
searchParams.set('props', serializedProps)

// TODO (RSC): During SSR we should not fetch (Is this function really
// called during SSR?)
const response = fetch(BASE_PATH + rscId + '?' + searchParams, {
headers: {
'rw-rsc': '1',
},
})

const options: Options<unknown[], React.ReactElement> = {
// React will hold on to `callServer` and use that when it detects a
// server action is invoked (like `action={onSubmit}` in a <form>
// element). So for now at least we need to send it with every RSC
// request, so React knows what `callServer` method to use for server
// actions inside the RSC.
callServer: async function (rsaId: string, args: unknown[]) {
// `args` is often going to be an array with just a single element,
// and that element will be FormData
console.log('rscFetchForClientRouter.ts :: callServer')
console.log(' rsaId', rsaId)
console.log(' args', args)

const searchParams = new URLSearchParams()
searchParams.set('action_id', rsaId)
searchParams.set('props', serializedProps)
const id = '_'

let body: Awaited<ReturnType<typeof encodeReply>> = ''

try {
body = await encodeReply(args)
} catch (e) {
console.error('Error encoding Server Action arguments', e)
}

const response = fetch(BASE_PATH + id + '?' + searchParams, {
method: 'POST',
body,
headers: {
'rw-rsc': '1',
},
})

// I'm not sure this recursive use of `options` is needed. I briefly
// tried without it, and things seemed to work. But keeping it for
// now, until we learn more.
const data = createFromFetch(response, options)

const dataValue = await data
console.log(
'rscFetchForClientRuoter.ts :: callServer dataValue',
dataValue,
)
// TODO (RSC): Fix the types for `createFromFetch`
// @ts-expect-error The type is wrong for createFromFetch
const Routes = dataValue.Routes?.[0]
console.log('Routes', Routes)

// TODO (RSC): Figure out how to trigger a rerender of the page with the
// new Routes

// TODO (RSC): Fix the types for `createFromFetch`
// @ts-expect-error The type is wrong for createFromFetch. It can really
// return anything, not just React.ReactElement. It all depends on what
// the server sends back.
return dataValue.__rwjs__rsa_data
},
}

const componentPromise = createFromFetch<never, React.ReactElement>(
response,
options,
)

rscCache.set(serializedProps, componentPromise)

return componentPromise
}

interface Props {
rscId: string
rscProps: RscProps
}

export const RscFetcher = ({ rscId, rscProps }: Props) => {
const serializedProps = JSON.stringify(rscProps)
const [component, setComponent] = useState<any>(() => {
console.log('RscFetcher :: useState callback')

return rscFetch(rscId, serializedProps)
})

console.log('RscFetcher rerender rscId', rscId)
console.log('RscFetcher rerender rscProps', rscProps)

if (!rscCache.get(serializedProps)) {
rscFetch(rscId, serializedProps)
}

useEffect(() => {
console.log('RscFetcher :: useEffect rscProps')
const componentPromise = rscFetch(rscId, serializedProps)
console.log('componentPromise', componentPromise)
setComponent(componentPromise)
}, [rscId, serializedProps])

return component
}
76 changes: 0 additions & 76 deletions packages/router/src/rsc/rscFetchForClientRouter.tsx

This file was deleted.

22 changes: 17 additions & 5 deletions packages/vite/src/rsc/rscWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,11 +399,12 @@ async function renderRsc(input: RenderInput): Promise<PipeableStream> {
const config = await getViteConfig()

const serverRoutes = await getRoutesComponent()
const element = createElement(serverRoutes, input.props)

return renderToPipeableStream(
createElement(serverRoutes, input.props),
getBundlerConfig(config),
)
console.log('rscWorker.ts renderRsc renderRsc props', input.props)
console.log('rscWorker.ts renderRsc element', element)

return renderToPipeableStream(element, getBundlerConfig(config))
// TODO (RSC): We used to transform() the stream here to remove
// "prefixToRemove", which was the common base path to all filenames. We
// then added it back in handleRsa with a simple
Expand Down Expand Up @@ -453,7 +454,18 @@ async function handleRsa(input: RenderInput): Promise<PipeableStream> {
console.log('rscWorker.ts args', ...input.args)

const data = await method(...input.args)
console.log('rscWorker.ts rsa return data', data)
const config = await getViteConfig()

return renderToPipeableStream(data, getBundlerConfig(config))
const serverRoutes = await getRoutesComponent()
console.log('rscWorker.ts handleRsa serverRoutes', serverRoutes)
const elements = {
Routes: createElement(serverRoutes, {
location: { pathname: '/', search: '' },
}),
__rwjs__rsa_data: data,
}
console.log('rscWorker.ts handleRsa elements', elements)

return renderToPipeableStream(elements, getBundlerConfig(config))
}
2 changes: 1 addition & 1 deletion packages/vite/src/rsc/rscWorkerCommunication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Worker } from 'node:worker_threads'

import type { ServerAuthState } from '@redwoodjs/auth/dist/AuthProvider/ServerAuthProvider.js'

import type { RscFetchProps } from '../../../router/src/rsc/rscFetchForClientRouter.jsx'
import type { RscFetchProps } from '../../../router/src/rsc/ClientRouter.tsx'

const workerPath = path.join(
// __dirname. Use fileURLToPath for windows compatibility
Expand Down
Loading