Skip to content

Commit

Permalink
0.9.0 (7) - Changes to Data Transport (#223)
Browse files Browse the repository at this point in the history
* switch data transport to event stream

* updates

* adjust integration test

* tooling config

* remove "experimental" from entry point name of manualDataTransport

* adjust import

* update another import

* remove comment

* update more references

* fixup trigger-rebuild script

* more script fixup

* tweaks

* test error handling

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

Co-authored-by: Jerel Miller <[email protected]>

---------

Co-authored-by: Jerel Miller <[email protected]>
  • Loading branch information
phryneas and jerelmiller authored Mar 12, 2024
1 parent f823d31 commit 37feeaa
Show file tree
Hide file tree
Showing 33 changed files with 445 additions and 280 deletions.
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

0 comments on commit 37feeaa

Please sign in to comment.