Skip to content

Incorrect dataState/data returned for incremental partial data written to cache with returnPartialData: false #13330

Description

@jerelmiller

Issue Description

When partial data is written to the cache and returnPartialData is either false or not set, incremental chunks may still return partial data in a response with a dataState of streaming.

This is primarily due to these lines in QueryInfo:

const diffOptions = {
  query,
  variables,
  returnPartialData: true,
  optimistic: true,
};

const lastDiff =
  skipCache ? undefined : this.cache.diff<TData>(diffOptions);

let result = this.maybeHandleIncrementalResult(
  lastDiff?.result,
  incoming,
  query
);

We unconditionally pull partial data from the cache and apply it with the incremental result which mixes the partial cache data into the final result. See the reproduction in the test case below which demonstrates the issue. What we should get is a result back where the only hole in the data is at the @defer boundary and ensure partial data inside that boundary is omitted.

Note

I (Jerel) plan to implement this fix as it requires code from another pull request and with a specific implementation that will fix this as the cache level. Please do not attempt to fix this yourself.

Link to Reproduction

n/a

Reproduction Steps

The following test demonstrates the issue

test('returns non-deferred cached data with a "cache-first" fetch policy and returnPartialData: false', async () => {
  const query = gql`
    query {
      greeting {
        message
        ... on Greeting @defer {
          recipient {
            name
            email
          }
        }
      }
    }
  `;

  const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } =
    mockDeferStreamGraphQL17Alpha9();
  const cache = new InMemoryCache();

  // We are intentionally writing partial data to the cache. Suppress console
  // warnings to avoid unnecessary noise in the test.
  {
    using _consoleSpy = spyOnConsole("error");
    cache.writeQuery({
      query,
      data: {
        greeting: {
          __typename: "Greeting",
          recipient: {
            email: "test@example.com",
          },
        },
      },
    });
  }

  const client = new ApolloClient({
    cache,
    link: httpLink,
    incrementalHandler: new GraphQL17Alpha9Handler(),
  });

  const stream = new ObservableStream(client.watchQuery({ query }));

  await expect(stream).toEmitTypedValue({
    data: undefined,
    dataState: "empty",
    loading: true,
    networkStatus: NetworkStatus.loading,
    partial: true,
  });

  enqueueInitialChunk({
    data: { greeting: { message: "Hello world", __typename: "Greeting" } },
    pending: [{ id: "0", path: ["greeting"] }],
    hasNext: true,
  });

  await expect(stream).toEmitTypedValue({
    data: markAsStreaming({
      greeting: {
        __typename: "Greeting",
        message: "Hello world",
      },
    }),
    dataState: "streaming",
    loading: true,
    networkStatus: NetworkStatus.streaming,
    partial: true,
  });

  enqueueSubsequentChunk({
    incremental: [
      {
        data: {
          __typename: "Greeting",
          recipient: { name: "Alice", __typename: "Person" },
        },
        id: "0",
      },
    ],
    completed: [{ id: "0" }],
    hasNext: false,
  });

  await expect(stream).toEmitTypedValue({
    data: {
      greeting: {
        __typename: "Greeting",
        message: "Hello world",
        recipient: { __typename: "Person", name: "Alice" },
      },
    },
    dataState: "complete",
    loading: false,
    networkStatus: NetworkStatus.ready,
    partial: false,
  });

  await expect(stream).not.toEmitAnything();
});

@apollo/client version

4.x

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions