diff --git a/integration-test/experimental-react/package.json b/integration-test/experimental-react/package.json index 280bdb37..888ebfd4 100644 --- a/integration-test/experimental-react/package.json +++ b/integration-test/experimental-react/package.json @@ -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", diff --git a/integration-test/jest/jest.config.js b/integration-test/jest/jest.config.js index 2f6b727f..7b30848a 100644 --- a/integration-test/jest/jest.config.js +++ b/integration-test/jest/jest.config.js @@ -2,6 +2,7 @@ const config = { testEnvironment: "jsdom", transformIgnorePatterns: [], + setupFilesAfterEnv: ["/setupAfterEnv.jest.ts"], }; module.exports = config; diff --git a/integration-test/jest/package.json b/integration-test/jest/package.json index a3d27ad6..842ce722 100644 --- a/integration-test/jest/package.json +++ b/integration-test/jest/package.json @@ -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", @@ -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" } } diff --git a/integration-test/jest/setupAfterEnv.jest.ts b/integration-test/jest/setupAfterEnv.jest.ts new file mode 100644 index 00000000..009f42e9 --- /dev/null +++ b/integration-test/jest/setupAfterEnv.jest.ts @@ -0,0 +1,3 @@ +import { TransformStream } from "node:stream/web"; + +globalThis.TransformStream = TransformStream; diff --git a/integration-test/nextjs/package.json b/integration-test/nextjs/package.json index 8bec5c94..904f0f43 100644 --- a/integration-test/nextjs/package.json +++ b/integration-test/nextjs/package.json @@ -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", diff --git a/integration-test/nextjs/src/app/cc/ApolloWrapper.tsx b/integration-test/nextjs/src/app/cc/ApolloWrapper.tsx index 76f61b17..04be669d 100644 --- a/integration-test/nextjs/src/app/cc/ApolloWrapper.tsx +++ b/integration-test/nextjs/src/app/cc/ApolloWrapper.tsx @@ -7,8 +7,6 @@ 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"; @@ -16,6 +14,7 @@ import { schema } from "../graphql/schema"; import { useSSROnlySecret } from "ssr-only-secrets"; import { errorLink } from "../../shared/errorLink"; +import { IncrementalSchemaLink } from "../graphql/IncrementalSchemaLink"; setVerbosity("debug"); loadDevMessages(); @@ -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 ), }); } diff --git a/integration-test/nextjs/src/app/graphql/IncrementalSchemaLink.ts b/integration-test/nextjs/src/app/graphql/IncrementalSchemaLink.ts new file mode 100644 index 00000000..49951cbf --- /dev/null +++ b/integration-test/nextjs/src/app/graphql/IncrementalSchemaLink.ts @@ -0,0 +1,92 @@ +import { + ApolloLink, + FetchResult, + Observable, + Operation, +} from "@apollo/client/index.js"; +import type { SchemaLink } from "@apollo/client/link/schema"; +import { + experimentalExecuteIncrementally, + SubsequentIncrementalExecutionResult, + validate, +} from "graphql"; +import { ObjMap } from "graphql/jsutils/ObjMap"; + +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 { + return new Observable((observer) => { + new Promise((resolve) => + resolve( + typeof this.context === "function" + ? this.context(operation) + : this.context + ) + ) + .then((context) => { + if (this.validate) { + const validationErrors = validate(this.schema, operation.query); + if (validationErrors.length > 0) { + return { errors: validationErrors }; + } + } + + return experimentalExecuteIncrementally({ + schema: this.schema, + document: operation.query, + rootValue: this.rootValue, + contextValue: context, + variableValues: operation.variables, + operationName: operation.operationName, + }); + }) + .then((data) => { + if (!observer.closed) { + if ("initialResult" in data) { + observer.next(data.initialResult); + return data.subsequentResults.next().then(function handleChunk( + next: IteratorResult< + SubsequentIncrementalExecutionResult< + ObjMap, + ObjMap + >, + void + > + ): Promise | void { + if (!observer.closed) { + if (next.value) { + observer.next(next.value); + } + if (next.done) { + observer.complete(); + } else { + return data.subsequentResults.next().then(handleChunk); + } + } + }); + } else { + observer.next(data); + observer.complete(); + } + } + }) + .catch((error) => { + if (!observer.closed) { + observer.error(error); + } + }); + }); + } +} diff --git a/integration-test/nextjs/src/app/graphql/schema.ts b/integration-test/nextjs/src/app/graphql/schema.ts index c27d2535..37e65a5c 100644 --- a/integration-test/nextjs/src/app/graphql/schema.ts +++ b/integration-test/nextjs/src/app/graphql/schema.ts @@ -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; diff --git a/integration-test/nextjs/src/app/rsc/client.ts b/integration-test/nextjs/src/app/rsc/client.ts index 9588eb91..63d863db 100644 --- a/integration-test/nextjs/src/app/rsc/client.ts +++ b/integration-test/nextjs/src/app/rsc/client.ts @@ -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(); @@ -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 })) + ), }); }); diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/PreloadQuery.test.ts b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/PreloadQuery.test.ts index 89e59658..19445783 100644 --- a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/PreloadQuery.test.ts +++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/PreloadQuery.test.ts @@ -2,11 +2,11 @@ import { expect } from "@playwright/test"; import { test } from "../../../../../fixture"; test.describe("PreloadQuery", () => { - for (const [decription, path] of [ + for (const [description, path] of [ ["with useSuspenseQuery", "useSuspenseQuery"], ["with queryRef and useReadQuery", "queryRef-useReadQuery"], ] as const) { - test.describe(decription, () => { + test.describe(description, () => { test("query resolves on the server", async ({ page, blockRequest }) => { await page.goto( `/rsc/dynamic/PreloadQuery/${path}?errorIn=ssr,browser`, @@ -23,13 +23,16 @@ test.describe("PreloadQuery", () => { ).toBeVisible(); }); - test("query errors on the server, restarts in the browser", async ({ + test("link chain errors on the server, restarts in the browser", async ({ page, }) => { page.allowErrors?.(); - await page.goto(`/rsc/dynamic/PreloadQuery/${path}?errorIn=rsc`, { - waitUntil: "commit", - }); + await page.goto( + `/rsc/dynamic/PreloadQuery/${path}?errorIn=rsc,network_error`, + { + waitUntil: "commit", + } + ); await expect(page).toBeInitiallyLoading(true); @@ -46,8 +49,67 @@ test.describe("PreloadQuery", () => { page.getByText("Queried in Browser environment") ).toBeVisible(); }); + + if (path === "queryRef-useReadQuery") { + // this only works for `useReadQuery`, because `useSuspenseQuery` won't attach + // to the exact same suspenseCache entry and as a result, it won't get the + // error message from the ReadableStream. + test("graphqlError on the server, transported to the browser, can be restarted", async ({ + page, + }) => { + page.allowErrors?.(); + await page.goto(`/rsc/dynamic/PreloadQuery/${path}?errorIn=rsc`, { + waitUntil: "commit", + }); + + await expect(page).toBeInitiallyLoading(true); + + await expect(page.getByText("loading")).not.toBeVisible(); + + await expect(page.getByText("Encountered an error:")).toBeVisible(); + await expect(page.getByText("Simulated error")).toBeVisible(); + + page.getByRole("button", { name: "Try again" }).click(); + + await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible(); + await expect( + page.getByText("Queried in Browser environment") + ).toBeVisible(); + }); + } else { + // instead, `useSuspenseQuery` will behave as if nothing had been transported + // and rerun the query in the browser. + // there is a chance it will also rerun the query during SSR, that's a timing + // question that might need further investigation + // the bottom line: `PreloadQuery` with `useSuspenseQuery` works in the happy + // path, but it's not as robust as `queryRef` with `useReadQuery`. + test("graphqlError 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", @@ -64,7 +126,9 @@ test.describe("PreloadQuery", () => { ).toBeVisible(); }); - test("queryRef: assumptions about referential equality", async ({ page }) => { + test.skip("queryRef: assumptions about referential equality", async ({ + page, + }) => { await page.goto(`/rsc/dynamic/PreloadQuery/queryRef-refTest`, { waitUntil: "commit", }); diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/defer-queryRef-useReadQuery/ClientChild.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/defer-queryRef-useReadQuery/ClientChild.tsx new file mode 100644 index 00000000..0655eed3 --- /dev/null +++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/defer-queryRef-useReadQuery/ClientChild.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { + useApolloClient, + useQueryRefHandlers, + useReadQuery, +} from "@apollo/client"; +import { DeferredDynamicProductResult } from "../shared"; +import { TransportedQueryRef } from "@apollo/experimental-nextjs-app-support"; +import { useTransition } from "react"; + +export function ClientChild({ + queryRef, +}: { + queryRef: TransportedQueryRef; +}) { + const { refetch } = useQueryRefHandlers(queryRef); + const [refetching, startTransition] = useTransition(); + const { data } = useReadQuery(queryRef); + const client = useApolloClient(); + + return ( + <> +
    + {data.products.map(({ id, title, rating }) => ( +
  • + {title} +
    + Rating:{" "} +
    + {rating?.value || ""} +
    + {rating ? `Queried in ${rating.env} environment` : "loading..."} +
    +
  • + ))} +
+

Queried in {data.env} environment

+ + + ); +} diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/defer-queryRef-useReadQuery/page.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/defer-queryRef-useReadQuery/page.tsx new file mode 100644 index 00000000..1ca54c0e --- /dev/null +++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/defer-queryRef-useReadQuery/page.tsx @@ -0,0 +1,28 @@ +import { ApolloWrapper } from "@/app/cc/ApolloWrapper"; +import { ClientChild } from "./ClientChild"; +import { DEFERRED_QUERY } from "../shared"; + +export const dynamic = "force-dynamic"; +import { PreloadQuery } from "../../../client"; +import { Suspense } from "react"; + +export default async function Page({ searchParams }: { searchParams?: any }) { + return ( + + + {(queryRef) => ( + loading}> + + + )} + + + ); +} diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/error.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/error.tsx new file mode 100644 index 00000000..61c516f8 --- /dev/null +++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/error.tsx @@ -0,0 +1,17 @@ +"use client"; // Error boundaries must be Client Components + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + <> +

Encountered an error:

+
{error.message}
+ + + ); +} diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-useReadQuery/ClientChild.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-useReadQuery/ClientChild.tsx index 88dc9fa5..96ff036d 100644 --- a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-useReadQuery/ClientChild.tsx +++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-useReadQuery/ClientChild.tsx @@ -2,7 +2,7 @@ import { useQueryRefHandlers, useReadQuery } from "@apollo/client"; import { DynamicProductResult } from "../shared"; -import { TransportedQueryRef } from "@apollo/experimental-nextjs-app-support/ssr"; +import { TransportedQueryRef } from "@apollo/experimental-nextjs-app-support"; export function ClientChild({ queryRef, diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-useReadQuery/page.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-useReadQuery/page.tsx index ee7de02d..c89e7fe4 100644 --- a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-useReadQuery/page.tsx +++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-useReadQuery/page.tsx @@ -6,14 +6,14 @@ export const dynamic = "force-dynamic"; import { PreloadQuery } from "../../../client"; import { Suspense } from "react"; -export default function Page({ searchParams }: { searchParams?: any }) { +export default async function Page({ searchParams }: { searchParams?: any }) { return ( {(queryRef) => ( diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/shared.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/shared.tsx index 2b6c1db2..f1b6a026 100644 --- a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/shared.tsx +++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/shared.tsx @@ -19,3 +19,30 @@ export const QUERY: TypedDocumentNode< env } `; + +export interface DeferredDynamicProductResult { + products: { + id: string; + title: string; + rating: undefined | { value: string; env: string }; + }[]; + env: string; +} +export const DEFERRED_QUERY: TypedDocumentNode< + DeferredDynamicProductResult, + { someArgument?: string; delayDeferred: number } +> = gql` + query dynamicProducts($delayDeferred: Int!, $someArgument: String) { + products(someArgument: $someArgument) { + id + title + ... @defer { + rating(delay: $delayDeferred) { + value + env + } + } + } + env + } +`; diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/useSuspenseQuery/page.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/useSuspenseQuery/page.tsx index ea2e632d..6c928f40 100644 --- a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/useSuspenseQuery/page.tsx +++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/useSuspenseQuery/page.tsx @@ -6,14 +6,14 @@ export const dynamic = "force-dynamic"; import { PreloadQuery } from "../../../client"; import { Suspense } from "react"; -export default function Page({ searchParams }: { searchParams?: any }) { +export default async function Page({ searchParams }: { searchParams?: any }) { return ( loading}> diff --git a/integration-test/nextjs/src/shared/errorLink.tsx b/integration-test/nextjs/src/shared/errorLink.tsx index d5b1c14c..bb503b15 100644 --- a/integration-test/nextjs/src/shared/errorLink.tsx +++ b/integration-test/nextjs/src/shared/errorLink.tsx @@ -1,29 +1,46 @@ import { ApolloLink, Observable } from "@apollo/client"; -import { GraphQLError } from "graphql"; +import { GraphQLError, GraphQLFormattedError } from "graphql"; import * as entryPoint from "@apollo/client-react-streaming"; declare module "@apollo/client" { type Env = "ssr" | "browser" | "rsc"; export interface DefaultContext { - error?: "always" | Env | `${Env},${Env}`; + error?: `${"always" | Env | `${Env},${Env}`}${"" | ",network_error"}`; } } export const errorLink = new ApolloLink((operation, forward) => { const context = operation.getContext(); + const errorConditions = context.error?.split(",") || []; if ( - context.error === "always" || - ("built_for_ssr" in entryPoint && - context.error?.split(",").includes("ssr")) || + errorConditions.includes("always") || + ("built_for_ssr" in entryPoint && errorConditions.includes("ssr")) || ("built_for_browser" in entryPoint && - context.error?.split(",").includes("browser")) || - ("built_for_rsc" in entryPoint && context.error?.split(",").includes("rsc")) + errorConditions.includes("browser")) || + ("built_for_rsc" in entryPoint && errorConditions.includes("rsc")) ) { + const env = + "built_for_ssr" in entryPoint + ? "SSR" + : "built_for_browser" in entryPoint + ? "Browser" + : "built_for_rsc" in entryPoint + ? "RSC" + : "unknown"; + return new Observable((subscriber) => { - subscriber.next({ - data: null, - errors: [new GraphQLError("Simulated error")], - }); + if (errorConditions.includes("network_error")) { + subscriber.error(new Error(`Simulated link chain error (${env})`)); + } else { + subscriber.next({ + data: null, + errors: [ + { + message: `Simulated error (${env})`, + } satisfies GraphQLFormattedError as GraphQLError, + ], + }); + } }); } return forward(operation); diff --git a/integration-test/package.json b/integration-test/package.json index c01c32e4..58354b06 100644 --- a/integration-test/package.json +++ b/integration-test/package.json @@ -3,7 +3,8 @@ "packageManager": "yarn@4.2.2", "resolutions": { "@apollo/client-react-streaming": "exec:./shared/build-client-react-streaming.cjs", - "@apollo/experimental-nextjs-app-support": "exec:./shared/build-experimental-nextjs-app-support.cjs" + "@apollo/experimental-nextjs-app-support": "exec:./shared/build-experimental-nextjs-app-support.cjs", + "graphql": "17.0.0-alpha.2" }, "workspaces": [ "*" diff --git a/integration-test/vite-streaming/package.json b/integration-test/vite-streaming/package.json index 0fef07b4..fb78b6b9 100644 --- a/integration-test/vite-streaming/package.json +++ b/integration-test/vite-streaming/package.json @@ -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", diff --git a/integration-test/vitest/package.json b/integration-test/vitest/package.json index 1c890c7a..dd9e8cb9 100644 --- a/integration-test/vitest/package.json +++ b/integration-test/vitest/package.json @@ -4,7 +4,7 @@ "test": "vitest" }, "dependencies": { - "@apollo/client": "3.10.4", + "@apollo/client": "^3.11.10", "@apollo/experimental-nextjs-app-support": "*", "@graphql-tools/schema": "^10.0.3", "graphql-tag": "^2.12.6", diff --git a/integration-test/yarn.lock b/integration-test/yarn.lock index 28360532..b50a8f06 100644 --- a/integration-test/yarn.lock +++ b/integration-test/yarn.lock @@ -32,7 +32,7 @@ __metadata: linkType: hard "@apollo/client-react-streaming@exec:./shared/build-client-react-streaming.cjs::locator=%40integration-test%2Froot%40workspace%3A.": - version: 0.11.7 + version: 0.11.8-alpha.1 resolution: "@apollo/client-react-streaming@exec:./shared/build-client-react-streaming.cjs#./shared/build-client-react-streaming.cjs::hash=4f1b75&locator=%40integration-test%2Froot%40workspace%3A." dependencies: ts-invariant: "npm:^0.10.3" @@ -43,9 +43,9 @@ __metadata: languageName: node linkType: hard -"@apollo/client@npm:3.10.4": - version: 3.10.4 - resolution: "@apollo/client@npm:3.10.4" +"@apollo/client@npm:^3.11.10": + version: 3.11.10 + resolution: "@apollo/client@npm:3.11.10" dependencies: "@graphql-typed-document-node/core": "npm:^3.1.1" "@wry/caches": "npm:^1.0.0" @@ -64,8 +64,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: @@ -76,15 +76,15 @@ __metadata: optional: true subscriptions-transport-ws: optional: true - checksum: 10/8db77625bb96f3330187a6b45c9792edf338c42d4e48ed66f6b0ce38c7cea503db9a5de27f9987b7d83306201a57f90e8ef7ebc06c8a6899aaadb8a090b175cb + checksum: 10/a7e242187e4c198c43b94d57d17c8bda123619977f659fda19c1a1a9dd7351d2d0d73db76e61f0161b4a0a69d8e7d8707a228398f3af4f78103d11843ba67101 languageName: node linkType: hard "@apollo/experimental-nextjs-app-support@exec:./shared/build-experimental-nextjs-app-support.cjs::locator=%40integration-test%2Froot%40workspace%3A.": - version: 0.11.7 + version: 0.11.8-alpha.1 resolution: "@apollo/experimental-nextjs-app-support@exec:./shared/build-experimental-nextjs-app-support.cjs#./shared/build-experimental-nextjs-app-support.cjs::hash=db9e8a&locator=%40integration-test%2Froot%40workspace%3A." dependencies: - "@apollo/client-react-streaming": "npm:0.11.7" + "@apollo/client-react-streaming": "npm:0.11.8-alpha.1" peerDependencies: "@apollo/client": ^3.10.4 next: ^13.4.1 || ^14.0.0 || ^15.0.0-rc.0 @@ -130,9 +130,9 @@ __metadata: languageName: node linkType: hard -"@apollo/server@npm:^4.9.5": - version: 4.10.0 - resolution: "@apollo/server@npm:4.10.0" +"@apollo/server@npm:^4.11.2": + version: 4.11.2 + resolution: "@apollo/server@npm:4.11.2" dependencies: "@apollo/cache-control-types": "npm:^1.0.3" "@apollo/server-gateway-interface": "npm:^1.1.1" @@ -145,13 +145,12 @@ __metadata: "@apollo/utils.usagereporting": "npm:^2.1.0" "@apollo/utils.withrequired": "npm:^2.0.0" "@graphql-tools/schema": "npm:^9.0.0" - "@josephg/resolvable": "npm:^1.0.0" "@types/express": "npm:^4.17.13" "@types/express-serve-static-core": "npm:^4.17.30" "@types/node-fetch": "npm:^2.6.1" async-retry: "npm:^1.2.1" cors: "npm:^2.8.5" - express: "npm:^4.17.1" + express: "npm:^4.21.1" loglevel: "npm:^1.6.8" lru-cache: "npm:^7.10.1" negotiator: "npm:^0.6.3" @@ -161,7 +160,7 @@ __metadata: whatwg-mimetype: "npm:^3.0.0" peerDependencies: graphql: ^16.6.0 - checksum: 10/91c7c5adf56c1edea23a301e290920a4a2e58bad713620908f5e31ef76b439012c7d9628be06c004e04c6b2ec5e575f74e10fe56e2e7adc8c72c44f15610d74b + checksum: 10/88137c67b7777a06e8cec7ad9923d71f7e537a2011d626503208c593cb6f99200834863f72ec4f59005f94081730a376a88b6362a33f5f26b0fb867549fd23e7 languageName: node linkType: hard @@ -285,13 +284,13 @@ __metadata: languageName: node linkType: hard -"@as-integrations/next@npm:^3.0.0": - version: 3.0.0 - resolution: "@as-integrations/next@npm:3.0.0" +"@as-integrations/next@npm:^3.2.0": + version: 3.2.0 + resolution: "@as-integrations/next@npm:3.2.0" peerDependencies: "@apollo/server": ^4.0.0 - next: ^12.0.0 || ^13.0.0 || ^14.0.0 - checksum: 10/463023499dd4d0170bc03d899aa471b17728edf71b259445035fb0ef62a064a546331b8737952deccfbe3bd01b2f4db802391dfe8228960aec467620ea4e6252 + next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 + checksum: 10/0ed9eb1a01961992e4d2d859f1b4c69f6a56d0b5009d473db5989b3202596ab21dfad51f078cb7f4bdf153acda048bb27d557f21fa348bcccbc8e08bb9ff5dfb languageName: node linkType: hard @@ -2162,7 +2161,7 @@ __metadata: version: 0.0.0-use.local resolution: "@integration-test/experimental-react@workspace:experimental-react" dependencies: - "@apollo/client": "npm:3.10.4" + "@apollo/client": "npm:^3.11.10" "@apollo/client-react-streaming": "npm:*" "@playwright/test": "npm:^1.49.1" "@tsconfig/vite-react": "npm:^3.0.0" @@ -2186,7 +2185,7 @@ __metadata: version: 0.0.0-use.local resolution: "@integration-test/jest@workspace:jest" dependencies: - "@apollo/client": "npm:3.10.4" + "@apollo/client": "npm:^3.11.10" "@apollo/client-react-streaming": "workspace:*" "@apollo/experimental-nextjs-app-support": "workspace:*" "@babel/core": "npm:^7.24.0" @@ -2200,6 +2199,7 @@ __metadata: graphql-tag: "npm:^2.12.6" jest: "npm:^29.7.0" jest-environment-jsdom: "npm:^29.7.0" + jest-fixed-jsdom: "npm:^0.0.9" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" languageName: unknown @@ -2209,10 +2209,10 @@ __metadata: version: 0.0.0-use.local resolution: "@integration-test/nextjs@workspace:nextjs" dependencies: - "@apollo/client": "npm:3.10.4" + "@apollo/client": "npm:^3.11.10" "@apollo/experimental-nextjs-app-support": "workspace:*" - "@apollo/server": "npm:^4.9.5" - "@as-integrations/next": "npm:^3.0.0" + "@apollo/server": "npm:^4.11.2" + "@as-integrations/next": "npm:^3.2.0" "@graphql-tools/schema": "npm:^10.0.0" "@playwright/test": "npm:^1.49.1" "@types/node": "npm:20.3.1" @@ -2220,7 +2220,7 @@ __metadata: "@types/react-dom": "npm:^19.0.0" graphql: "npm:^16.7.1" graphql-tag: "npm:^2.12.6" - next: "npm:^15.0.0" + next: "npm:^15.0.3" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" react-error-boundary: "npm:^4.0.13" @@ -2248,7 +2248,7 @@ __metadata: version: 0.0.0-use.local resolution: "@integration-test/vite-streaming@workspace:vite-streaming" dependencies: - "@apollo/client": "npm:3.10.4" + "@apollo/client": "npm:^3.11.10" "@apollo/client-react-streaming": "npm:*" "@playwright/test": "npm:^1.49.1" "@tsconfig/vite-react": "npm:^3.0.0" @@ -2272,7 +2272,7 @@ __metadata: version: 0.0.0-use.local resolution: "@integration-test/vitest@workspace:vitest" dependencies: - "@apollo/client": "npm:3.10.4" + "@apollo/client": "npm:^3.11.10" "@apollo/experimental-nextjs-app-support": "npm:*" "@graphql-tools/schema": "npm:^10.0.3" "@testing-library/jest-dom": "npm:^6.4.2" @@ -2549,13 +2549,6 @@ __metadata: languageName: node linkType: hard -"@josephg/resolvable@npm:^1.0.0": - version: 1.0.1 - resolution: "@josephg/resolvable@npm:1.0.1" - checksum: 10/64eb763b5138bdae4fb59c0c0e89ed261b690917ae6bd777b533257668f151b8868698fb73dfd7665746ad07c7c917fe89ccfdf2404048d39f373f57f1a14e34 - languageName: node - linkType: hard - "@jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" @@ -2598,65 +2591,65 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.0.0": - version: 15.0.0 - resolution: "@next/env@npm:15.0.0" - checksum: 10/6ba449ba35edfc520db51bcc5fa4352647be954ccfee7b073a6c36a2e9a3ebb294c01b3d54268cc01c333ffeba7be0768b5fb190951e48bf25d5031988bab6e3 +"@next/env@npm:15.0.3": + version: 15.0.3 + resolution: "@next/env@npm:15.0.3" + checksum: 10/50103908b2eff0517e267217c866eab6b6f532d44c9d0d71b24d2d5476ad5308a1347ab0b81cdfcd9ebda29517f3703a8af5eaf57987a1335411fb599ed1f321 languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.0.0": - version: 15.0.0 - resolution: "@next/swc-darwin-arm64@npm:15.0.0" +"@next/swc-darwin-arm64@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-darwin-arm64@npm:15.0.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.0.0": - version: 15.0.0 - resolution: "@next/swc-darwin-x64@npm:15.0.0" +"@next/swc-darwin-x64@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-darwin-x64@npm:15.0.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.0.0": - version: 15.0.0 - resolution: "@next/swc-linux-arm64-gnu@npm:15.0.0" +"@next/swc-linux-arm64-gnu@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-linux-arm64-gnu@npm:15.0.3" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.0.0": - version: 15.0.0 - resolution: "@next/swc-linux-arm64-musl@npm:15.0.0" +"@next/swc-linux-arm64-musl@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-linux-arm64-musl@npm:15.0.3" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.0.0": - version: 15.0.0 - resolution: "@next/swc-linux-x64-gnu@npm:15.0.0" +"@next/swc-linux-x64-gnu@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-linux-x64-gnu@npm:15.0.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.0.0": - version: 15.0.0 - resolution: "@next/swc-linux-x64-musl@npm:15.0.0" +"@next/swc-linux-x64-musl@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-linux-x64-musl@npm:15.0.3" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.0.0": - version: 15.0.0 - resolution: "@next/swc-win32-arm64-msvc@npm:15.0.0" +"@next/swc-win32-arm64-msvc@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-win32-arm64-msvc@npm:15.0.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.0.0": - version: 15.0.0 - resolution: "@next/swc-win32-x64-msvc@npm:15.0.0" +"@next/swc-win32-x64-msvc@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-win32-x64-msvc@npm:15.0.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3706,6 +3699,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:1.20.3": + version: 1.20.3 + resolution: "body-parser@npm:1.20.3" + dependencies: + bytes: "npm:3.1.2" + content-type: "npm:~1.0.5" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.4.24" + on-finished: "npm:2.4.1" + qs: "npm:6.13.0" + raw-body: "npm:2.5.2" + type-is: "npm:~1.6.18" + unpipe: "npm:1.0.0" + checksum: 10/8723e3d7a672eb50854327453bed85ac48d045f4958e81e7d470c56bf111f835b97e5b73ae9f6393d0011cc9e252771f46fd281bbabc57d33d3986edf1e6aeca + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -4106,6 +4119,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:0.7.1": + version: 0.7.1 + resolution: "cookie@npm:0.7.1" + checksum: 10/aec6a6aa0781761bf55d60447d6be08861d381136a0fe94aa084fddd4f0300faa2b064df490c6798adfa1ebaef9e0af9b08a189c823e0811b8b313b3d9a03380 + languageName: node + linkType: hard + "core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.34.0": version: 3.36.0 resolution: "core-js-compat@npm:3.36.0" @@ -4410,6 +4430,13 @@ __metadata: languageName: node linkType: hard +"encodeurl@npm:~2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10/abf5cd51b78082cf8af7be6785813c33b6df2068ce5191a40ca8b1afe6a86f9230af9a9ce694a5ce4665955e5c1120871826df9c128a642e09c58d592e2807fe + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -4692,7 +4719,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.1, express@npm:^4.18.2": +"express@npm:^4.18.2": version: 4.18.3 resolution: "express@npm:4.18.3" dependencies: @@ -4731,6 +4758,45 @@ __metadata: languageName: node linkType: hard +"express@npm:^4.21.1": + version: 4.21.1 + resolution: "express@npm:4.21.1" + dependencies: + accepts: "npm:~1.3.8" + array-flatten: "npm:1.1.1" + body-parser: "npm:1.20.3" + content-disposition: "npm:0.5.4" + content-type: "npm:~1.0.4" + cookie: "npm:0.7.1" + cookie-signature: "npm:1.0.6" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + finalhandler: "npm:1.3.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + merge-descriptors: "npm:1.0.3" + methods: "npm:~1.1.2" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + path-to-regexp: "npm:0.1.10" + proxy-addr: "npm:~2.0.7" + qs: "npm:6.13.0" + range-parser: "npm:~1.2.1" + safe-buffer: "npm:5.2.1" + send: "npm:0.19.0" + serve-static: "npm:1.16.2" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + type-is: "npm:~1.6.18" + utils-merge: "npm:1.0.1" + vary: "npm:~1.1.2" + checksum: 10/5d4a36dd03c1d1cce93172e9b185b5cd13a978d29ee03adc51cd278be7b4a514ae2b63e2fdaec0c00fdc95c6cfb396d9dd1da147917ffd337d6cd0778e08c9bc + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -4771,6 +4837,21 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:1.3.1": + version: 1.3.1 + resolution: "finalhandler@npm:1.3.1" + dependencies: + debug: "npm:2.6.9" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + on-finished: "npm:2.4.1" + parseurl: "npm:~1.3.3" + statuses: "npm:2.0.1" + unpipe: "npm:~1.0.0" + checksum: 10/4babe72969b7373b5842bc9f75c3a641a4d0f8eb53af6b89fa714d4460ce03fb92b28de751d12ba415e96e7e02870c436d67412120555e2b382640535697305b + languageName: node + linkType: hard + "find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" @@ -5004,10 +5085,10 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^16.7.1, graphql@npm:^16.8.1": - version: 16.8.1 - resolution: "graphql@npm:16.8.1" - checksum: 10/7a09d3ec5f75061afe2bd2421a2d53cf37273d2ecaad8f34febea1f1ac205dfec2834aec3419fa0a10fcc9fb345863b2f893562fb07ea825da2ae82f6392893c +"graphql@npm:17.0.0-alpha.2": + version: 17.0.0-alpha.2 + resolution: "graphql@npm:17.0.0-alpha.2" + checksum: 10/3bd59bf86eaee169ea710fe6b3d4331d16316eac3551076968c4e2f8ed4a3577bbaef6821ba99fd31ae52d091a914b2ea21c238d60e4892f17467599ca0523ab languageName: node linkType: hard @@ -5571,6 +5652,15 @@ __metadata: languageName: node linkType: hard +"jest-fixed-jsdom@npm:^0.0.9": + version: 0.0.9 + resolution: "jest-fixed-jsdom@npm:0.0.9" + peerDependencies: + jest-environment-jsdom: ">=28.0.0" + checksum: 10/c0c1502c81de2c628f728d4c8cff1d66adf7dba3933d72198905bd395b95d393cc535d36469076e03c32f8f0c2200bffcc61065de0a4e5d6f83cfccad0da95cb + languageName: node + linkType: hard + "jest-get-type@npm:^29.6.3": version: 29.6.3 resolution: "jest-get-type@npm:29.6.3" @@ -6159,6 +6249,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:1.0.3": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 10/52117adbe0313d5defa771c9993fe081e2d2df9b840597e966aadafde04ae8d0e3da46bac7ca4efc37d4d2b839436582659cd49c6a43eacb3fe3050896a105d1 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -6403,19 +6500,19 @@ __metadata: languageName: node linkType: hard -"next@npm:^15.0.0": - version: 15.0.0 - resolution: "next@npm:15.0.0" +"next@npm:^15.0.3": + version: 15.0.3 + resolution: "next@npm:15.0.3" dependencies: - "@next/env": "npm:15.0.0" - "@next/swc-darwin-arm64": "npm:15.0.0" - "@next/swc-darwin-x64": "npm:15.0.0" - "@next/swc-linux-arm64-gnu": "npm:15.0.0" - "@next/swc-linux-arm64-musl": "npm:15.0.0" - "@next/swc-linux-x64-gnu": "npm:15.0.0" - "@next/swc-linux-x64-musl": "npm:15.0.0" - "@next/swc-win32-arm64-msvc": "npm:15.0.0" - "@next/swc-win32-x64-msvc": "npm:15.0.0" + "@next/env": "npm:15.0.3" + "@next/swc-darwin-arm64": "npm:15.0.3" + "@next/swc-darwin-x64": "npm:15.0.3" + "@next/swc-linux-arm64-gnu": "npm:15.0.3" + "@next/swc-linux-arm64-musl": "npm:15.0.3" + "@next/swc-linux-x64-gnu": "npm:15.0.3" + "@next/swc-linux-x64-musl": "npm:15.0.3" + "@next/swc-win32-arm64-msvc": "npm:15.0.3" + "@next/swc-win32-x64-msvc": "npm:15.0.3" "@swc/counter": "npm:0.1.3" "@swc/helpers": "npm:0.5.13" busboy: "npm:1.6.0" @@ -6427,8 +6524,8 @@ __metadata: "@opentelemetry/api": ^1.1.0 "@playwright/test": ^1.41.2 babel-plugin-react-compiler: "*" - react: ^18.2.0 || 19.0.0-rc-65a56d0e-20241020 - react-dom: ^18.2.0 || 19.0.0-rc-65a56d0e-20241020 + react: ^18.2.0 || 19.0.0-rc-66855b96-20241106 + react-dom: ^18.2.0 || 19.0.0-rc-66855b96-20241106 sass: ^1.3.0 dependenciesMeta: "@next/swc-darwin-arm64": @@ -6460,7 +6557,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/038c8358081b931edb5aaba24ee5ac08e1971080217c01d0d558a4ad2f35d639124dba3002fa096f1da5651860af6b4f5bccdcbb9008643c1eaa8ebe9d0caa62 + checksum: 10/d7044b22cd0724970464e88c51adc584d6252ccacff61b7c60dc3f248cca7227daa5a7265c2cc5ee1e19646ed51d1398c7c8d97c2a424cc1c951dddf7bb6b16e languageName: node linkType: hard @@ -6767,6 +6864,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:0.1.10": + version: 0.1.10 + resolution: "path-to-regexp@npm:0.1.10" + checksum: 10/894e31f1b20e592732a87db61fff5b95c892a3fe430f9ab18455ebe69ee88ef86f8eb49912e261f9926fc53da9f93b46521523e33aefd9cb0a7b0d85d7096006 + languageName: node + linkType: hard + "path-to-regexp@npm:0.1.7": version: 0.1.7 resolution: "path-to-regexp@npm:0.1.7" @@ -6973,6 +7077,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:6.13.0": + version: 6.13.0 + resolution: "qs@npm:6.13.0" + dependencies: + side-channel: "npm:^1.0.6" + checksum: 10/f548b376e685553d12e461409f0d6e5c59ec7c7d76f308e2a888fd9db3e0c5e89902bedd0754db3a9038eda5f27da2331a6f019c8517dc5e0a16b3c9a6e9cef8 + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -7321,11 +7434,11 @@ __metadata: linkType: hard "scheduler@npm:^0.23.0": - version: 0.23.2 - resolution: "scheduler@npm:0.23.2" + version: 0.23.1 + resolution: "scheduler@npm:0.23.1" dependencies: loose-envify: "npm:^1.1.0" - checksum: 10/e8d68b89d18d5b028223edf090092846868a765a591944760942b77ea1f69b17235f7e956696efbb62c8130ab90af7e0949bfb8eba7896335507317236966bc9 + checksum: 10/6e194e726210c2d619cbf69a73fdb068cb0c9b0c99222de429ec5fc562c2f28e59a8cb3526e9104e16521e7e57c785a82bc44dd3f78fd0de86aea719a218f3c3 languageName: node linkType: hard @@ -7386,6 +7499,27 @@ __metadata: languageName: node linkType: hard +"send@npm:0.19.0": + version: 0.19.0 + resolution: "send@npm:0.19.0" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:2.0.1" + checksum: 10/1f6064dea0ae4cbe4878437aedc9270c33f2a6650a77b56a16b62d057527f2766d96ee282997dd53ec0339082f2aad935bc7d989b46b48c82fc610800dc3a1d0 + languageName: node + linkType: hard + "serve-static@npm:1.15.0": version: 1.15.0 resolution: "serve-static@npm:1.15.0" @@ -7398,6 +7532,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:1.16.2": + version: 1.16.2 + resolution: "serve-static@npm:1.16.2" + dependencies: + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + parseurl: "npm:~1.3.3" + send: "npm:0.19.0" + checksum: 10/7fa9d9c68090f6289976b34fc13c50ac8cd7f16ae6bce08d16459300f7fc61fbc2d7ebfa02884c073ec9d6ab9e7e704c89561882bbe338e99fcacb2912fde737 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.1": version: 1.2.1 resolution: "set-function-length@npm:1.2.1" @@ -7516,7 +7662,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.4": +"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" dependencies: diff --git a/packages/client-react-streaming/package-shape.json b/packages/client-react-streaming/package-shape.json index cd71fc14..94572cdc 100644 --- a/packages/client-react-streaming/package-shape.json +++ b/packages/client-react-streaming/package-shape.json @@ -11,6 +11,9 @@ "TeeToReadableStreamLink", "readFromReadableStream", "teeToReadableStream", + "createTransportedQueryPreloader", + "isTransportedQueryRef", + "reviveTransportedQueryRef", "built_for_rsc" ], "browser": [ @@ -27,6 +30,9 @@ "TeeToReadableStreamLink", "readFromReadableStream", "teeToReadableStream", + "createTransportedQueryPreloader", + "isTransportedQueryRef", + "reviveTransportedQueryRef", "built_for_browser" ], "node": [ @@ -43,6 +49,9 @@ "TeeToReadableStreamLink", "readFromReadableStream", "teeToReadableStream", + "createTransportedQueryPreloader", + "isTransportedQueryRef", + "reviveTransportedQueryRef", "built_for_ssr" ], "edge-light,worker,browser": [ @@ -59,6 +68,9 @@ "TeeToReadableStreamLink", "readFromReadableStream", "teeToReadableStream", + "createTransportedQueryPreloader", + "isTransportedQueryRef", + "reviveTransportedQueryRef", "built_for_ssr" ] }, @@ -81,17 +93,25 @@ ] }, "@apollo/client-react-streaming/stream-utils": { - "react-server": ["built_for_other"], - "browser": ["built_for_other"], + "react-server": [ + "built_for_browser", + "JSONDecodeStream", + "JSONEncodeStream" + ], + "browser": ["built_for_browser", "JSONDecodeStream", "JSONEncodeStream"], "node": [ "built_for_ssr", "createInjectionTransformStream", - "pipeReaderToResponse" + "pipeReaderToResponse", + "JSONDecodeStream", + "JSONEncodeStream" ], "edge-light,worker,browser": [ "built_for_ssr", "createInjectionTransformStream", - "pipeReaderToResponse" + "pipeReaderToResponse", + "JSONDecodeStream", + "JSONEncodeStream" ] } } diff --git a/packages/client-react-streaming/package.json b/packages/client-react-streaming/package.json index dc51d1f4..cf3c4393 100644 --- a/packages/client-react-streaming/package.json +++ b/packages/client-react-streaming/package.json @@ -78,18 +78,18 @@ }, "./stream-utils": { "require": { - "types": "./dist/stream-utils.node.d.cts", - "edge-light": "./dist/stream-utils.node.cjs", - "react-server": "./dist/empty.cjs", - "browser": "./dist/empty.cjs", - "node": "./dist/stream-utils.node.cjs" + "types": "./dist/stream-utils.combined.d.cts", + "react-server": "./dist/stream-utils.cjs", + "edge-light": "./dist/stream-utils.ssr.cjs", + "browser": "./dist/stream-utils.cjs", + "node": "./dist/stream-utils.ssr.cjs" }, "import": { - "types": "./dist/stream-utils.node.d.ts", - "edge-light": "./dist/stream-utils.node.js", - "react-server": "./dist/empty.js", - "browser": "./dist/empty.js", - "node": "./dist/stream-utils.node.js" + "types": "./dist/stream-utils.combined.d.ts", + "react-server": "./dist/stream-utils.js", + "edge-light": "./dist/stream-utils.ssr.js", + "browser": "./dist/stream-utils.js", + "node": "./dist/stream-utils.ssr.js" } }, "./package.json": "./package.json" @@ -100,7 +100,7 @@ "./dist/manual-transport.ssr.d.ts" ], "stream-utils": [ - "./dist/stream-utils.node.d.ts" + "./dist/stream-utils.combined.d.ts" ] } }, diff --git a/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx b/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx index 40c26230..2bc34bfe 100644 --- a/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx +++ b/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx @@ -6,6 +6,7 @@ import type { FetchResult, DocumentNode, NormalizedCacheObject, + ApolloLink, } from "@apollo/client/index.js"; import { ApolloClient as OrigApolloClient, @@ -28,6 +29,10 @@ import type { import { bundle, sourceSymbol } from "../bundleInfo.js"; import { serializeOptions, deserializeOptions } from "./transportedOptions.js"; import { assertInstance } from "../assertInstance.js"; +import { + ReadFromReadableStreamLink, + TeeToReadableStreamLink, +} from "../ReadableStreamLink.js"; function getQueryManager( client: OrigApolloClient @@ -320,6 +325,15 @@ export function skipDataTransport>( class ApolloClientSSRImpl extends ApolloClientClientBaseImpl { private forwardedQueries = new (getTrieConstructor(this))(); + constructor(options: WrappedApolloClientOptions) { + super(options); + this.setLink(this.link); + } + + setLink(newLink: ApolloLink) { + super.setLink.call(this, ReadFromReadableStreamLink.concat(newLink)); + } + watchQueryQueue = createBackpressuredCallback<{ event: Extract; observable: Observable>; @@ -398,14 +412,34 @@ class ApolloClientSSRImpl extends ApolloClientClientBaseImpl { } } -export class ApolloClientBrowserImpl extends ApolloClientClientBaseImpl {} +export class ApolloClientBrowserImpl extends ApolloClientClientBaseImpl { + constructor(options: WrappedApolloClientOptions) { + super(options); + this.setLink(this.link); + } + + setLink(newLink: ApolloLink) { + super.setLink.call(this, ReadFromReadableStreamLink.concat(newLink)); + } +} + +export class ApolloClientRSCImpl extends ApolloClientBase { + constructor(options: WrappedApolloClientOptions) { + super(options); + this.setLink(this.link); + } + + setLink(newLink: ApolloLink) { + super.setLink.call(this, TeeToReadableStreamLink.concat(newLink)); + } +} const ApolloClientImplementation = /*#__PURE__*/ process.env.REACT_ENV === "ssr" ? ApolloClientSSRImpl : process.env.REACT_ENV === "browser" ? ApolloClientBrowserImpl - : ApolloClientBase; + : ApolloClientRSCImpl; /** * A version of `ApolloClient` to be used with streaming SSR or in React Server Components. diff --git a/packages/client-react-streaming/src/PreloadQuery.tsx b/packages/client-react-streaming/src/PreloadQuery.tsx index 628d037a..18071b24 100644 --- a/packages/client-react-streaming/src/PreloadQuery.tsx +++ b/packages/client-react-streaming/src/PreloadQuery.tsx @@ -1,30 +1,26 @@ import { SimulatePreloadedQuery } from "./index.cc.js"; import type { ApolloClient, + DocumentNode, OperationVariables, - QueryOptions, + TypedDocumentNode, } from "@apollo/client"; import type { ReactNode } from "react"; import React from "react"; -import { serializeOptions } from "./DataTransportAbstraction/transportedOptions.js"; -import type { TransportedQueryRef } from "./transportedQueryRef.js"; -import { createTransportedQueryRef } from "./transportedQueryRef.js"; -import type { ProgressEvent } from "./DataTransportAbstraction/DataTransportAbstraction.js"; - -export type RestrictedPreloadOptions = { - fetchPolicy?: "cache-first"; - returnPartialData?: false; - nextFetchPolicy?: undefined; - pollInterval?: undefined; -}; - -export type PreloadQueryOptions = QueryOptions< - TVariables, - TData -> & - RestrictedPreloadOptions; - -export function PreloadQuery({ +import type { + PreloadTransportedQueryOptions, + TransportedQueryRef, +} from "./transportedQueryRef.js"; +import { createTransportedQueryPreloader } from "./transportedQueryRef.js"; + +export type PreloadQueryOptions = + PreloadTransportedQueryOptions & { + query: DocumentNode | TypedDocumentNode; + }; +export async function PreloadQuery< + TData, + TVariables extends OperationVariables, +>({ getClient, children, ...options @@ -35,50 +31,14 @@ export function PreloadQuery({ | (( queryRef: TransportedQueryRef, NoInfer> ) => ReactNode); -}): React.ReactElement { - const preloadOptions = { - ...options, - fetchPolicy: "cache-first" as const, - returnPartialData: false, - pollInterval: undefined, - nextFetchPolicy: undefined, - } satisfies RestrictedPreloadOptions; - - const transportedOptions = sanitizeForTransport( - 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 queryKey = crypto.randomUUID(); +}): Promise { + const preloader = createTransportedQueryPreloader(await getClient()); + const { query, ...transportedOptions } = options; + const queryRef = preloader(query, transportedOptions); return ( - - options={transportedOptions} - result={resultPromise} - queryKey={typeof children === "function" ? queryKey : undefined} - > - {typeof children === "function" - ? children( - createTransportedQueryRef( - transportedOptions, - queryKey, - resultPromise - ) - ) - : children} + queryRef={queryRef}> + {typeof children === "function" ? children(queryRef) : children} ); } - -function sanitizeForTransport(value: T) { - return JSON.parse(JSON.stringify(value)) as T; -} diff --git a/packages/client-react-streaming/src/ReadableStreamLink.tsx b/packages/client-react-streaming/src/ReadableStreamLink.tsx index 3f6372f7..9672732f 100644 --- a/packages/client-react-streaming/src/ReadableStreamLink.tsx +++ b/packages/client-react-streaming/src/ReadableStreamLink.tsx @@ -56,6 +56,13 @@ export const TeeToReadableStreamLink = new ApolloLink((operation, forward) => { const controller = context[teeToReadableStreamKey]; if (controller) { + const tryClose = () => { + try { + controller.close(); + } catch { + // maybe we already tried to close the stream, nothing to worry about + } + }; return new Observable((observer) => { const subscription = forward(operation).subscribe({ next(result) { @@ -75,6 +82,7 @@ export const TeeToReadableStreamLink = new ApolloLink((operation, forward) => { }); return () => { + tryClose(); subscription.unsubscribe(); }; }); @@ -95,15 +103,37 @@ export const ReadFromReadableStreamLink = new ApolloLink( if (eventSteam) { return new Observable((observer) => { let aborted = false as boolean; - const reader = eventSteam.getReader(); - consumeReader(); + const reader = (() => { + try { + return eventSteam.getReader(); + } catch { + /** + * The reader could not be created, usually because the stream has + * already been consumed. + * This would be the case if we call `refetch` on a queryRef that has + * the `readFromReadableStreamKey` property in context. + * In that case, we want to do a normal network request. + */ + } + })(); + + if (!reader) { + // if we can't create a reader, we want to do a normal network request + const subscription = forward(operation).subscribe(observer); + return () => subscription.unsubscribe(); + } + consume(reader); - return () => { + let onAbort = () => { aborted = true; reader.cancel(); }; - async function consumeReader() { + return () => onAbort(); + + async function consume( + reader: ReadableStreamDefaultReader + ) { let event: | ReadableStreamReadResult | undefined = undefined; @@ -119,11 +149,20 @@ export const ReadFromReadableStreamLink = new ApolloLink( observer.complete(); break; case "error": - observer.error( - new Error( - "Error from event stream. Redacted for security concerns." - ) - ); + // in case a network error happened on the sending side, + if (process.env.REACT_ENV === "ssr") { + // we want to fail SSR for this tree + observer.error( + new Error( + "Error from event stream. Redacted for security concerns." + ) + ); + } else { + // we want to retry the operation on the receiving side + onAbort(); + const subscription = forward(operation).subscribe(observer); + onAbort = () => subscription.unsubscribe(); + } break; } } diff --git a/packages/client-react-streaming/src/SimulatePreloadedQuery.cc.ts b/packages/client-react-streaming/src/SimulatePreloadedQuery.cc.ts index b7122b4e..1789dc98 100644 --- a/packages/client-react-streaming/src/SimulatePreloadedQuery.cc.ts +++ b/packages/client-react-streaming/src/SimulatePreloadedQuery.cc.ts @@ -1,80 +1,33 @@ "use client"; +import { useApolloClient, useBackgroundQuery } from "@apollo/client/index.js"; +import { useMemo, type ReactNode } from "react"; import { - skipToken, - useApolloClient, - useBackgroundQuery, -} from "@apollo/client/index.js"; -import type { ApolloClient as WrappedApolloClient } from "./DataTransportAbstraction/WrappedApolloClient.js"; -import type { - ProgressEvent, - TransportIdentifier, -} from "./DataTransportAbstraction/DataTransportAbstraction.js"; -import { - deserializeOptions, - type TransportedOptions, -} from "./DataTransportAbstraction/transportedOptions.js"; -import type { QueryManager } from "@apollo/client/core/QueryManager.js"; -import { useMemo } from "react"; -import type { ReactNode } from "react"; -import invariant from "ts-invariant"; -import type { TransportedQueryRefOptions } from "./transportedQueryRef.js"; + reviveTransportedQueryRef, + type TransportedQueryRef, +} from "./transportedQueryRef.js"; +import { deserializeOptions } from "./DataTransportAbstraction/transportedOptions.js"; import type { PreloadQueryOptions } from "./PreloadQuery.js"; -const handledRequests = new WeakMap(); - export default function SimulatePreloadedQuery({ - options, - result, + queryRef, children, - queryKey, }: { - options: TransportedQueryRefOptions; - result: Promise>>; + queryRef: TransportedQueryRef; children: ReactNode; - queryKey?: string; }) { - const client = useApolloClient() as WrappedApolloClient; - if (!handledRequests.has(options)) { - const id = - `preloadedQuery:${(client["queryManager"] as QueryManager).generateQueryId()}` as TransportIdentifier; - handledRequests.set(options, id); - invariant.debug( - "Preloaded query %s started on the server, simulating ongoing request", - id - ); - client.onQueryStarted!({ - type: "started", - id, - 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); - } - }); - } + const client = useApolloClient(); + reviveTransportedQueryRef(queryRef, client); const bgQueryArgs = useMemo>(() => { const { query, ...hydratedOptions } = deserializeOptions( - options + queryRef.$__apollo_queryRef.options ) as PreloadQueryOptions; return [ query, - // If we didn't pass in a `queryKey` prop, the user didn't use the render props form and we don't - // need to create a real `queryRef` => skip. - // Otherwise we call `useBackgroundQuery` with options in this component to create a `queryRef` - // and have it soft-retained in the SuspenseCache. - queryKey - ? { - ...hydratedOptions, - queryKey, - } - : skipToken, - ]; - }, [options, queryKey]); + { ...hydratedOptions, queryKey: queryRef.$__apollo_queryRef.queryKey }, + ] as const; + }, [queryRef.$__apollo_queryRef]); useBackgroundQuery(...bgQueryArgs); diff --git a/packages/client-react-streaming/src/index.shared.ts b/packages/client-react-streaming/src/index.shared.ts index ed6b16cc..4b7c949d 100644 --- a/packages/client-react-streaming/src/index.shared.ts +++ b/packages/client-react-streaming/src/index.shared.ts @@ -5,7 +5,16 @@ export { ApolloClient, InMemoryCache, } from "./DataTransportAbstraction/index.js"; -export type { TransportedQueryRef } from "./transportedQueryRef.js"; +export type { + TransportedQueryRef, + PreloadTransportedQueryOptions, + PreloadTransportedQueryFunction, +} from "./transportedQueryRef.js"; +export { + createTransportedQueryPreloader, + isTransportedQueryRef, + reviveTransportedQueryRef, +} from "./transportedQueryRef.js"; export { ReadFromReadableStreamLink, TeeToReadableStreamLink, diff --git a/packages/client-react-streaming/src/registerApolloClient.tsx b/packages/client-react-streaming/src/registerApolloClient.tsx index 3bb9e52e..95ab15fd 100644 --- a/packages/client-react-streaming/src/registerApolloClient.tsx +++ b/packages/client-react-streaming/src/registerApolloClient.tsx @@ -1,5 +1,5 @@ import type { ApolloClient, OperationVariables } from "@apollo/client/index.js"; -import type React from "react"; +import React from "react"; import { cache } from "react"; import type { ReactNode } from "react"; import type { PreloadQueryOptions } from "./PreloadQuery.js"; @@ -149,7 +149,7 @@ return a new instance every time \`makeClient\` is called. * @see {@link PreloadQueryComponent} * @public */ -export interface PreloadQueryProps +export interface PreloadQueryProps extends PreloadQueryOptions { children: | ReactNode @@ -209,6 +209,6 @@ function makePreloadQuery( props: PreloadQueryProps ): React.ReactElement { // we directly execute the bound component instead of returning JSX to keep the tree a bit tidier - return UnboundPreloadQuery({ ...props, getClient }); + return ; }; } diff --git a/packages/client-react-streaming/src/stream-utils/JSONTransformStreams.tsx b/packages/client-react-streaming/src/stream-utils/JSONTransformStreams.tsx new file mode 100644 index 00000000..3f7ad7c0 --- /dev/null +++ b/packages/client-react-streaming/src/stream-utils/JSONTransformStreams.tsx @@ -0,0 +1,27 @@ +export class JSONEncodeStream extends TransformStream> { + constructor() { + super({ + transform(chunk, controller) { + controller.enqueue(JSON.stringify(chunk)); + }, + }); + } +} + +export class JSONDecodeStream extends TransformStream< + JsonString | AllowSharedBufferSource, + T +> { + constructor() { + super({ + transform(chunk, controller) { + if (typeof chunk !== "string") { + chunk = new TextDecoder().decode(chunk); + } + controller.enqueue(JSON.parse(chunk)); + }, + }); + } +} + +export type JsonString = string & { __jsonString?: [Encoded] }; diff --git a/packages/client-react-streaming/src/stream-utils/combined.ts b/packages/client-react-streaming/src/stream-utils/combined.ts new file mode 100644 index 00000000..69e76319 --- /dev/null +++ b/packages/client-react-streaming/src/stream-utils/combined.ts @@ -0,0 +1,17 @@ +/** + * TypeScript does not have the concept of these environments, + * so we need to create a single entry point that combines all + * possible exports. + * That means that users will be offered "RSC" exports in a + * "SSR/Browser" code file, but those will error in a compilation + * step. + * + * This is a limitation of TypeScript, and we can't do anything + * about it. + * + * The build process will only create `.d.ts`/`d.cts` files from + * this, and not actual runtime code. + */ + +export * from "./index.ssr.js"; +export * from "./index.js"; diff --git a/packages/client-react-streaming/src/stream-utils/index.shared.ts b/packages/client-react-streaming/src/stream-utils/index.shared.ts new file mode 100644 index 00000000..b3213e77 --- /dev/null +++ b/packages/client-react-streaming/src/stream-utils/index.shared.ts @@ -0,0 +1,5 @@ +export { + JSONDecodeStream, + JSONEncodeStream, + type JsonString, +} from "./JSONTransformStreams.js"; diff --git a/packages/client-react-streaming/src/stream-utils/index.ssr.ts b/packages/client-react-streaming/src/stream-utils/index.ssr.ts new file mode 100644 index 00000000..aef629d6 --- /dev/null +++ b/packages/client-react-streaming/src/stream-utils/index.ssr.ts @@ -0,0 +1,3 @@ +export * from "./index.shared.js"; +export { createInjectionTransformStream } from "./createInjectionTransformStream.js"; +export { pipeReaderToResponse } from "./pipeReaderToResponse.js"; diff --git a/packages/client-react-streaming/src/stream-utils/index.ts b/packages/client-react-streaming/src/stream-utils/index.ts index a2b5dd2d..9bb0939e 100644 --- a/packages/client-react-streaming/src/stream-utils/index.ts +++ b/packages/client-react-streaming/src/stream-utils/index.ts @@ -1,2 +1 @@ -export { createInjectionTransformStream } from "./createInjectionTransformStream.js"; -export { pipeReaderToResponse } from "./pipeReaderToResponse.js"; +export * from "./index.shared.js"; diff --git a/packages/client-react-streaming/src/stream-utils/pipeReaderToResponse.ts b/packages/client-react-streaming/src/stream-utils/pipeReaderToResponse.ts index 9a2eacfb..a28a9a30 100644 --- a/packages/client-react-streaming/src/stream-utils/pipeReaderToResponse.ts +++ b/packages/client-react-streaming/src/stream-utils/pipeReaderToResponse.ts @@ -1,10 +1,8 @@ -import type { ServerResponse } from "node:http"; /** - /** * > This export is only available in streaming SSR Server environments * * Used to pipe a `ReadableStreamDefaultReader` to a `ServerResponse`. - * + * * @example * ```js * const { injectIntoStream, transformStream } = createInjectionTransformStream(); @@ -18,7 +16,11 @@ import type { ServerResponse } from "node:http"; */ export async function pipeReaderToResponse( reader: ReadableStreamDefaultReader, - res: ServerResponse + res: { + write: (chunk: any) => void; + end: () => void; + destroy: (e: Error) => void; + } ) { try { // eslint-disable-next-line no-constant-condition diff --git a/packages/client-react-streaming/src/transportedQueryRef.ts b/packages/client-react-streaming/src/transportedQueryRef.ts index 5100df2b..6a39649d 100644 --- a/packages/client-react-streaming/src/transportedQueryRef.ts +++ b/packages/client-react-streaming/src/transportedQueryRef.ts @@ -1,23 +1,49 @@ -import type { CacheKey } from "@apollo/client/react/internal/index.js"; +import type { CacheKey } from "@apollo/client/react/internal"; import { - wrapQueryRef, getSuspenseCache, unwrapQueryRef, - assertWrappedQueryRef, + wrapQueryRef, } from "@apollo/client/react/internal/index.js"; - import { - useApolloClient, - type ApolloClient, - type QueryRef, + readFromReadableStream, + teeToReadableStream, +} from "./ReadableStreamLink.js"; +import { skipDataTransport } from "./DataTransportAbstraction/index.js"; +import type { ReadableStreamLinkEvent } from "./ReadableStreamLink.js"; +import { useApolloClient } from "@apollo/client/index.js"; +import type { + DocumentNode, + ApolloClient, + QueryRef, + QueryOptions, + OperationVariables, + TypedDocumentNode, } from "@apollo/client/index.js"; import { + serializeOptions, deserializeOptions, type TransportedOptions, } from "./DataTransportAbstraction/transportedOptions.js"; import { useEffect } from "react"; import { canonicalStringify } from "@apollo/client/cache/index.js"; -import type { RestrictedPreloadOptions } from "./PreloadQuery.js"; +import { + JSONDecodeStream, + JSONEncodeStream, + type JsonString, +} from "@apollo/client-react-streaming/stream-utils"; + +export type RestrictedPreloadOptions = { + fetchPolicy?: "network-only" | "cache-and-network" | "cache-first"; + returnPartialData?: false; + nextFetchPolicy?: undefined; + pollInterval?: undefined; +}; + +export type PreloadTransportedQueryOptions = Omit< + QueryOptions, + "query" +> & + RestrictedPreloadOptions; export type TransportedQueryRefOptions = TransportedOptions & RestrictedPreloadOptions; @@ -29,92 +55,166 @@ export type TransportedQueryRefOptions = TransportedOptions & * * @public */ -export interface TransportedQueryRef - extends QueryRef { +export interface TransportedQueryRef< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> extends QueryRef { /** * Temporarily disabled - see https://github.com/apollographql/apollo-client-nextjs/issues/332 * * Will now be be `undefined` both in React Server Components and Client Components until we can find a better resolution. */ toPromise?: undefined; + /** @private */ + $__apollo_queryRef: { + options: TransportedQueryRefOptions; + stream: ReadableStream>; + /** + * A unique key for this query, to ensure it is only hydrated once, + * even if it should get transported over the wire in a way that results + * in multiple objects describing the same queryRef. + * This key will be used to store the queryRef in the suspence cache. + * + * The chances of this happening should be slim (it is handled within + * React thanks to https://github.com/facebook/react/pull/28996), but + * as we use transported queryRefs with multiple frameworks with distinct + * transport mechanisms, this seems like a safe option. + */ + queryKey: string; + }; + /** @private */ + _hydrated?: CacheKey; } -export interface InternalTransportedQueryRef< - TData = unknown, - TVariables = unknown, -> extends TransportedQueryRef { - __transportedQueryRef: true | QueryRef; - options: TransportedQueryRefOptions; - queryKey: string; +export interface PreloadTransportedQueryFunction { + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadTransportedQueryOptions, TData> + ): TransportedQueryRef; +} + +export function createTransportedQueryPreloader( + client: ApolloClient +): PreloadTransportedQueryFunction { + return (...[query, options]: Parameters) => { + // unset options that we do not support + options = { ...options }; + delete options.returnPartialData; + delete options.nextFetchPolicy; + delete options.pollInterval; + + let __injectIntoStream: + | ReadableStreamDefaultController + | undefined; + const __eventStream = new ReadableStream({ + start(controller) { + __injectIntoStream = controller; + }, + }); + + // Instead of creating the queryRef, we kick off a query that will feed the network response + // into our custom event stream. + client + .query({ + query, + ...options, + // ensure that this query makes it to the network + fetchPolicy: "network-only", + context: skipDataTransport( + teeToReadableStream(__injectIntoStream!, { + ...options?.context, + // we want to do this even if the query is already running for another reason + queryDeduplication: false, + }) + ), + }) + .catch(() => { + /* we want to avoid any floating promise rejections */ + }); + + return createTransportedQueryRef( + query, + options, + crypto.randomUUID(), + __eventStream + ); + }; } -export function createTransportedQueryRef( - options: TransportedQueryRefOptions, +export function createTransportedQueryRef< + TData, + TVariables extends OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: PreloadTransportedQueryOptions, TData>, queryKey: string, - _promise: Promise -): InternalTransportedQueryRef { - const ref: InternalTransportedQueryRef = { - __transportedQueryRef: true, - options, - queryKey, + stream: ReadableStream +): TransportedQueryRef { + return { + $__apollo_queryRef: { + options: sanitizeForTransport(serializeOptions({ query, ...options })), + queryKey, + stream: stream.pipeThrough(new JSONEncodeStream()), + }, }; - /* - Temporarily disabled - see https://github.com/apollographql/apollo-client-nextjs/issues/332 - This causes a dev-mode warning: - Warning: Only plain objects can be passed to Client Components from Server Components. Classes or other objects with methods are not supported. - <... queryRef={{__transportedQueryRef: true, options: ..., queryKey: ...}}> - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - */ - // Object.defineProperty(ref, "toPromise", { - // value: () => promise.then(() => ref), - // enumerable: false, - // }); - return ref; } export function reviveTransportedQueryRef( - queryRef: InternalTransportedQueryRef, + queryRef: TransportedQueryRef, client: ApolloClient -): [QueryRef, CacheKey] { - const hydratedOptions = deserializeOptions(queryRef.options); +): asserts queryRef is TransportedQueryRef & + ReturnType> & { _hydrated: CacheKey } { + const { + $__apollo_queryRef: { options, stream, queryKey }, + } = queryRef; + const hydratedOptions = deserializeOptions(options); const cacheKey: CacheKey = [ hydratedOptions.query, canonicalStringify(hydratedOptions.variables), - queryRef.queryKey, + queryKey, ]; - if (queryRef.__transportedQueryRef === true) { - queryRef.__transportedQueryRef = wrapQueryRef( - getSuspenseCache(client).getQueryRef(cacheKey, () => - client.watchQuery(hydratedOptions) - ) + if (!queryRef._hydrated) { + queryRef._hydrated = cacheKey; + const internalQueryRef = getSuspenseCache(client).getQueryRef( + cacheKey, + () => + client.watchQuery({ + ...hydratedOptions, + fetchPolicy: "network-only", + context: skipDataTransport( + readFromReadableStream(stream.pipeThrough(new JSONDecodeStream()), { + ...hydratedOptions.context, + queryDeduplication: true, + }) + ), + }) ); + Object.assign(queryRef, wrapQueryRef(internalQueryRef)); } - return [queryRef.__transportedQueryRef, cacheKey]; } -function isTransportedQueryRef( - queryRef: object -): queryRef is InternalTransportedQueryRef { - return "__transportedQueryRef" in queryRef; +export function isTransportedQueryRef( + queryRef: any +): queryRef is TransportedQueryRef { + return !!(queryRef && queryRef.$__apollo_queryRef); } export function useWrapTransportedQueryRef( - queryRef: QueryRef | InternalTransportedQueryRef + queryRef: QueryRef | TransportedQueryRef ): QueryRef { const client = useApolloClient(); let cacheKey: CacheKey | undefined; let isTransported: boolean; if ((isTransported = isTransportedQueryRef(queryRef))) { - [queryRef, cacheKey] = reviveTransportedQueryRef(queryRef, client); + reviveTransportedQueryRef(queryRef, client); + cacheKey = queryRef._hydrated; } - assertWrappedQueryRef(queryRef); - const unwrapped = unwrapQueryRef(queryRef); + const unwrapped = unwrapQueryRef(queryRef)!; useEffect(() => { // We only want this to execute if the queryRef is a transported query. if (!isTransported) return; // We want to always keep this queryRef in the suspense cache in case another component has another instance of this transported queryRef. - // This effect could be removed after https://github.com/facebook/react/pull/28996 has been merged and we've updated deps to that version. if (cacheKey) { if (unwrapped.disposed) { getSuspenseCache(client).add(cacheKey, unwrapped); @@ -124,7 +224,6 @@ export function useWrapTransportedQueryRef( // conditional ensures we aren't running the logic on each render. }); // Soft-retaining because useQueryRefHandlers doesn't do it for us. - // This effect could be removed after https://github.com/facebook/react/pull/28996 has been merged and we've updated deps to that version. useEffect(() => { if (isTransported) { return unwrapped.softRetain(); @@ -132,3 +231,7 @@ export function useWrapTransportedQueryRef( }, [isTransported, unwrapped]); return queryRef; } + +function sanitizeForTransport(value: T) { + return JSON.parse(JSON.stringify(value)) as T; +} diff --git a/packages/client-react-streaming/tsconfig.json b/packages/client-react-streaming/tsconfig.json index d858c4da..27b7ea11 100644 --- a/packages/client-react-streaming/tsconfig.json +++ b/packages/client-react-streaming/tsconfig.json @@ -10,12 +10,22 @@ "sourceMap": true, "jsx": "react", "declarationMap": true, - "types": ["react/canary", "node"], + "types": [ + "react/canary", + "node" + ], "esModuleInterop": true, "allowSyntheticDefaultImports": true, "paths": { - "@apollo/client-react-streaming": ["./src/combined.ts"] + "@apollo/client-react-streaming": [ + "./src/combined.ts" + ], + "@apollo/client-react-streaming/stream-utils": [ + "src/stream-utils/index.shared.ts" + ] } }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/client-react-streaming/tsup.config.ts b/packages/client-react-streaming/tsup.config.ts index 43168e23..e46ce4cb 100644 --- a/packages/client-react-streaming/tsup.config.ts +++ b/packages/client-react-streaming/tsup.config.ts @@ -70,7 +70,16 @@ export default defineConfig((options) => { "src/ManualDataTransport/index.ts", "manual-transport.browser" ), - entry("ssr", "src/stream-utils/index.ts", "stream-utils.node"), + entry("ssr", "src/stream-utils/index.ssr.ts", "stream-utils.ssr"), + entry("browser", "src/stream-utils/index.ts", "stream-utils"), + { + ...entry( + "other", + "src/stream-utils/combined.ts", + "stream-utils.combined" + ), + dts: { only: true }, + }, { ...entry("browser", "src/index.cc.tsx", "index.cc"), treeshake: false, // would remove the "use client" directive diff --git a/packages/experimental-nextjs-app-support/src/rsc/index.ts b/packages/experimental-nextjs-app-support/src/rsc/index.ts index e33f76e0..5ff429ca 100644 --- a/packages/experimental-nextjs-app-support/src/rsc/index.ts +++ b/packages/experimental-nextjs-app-support/src/rsc/index.ts @@ -1,3 +1,4 @@ +import type { OperationVariables } from "@apollo/client/index.js"; import { registerApolloClient as _registerApolloClient, type TransportedQueryRef as _TransportedQueryRef, @@ -25,5 +26,5 @@ export const registerApolloClient = _registerApolloClient; */ export type TransportedQueryRef< TData = unknown, - TVariables = unknown, + TVariables extends OperationVariables = OperationVariables, > = _TransportedQueryRef; diff --git a/packages/experimental-nextjs-app-support/src/ssr/index.ts b/packages/experimental-nextjs-app-support/src/ssr/index.ts index 474637e2..b7648d25 100644 --- a/packages/experimental-nextjs-app-support/src/ssr/index.ts +++ b/packages/experimental-nextjs-app-support/src/ssr/index.ts @@ -14,6 +14,7 @@ import { useQuery as _useQuery, useReadQuery as _useReadQuery, useSuspenseQuery as _useSuspenseQuery, + OperationVariables, } from "@apollo/client/index.js"; /** @@ -97,7 +98,7 @@ export const ApolloNextAppProvider = _ApolloNextAppProvider; */ export type TransportedQueryRef< TData = unknown, - TVariables = unknown, + TVariables extends OperationVariables = OperationVariables, > = _TransportedQueryRef; /** * @deprecated