From 814e92dc07ada0e3c9fc4e67e1847f9b339c70bf Mon Sep 17 00:00:00 2001 From: alessia Date: Fri, 26 May 2023 15:54:36 -0400 Subject: [PATCH 01/21] wip --- examples/app-dir-experiments/package.json | 2 +- examples/hack-the-supergraph-ssr/package.json | 2 +- examples/polls-demo/app/cc/apollo-wrapper.tsx | 5 +- examples/polls-demo/app/cc/loading.tsx | 5 - examples/polls-demo/app/cc/poll-cc.tsx | 36 +++++- examples/polls-demo/package.json | 2 +- package/package.json | 5 +- package/src/ssr/ApolloRehydrateSymbols.tsx | 11 +- package/src/ssr/NextSSRApolloClient.tsx | 110 ++++++++++++++++++ package/src/ssr/NextSSRInMemoryCache.tsx | 34 +++--- package/src/ssr/RehydrationContext.tsx | 19 +-- package/src/ssr/dataTransport.ts | 7 +- package/src/ssr/hooks.ts | 5 + package/src/ssr/index.ts | 3 +- package/src/ssr/types.tsx | 8 +- yarn.lock | 58 +++++---- 16 files changed, 245 insertions(+), 67 deletions(-) delete mode 100644 examples/polls-demo/app/cc/loading.tsx create mode 100644 package/src/ssr/NextSSRApolloClient.tsx diff --git a/examples/app-dir-experiments/package.json b/examples/app-dir-experiments/package.json index 971ac0f0..0910bb58 100644 --- a/examples/app-dir-experiments/package.json +++ b/examples/app-dir-experiments/package.json @@ -10,7 +10,7 @@ "lint": "next lint" }, "dependencies": { - "@apollo/client": "3.8.0-alpha.13", + "@apollo/client": "3.8.0-alpha.15", "@apollo/experimental-nextjs-app-support": "workspace:^", "@apollo/server": "^4.6.0", "@as-integrations/next": "^1.3.0", diff --git a/examples/hack-the-supergraph-ssr/package.json b/examples/hack-the-supergraph-ssr/package.json index adbd8e87..ec3c8b8f 100644 --- a/examples/hack-the-supergraph-ssr/package.json +++ b/examples/hack-the-supergraph-ssr/package.json @@ -10,7 +10,7 @@ "lint": "next lint" }, "dependencies": { - "@apollo/client": "3.8.0-alpha.13", + "@apollo/client": "3.8.0-alpha.15", "@apollo/experimental-nextjs-app-support": "workspace:^", "@apollo/space-kit": "^9.11.0", "@chakra-ui/next-js": "^2.1.2", diff --git a/examples/polls-demo/app/cc/apollo-wrapper.tsx b/examples/polls-demo/app/cc/apollo-wrapper.tsx index e59dfcac..58175285 100644 --- a/examples/polls-demo/app/cc/apollo-wrapper.tsx +++ b/examples/polls-demo/app/cc/apollo-wrapper.tsx @@ -1,12 +1,13 @@ "use client"; import { - ApolloClient, ApolloLink, HttpLink, SuspenseCache, + InMemoryCache, } from "@apollo/client"; import { + NextSSRApolloClient, ApolloNextAppProvider, NextSSRInMemoryCache, SSRMultipartLink, @@ -17,7 +18,7 @@ function makeClient() { uri: "https://fragrant-shadow-9470.fly.dev/", }); - return new ApolloClient({ + return new NextSSRApolloClient({ cache: new NextSSRInMemoryCache(), link: typeof window === "undefined" diff --git a/examples/polls-demo/app/cc/loading.tsx b/examples/polls-demo/app/cc/loading.tsx deleted file mode 100644 index 999450c4..00000000 --- a/examples/polls-demo/app/cc/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { PollSkeleton } from "@/components/poll"; - -export default function Loading() { - return ; -} diff --git a/examples/polls-demo/app/cc/poll-cc.tsx b/examples/polls-demo/app/cc/poll-cc.tsx index 7522b0c8..7a673030 100644 --- a/examples/polls-demo/app/cc/poll-cc.tsx +++ b/examples/polls-demo/app/cc/poll-cc.tsx @@ -1,7 +1,14 @@ "use client"; - -import { useSuspenseQuery } from "@apollo/experimental-nextjs-app-support/ssr"; -import { useMutation } from "@apollo/client"; +import { Suspense } from "react"; +import { + useReadQuery, + useSuspenseQuery, +} from "@apollo/experimental-nextjs-app-support/ssr"; +import { + useMutation, + useBackgroundQuery_experimental as useBackgroundQuery, +} from "@apollo/client"; +import { QueryReference } from "@apollo/client/react/cache/QueryReference"; import { Poll as PollInner } from "@/components/poll"; import { useState, useCallback } from "react"; @@ -9,15 +16,32 @@ import { useState, useCallback } from "react"; import { AnswerPollDocument, GetPollDocument, + GetPollQuery, } from "@/components/poll/documents.generated"; export const Poll = () => { - const [showResults, setShowResults] = useState(false); - - const { data } = useSuspenseQuery(GetPollDocument, { + const [queryRef] = useBackgroundQuery(GetPollDocument, { variables: { id: "1", delay: 0 }, }); + return ( + <> +

Testing 1234...

+ Loading...}> + + + + ); +}; + +const PollWrapper = ({ + queryRef, +}: { + queryRef: QueryReference; +}) => { + const { data } = useReadQuery(queryRef); + // console.log({ data }); + const [showResults, setShowResults] = useState(false); const [mutate, { loading: mutationLoading }] = useMutation(AnswerPollDocument); diff --git a/examples/polls-demo/package.json b/examples/polls-demo/package.json index 0d601b5c..253b78a3 100644 --- a/examples/polls-demo/package.json +++ b/examples/polls-demo/package.json @@ -34,10 +34,10 @@ "typescript": "5.0.4" }, "devDependencies": { - "@graphql-codegen/typed-document-node": "^4.0.1", "@graphql-codegen/cli": "3.3.1", "@graphql-codegen/client-preset": "3.0.1", "@graphql-codegen/near-operation-file-preset": "^2.5.0", + "@graphql-codegen/typed-document-node": "^4.0.1", "@graphql-codegen/typescript": "3.0.4", "@graphql-codegen/typescript-resolvers": "3.2.1" } diff --git a/package/package.json b/package/package.json index bfda3076..ba3c7a87 100644 --- a/package/package.json +++ b/package/package.json @@ -13,7 +13,6 @@ "rsc", "app-router", "app" - ], "exports": { "./ssr": "./dist/ssr/index.js", @@ -45,7 +44,7 @@ }, "packageManager": "yarn@3.5.0", "devDependencies": { - "@apollo/client": "3.8.0-alpha.13", + "@apollo/client": "3.8.0-alpha.15", "@total-typescript/shoehorn": "^0.1.0", "@tsconfig/recommended": "^1.0.1", "@typescript-eslint/eslint-plugin": "latest", @@ -60,7 +59,7 @@ "vitest": "^0.30.1" }, "peerDependencies": { - "@apollo/client": ">= 3.8.0-alpha.13", + "@apollo/client": ">= 3.8.0-alpha.15", "next": "^13.4.1", "react": "^18" }, diff --git a/package/src/ssr/ApolloRehydrateSymbols.tsx b/package/src/ssr/ApolloRehydrateSymbols.tsx index 5a83c98e..9414ccde 100644 --- a/package/src/ssr/ApolloRehydrateSymbols.tsx +++ b/package/src/ssr/ApolloRehydrateSymbols.tsx @@ -1,5 +1,9 @@ import { SuperJSONResult } from "superjson/dist/types"; -import { RehydrationCache, ResultsCache } from "./types"; +import { + RehydrationCache, + ResultsCache, + BackgroundQueriesCache, +} from "./types"; import type { DataTransport } from "./dataTransport"; declare global { @@ -7,8 +11,13 @@ declare global { [ApolloRehydrationCache]?: RehydrationCache; [ApolloResultCache]?: ResultsCache; [ApolloSSRDataTransport]?: DataTransport; + // TODO: types + [ApolloBackgroundQueryTransport]?: BackgroundQueriesCache; } } export const ApolloRehydrationCache = Symbol.for("ApolloRehydrationCache"); export const ApolloResultCache = Symbol.for("ApolloResultCache"); export const ApolloSSRDataTransport = Symbol.for("ApolloSSRDataTransport"); +export const ApolloBackgroundQueryTransport = Symbol.for( + "ApolloBackgroundQueryTransport" +); diff --git a/package/src/ssr/NextSSRApolloClient.tsx b/package/src/ssr/NextSSRApolloClient.tsx new file mode 100644 index 00000000..7792d990 --- /dev/null +++ b/package/src/ssr/NextSSRApolloClient.tsx @@ -0,0 +1,110 @@ +import { Trie } from "@wry/trie"; +import { + ApolloClient, + ApolloClientOptions, + OperationVariables, + WatchQueryOptions, +} from "@apollo/client"; +import { canUseWeakMap } from "@apollo/client/utilities"; +import { canonicalStringify } from "@apollo/client/cache"; +import { RehydrationContextValue } from "./types"; +import { registerLateInitializingQueue } from "./lateInitializingQueue"; +import { + ApolloBackgroundQueryTransport, + ApolloResultCache, +} from "./ApolloRehydrateSymbols"; + +// uBQ: we want to skip it in the browser until data comes in +// the first time it renders on the client, if we don't yet have data, +// we tell it to wait for the existing request +// watchQuery: +// watchQuery on the server doesn't do anything new except transport to the +// client which query + variables are in flight +// on the client - the data comes in, creates the fake in-flight observable to +// force the cache to wait for the pending request + +export class NextSSRApolloClient< + TCacheShape +> extends ApolloClient { + private rehydrationContext: Pick< + RehydrationContextValue, + "incomingBackgroundQueries" + > & { uninitialized?: boolean } = { + incomingBackgroundQueries: [], + uninitialized: true, + }; + + private backgroundQueriesAndResults = new Map(); + + constructor(options: ApolloClientOptions) { + super(options); + + this.registerWindowHook(); + } + private cacheKeys = new Trie( + canUseWeakMap, + (cacheKey: any[]) => cacheKey + ); + private registerWindowHook() { + let stableCacheKey: any[] | undefined; + if (typeof window !== "undefined") { + // shared variables - could be a trie + + if (Array.isArray(window[ApolloBackgroundQueryTransport] || [])) { + console.log("background query transport"); + registerLateInitializingQueue( + ApolloBackgroundQueryTransport, + (options) => { + const cacheKey = [ + options.query, + canonicalStringify(options.variables), + ].concat(); + + stableCacheKey = this.cacheKeys.lookupArray(cacheKey); + + // instead of null, set an observable in the map... + this.backgroundQueriesAndResults.set(stableCacheKey, "testing 123"); + } + ); + } + + if (Array.isArray(window[ApolloResultCache] || [])) { + console.log("result cache"); + registerLateInitializingQueue(ApolloResultCache, (data) => { + console.log("cb 2", data); + if (stableCacheKey) { + console.log(this.backgroundQueriesAndResults.get(stableCacheKey)); + // call obserable with result + } + }); + } + } + } + + // cache has the queue of incoming results + // instead of calling this.write in the nextssrinmemorycache, we'd resolve the + // fake observable with the real result data + watchQuery< + T = any, + TVariables extends OperationVariables = OperationVariables + >(options: WatchQueryOptions) { + if (typeof window == "undefined") { + // @ts-ignore + this.rehydrationContext.incomingBackgroundQueries.push(options); + } + if (typeof window !== "undefined") { + console.log("inside watchQuery"); + } + return super.watchQuery(options); + } + + setRehydrationContext(rehydrationContext: RehydrationContextValue) { + if (this.rehydrationContext.uninitialized) { + rehydrationContext.incomingBackgroundQueries.push( + ...this.rehydrationContext.incomingBackgroundQueries + ); + } + this.rehydrationContext = rehydrationContext; + this.rehydrationContext.uninitialized = false; + } +} diff --git a/package/src/ssr/NextSSRInMemoryCache.tsx b/package/src/ssr/NextSSRInMemoryCache.tsx index ac4e2edd..ac46d89b 100644 --- a/package/src/ssr/NextSSRInMemoryCache.tsx +++ b/package/src/ssr/NextSSRInMemoryCache.tsx @@ -4,9 +4,9 @@ import { Cache, Reference, } from "@apollo/client"; -import { ApolloResultCache } from "./ApolloRehydrateSymbols"; import { RehydrationContextValue } from "./types"; -import { registerLateInitializingQueue } from "./lateInitializingQueue"; +// import { ApolloResultCache } from "./ApolloRehydrateSymbols"; +// import { registerLateInitializingQueue } from "./lateInitializingQueue"; export class NextSSRInMemoryCache extends InMemoryCache { private rehydrationContext: Pick< @@ -19,23 +19,25 @@ export class NextSSRInMemoryCache extends InMemoryCache { constructor(config?: InMemoryCacheConfig) { super(config); - this.registerWindowHook(); - } - private registerWindowHook() { - if (typeof window !== "undefined") { - if (Array.isArray(window[ApolloResultCache] || [])) { - registerLateInitializingQueue(ApolloResultCache, (data) => - this.write(data) - ); - } else { - throw new Error( - "On the client side, only one instance of `NextSSRInMemoryCache` can be created!" - ); - } - } + // this.registerWindowHook(); } + // this could be removed here and moved over/merged with the other register window hook fn + // private registerWindowHook() { + // if (typeof window !== "undefined") { + // if (Array.isArray(window[ApolloResultCache] || [])) { + // registerLateInitializingQueue(ApolloResultCache, (data) => + // this.write(data) + // ); + // } else { + // throw new Error( + // "On the client side, only one instance of `NextSSRInMemoryCache` can be created!" + // ); + // } + // } + // } write(options: Cache.WriteOptions): Reference | undefined { + console.log("write"); if (typeof window == "undefined") { this.rehydrationContext.incomingResults.push(options); } diff --git a/package/src/ssr/RehydrationContext.tsx b/package/src/ssr/RehydrationContext.tsx index a2f5d26f..22ae7114 100644 --- a/package/src/ssr/RehydrationContext.tsx +++ b/package/src/ssr/RehydrationContext.tsx @@ -5,6 +5,7 @@ import { ServerInsertedHTMLContext } from "next/navigation"; import { RehydrationContextValue } from "./types"; import { registerDataTransport, transportDataToJS } from "./dataTransport"; import invariant from "ts-invariant"; +import { NextSSRApolloClient } from "./NextSSRApolloClient"; const ApolloRehydrationContext = React.createContext< RehydrationContextValue | undefined @@ -13,19 +14,18 @@ const ApolloRehydrationContext = React.createContext< export const RehydrationContextProvider = ({ children, }: React.PropsWithChildren) => { - const { cache } = useApolloClient(); + const client = useApolloClient(); const rehydrationContext = React.useRef(); if (typeof window == "undefined") { if (!rehydrationContext.current) { rehydrationContext.current = buildApolloRehydrationContext(); } - - if (cache instanceof NextSSRInMemoryCache) { - cache.setRehydrationContext(rehydrationContext.current); - } else { - throw new Error( - "When using Next SSR, you must use the `NextSSRInMemoryCache`" - ); + if (client instanceof NextSSRApolloClient) { + console.log("SET HYDRATION CONTEXT"); + client.setRehydrationContext(rehydrationContext.current); + } + if (client.cache instanceof NextSSRInMemoryCache) { + client.cache.setRehydrationContext(rehydrationContext.current); } } else { registerDataTransport(); @@ -61,6 +61,7 @@ function buildApolloRehydrationContext(): RehydrationContextValue { transportValueData: {}, transportedValues: {}, incomingResults: [], + incomingBackgroundQueries: [], RehydrateOnClient() { rehydrationContext.currentlyInjected = false; if ( @@ -85,6 +86,7 @@ function buildApolloRehydrationContext(): RehydrationContextValue { ) ), results: rehydrationContext.incomingResults, + backgroundQueries: rehydrationContext.incomingBackgroundQueries, }); Object.assign( rehydrationContext.transportedValues, @@ -92,6 +94,7 @@ function buildApolloRehydrationContext(): RehydrationContextValue { ); rehydrationContext.transportValueData = {}; rehydrationContext.incomingResults = []; + rehydrationContext.incomingBackgroundQueries = []; return (