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

0.9.0 (7) - Changes to Data Transport #223

Merged
merged 16 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ jobs:

- name: Install Packages (Integration Test)
run: |
sed -ire '/^ checksum/d' yarn.lock
yarn install
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: "false"
Expand Down
2 changes: 1 addition & 1 deletion integration-test/.yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ nodeLinker: node-modules
npmAuthToken: "${NODE_AUTH_TOKEN-}"
yarnPath: ../.yarn/releases/yarn-4.1.0.cjs
cacheFolder: "../.yarn/cache"
installStatePath: "./.yarn/integration-test-install-state.gz"
installStatePath: "../.yarn/integration-test-install-state.gz"
enableInlineBuilds: true
checksumBehavior: ignore
53 changes: 30 additions & 23 deletions integration-test/experimental-react/src/WrappedApolloProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import {
WrapApolloProvider,
DataTransportContext,
} from "@apollo/client-react-streaming";
import type { DataTransportProviderImplementation } from "@apollo/client-react-streaming";
import type {
DataTransportProviderImplementation,
QueryEvent,
} from "@apollo/client-react-streaming";
import { useMemo, useActionChannel, useStaticValue, useRef } from "react";

import type { Cache, WatchQueryOptions } from "@apollo/client/index.js";
import { invariant } from "ts-invariant";

declare module "react" {
const useActionChannel: <T>(onData: (data: T) => void) => (data: T) => void;
const useActionChannel: <T>(
onData: (data: T) => void
) => (data: T | Promise<T>) => void;
/**
* This api design lends itself to a memory leak - the value passed in here
* can never be removed from memory.
Expand All @@ -26,25 +30,28 @@ declare module "react" {
}

export const ExperimentalReactDataTransport: DataTransportProviderImplementation =
({
onRequestData,
onRequestStarted,
registerDispatchRequestStarted,
registerDispatchRequestData,
children,
}) => {
const dispatchRequestStarted = useActionChannel(
(options: WatchQueryOptions) => {
onRequestStarted?.(options);
}
);
const dispatchRequestData = useActionChannel(
(options: Cache.WriteOptions) => {
onRequestData?.(options);
}
);
registerDispatchRequestStarted?.(dispatchRequestStarted);
registerDispatchRequestData?.(dispatchRequestData);
({ onQueryEvent, registerDispatchRequestStarted, children }) => {
const dispatchQueryEvent = useActionChannel<QueryEvent>((event) => {
invariant.debug("received event", event);
onQueryEvent?.(event);
});
registerDispatchRequestStarted?.(({ event, observable }) => {
let resolve: undefined | ((event: QueryEvent) => void);
invariant.debug("sending start event", event);
dispatchQueryEvent(event);
dispatchQueryEvent(new Promise<QueryEvent>((r) => (resolve = r)));
observable.subscribe({
next(event) {
if (event.type === "data") {
invariant.debug("sending event", event);
dispatchQueryEvent(event);
} else {
invariant.debug("resolving event promise", event);
resolve!(event);
}
},
});
});

return (
<DataTransportContext.Provider
Expand Down
3 changes: 3 additions & 0 deletions integration-test/experimental-react/src/entry-client.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import Html from "./Html";
import { setVerbosity } from "ts-invariant";

setVerbosity("debug");

ReactDOM.hydrateRoot(
document,
Expand Down
3 changes: 3 additions & 0 deletions integration-test/experimental-react/src/entry-server.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { Suspense } from "react";
import App from "./App";
import Html from "./Html";
import { setVerbosity } from "ts-invariant";

setVerbosity("debug");

export function render({ isProduction, assets }) {
return (
Expand Down
12 changes: 10 additions & 2 deletions integration-test/nextjs/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@ expect.extend({
},
});

declare module "@playwright/test" {
interface Page {
allowErrors?: () => void;
}
}

export const test = base.extend<{
blockRequest: import("@playwright/test").Page;
hydrationFinished: Promise<void>;
}>({
page: async ({ page }, use) => {
page.on("pageerror", (error) => {
function errorListener(error: Error) {
expect(error.stack || error).toBe("no error");
});
}
page.on("pageerror", errorListener);
page.allowErrors = () => page.off("pageerror", errorListener);
// this prevents the playwright http cache to kick in in test development
page.route("**", (route) => route.continue());
await use(page);
Expand Down
1 change: 1 addition & 0 deletions integration-test/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"next": "^14.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.13",
"ssr-only-secrets": "^0.0.5",
"typescript": "5.1.3"
},
Expand Down
28 changes: 24 additions & 4 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 { HttpLink } from "@apollo/client";
import { ApolloLink, HttpLink, Observable } from "@apollo/client";
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
Expand All @@ -15,11 +15,29 @@ import { delayLink } from "@/shared/delayLink";
import { schema } from "../graphql/schema";

import { useSSROnlySecret } from "ssr-only-secrets";
import { GraphQLError } from "graphql";

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 All @@ -43,9 +61,11 @@ export function ApolloWrapper({

return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link: delayLink.concat(
typeof window === "undefined" ? new SchemaLink({ schema }) : httpLink
),
link: delayLink
.concat(errorLink)
.concat(
typeof window === "undefined" ? new SchemaLink({ schema }) : httpLink
),
});
}
}
55 changes: 50 additions & 5 deletions integration-test/nextjs/src/app/cc/dynamic/dynamic.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
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/;
const regex_query_error_restart =
/query failed on server, rerunning in browser/;

test.describe("CC dynamic", () => {
test.describe("useSuspenseQuery", () => {
test("one query", async ({ page, blockRequest, hydrationFinished }) => {
Expand All @@ -14,7 +19,42 @@ test.describe("CC dynamic", () => {
await hydrationFinished;
await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
});

test("error during SSR restarts query in browser", async ({
page,
hydrationFinished,
}) => {
page.allowErrors?.();
let allLogs: string[] = [];
page.on("console", (message) => {
allLogs.push(message.text());
});

await page.goto(
"http://localhost:3000/cc/dynamic/useSuspenseQueryWithError",
{
waitUntil: "commit",
}
);

await expect(page).toBeInitiallyLoading(true);

await page.waitForEvent("console", (message) => {
return regex_query_error_restart.test(message.text());
});
await page.waitForEvent("pageerror", (error) => {
return error.message.includes("Minified React error #419");
});

await hydrationFinished;
await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();

for (const log of allLogs) {
expect(log).not.toMatch(regex_connection_closed_early);
}
});
});

test.describe("useBackgroundQuery + useReadQuery", () => {
test("one query", async ({ page, blockRequest, hydrationFinished }) => {
await page.goto("http://localhost:3000/cc/dynamic/useBackgroundQuery", {
Expand All @@ -39,6 +79,11 @@ test.describe("CC dynamic", () => {
);

await expect(page.getByText("rendered on server")).toBeVisible();

await page.waitForEvent("console", (message) => {
return regex_connection_closed_early.test(message.text());
});

await expect(page.getByText("rendered on client")).toBeVisible();
await expect(page.getByText("loading")).toBeVisible();
await expect(page.getByText("loading")).not.toBeVisible();
Expand Down Expand Up @@ -74,11 +119,11 @@ test.describe("CC dynamic", () => {
}
);

const messagePromise = page.waitForEvent("console");
const message = await messagePromise;
expect(message.text()).toMatch(
/^Refused to execute inline script because it violates the following Content Security Policy/
);
await page.waitForEvent("console", (message) => {
return /^Refused to execute inline script because it violates the following Content Security Policy/.test(
message.text()
);
});
});
test("valid: does not log an error", async ({ page, blockRequest }) => {
await page.goto(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { useSuspenseQuery } from "@apollo/experimental-nextjs-app-support/ssr";
import type { TypedDocumentNode } from "@apollo/client";
import { gql } from "@apollo/client";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import { Suspense, startTransition, useState, useTransition } from "react";

const QUERY: TypedDocumentNode<{
products: {
id: string;
title: string;
}[];
}> = gql`
query dynamicProducts {
products {
id
title
}
}
`;

export const dynamic = "force-dynamic";

export default function Page() {
return (
<Suspense fallback={"loading"}>
<ErrorBoundary FallbackComponent={FallbackComponent}>
<Component errorLevel={"ssr"} />
</ErrorBoundary>
</Suspense>
);
}

function FallbackComponent({ error, resetErrorBoundary }: FallbackProps) {
return (
<>
<p>{error.message}</p>
</>
);
}

function Component({ errorLevel }: { errorLevel: "ssr" | "always" }) {
const { data } = useSuspenseQuery(QUERY, {
context: { error: errorLevel },
});
globalThis.hydrationFinished?.();

return (
<ul>
{data.products.map(({ id, title }) => (
<li key={id}>{title}</li>
))}
</ul>
);
}
2 changes: 1 addition & 1 deletion integration-test/vite-streaming/src/Transport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { WrapApolloProvider } from "@apollo/client-react-streaming";
import { buildManualDataTransport } from "@apollo/client-react-streaming/experimental-manual-transport";
import { buildManualDataTransport } from "@apollo/client-react-streaming/manual-transport";
import { renderToString } from "react-dom/server";
import * as React from "react";

Expand Down
Loading
Loading