diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index c7343506bff..bbfb571572e 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -373,6 +373,7 @@ Array [ "setupVariablesCase", "spyOnConsole", "wait", + "withCacheSizes", "withCleanup", "withProdMode", ] diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index 73fdf2af688..9d18bae46d7 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -1,6 +1,6 @@ import { gql } from "graphql-tag"; import { assign, cloneDeep } from "lodash"; -import { Observable } from "rxjs"; +import { delay, Observable, of } from "rxjs"; import type { TypedDocumentNode } from "@apollo/client"; import { @@ -22,6 +22,7 @@ import { mockDeferStream, ObservableStream, setupPaginatedCase, + withCacheSizes, } from "@apollo/client/testing/internal"; import { concatPagination, @@ -2686,6 +2687,166 @@ test("does not allow fetchMore on a cache-only query", async () => { await expect(stream).not.toEmitAnything(); }); +// https://github.com/apollographql/apollo-client/issues/12932 +test("emits final result from fetchMore when executeSubSelectedArray cache is full", async () => { + using _ = withCacheSizes({ "inMemoryCache.executeSubSelectedArray": 10 }); + + const itemData = Array(20) + .fill(undefined) + .map((_, index) => ({ + __typename: "Item" as const, + id: index.toString(), + attributes: ["data"], + })); + type GetCommentsData = { + items: Array<{ + __typename: "Item"; + id: string; + attributes: string[]; + }>; + }; + + type GetCommentsVariables = { + offset: number; + limit: number; + }; + + const query: TypedDocumentNode = gql` + query GetComments($offset: Int!, $limit: Int!) { + items(offset: $offset, limit: $limit) { + id + attributes + } + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + items: offsetLimitPagination(), + }, + }, + }, + }), + link: new ApolloLink((operation) => { + const { offset, limit } = operation.variables; + + return of({ + data: { + items: itemData.slice(offset, offset + limit), + }, + }).pipe(delay(10)); + }), + }); + + const observable = client.watchQuery({ + query, + variables: { offset: 0, limit: 10 }, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(stream).toEmitTypedValue({ + data: { items: itemData.slice(0, 10) }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect( + observable.fetchMore({ + variables: { + offset: stream.getCurrent()?.data?.items?.length ?? 0, + limit: 5, + }, + }) + ).resolves.toStrictEqualTyped({ + data: { items: itemData.slice(10, 15) }, + }); + + await expect(stream).toEmitTypedValue({ + data: { items: itemData.slice(0, 10) }, + dataState: "complete", + loading: true, + networkStatus: NetworkStatus.fetchMore, + partial: false, + }); + + await expect(stream).toEmitTypedValue({ + data: { items: itemData.slice(0, 15) }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect( + observable.fetchMore({ + variables: { + offset: stream.getCurrent()?.data?.items?.length ?? 0, + limit: 5, + }, + }) + ).resolves.toStrictEqualTyped({ + data: { items: itemData.slice(15, 20) }, + }); + + await expect(stream).toEmitTypedValue({ + data: { items: itemData.slice(0, 15) }, + dataState: "complete", + loading: true, + networkStatus: NetworkStatus.fetchMore, + partial: false, + }); + + await expect(stream).toEmitTypedValue({ + data: { items: itemData.slice(0, 20) }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect( + observable.fetchMore({ + variables: { + offset: stream.getCurrent()?.data?.items?.length ?? 0, + limit: 5, + }, + }) + ).resolves.toStrictEqualTyped({ + data: { items: [] }, + }); + + await expect(stream).toEmitTypedValue({ + data: { items: itemData.slice(0, 20) }, + dataState: "complete", + loading: true, + networkStatus: NetworkStatus.fetchMore, + partial: false, + }); + + await expect(stream).toEmitTypedValue({ + data: { items: itemData.slice(0, 20) }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect(stream).not.toEmitAnything(); +}); + function commentsInRange( start: number, end: number, diff --git a/src/testing/internal/disposables/index.ts b/src/testing/internal/disposables/index.ts index b2e30d3808e..03304db1df8 100644 --- a/src/testing/internal/disposables/index.ts +++ b/src/testing/internal/disposables/index.ts @@ -2,3 +2,4 @@ export { spyOnConsole } from "./spyOnConsole.js"; export { withCleanup } from "./withCleanup.js"; export { enableFakeTimers } from "./enableFakeTimers.js"; export { withProdMode } from "./withProdMode.js"; +export { withCacheSizes } from "./withCacheSizes.js"; diff --git a/src/testing/internal/disposables/withCacheSizes.ts b/src/testing/internal/disposables/withCacheSizes.ts new file mode 100644 index 00000000000..b426688998c --- /dev/null +++ b/src/testing/internal/disposables/withCacheSizes.ts @@ -0,0 +1,23 @@ +import type { CacheSizes } from "@apollo/client/utilities"; +import { cacheSizes } from "@apollo/client/utilities"; + +import { withCleanup } from "./withCleanup.js"; + +export function withCacheSizes(tempCacheSizes: Partial) { + const prev = { prevCacheSizes: { ...cacheSizes } }; + Object.entries(tempCacheSizes).forEach(([key, value]) => { + cacheSizes[key as keyof CacheSizes] = value; + }); + + return withCleanup(prev, ({ prevCacheSizes }) => { + Object.keys(tempCacheSizes).forEach((k) => { + const key = k as keyof CacheSizes; + if (key in prevCacheSizes) { + cacheSizes[key as keyof CacheSizes] = + prevCacheSizes[key as keyof CacheSizes]; + } else { + delete cacheSizes[key]; + } + }); + }); +} diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 1ebe8234c9c..0cba1ab399f 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -1,6 +1,7 @@ export { enableFakeTimers, spyOnConsole, + withCacheSizes, withCleanup, withProdMode, } from "./disposables/index.js";