Skip to content

Commit

Permalink
RSC preloading mechanism (#258)
Browse files Browse the repository at this point in the history
* ApolloClient: make browser event-replaying logic available in SSR

* RSC preloading mechanism prototype

* fix build, update expected shape

* remove console.log

* progress

* [WIP] queryRef

* tests for both notations

* fix up import

* different resolutions

* test fixups

* integrate `useQueryRefHandlers`

* typings

* fix up test
see apollographql/apollo-client#11772

* merge fixup

* more fixup

* refactor queryRef handling

* add test about referential assumptions

* schema adjustments

* bind `PreloadQuery` to `registerApolloClient`

* move `getClient` into the promise chain

* trigger CI

* adjust shape

* update AC build

* `gql(print(gql`

* pin types

* tweaks

* udpate lockfile

* forbid `nextFetchPolicy` in `PreloadQuery`

* fix up build, bump dep

* use uuid, not useId

* update urls

* disable all kinds of minification

* change transport to events

* simulate GraphQL error, not network error

* use `query` in a test

* undo disabling minification

* add clarifying comment

* Revert "simulate GraphQL error, not network error"

This reverts commit c8a2ad5.

* prevent unhandled promise rejections

* debugging

* Revert "undo disabling minification"

This reverts commit 00585ea.

* test?

* clean up debugging things

* update dependencies

* more version pinning

* update lockfile even more

* also update `react-server-dom-webpack`

* adjust react version for vite-streaming

* TransportedQueryRef: inherit QueryReferenceBase

* split `TransportedQueryReference` type

* queryOptions as props on `PreloadQuery`

* Update packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx

Co-authored-by: Jerel Miller <jerelmiller@gmail.com>

* adjust generic name and comment

* change to QueryRef base type

* rename command to match parent project

* update dependency "@apollo/client": "^3.10.4"

---------

Co-authored-by: Jerel Miller <jerelmiller@gmail.com>
  • Loading branch information
phryneas and jerelmiller authored May 15, 2024
1 parent 8856b72 commit cc189b8
Show file tree
Hide file tree
Showing 50 changed files with 1,227 additions and 372 deletions.
6 changes: 3 additions & 3 deletions examples/app-dir-experiments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "next lint"
},
"dependencies": {
"@apollo/client": "^3.9.9",
"@apollo/client": "3.10.4",
"@apollo/experimental-nextjs-app-support": "workspace:^",
"@apollo/server": "^4.9.5",
"@as-integrations/next": "^3.0.0",
Expand All @@ -24,8 +24,8 @@
"graphql": "^16.6.0",
"html-differ": "^1.4.0",
"next": "^14.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "18.3.0",
"react-dom": "18.3.0",
"server-only": "^0.0.1",
"typescript": "5.4.5"
}
Expand Down
6 changes: 3 additions & 3 deletions examples/hack-the-supergraph-ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "next lint"
},
"dependencies": {
"@apollo/client": "^3.9.9",
"@apollo/client": "3.10.4",
"@apollo/experimental-nextjs-app-support": "workspace:^",
"@apollo/space-kit": "^9.11.0",
"@chakra-ui/next-js": "^2.1.2",
Expand All @@ -28,8 +28,8 @@
"graphql": "^16.6.0",
"js-cookie": "^3.0.1",
"next": "^14.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "18.3.0",
"react-dom": "18.3.0",
"react-icons": "^4.8.0",
"react-rating-stars-component": "^2.2.0",
"typescript": "5.4.5"
Expand Down
6 changes: 3 additions & 3 deletions examples/polls-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"codegen": "graphql-codegen --config codegen.ts"
},
"dependencies": {
"@apollo/client": "^3.9.9",
"@apollo/client": "3.10.4",
"@apollo/experimental-nextjs-app-support": "workspace:^",
"@apollo/server": "^4.9.5",
"@types/node": "20.12.11",
Expand All @@ -28,8 +28,8 @@
"graphql-tag": "^2.12.6",
"next": "^14.1.0",
"postcss": "8.4.23",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "18.3.0",
"react-dom": "18.3.0",
"tailwindcss": "3.3.2",
"typescript": "5.4.5"
},
Expand Down
2 changes: 1 addition & 1 deletion integration-test/experimental-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test": "yarn playwright test"
},
"dependencies": {
"@apollo/client": "^3.9.6",
"@apollo/client": "3.10.4",
"@apollo/client-react-streaming": "*",
"compression": "^1.7.4",
"express": "^4.18.2",
Expand Down
6 changes: 3 additions & 3 deletions integration-test/jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"test": "jest"
},
"dependencies": {
"@apollo/client": "^3.9.6",
"@apollo/client": "3.10.4",
"@apollo/client-react-streaming": "workspace:*",
"@apollo/experimental-nextjs-app-support": "workspace:*",
"@graphql-tools/schema": "^10.0.3",
"graphql-tag": "^2.12.6",
"react": "18.2.0",
"react-dom": "18.2.0"
"react": "18.3.0",
"react-dom": "18.3.0"
},
"devDependencies": {
"@babel/core": "^7.24.0",
Expand Down
10 changes: 5 additions & 5 deletions integration-test/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
"test": "yarn playwright test"
},
"dependencies": {
"@apollo/client": "^3.9.6",
"@apollo/client": "3.10.4",
"@apollo/experimental-nextjs-app-support": "workspace:*",
"@apollo/server": "^4.9.5",
"@as-integrations/next": "^3.0.0",
"@graphql-tools/schema": "^10.0.0",
"@types/node": "20.3.1",
"@types/react": "^18.2.55",
"@types/react-dom": "18.2.6",
"@types/react": "18.3.0",
"@types/react-dom": "18.3.0",
"graphql": "^16.7.1",
"graphql-tag": "^2.12.6",
"next": "^14.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "18.3.0",
"react-dom": "18.3.0",
"react-error-boundary": "^4.0.13",
"ssr-only-secrets": "^0.0.5",
"typescript": "5.1.3"
Expand Down
21 changes: 2 additions & 19 deletions integration-test/nextjs/src/app/cc/ApolloWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import React from "react";
import { ApolloLink, HttpLink, Observable } from "@apollo/client";
import { HttpLink } from "@apollo/client";
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
Expand All @@ -15,29 +15,12 @@ import { delayLink } from "@/shared/delayLink";
import { schema } from "../graphql/schema";

import { useSSROnlySecret } from "ssr-only-secrets";
import { GraphQLError } from "graphql";
import { errorLink } from "../../shared/errorLink";

setVerbosity("debug");
loadDevMessages();
loadErrorMessages();

const errorLink = new ApolloLink((operation, forward) => {
const context = operation.getContext();
if (
context.error === "always" ||
(typeof window === "undefined" && context.error === "ssr") ||
(typeof window !== "undefined" && context.error === "browser")
) {
return new Observable((subscriber) => {
subscriber.next({
data: null,
errors: [new GraphQLError("Simulated error")],
});
});
}
return forward(operation);
});

export function ApolloWrapper({
children,
nonce,
Expand Down
4 changes: 2 additions & 2 deletions integration-test/nextjs/src/app/cc/dynamic/dynamic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { expect } from "@playwright/test";
import { test } from "../../../../fixture";

const regex_connection_closed_early =
/streaming connection closed before server query could be fully transported, rerunning/;
/streaming connection closed before server query could be fully transported, rerunning/i;
const regex_query_error_restart =
/query failed on server, rerunning in browser/;
/query failed on server, rerunning in browser/i;

test.describe("CC dynamic", () => {
test.describe("useSuspenseQuery", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ const QUERY: TypedDocumentNode<Data> = gql`
export const dynamic = "force-dynamic";

export default function Page() {
const [queryRef] = useBackgroundQuery(QUERY, { context: { delay: 2000 } });
const [queryRef] = useBackgroundQuery(QUERY, {
context: { delay: 2000, error: "browser" },
});
return (
<Suspense fallback={<p>loading</p>}>
<DisplayData queryRef={queryRef} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const QUERY: TypedDocumentNode<{
export const dynamic = "force-dynamic";

export default function Page() {
const { data } = useSuspenseQuery(QUERY);
const { data } = useSuspenseQuery(QUERY, {
context: { delay: 1000 },
});
globalThis.hydrationFinished?.();

return (
Expand Down
8 changes: 7 additions & 1 deletion integration-test/nextjs/src/app/graphql/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ const server = new ApolloServer({
schema,
});

const handler = startServerAndCreateNextHandler(server);
const handler = startServerAndCreateNextHandler(server, {
context: async () => {
return {
from: "network",
};
},
});

export async function GET(request: Request) {
return handler(request);
Expand Down
18 changes: 16 additions & 2 deletions integration-test/nextjs/src/app/graphql/schema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { makeExecutableSchema } from "@graphql-tools/schema";
import gql from "graphql-tag";
import * as entryPoint from "@apollo/client-react-streaming";
import type { IResolvers } from "@graphql-tools/utils";

const typeDefs = gql`
type Product {
id: String!
title: String!
}
type Query {
products: [Product!]!
products(someArgument: String): [Product!]!
env: String!
}
`;

Expand Down Expand Up @@ -39,8 +42,19 @@ const resolvers = {
title: "The Apollo Socks",
},
],
env: (source, args, context) => {
return context && context.from === "network"
? "browser"
: "built_for_ssr" in entryPoint
? "SSR"
: "built_for_browser" in entryPoint
? "Browser"
: "built_for_rsc" in entryPoint
? "RSC"
: "unknown";
},
},
};
} satisfies IResolvers;

export const schema = makeExecutableSchema({
typeDefs,
Expand Down
13 changes: 4 additions & 9 deletions integration-test/nextjs/src/app/rsc/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { ApolloClient, InMemoryCache } from "@apollo/client";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";

import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
import { setVerbosity } from "ts-invariant";
import { delayLink } from "@/shared/delayLink";
import { errorLink } from "@/shared/errorLink";
import { SchemaLink } from "@apollo/client/link/schema";

import { schema } from "../graphql/schema";
Expand All @@ -12,15 +13,9 @@ setVerbosity("debug");
loadDevMessages();
loadErrorMessages();

export const { getClient } = registerApolloClient(() => {
export const { getClient, PreloadQuery, query } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: delayLink.concat(
typeof window === "undefined"
? new SchemaLink({ schema })
: new HttpLink({
uri: "/graphql",
})
),
link: delayLink.concat(errorLink.concat(new SchemaLink({ schema }))),
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect } from "@playwright/test";
import { test } from "../../../../../fixture";

test.describe("PreloadQuery", () => {
for (const [decription, path] of [
["with useSuspenseQuery", "useSuspenseQuery"],
["with queryRef and useReadQuery", "queryRef-useReadQuery"],
] as const) {
test.describe(decription, () => {
test("query resolves on the server", async ({ page, blockRequest }) => {
await page.goto(
`/rsc/dynamic/PreloadQuery/${path}?errorIn=ssr,browser`,
{
waitUntil: "commit",
}
);

await expect(page).toBeInitiallyLoading(true);
await expect(page.getByText("loading")).not.toBeVisible();
await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
await expect(
page.getByText("Queried in RSC environment")
).toBeVisible();
});

test("query errors on the server, restarts in the browser", async ({
page,
}) => {
page.allowErrors?.();
await page.goto(`/rsc/dynamic/PreloadQuery/${path}?errorIn=rsc`, {
waitUntil: "commit",
});

await expect(page).toBeInitiallyLoading(true);

await page.waitForEvent("pageerror", (error) => {
return (
/* prod */ error.message.includes("Minified React error #419") ||
/* dev */ error.message.includes("Query failed upstream.")
);
});

await expect(page.getByText("loading")).not.toBeVisible();
await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
await expect(
page.getByText("Queried in Browser environment")
).toBeVisible();
});
});
}
test("queryRef works with useQueryRefHandlers", async ({ page }) => {
await page.goto(`/rsc/dynamic/PreloadQuery/queryRef-useReadQuery`, {
waitUntil: "commit",
});

await expect(page).toBeInitiallyLoading(true);
await expect(page.getByText("loading")).not.toBeVisible();
await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
await expect(page.getByText("Queried in RSC environment")).toBeVisible();

await page.getByRole("button", { name: "refetch" }).click();
await expect(
page.getByText("Queried in Browser environment")
).toBeVisible();
});

test("queryRef: assumptions about referential equality", async ({ page }) => {
await page.goto(`/rsc/dynamic/PreloadQuery/queryRef-refTest`, {
waitUntil: "commit",
});

await page.getByRole("spinbutton").nth(11).waitFor();

for (let i = 0; i < 12; i++) {
await expect(page.getByRole("spinbutton").nth(i)).toHaveClass("valid");
}
});
});
Loading

0 comments on commit cc189b8

Please sign in to comment.