Skip to content

Commit

Permalink
implement multipart streaming for PreloadQuery (#389)
Browse files Browse the repository at this point in the history
Co-authored-by: Jerel Miller <[email protected]>
  • Loading branch information
phryneas and jerelmiller authored Jan 8, 2025
1 parent 9acf761 commit 8209093
Show file tree
Hide file tree
Showing 42 changed files with 1,057 additions and 385 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-ears-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client-react-streaming": minor
---

Implement multipart streaming support for `PreloadQuery`
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.10.4",
"@apollo/client": "^3.11.10",
"@apollo/client-react-streaming": "*",
"compression": "^1.7.4",
"express": "^4.18.2",
Expand Down
1 change: 1 addition & 0 deletions integration-test/jest/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const config = {
testEnvironment: "jsdom",
transformIgnorePatterns: [],
setupFilesAfterEnv: ["<rootDir>/setupAfterEnv.jest.ts"],
};

module.exports = config;
5 changes: 3 additions & 2 deletions integration-test/jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"test": "jest"
},
"dependencies": {
"@apollo/client": "3.10.4",
"@apollo/client": "^3.11.10",
"@apollo/client-react-streaming": "workspace:*",
"@apollo/experimental-nextjs-app-support": "workspace:*",
"@graphql-tools/schema": "^10.0.3",
Expand All @@ -21,6 +21,7 @@
"@testing-library/user-event": "^14.5.2",
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0"
"jest-environment-jsdom": "^29.7.0",
"jest-fixed-jsdom": "^0.0.9"
}
}
3 changes: 3 additions & 0 deletions integration-test/jest/setupAfterEnv.jest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TransformStream } from "node:stream/web";

globalThis.TransformStream = TransformStream;
8 changes: 4 additions & 4 deletions integration-test/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@
"test": "yarn playwright test"
},
"dependencies": {
"@apollo/client": "3.10.4",
"@apollo/client": "^3.11.10",
"@apollo/experimental-nextjs-app-support": "workspace:*",
"@apollo/server": "^4.9.5",
"@as-integrations/next": "^3.0.0",
"@apollo/server": "^4.11.2",
"@as-integrations/next": "^3.2.0",
"@graphql-tools/schema": "^10.0.0",
"@types/node": "20.3.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"graphql": "^16.7.1",
"graphql-tag": "^2.12.6",
"next": "^15.0.0",
"next": "^15.0.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^4.0.13",
Expand Down
7 changes: 4 additions & 3 deletions integration-test/nextjs/src/app/cc/ApolloWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import {
ApolloClient,
} from "@apollo/experimental-nextjs-app-support";

import { SchemaLink } from "@apollo/client/link/schema";

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

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

setVerbosity("debug");
loadDevMessages();
Expand Down Expand Up @@ -47,7 +46,9 @@ export function ApolloWrapper({
link: delayLink
.concat(errorLink)
.concat(
typeof window === "undefined" ? new SchemaLink({ schema }) : httpLink
typeof window === "undefined"
? new IncrementalSchemaLink({ schema })
: httpLink
),
});
}
Expand Down
73 changes: 73 additions & 0 deletions integration-test/nextjs/src/app/graphql/IncrementalSchemaLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
ApolloLink,
FetchResult,
Observable,
Operation,
} from "@apollo/client/index.js";
import type { SchemaLink } from "@apollo/client/link/schema";
import { experimentalExecuteIncrementally, validate } from "graphql";

export class IncrementalSchemaLink extends ApolloLink {
public schema: SchemaLink.Options["schema"];
public rootValue: SchemaLink.Options["rootValue"];
public context: SchemaLink.Options["context"];
public validate: boolean;

constructor(options: SchemaLink.Options) {
super();
this.schema = options.schema;
this.rootValue = options.rootValue;
this.context = options.context;
this.validate = !!options.validate;
}

public request(operation: Operation): Observable<FetchResult> {
return new Observable<FetchResult>((observer) => {
(async () => {
try {
const context = await (typeof this.context === "function"
? this.context(operation)
: this.context);
if (this.validate) {
const validationErrors = validate(this.schema, operation.query);
if (validationErrors.length > 0) {
return { errors: validationErrors };
}
}

if (observer.closed) return;
const data = await experimentalExecuteIncrementally({
schema: this.schema,
document: operation.query,
rootValue: this.rootValue,
contextValue: context,
variableValues: operation.variables,
operationName: operation.operationName,
});

if ("initialResult" in data) {
if (observer.closed) return;
observer.next(data.initialResult);

for await (const value of data.subsequentResults) {
if (observer.closed) return;
observer.next(value);
}

if (observer.closed) return;
observer.complete();
} else {
if (observer.closed) return;

observer.next(data);
observer.complete();
}
} catch (error) {
if (observer.closed) return;

observer.error(error);
}
})();
});
}
}
104 changes: 68 additions & 36 deletions integration-test/nextjs/src/app/graphql/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,86 @@ import * as entryPoint from "@apollo/client-react-streaming";
import type { IResolvers } from "@graphql-tools/utils";

const typeDefs = gql`
directive @defer(
if: Boolean! = true
label: String
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
type RatingWithEnv {
value: String!
env: String!
}
type Product {
id: String!
title: String!
rating(delay: Int!): RatingWithEnv!
}
type Query {
products(someArgument: String): [Product!]!
env: String!
}
`;

const products = [
{
id: "product:5",
title: "Soft Warm Apollo Beanie",
rating: "5/5",
},
{
id: "product:2",
title: "Stainless Steel Water Bottle",
rating: "5/5",
},
{
id: "product:3",
title: "Athletic Baseball Cap",
rating: "5/5",
},
{
id: "product:4",
title: "Baby Onesies",
rating: "cuteness overload",
},
{
id: "product:1",
title: "The Apollo T-Shirt",
rating: "5/5",
},
{
id: "product:6",
title: "The Apollo Socks",
rating: "5/5",
},
];

function getEnv(context?: any) {
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";
}

const resolvers = {
Query: {
products: async () => [
{
id: "product:5",
title: "Soft Warm Apollo Beanie",
},
{
id: "product:2",
title: "Stainless Steel Water Bottle",
},
{
id: "product:3",
title: "Athletic Baseball Cap",
},
{
id: "product:4",
title: "Baby Onesies",
},
{
id: "product:1",
title: "The Apollo T-Shirt",
},
{
id: "product:6",
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";
products: async () => products.map(({ id, title }) => ({ id, title })),
env: (source, args, context) => getEnv(context),
},
Product: {
rating: (source, args, context) => {
return new Promise((resolve) =>
setTimeout(resolve, Math.random() * 2 * args.delay, {
value: products.find((p) => p.id === source.id)?.rating,
env: getEnv(context),
})
);
},
},
} satisfies IResolvers;
Expand Down
6 changes: 4 additions & 2 deletions integration-test/nextjs/src/app/rsc/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ 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";
import { IncrementalSchemaLink } from "../graphql/IncrementalSchemaLink";

setVerbosity("debug");
loadDevMessages();
Expand All @@ -19,6 +19,8 @@ loadErrorMessages();
export const { getClient, PreloadQuery, query } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: delayLink.concat(errorLink.concat(new SchemaLink({ schema }))),
link: delayLink.concat(
errorLink.concat(new IncrementalSchemaLink({ schema }))
),
});
});
Loading

0 comments on commit 8209093

Please sign in to comment.