diff --git a/packages/toolkit/src/query/react/HydrateEndpoints.cc.tsx b/packages/toolkit/src/query/react/HydrateEndpoints.cc.tsx new file mode 100644 index 0000000000..68ed2ebd39 --- /dev/null +++ b/packages/toolkit/src/query/react/HydrateEndpoints.cc.tsx @@ -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 + }, + }) + } + } +} diff --git a/packages/toolkit/src/query/react/PrefetchEndpoints.tsx b/packages/toolkit/src/query/react/PrefetchEndpoints.tsx new file mode 100644 index 0000000000..c0164f34a4 --- /dev/null +++ b/packages/toolkit/src/query/react/PrefetchEndpoints.tsx @@ -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> + ) +} diff --git a/packages/toolkit/src/query/react/cc-entry-point.ts b/packages/toolkit/src/query/react/cc-entry-point.ts new file mode 100644 index 0000000000..8ab2d1c778 --- /dev/null +++ b/packages/toolkit/src/query/react/cc-entry-point.ts @@ -0,0 +1,3 @@ +'use client' + +export { HydrateEndpoints } from './HydrateEndpoints.cc.jsx'