From 7016ab267883fa7a5045c7b33fe70cf4c3caa483 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 27 Mar 2024 15:57:07 +0100 Subject: [PATCH] RSC preloading mechanism prototype --- .../src/PreloadQuery.tsx | 27 +++++++++ .../client-react-streaming/src/index.cc.ts | 58 +++++++++++++++++++ .../client-react-streaming/src/index.rsc.ts | 1 + .../client-react-streaming/tsup.config.ts | 15 +++++ 4 files changed, 101 insertions(+) create mode 100644 packages/client-react-streaming/src/PreloadQuery.tsx create mode 100644 packages/client-react-streaming/src/index.cc.ts diff --git a/packages/client-react-streaming/src/PreloadQuery.tsx b/packages/client-react-streaming/src/PreloadQuery.tsx new file mode 100644 index 00000000..de4ddee7 --- /dev/null +++ b/packages/client-react-streaming/src/PreloadQuery.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; +import { SimulatePreloadedQuery } from "./index.cc.js"; +import type { ApolloClient, QueryOptions } from "@apollo/client"; +import React from "react"; + +export function PreloadQuery({ + options, + getClient, + children, +}: { + options: QueryOptions; + getClient: () => ApolloClient; + children: ReactNode; +}) { + const resultPromise = getClient().query({ + ...options, + // TODO: create a second Client instance only for `PreloadQuery` calls + // We want to prevent "client" data from leaking into our "RSC" cache, + // as that data should always be strictly separated. + fetchPolicy: "no-cache", + }); + return ( + + {children} + + ); +} diff --git a/packages/client-react-streaming/src/index.cc.ts b/packages/client-react-streaming/src/index.cc.ts new file mode 100644 index 00000000..d58c288f --- /dev/null +++ b/packages/client-react-streaming/src/index.cc.ts @@ -0,0 +1,58 @@ +"use client"; + +import type { FetchResult, WatchQueryOptions } from "@apollo/client/index.js"; +import { useApolloClient } from "@apollo/client/index.js"; +import type { ApolloClient as WrappedApolloClient } from "./DataTransportAbstraction/WrappedApolloClient.js"; +import type { TransportIdentifier } from "./DataTransportAbstraction/DataTransportAbstraction.js"; +import type { QueryManager } from "@apollo/client/core/QueryManager.js"; +import type { ReactNode } from "react"; + +const handledRequests = new WeakMap(); + +export function SimulatePreloadedQuery({ + options, + result, + children, +}: { + options: WatchQueryOptions; + result: Promise; + children: ReactNode; +}) { + const client = useApolloClient() as WrappedApolloClient; + if (!handledRequests.has(options)) { + const id = + `preloadedQuery:${(client["queryManager"] as QueryManager).generateQueryId()}` as TransportIdentifier; + handledRequests.set(options, id); + client.onQueryStarted!({ + type: "started", + id, + options, + }); + result.then( + (result) => { + client.onQueryProgress!({ + type: "data", + id, + result, + }); + client.onQueryProgress!({ + type: "complete", + id, + }); + }, + () => { + // TODO: + // This will restart the query in SSR **and** in the browser. + // Currently there is no way of transporting the result received in SSR to the browser. + // Layers over layers... + // Maybe instead we should just "fail" the simulated request on the SSR level + // and only have it re-attempt in the browser? + client.onQueryProgress!({ + type: "error", + id, + }); + } + ); + } + return children; +} diff --git a/packages/client-react-streaming/src/index.rsc.ts b/packages/client-react-streaming/src/index.rsc.ts index 3abfdd05..2a56393a 100644 --- a/packages/client-react-streaming/src/index.rsc.ts +++ b/packages/client-react-streaming/src/index.rsc.ts @@ -1,2 +1,3 @@ export { registerApolloClient } from "./registerApolloClient.js"; export * from "./index.shared.js"; +export { PreloadQuery } from "./PreloadQuery.js"; diff --git a/packages/client-react-streaming/tsup.config.ts b/packages/client-react-streaming/tsup.config.ts index 625baa6e..4b27bea5 100644 --- a/packages/client-react-streaming/tsup.config.ts +++ b/packages/client-react-streaming/tsup.config.ts @@ -61,6 +61,10 @@ export default defineConfig((options) => { "src/ManualDataTransport/index.ts", "manual-transport.browser" ), + { + ...entry("browser", "src/index.cc.ts", "index.cc"), + treeshake: false, // would remove the "use client" directive + }, ]; }); @@ -74,5 +78,16 @@ const acModuleImports: Plugin = { } return { path: args.path, external: true }; }); + // handle "client component" boundary imports + build.onResolve({ filter: /.cc.js/ }, async (args) => { + console.log(args); + if (build.initialOptions.define["TSUP_FORMAT"] === '"cjs"') { + return { + path: args.path.replace(/\/.cc.js$/, "/.cc.cjs"), + external: true, + }; + } + return { path: args.path, external: true }; + }); }, };