diff --git a/examples/hack-the-supergraph-ssr/app/layout.tsx b/examples/hack-the-supergraph-ssr/app/layout.tsx index ffa68164..b5fdacb6 100644 --- a/examples/hack-the-supergraph-ssr/app/layout.tsx +++ b/examples/hack-the-supergraph-ssr/app/layout.tsx @@ -1,21 +1,87 @@ import { cookies } from "next/headers"; -import { ClientLayout } from "./ClientLayout"; +import { registerApolloClient } from "@apollo/client-react-streaming"; +import { + gql, + ApolloClient, + InMemoryCache, + HttpLink, + TypedDocumentNode, +} from "@apollo/client"; +import { ClientLayout } from "./ClientLayout"; import { ApolloWrapper } from "./ApolloWrapper"; +const { PreloadQuery } = registerApolloClient(() => { + return new ApolloClient({ + cache: new InMemoryCache(), + link: new HttpLink({ + // this needs to be an absolute url, as relative urls cannot be used in SSR + uri: "https://main--hack-the-e-commerce.apollographos.net/graphql", + // you can disable result caching here if you want to + // (this does not work if you are rendering your page with `export const dynamic = "force-static"`) + fetchOptions: { cache: "no-store" }, + // fetchOptions: { headers: { "x-custom-delay": "5000" } }, + }), + }); +}); + +const ProductCardProductFragment: TypedDocumentNode<{ + id: string; + title: string; + description: string; + mediaUrl: string; +}> = gql` + fragment ProductCardProductFragment on Product { + id + title + description + mediaUrl + } +`; + +const ReviewsFragment: TypedDocumentNode<{ + id: string; + reviews: { + rating: number; + }; +}> = gql` + fragment ReviewsFragment on Product { + description + reviews { + rating + } + } +`; + +const GET_LATEST_PRODUCTS: TypedDocumentNode<{ + products: { id: string }[]; +}> = gql` + query HomepageProducts { + products { + id + ...ProductCardProductFragment + ...ReviewsFragment @defer + } + } + ${ProductCardProductFragment} + ${ReviewsFragment} +`; + export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const cookieStore = cookies(); - const delay = Number(cookieStore.get("apollo-x-custom-delay")?.value ?? 1000); + const delay = Number(cookieStore.get("apollo-x-custom-delay")?.value ?? 5000); return ( - {children} + + {children} + diff --git a/examples/hack-the-supergraph-ssr/app/not-found.tsx b/examples/hack-the-supergraph-ssr/app/not-found.tsx index afa86895..98117e00 100644 --- a/examples/hack-the-supergraph-ssr/app/not-found.tsx +++ b/examples/hack-the-supergraph-ssr/app/not-found.tsx @@ -6,11 +6,8 @@ import Error from "./error"; import Link from "next/link"; export const Fallback = () => ( - - - +
hi
+ // ); export default Fallback; diff --git a/examples/hack-the-supergraph-ssr/app/page.tsx b/examples/hack-the-supergraph-ssr/app/page.tsx index 0a4dd36f..b5e814e2 100644 --- a/examples/hack-the-supergraph-ssr/app/page.tsx +++ b/examples/hack-the-supergraph-ssr/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import ProductCard from "../components/ProductCard"; +import ProductCard, { Reviews } from "../components/ProductCard"; import { Heading, SimpleGrid, Stack, Text, VStack } from "@chakra-ui/react"; import { useSuspenseQuery, gql, TypedDocumentNode } from "@apollo/client"; @@ -11,14 +11,15 @@ const GET_LATEST_PRODUCTS: TypedDocumentNode<{ products { id ...ProductCardProductFragment + ...ReviewsFragment @defer } } ${ProductCard.fragments.ProductCardProductFragment} + ${Reviews.fragments.ReviewsFragment} `; export default function HomePage() { - const { data } = useSuspenseQuery(GET_LATEST_PRODUCTS, { - fetchPolicy: "cache-first", - }); + const { data } = useSuspenseQuery(GET_LATEST_PRODUCTS); + return ( diff --git a/examples/hack-the-supergraph-ssr/app/product/[id]/page.tsx b/examples/hack-the-supergraph-ssr/app/product/[id]/page.tsx index 516c9077..23601cc5 100644 --- a/examples/hack-the-supergraph-ssr/app/product/[id]/page.tsx +++ b/examples/hack-the-supergraph-ssr/app/product/[id]/page.tsx @@ -31,7 +31,6 @@ const GET_PRODUCT_DETAILS: TypedDocumentNode<{ }; }> = gql` fragment ProductFragment on Product { - averageRating reviews { content rating diff --git a/examples/hack-the-supergraph-ssr/components/DelaySlider.tsx b/examples/hack-the-supergraph-ssr/components/DelaySlider.tsx index 33d8bc13..8b8a1549 100644 --- a/examples/hack-the-supergraph-ssr/components/DelaySlider.tsx +++ b/examples/hack-the-supergraph-ssr/components/DelaySlider.tsx @@ -25,7 +25,7 @@ export default function DelaySlider() { Custom @defer Delay: {delay}ms - + diff --git a/examples/hack-the-supergraph-ssr/components/ProductCard.tsx b/examples/hack-the-supergraph-ssr/components/ProductCard.tsx index 06650aea..86ba81f2 100644 --- a/examples/hack-the-supergraph-ssr/components/ProductCard.tsx +++ b/examples/hack-the-supergraph-ssr/components/ProductCard.tsx @@ -1,3 +1,4 @@ +import { Suspense } from "react"; import Button from "./Button"; import ReviewRating from "./ReviewRating"; import { @@ -9,22 +10,33 @@ import { usePrefersReducedMotion, } from "@chakra-ui/react"; import Link from "next/link"; -import { useFragment, TypedDocumentNode, gql } from "@apollo/client"; +import { useSuspenseFragment, TypedDocumentNode, gql } from "@apollo/client"; const ProductCardProductFragment: TypedDocumentNode<{ id: string; title: string; description: string; mediaUrl: string; - averageRating: number; }> = gql` fragment ProductCardProductFragment on Product { id title description mediaUrl - ... @defer { - averageRating + } +`; + +const ReviewsFragment: TypedDocumentNode<{ + id: string; + description: string; + reviews: Array<{ + rating: number; + }>; +}> = gql` + fragment ReviewsFragment on Product { + description + reviews { + rating } } `; @@ -32,7 +44,7 @@ const ProductCardProductFragment: TypedDocumentNode<{ function ProductCard({ id }: { id: string }) { const prefersReducedMotion = usePrefersReducedMotion(); - const { data } = useFragment({ + const { data } = useSuspenseFragment({ fragment: ProductCardProductFragment, from: `Product:${id}`, }); @@ -60,24 +72,52 @@ function ProductCard({ id }: { id: string }) { {data?.title} - {data?.averageRating ? ( - - {`"${data?.description}"`} - - - - - - ) : ( - - - - )} + + + ); } +export function Reviews({ id }: { id: string }) { + const { data } = useSuspenseFragment({ + fragment: ReviewsFragment, + from: `Product:${id}`, + }); + + // console.log({ data }); + + const average = (array: Array) => + array.reduce((a, b) => a + b) / array.length; + + const averageRating = data.reviews.length + ? average(data.reviews.map((review) => review.rating)) + : 0; + + // console.log({ averageRating }); + + return ( + <> + {averageRating ? ( + + {`"${data?.description}"`} + + + + + + ) : ( + + + + )} + + ); +} + +Reviews.fragments = { ReviewsFragment }; + ProductCard.fragments = { ProductCardProductFragment, }; diff --git a/examples/hack-the-supergraph-ssr/components/SettingsModal.tsx b/examples/hack-the-supergraph-ssr/components/SettingsModal.tsx index cace5837..77203ee1 100644 --- a/examples/hack-the-supergraph-ssr/components/SettingsModal.tsx +++ b/examples/hack-the-supergraph-ssr/components/SettingsModal.tsx @@ -1,3 +1,4 @@ +"use client"; import DelaySlider from "./DelaySlider"; import { Button, diff --git a/examples/hack-the-supergraph-ssr/package.json b/examples/hack-the-supergraph-ssr/package.json index 69a58a4e..eff8f912 100644 --- a/examples/hack-the-supergraph-ssr/package.json +++ b/examples/hack-the-supergraph-ssr/package.json @@ -4,13 +4,14 @@ "private": true, "scripts": { "dev": "next dev", - "prebuild": "yarn workspace @apollo/experimental-nextjs-app-support run build", + "prebuild": "yarn workspace @apollo/experimental-nextjs-app-support run build && yarn workspace @apollo/client-react-streaming run build", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { - "@apollo/client": "3.10.4", + "@apollo/client": "https://pkg.pr.new/@apollo/client@12066", + "@apollo/client-react-streaming": "workspace:^", "@apollo/experimental-nextjs-app-support": "workspace:^", "@apollo/space-kit": "^9.11.0", "@chakra-ui/next-js": "^2.1.2", diff --git a/package.json b/package.json index 95c7b55b..7869a1f6 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "build:docmodel": "yarn workspaces foreach --all --include \"@apollo/*\" exec api-extractor run" }, "resolutions": { + "@apollo/client": "https://pkg.pr.new/@apollo/client@12066", "react": "19.0.0-rc-935180c7e0-20240524", "react-dom": "19.0.0-rc-935180c7e0-20240524", "react-server-dom-webpack": "19.0.0-beta-94eed63c49-20240425", diff --git a/packages/client-react-streaming/src/PreloadQuery.tsx b/packages/client-react-streaming/src/PreloadQuery.tsx index 628d037a..30db778e 100644 --- a/packages/client-react-streaming/src/PreloadQuery.tsx +++ b/packages/client-react-streaming/src/PreloadQuery.tsx @@ -1,6 +1,8 @@ import { SimulatePreloadedQuery } from "./index.cc.js"; import type { ApolloClient, + ApolloQueryResult, + Observable, OperationVariables, QueryOptions, } from "@apollo/client"; @@ -48,30 +50,88 @@ export function PreloadQuery({ serializeOptions(preloadOptions) ); - const resultPromise = Promise.resolve(getClient()) - .then((client) => client.query(preloadOptions)) - .then>, Array>>( - (result) => [ - { type: "data", result: sanitizeForTransport(result) }, - { type: "complete" }, - ], - () => [{ type: "error" }] + // const resultPromise = Promise.resolve(getClient()) + // .then((client) => client.query(preloadOptions)) + // .then>, Array>>( + // (result) => [ + // { type: "data", result: sanitizeForTransport(result) }, + // { type: "complete" }, + // ], + // () => [{ type: "error" }] + // ); + + type ObservableEvent = + | { type: "error" | "complete" } + | { type: "data"; result: ApolloQueryResult }; + + async function* observableToAsyncEventIterator( + observable: Observable> + ) { + let resolveNext: (value: ObservableEvent) => void; + const promises: Promise>[] = []; + queuePromise(); + + function queuePromise() { + promises.push( + new Promise>((resolve) => { + resolveNext = (event: ObservableEvent) => { + resolve(event); + queuePromise(); + }; + }) + ); + } + + observable.subscribe( + (value) => + resolveNext({ type: "data", result: sanitizeForTransport(value) }), + () => resolveNext({ type: "error" }), + () => resolveNext({ type: "complete" }) ); + yield "initialization value" as unknown as Promise>; + + while (true) { + const val = await promises.shift()!; + yield val; + } + } + + async function* resultAsyncGeneratorFunction(): AsyncGenerator< + Omit + > { + const client = await getClient(); + + const obsQuery = client.watchQuery(preloadOptions); + + const asyncEventIterator = observableToAsyncEventIterator(obsQuery); + + for await (const event of asyncEventIterator) { + yield event; + const cacheDiff = client.cache.diff({ + query: preloadOptions.query, + optimistic: false, + // variables: preloadOptions.variables, + }); + if (cacheDiff.complete || event.type === "error") { + return; + } + } + } const queryKey = crypto.randomUUID(); return ( options={transportedOptions} - result={resultPromise} + result={resultAsyncGeneratorFunction()} queryKey={typeof children === "function" ? queryKey : undefined} > {typeof children === "function" ? children( createTransportedQueryRef( transportedOptions, - queryKey, - resultPromise + queryKey + // resultAsyncGeneratorFunction ) ) : children} diff --git a/packages/client-react-streaming/src/SimulatePreloadedQuery.cc.ts b/packages/client-react-streaming/src/SimulatePreloadedQuery.cc.ts index b7122b4e..6b20defb 100644 --- a/packages/client-react-streaming/src/SimulatePreloadedQuery.cc.ts +++ b/packages/client-react-streaming/src/SimulatePreloadedQuery.cc.ts @@ -15,7 +15,7 @@ import { type TransportedOptions, } from "./DataTransportAbstraction/transportedOptions.js"; import type { QueryManager } from "@apollo/client/core/QueryManager.js"; -import { useMemo } from "react"; +import { useMemo, useEffect } from "react"; import type { ReactNode } from "react"; import invariant from "ts-invariant"; import type { TransportedQueryRefOptions } from "./transportedQueryRef.js"; @@ -30,11 +30,13 @@ export default function SimulatePreloadedQuery({ queryKey, }: { options: TransportedQueryRefOptions; - result: Promise>>; + // result: Promise>>; + result: AsyncGenerator>; children: ReactNode; queryKey?: string; }) { const client = useApolloClient() as WrappedApolloClient; + if (!handledRequests.has(options)) { const id = `preloadedQuery:${(client["queryManager"] as QueryManager).generateQueryId()}` as TransportIdentifier; @@ -49,12 +51,15 @@ export default function SimulatePreloadedQuery({ options, }); - result.then((results) => { - invariant.debug("Preloaded query %s: received events: %o", id, results); - for (const event of results) { - client.onQueryProgress!({ ...event, id } as ProgressEvent); + // eslint-disable-next-line no-inner-declarations + async function consume() { + for await (const chunk of result) { + invariant.debug("Preloaded query %s: received events: %o", id, chunk); + client.onQueryProgress!({ ...chunk, id } as ProgressEvent); } - }); + } + + consume(); } const bgQueryArgs = useMemo>(() => { diff --git a/packages/client-react-streaming/src/transportedQueryRef.ts b/packages/client-react-streaming/src/transportedQueryRef.ts index 5100df2b..1681435a 100644 --- a/packages/client-react-streaming/src/transportedQueryRef.ts +++ b/packages/client-react-streaming/src/transportedQueryRef.ts @@ -50,8 +50,8 @@ export interface InternalTransportedQueryRef< export function createTransportedQueryRef( options: TransportedQueryRefOptions, - queryKey: string, - _promise: Promise + queryKey: string + // _promise: Promise ): InternalTransportedQueryRef { const ref: InternalTransportedQueryRef = { __transportedQueryRef: true, diff --git a/yarn.lock b/yarn.lock index 2389574c..8c145f8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45,7 +45,7 @@ __metadata: languageName: node linkType: hard -"@apollo/client-react-streaming@workspace:*, @apollo/client-react-streaming@workspace:packages/client-react-streaming": +"@apollo/client-react-streaming@workspace:*, @apollo/client-react-streaming@workspace:^, @apollo/client-react-streaming@workspace:packages/client-react-streaming": version: 0.0.0-use.local resolution: "@apollo/client-react-streaming@workspace:packages/client-react-streaming" dependencies: @@ -86,9 +86,9 @@ __metadata: languageName: unknown linkType: soft -"@apollo/client@npm:3.10.4, @apollo/client@npm:^3.10.4": - version: 3.10.4 - resolution: "@apollo/client@npm:3.10.4" +"@apollo/client@https://pkg.pr.new/@apollo/client@12066": + version: 3.11.8 + resolution: "@apollo/client@https://pkg.pr.new/@apollo/client@12066" dependencies: "@graphql-typed-document-node/core": "npm:^3.1.1" "@wry/caches": "npm:^1.0.0" @@ -107,8 +107,8 @@ __metadata: peerDependencies: graphql: ^15.0.0 || ^16.0.0 graphql-ws: ^5.5.5 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 subscriptions-transport-ws: ^0.9.0 || ^0.11.0 peerDependenciesMeta: graphql-ws: @@ -119,7 +119,7 @@ __metadata: optional: true subscriptions-transport-ws: optional: true - checksum: 10/8db77625bb96f3330187a6b45c9792edf338c42d4e48ed66f6b0ce38c7cea503db9a5de27f9987b7d83306201a57f90e8ef7ebc06c8a6899aaadb8a090b175cb + checksum: 10/07390b7bb044f64f5228844a8e5cfa1aa9c13c419afc7d0f32b4d20831a995c0d0c2a044e59211923a33f6407e8131a811587a18fb4adb20137cd134b8e8789e languageName: node linkType: hard @@ -10093,7 +10093,8 @@ __metadata: version: 0.0.0-use.local resolution: "hack-the-supergraph-ssr@workspace:examples/hack-the-supergraph-ssr" dependencies: - "@apollo/client": "npm:3.10.4" + "@apollo/client": "https://pkg.pr.new/@apollo/client@12066" + "@apollo/client-react-streaming": "workspace:^" "@apollo/experimental-nextjs-app-support": "workspace:^" "@apollo/space-kit": "npm:^9.11.0" "@chakra-ui/next-js": "npm:^2.1.2"