Skip to content

Commit c9756bd

Browse files
committed
rough outline for RSC prefetching
1 parent 0056315 commit c9756bd

File tree

3 files changed

+181
-0
lines changed

3 files changed

+181
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useStore } from 'react-redux'
2+
3+
export interface EndpointRequest {
4+
apiPath: string
5+
serializedQueryArgs: string
6+
resolvedAndTransformedData: Promise<unknown>
7+
}
8+
9+
interface HydrateEndpointsProps {
10+
immediateRequests: Array<EndpointRequest>
11+
lateRequests: AsyncGenerator<EndpointRequest>
12+
children?: any
13+
}
14+
15+
const seen = new WeakSet<
16+
Array<EndpointRequest> | AsyncGenerator<EndpointRequest>
17+
>()
18+
19+
export function HydrateEndpoints({
20+
immediateRequests,
21+
lateRequests,
22+
children,
23+
}: HydrateEndpointsProps) {
24+
if (!seen.has(immediateRequests)) {
25+
seen.add(immediateRequests)
26+
for (const request of immediateRequests) {
27+
handleRequest(request)
28+
}
29+
}
30+
if (!seen.has(lateRequests)) {
31+
seen.add(lateRequests)
32+
handleLateRequests()
33+
async function handleLateRequests() {
34+
for await (const request of lateRequests) {
35+
for (const request of immediateRequests) {
36+
handleRequest(request)
37+
}
38+
}
39+
}
40+
}
41+
const store = useStore()
42+
return children
43+
44+
async function handleRequest(request: EndpointRequest) {
45+
store.dispatch({
46+
type: 'simulate-endpoint-start',
47+
payload: {
48+
serializedQueryArgs: request.serializedQueryArgs,
49+
apiPath: request.apiPath,
50+
},
51+
})
52+
try {
53+
const data = await request.resolvedAndTransformedData
54+
store.dispatch({
55+
type: 'simulate-endpoint-success',
56+
payload: {
57+
data,
58+
serializedQueryArgs: request.serializedQueryArgs,
59+
apiPath: request.apiPath,
60+
},
61+
})
62+
} catch (error) {
63+
store.dispatch({
64+
type: 'simulate-endpoint-error',
65+
payload: {
66+
serializedQueryArgs: request.serializedQueryArgs,
67+
apiPath: request.apiPath,
68+
// no error details here as it won't be transported over by React
69+
// to not leak sensitive information from the server
70+
// that's a good thing
71+
},
72+
})
73+
}
74+
}
75+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { createApi } from '.'
2+
import type { BaseQueryFn } from '../baseQueryTypes'
3+
import type { ApiEndpointQuery } from '../core'
4+
import type { QueryDefinition } from '../endpointDefinitions'
5+
import { fetchBaseQuery } from '../fetchBaseQuery'
6+
import type { EndpointRequest } from './HydrateEndpoints.cc'
7+
// this needs to be a separately bundled entry point prefixed with "use client"
8+
import { HydrateEndpoints } from './HydrateEndpoints.cc'
9+
10+
interface PrefetchEndpointsProps<BaseQuery extends BaseQueryFn> {
11+
baseQuery: BaseQueryFn
12+
run: (
13+
prefetchEndpoint: <QueryArg, ReturnType>(
14+
endpoint: ApiEndpointQuery<
15+
QueryDefinition<QueryArg, BaseQuery, any, ReturnType, any>,
16+
any
17+
>,
18+
arg: QueryArg,
19+
) => Promise<ReturnType>,
20+
) => Promise<void> | undefined
21+
children?: any
22+
}
23+
24+
export function PrefetchEndpoints<BaseQuery extends BaseQueryFn>({
25+
baseQuery,
26+
run,
27+
children,
28+
}: PrefetchEndpointsProps<BaseQuery>) {
29+
const immediateRequests: Array<EndpointRequest> = []
30+
const lateRequests = generateRequests()
31+
async function* generateRequests(): AsyncGenerator<EndpointRequest> {
32+
let resolveNext: undefined | PromiseWithResolvers<EndpointRequest>
33+
const running = run((endpoint, arg) => {
34+
// something something magic
35+
const request = {
36+
serializedQueryArgs: '...',
37+
resolvedAndTransformedData: {}, // ...
38+
} as any as EndpointRequest
39+
if (!resolveNext) {
40+
immediateRequests.push(request)
41+
} else {
42+
const oldResolveNext = resolveNext
43+
resolveNext = Promise.withResolvers()
44+
oldResolveNext.resolve(request)
45+
}
46+
return request.resolvedAndTransformedData
47+
})
48+
49+
// not an async function, no need to wait for late requests
50+
if (!running) return
51+
52+
let runningResolved = false
53+
running.then(() => {
54+
runningResolved = true
55+
})
56+
57+
resolveNext = Promise.withResolvers()
58+
while (!runningResolved) {
59+
yield await resolveNext.promise
60+
}
61+
}
62+
return (
63+
<HydrateEndpoints
64+
immediateRequests={immediateRequests}
65+
lateRequests={lateRequests}
66+
>
67+
{children}
68+
</HydrateEndpoints>
69+
)
70+
}
71+
72+
// usage:
73+
74+
const baseQuery = fetchBaseQuery()
75+
const api = createApi({
76+
baseQuery,
77+
endpoints: (build) => ({
78+
foo: build.query<string, string>({
79+
query(arg) {
80+
return { url: '/foo' + arg }
81+
},
82+
}),
83+
}),
84+
})
85+
86+
function Page() {
87+
return (
88+
<PrefetchEndpoints
89+
baseQuery={baseQuery}
90+
run={async (prefetch) => {
91+
// immediate prefetching
92+
const promise1 = prefetch(api.endpoints.foo, 'bar')
93+
const promise2 = prefetch(api.endpoints.foo, 'baz')
94+
// and a "dependent endpoint" that can only be prefetched with the result of the first two
95+
const result1 = await promise1
96+
const result2 = await promise2
97+
prefetch(api.endpoints.foo, result1 + result2)
98+
}}
99+
>
100+
foo
101+
</PrefetchEndpoints>
102+
)
103+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use client'
2+
3+
export { HydrateEndpoints } from './HydrateEndpoints.cc.jsx'

0 commit comments

Comments
 (0)