Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSC preloading mechanism #258

Merged
merged 60 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
c6119dd
ApolloClient: make browser event-replaying logic available in SSR
phryneas Mar 27, 2024
08ba665
RSC preloading mechanism prototype
phryneas Mar 27, 2024
047482c
fix build, update expected shape
phryneas Mar 27, 2024
a47cac9
remove console.log
phryneas Mar 27, 2024
e8057fc
progress
phryneas Apr 3, 2024
293e03e
[WIP] queryRef
phryneas Apr 4, 2024
50642e4
tests for both notations
phryneas Apr 5, 2024
fd7a7ed
fix up import
phryneas Apr 5, 2024
558e52c
different resolutions
phryneas Apr 5, 2024
8600cdc
test fixups
phryneas Apr 5, 2024
3a014a4
integrate `useQueryRefHandlers`
phryneas Apr 9, 2024
106e0c3
typings
phryneas Apr 9, 2024
8ac604e
fix up test
phryneas Apr 10, 2024
eb0e41b
Merge remote-tracking branch 'origin/v0.11.0' into pr/rsc-preload
phryneas Apr 11, 2024
750eecb
merge fixup
phryneas Apr 11, 2024
f9ea406
more fixup
phryneas Apr 11, 2024
e583479
refactor queryRef handling
phryneas Apr 12, 2024
4051ae7
add test about referential assumptions
phryneas Apr 12, 2024
6c54609
schema adjustments
phryneas Apr 12, 2024
f566049
bind `PreloadQuery` to `registerApolloClient`
phryneas Apr 15, 2024
4e05fcb
move `getClient` into the promise chain
phryneas Apr 15, 2024
4f65c0f
trigger CI
phryneas Apr 15, 2024
66dffb5
adjust shape
phryneas Apr 15, 2024
4eb91c7
update AC build
phryneas Apr 15, 2024
54f3569
`gql(print(gql`
phryneas Apr 15, 2024
5da45f9
pin types
phryneas Apr 15, 2024
8f3e580
tweaks
phryneas Apr 15, 2024
40d0651
udpate lockfile
phryneas Apr 15, 2024
eb063ab
forbid `nextFetchPolicy` in `PreloadQuery`
phryneas Apr 15, 2024
0916027
fix up build, bump dep
phryneas Apr 16, 2024
4615c72
use uuid, not useId
phryneas Apr 16, 2024
b0339ce
Merge branch 'main' into pr/rsc-preload
phryneas Apr 16, 2024
1548891
update urls
phryneas Apr 16, 2024
8591ba7
disable all kinds of minification
phryneas Apr 16, 2024
902a598
change transport to events
phryneas Apr 16, 2024
c8a2ad5
simulate GraphQL error, not network error
phryneas Apr 17, 2024
15311cf
use `query` in a test
phryneas Apr 18, 2024
00585ea
undo disabling minification
phryneas Apr 18, 2024
0cc5dec
add clarifying comment
phryneas Apr 18, 2024
22b2924
Revert "simulate GraphQL error, not network error"
phryneas Apr 18, 2024
56c0f3f
prevent unhandled promise rejections
phryneas Apr 18, 2024
2177834
debugging
phryneas Apr 18, 2024
7cb215c
Revert "undo disabling minification"
phryneas Apr 18, 2024
3413d9b
test?
phryneas Apr 18, 2024
ab600e5
clean up debugging things
phryneas Apr 18, 2024
0f59a31
Merge branch 'v0.11.0' into pr/rsc-preload
phryneas Apr 26, 2024
e10a769
update dependencies
phryneas Apr 26, 2024
3af3000
more version pinning
phryneas Apr 26, 2024
572f209
update lockfile even more
phryneas Apr 26, 2024
902f673
also update `react-server-dom-webpack`
phryneas Apr 26, 2024
ba35077
adjust react version for vite-streaming
phryneas Apr 26, 2024
2d454ff
TransportedQueryRef: inherit QueryReferenceBase
phryneas May 3, 2024
e64236d
split `TransportedQueryReference` type
phryneas May 3, 2024
e08acd9
queryOptions as props on `PreloadQuery`
phryneas May 3, 2024
1e97538
Update packages/client-react-streaming/src/DataTransportAbstraction/W…
phryneas May 3, 2024
b162c52
adjust generic name and comment
phryneas May 3, 2024
4735d7a
change to QueryRef base type
phryneas May 8, 2024
50e1a7c
rename command to match parent project
phryneas May 15, 2024
799a6a1
Merge branch 'v0.11.0' into pr/rsc-preload
phryneas May 15, 2024
74c2fd2
update dependency "@apollo/client": "^3.10.4"
phryneas May 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" },
});
Comment on lines +30 to +32
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an additional guard to ensure this test never makes a query in the browser - not necessary but with the error context flag we now can do it :)

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
Loading