Skip to content

Commit

Permalink
Merge branch 'main' into secops/add-gitleaks
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas authored Oct 10, 2023
2 parents 1acd7cb + b29f000 commit f559aad
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/lemon-tigers-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Start the query ref auto dispose timeout after the initial promise has settled. This prevents requests that run longer than the timeout duration from keeping the component suspended indefinitely.
2 changes: 1 addition & 1 deletion .size-limit.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const checks = [
{
path: "dist/apollo-client.min.cjs",
limit: "37940",
limit: "37960",
},
{
path: "dist/main.cjs",
Expand Down
17 changes: 13 additions & 4 deletions src/react/cache/QueryReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,19 @@ export class InternalQueryReference<TData = unknown> {
// suspended resource does not use this queryRef in the given time. This
// helps prevent memory leaks when a component has unmounted before the
// query has finished loading.
this.autoDisposeTimeoutId = setTimeout(
this.dispose,
options.autoDisposeTimeoutMs ?? 30_000
);
const startDisposeTimer = () => {
if (!this.references) {
this.autoDisposeTimeoutId = setTimeout(
this.dispose,
options.autoDisposeTimeoutMs ?? 30_000
);
}
};

// We wait until the request has settled to ensure we don't dispose of the
// query ref before the request finishes, otherwise we would leave the
// promise in a pending state rendering the suspense boundary indefinitely.
this.promise.then(startDisposeTimer, startDisposeTimer);
}

get watchQueryOptions() {
Expand Down
75 changes: 63 additions & 12 deletions src/react/hooks/__tests__/useSuspenseQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -805,8 +805,12 @@ describe("useSuspenseQuery", () => {

expect(screen.queryByText("Loading greeting...")).not.toBeInTheDocument();

link.simulateResult({ result: { data: { greeting: "Hello" } } });
link.simulateComplete();
await act(() => {
link.simulateResult({ result: { data: { greeting: "Hello" } } }, true);
// Ensure simulateResult will deliver the result since its wrapped with
// setTimeout
jest.advanceTimersByTime(10);
});

expect(client.getObservableQueries().size).toBe(1);
expect(client).toHaveSuspenseCacheEntryUsing(query);
Expand All @@ -817,10 +821,6 @@ describe("useSuspenseQuery", () => {
expect(client).not.toHaveSuspenseCacheEntryUsing(query);

jest.useRealTimers();

// Avoid act warnings for a suspended resource
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(() => wait(0));
});

it("has configurable auto dispose timer if the component never renders again after suspending", async () => {
Expand Down Expand Up @@ -871,8 +871,12 @@ describe("useSuspenseQuery", () => {

expect(screen.queryByText("Loading greeting...")).not.toBeInTheDocument();

link.simulateResult({ result: { data: { greeting: "Hello" } } });
link.simulateComplete();
await act(() => {
link.simulateResult({ result: { data: { greeting: "Hello" } } }, true);
// Ensure simulateResult will deliver the result since its wrapped with
// setTimeout
jest.advanceTimersByTime(10);
});

expect(client.getObservableQueries().size).toBe(1);
expect(client).toHaveSuspenseCacheEntryUsing(query);
Expand All @@ -883,10 +887,6 @@ describe("useSuspenseQuery", () => {
expect(client).not.toHaveSuspenseCacheEntryUsing(query);

jest.useRealTimers();

// Avoid act warnings for a suspended resource
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(() => wait(0));
});

it("cancels auto dispose if the component renders before timer finishes", async () => {
Expand Down Expand Up @@ -940,6 +940,57 @@ describe("useSuspenseQuery", () => {
jest.useRealTimers();
});

// https://github.com/apollographql/apollo-client/issues/11270
it("does not leave component suspended if query completes if request takes longer than auto dispose timeout", async () => {
jest.useFakeTimers();
const { query } = useSimpleQueryCase();
const link = new MockSubscriptionLink();
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
defaultOptions: {
react: {
suspense: {
autoDisposeTimeoutMs: 10,
},
},
},
});

function App() {
return (
<ApolloProvider client={client}>
<Suspense fallback="Loading greeting...">
<Greeting />
</Suspense>
</ApolloProvider>
);
}

function Greeting() {
const { data } = useSuspenseQuery(query);

return <span>{data.greeting}</span>;
}

render(<App />);

// Ensure <Greeting /> suspends immediately
expect(screen.getByText("Loading greeting...")).toBeInTheDocument();

jest.advanceTimersByTime(20);

link.simulateResult({ result: { data: { greeting: "Hello" } } }, true);

await waitFor(() => {
expect(screen.queryByText("Loading greeting...")).not.toBeInTheDocument();
});

expect(screen.getByText("Hello")).toBeInTheDocument();

jest.useRealTimers();
});

it("allows the client to be overridden", async () => {
const { query } = useSimpleQueryCase();

Expand Down

0 comments on commit f559aad

Please sign in to comment.