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

rough outline for RSC prefetching #4676

Draft
wants to merge 1 commit into
base: pr/rsc
Choose a base branch
from
Draft
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
75 changes: 75 additions & 0 deletions packages/toolkit/src/query/react/HydrateEndpoints.cc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useStore } from 'react-redux'

export interface EndpointRequest {
apiPath: string
serializedQueryArgs: string
resolvedAndTransformedData: Promise<unknown>
}

interface HydrateEndpointsProps {
immediateRequests: Array<EndpointRequest>
lateRequests: AsyncGenerator<EndpointRequest>
children?: any
}

const seen = new WeakSet<
Array<EndpointRequest> | AsyncGenerator<EndpointRequest>
>()

export function HydrateEndpoints({
immediateRequests,
lateRequests,
children,
}: HydrateEndpointsProps) {
if (!seen.has(immediateRequests)) {
seen.add(immediateRequests)
for (const request of immediateRequests) {
handleRequest(request)
}
}
if (!seen.has(lateRequests)) {
seen.add(lateRequests)
handleLateRequests()
async function handleLateRequests() {
for await (const request of lateRequests) {
for (const request of immediateRequests) {
handleRequest(request)
}
}
}
}
const store = useStore()
return children

async function handleRequest(request: EndpointRequest) {
store.dispatch({
type: 'simulate-endpoint-start',
payload: {
serializedQueryArgs: request.serializedQueryArgs,
apiPath: request.apiPath,
},
})
try {
const data = await request.resolvedAndTransformedData
store.dispatch({
type: 'simulate-endpoint-success',
payload: {
data,
serializedQueryArgs: request.serializedQueryArgs,
apiPath: request.apiPath,
},
})
} catch (error) {
store.dispatch({
type: 'simulate-endpoint-error',
payload: {
serializedQueryArgs: request.serializedQueryArgs,
apiPath: request.apiPath,
// no error details here as it won't be transported over by React
// to not leak sensitive information from the server
// that's a good thing
},
})
}
}
}
103 changes: 103 additions & 0 deletions packages/toolkit/src/query/react/PrefetchEndpoints.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { createApi } from '.'
import type { BaseQueryFn } from '../baseQueryTypes'
import type { ApiEndpointQuery } from '../core'
import type { QueryDefinition } from '../endpointDefinitions'
import { fetchBaseQuery } from '../fetchBaseQuery'
import type { EndpointRequest } from './HydrateEndpoints.cc'
// this needs to be a separately bundled entry point prefixed with "use client"
import { HydrateEndpoints } from './HydrateEndpoints.cc'

interface PrefetchEndpointsProps<BaseQuery extends BaseQueryFn> {
baseQuery: BaseQueryFn
run: (
prefetchEndpoint: <QueryArg, ReturnType>(
endpoint: ApiEndpointQuery<
QueryDefinition<QueryArg, BaseQuery, any, ReturnType, any>,
any
>,
arg: QueryArg,
) => Promise<ReturnType>,
) => Promise<void> | undefined
children?: any
}

export function PrefetchEndpoints<BaseQuery extends BaseQueryFn>({
baseQuery,
run,
children,
}: PrefetchEndpointsProps<BaseQuery>) {
const immediateRequests: Array<EndpointRequest> = []
const lateRequests = generateRequests()
async function* generateRequests(): AsyncGenerator<EndpointRequest> {
let resolveNext: undefined | PromiseWithResolvers<EndpointRequest>
const running = run((endpoint, arg) => {
// something something magic
const request = {
serializedQueryArgs: '...',
resolvedAndTransformedData: {}, // ...
} as any as EndpointRequest
if (!resolveNext) {
immediateRequests.push(request)
} else {
const oldResolveNext = resolveNext
resolveNext = Promise.withResolvers()
oldResolveNext.resolve(request)
}
return request.resolvedAndTransformedData
})

// not an async function, no need to wait for late requests
if (!running) return

let runningResolved = false
running.then(() => {
runningResolved = true
})

resolveNext = Promise.withResolvers()
while (!runningResolved) {
yield await resolveNext.promise
}
}
return (
<HydrateEndpoints
immediateRequests={immediateRequests}
lateRequests={lateRequests}
>
{children}
</HydrateEndpoints>
)
}

// usage:

const baseQuery = fetchBaseQuery()
const api = createApi({
baseQuery,
endpoints: (build) => ({
foo: build.query<string, string>({
query(arg) {
return { url: '/foo' + arg }
},
}),
}),
})

function Page() {
return (
<PrefetchEndpoints
baseQuery={baseQuery}
run={async (prefetch) => {
// immediate prefetching
const promise1 = prefetch(api.endpoints.foo, 'bar')
const promise2 = prefetch(api.endpoints.foo, 'baz')
// and a "dependent endpoint" that can only be prefetched with the result of the first two
const result1 = await promise1
const result2 = await promise2
prefetch(api.endpoints.foo, result1 + result2)
}}
>
foo
</PrefetchEndpoints>
)
}
3 changes: 3 additions & 0 deletions packages/toolkit/src/query/react/cc-entry-point.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use client'

export { HydrateEndpoints } from './HydrateEndpoints.cc.jsx'
Loading